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. */
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
/* 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 '@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 React from 'react'
|
||||||
|
|
||||||
import { importMap } from './admin/importMap.js'
|
import { importMap } from './admin/importMap.js'
|
||||||
@@ -12,8 +14,17 @@ type Args = {
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serverFunction: ServerFunctionClient = async function (args) {
|
||||||
|
'use server'
|
||||||
|
return handleServerFunctions({
|
||||||
|
...args,
|
||||||
|
config,
|
||||||
|
importMap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const Layout = ({ children }: Args) => (
|
const Layout = ({ children }: Args) => (
|
||||||
<RootLayout config={configPromise} importMap={importMap}>
|
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||||
{children}
|
{children}
|
||||||
</RootLayout>
|
</RootLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -228,7 +228,6 @@ The following additional properties are also provided to the `field` prop:
|
|||||||
|
|
||||||
| Property | Description |
|
| 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`. |
|
| **`_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` |
|
| **`_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
|
// Import editor state into your headless editor
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
logger.error({ err: e }, 'ERROR parsing editor state')
|
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
|
## Lexical => Plain Text
|
||||||
|
|
||||||
Export content from the Lexical editor into plain text using these steps:
|
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
|
// Import editor state into your headless editor
|
||||||
try {
|
try {
|
||||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately
|
headlessEditor.update(
|
||||||
} catch (e) {
|
() => {
|
||||||
|
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
|
||||||
|
},
|
||||||
|
{ discrete: true }, // This should commit the editor state immediately
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
logger.error({ err: e }, 'ERROR parsing editor state')
|
logger.error({ err: e }, 'ERROR parsing editor state')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
/* 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 '@payloadcms/next/css'
|
||||||
import { RootLayout } from '@payloadcms/next/layouts'
|
import { RootLayout } from '@payloadcms/next/layouts'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -13,7 +13,7 @@ type Args = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Layout = ({ children }: Args) => (
|
const Layout = ({ children }: Args) => (
|
||||||
<RootLayout config={configPromise} importMap={importMap}>
|
<RootLayout config={config} importMap={importMap}>
|
||||||
{children}
|
{children}
|
||||||
</RootLayout>
|
</RootLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
/* 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 configPromise from '@payload-config'
|
||||||
import '@payloadcms/next/css'
|
import '@payloadcms/next/css'
|
||||||
import { RootLayout } from '@payloadcms/next/layouts'
|
import { RootLayout } from '@payloadcms/next/layouts'
|
||||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import './custom.scss'
|
import './custom.scss'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
/* 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 '@payloadcms/next/css'
|
||||||
import { RootLayout } from '@payloadcms/next/layouts'
|
import { RootLayout } from '@payloadcms/next/layouts'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -13,7 +13,7 @@ type Args = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Layout = ({ children }: Args) => (
|
const Layout = ({ children }: Args) => (
|
||||||
<RootLayout config={configPromise} importMap={importMap}>
|
<RootLayout config={config} importMap={importMap}>
|
||||||
{children}
|
{children}
|
||||||
</RootLayout>
|
</RootLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ const config = withBundleAnalyzer(
|
|||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
|
experimental: {
|
||||||
|
serverActions: {
|
||||||
|
bodySizeLimit: '5mb',
|
||||||
|
},
|
||||||
|
},
|
||||||
env: {
|
env: {
|
||||||
PAYLOAD_CORE_DEV: 'true',
|
PAYLOAD_CORE_DEV: 'true',
|
||||||
ROOT_DIR: path.resolve(dirname),
|
ROOT_DIR: path.resolve(dirname),
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
|
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
|
||||||
"dev:generate-types": "pnpm runts ./test/generateTypes.ts",
|
"dev:generate-types": "pnpm runts ./test/generateTypes.ts",
|
||||||
"dev:postgres": "cross-env PAYLOAD_DATABASE=postgres pnpm runts ./test/dev.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",
|
"dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts",
|
||||||
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
|
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
|
||||||
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
|
"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",
|
"docker:stop": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml down",
|
||||||
"force:build": "pnpm run build:core:force",
|
"force:build": "pnpm run build:core:force",
|
||||||
"lint": "turbo run lint --concurrency 1 --continue",
|
"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",
|
"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 {} +",
|
"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": "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": "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 -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",
|
"reinstall": "pnpm clean:all && pnpm install",
|
||||||
"release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
|
"release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
|
||||||
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
|
"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 { connect } from './connect.js'
|
||||||
import { count } from './count.js'
|
import { count } from './count.js'
|
||||||
|
import { countGlobalVersions } from './countGlobalVersions.js'
|
||||||
|
import { countVersions } from './countVersions.js'
|
||||||
import { create } from './create.js'
|
import { create } from './create.js'
|
||||||
import { createGlobal } from './createGlobal.js'
|
import { createGlobal } from './createGlobal.js'
|
||||||
import { createGlobalVersion } from './createGlobalVersion.js'
|
import { createGlobalVersion } from './createGlobalVersion.js'
|
||||||
@@ -154,7 +156,6 @@ export function mongooseAdapter({
|
|||||||
collections: {},
|
collections: {},
|
||||||
connection: undefined,
|
connection: undefined,
|
||||||
connectOptions: connectOptions || {},
|
connectOptions: connectOptions || {},
|
||||||
count,
|
|
||||||
disableIndexHints,
|
disableIndexHints,
|
||||||
globals: undefined,
|
globals: undefined,
|
||||||
mongoMemoryServer,
|
mongoMemoryServer,
|
||||||
@@ -166,6 +167,9 @@ export function mongooseAdapter({
|
|||||||
beginTransaction: transactionOptions === false ? defaultBeginTransaction() : beginTransaction,
|
beginTransaction: transactionOptions === false ? defaultBeginTransaction() : beginTransaction,
|
||||||
commitTransaction,
|
commitTransaction,
|
||||||
connect,
|
connect,
|
||||||
|
count,
|
||||||
|
countGlobalVersions,
|
||||||
|
countVersions,
|
||||||
create,
|
create,
|
||||||
createGlobal,
|
createGlobal,
|
||||||
createGlobalVersion,
|
createGlobalVersion,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { PipelineStage } from 'mongoose'
|
import type { PipelineStage } from 'mongoose'
|
||||||
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
|
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
|
||||||
|
|
||||||
import { combineQueries } from 'payload'
|
|
||||||
|
|
||||||
import type { MongooseAdapter } from '../index.js'
|
import type { MongooseAdapter } from '../index.js'
|
||||||
|
|
||||||
import { buildSortParam } from '../queries/buildSortParam.js'
|
import { buildSortParam } from '../queries/buildSortParam.js'
|
||||||
@@ -60,11 +58,11 @@ export const buildJoinAggregation = async ({
|
|||||||
for (const join of joinConfig[slug]) {
|
for (const join of joinConfig[slug]) {
|
||||||
const joinModel = adapter.collections[join.field.collection]
|
const joinModel = adapter.collections[join.field.collection]
|
||||||
|
|
||||||
if (projection && !projection[join.schemaPath]) {
|
if (projection && !projection[join.joinPath]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (joins?.[join.schemaPath] === false) {
|
if (joins?.[join.joinPath] === false) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +70,7 @@ export const buildJoinAggregation = async ({
|
|||||||
limit: limitJoin = join.field.defaultLimit ?? 10,
|
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||||
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||||
where: whereJoin,
|
where: whereJoin,
|
||||||
} = joins?.[join.schemaPath] || {}
|
} = joins?.[join.joinPath] || {}
|
||||||
|
|
||||||
const sort = buildSortParam({
|
const sort = buildSortParam({
|
||||||
config: adapter.payload.config,
|
config: adapter.payload.config,
|
||||||
@@ -105,7 +103,7 @@ export const buildJoinAggregation = async ({
|
|||||||
|
|
||||||
if (adapter.payload.config.localization && locale === 'all') {
|
if (adapter.payload.config.localization && locale === 'all') {
|
||||||
adapter.payload.config.localization.localeCodes.forEach((code) => {
|
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(
|
aggregate.push(
|
||||||
{
|
{
|
||||||
@@ -146,7 +144,7 @@ export const buildJoinAggregation = async ({
|
|||||||
} else {
|
} else {
|
||||||
const localeSuffix =
|
const localeSuffix =
|
||||||
join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : ''
|
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(
|
aggregate.push(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export const handleError = ({
|
|||||||
collection,
|
collection,
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
field: Object.keys(error.keyValue)[0],
|
|
||||||
message: req.t('error:valueMustBeUnique'),
|
message: req.t('error:valueMustBeUnique'),
|
||||||
|
path: Object.keys(error.keyValue)[0],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
global,
|
global,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
beginTransaction,
|
beginTransaction,
|
||||||
commitTransaction,
|
commitTransaction,
|
||||||
count,
|
count,
|
||||||
|
countGlobalVersions,
|
||||||
|
countVersions,
|
||||||
create,
|
create,
|
||||||
createGlobal,
|
createGlobal,
|
||||||
createGlobalVersion,
|
createGlobalVersion,
|
||||||
@@ -126,6 +128,8 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
|||||||
convertPathToJSONTraversal,
|
convertPathToJSONTraversal,
|
||||||
count,
|
count,
|
||||||
countDistinct,
|
countDistinct,
|
||||||
|
countGlobalVersions,
|
||||||
|
countVersions,
|
||||||
create,
|
create,
|
||||||
createGlobal,
|
createGlobal,
|
||||||
createGlobalVersion,
|
createGlobalVersion,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
beginTransaction,
|
beginTransaction,
|
||||||
commitTransaction,
|
commitTransaction,
|
||||||
count,
|
count,
|
||||||
|
countGlobalVersions,
|
||||||
|
countVersions,
|
||||||
create,
|
create,
|
||||||
createGlobal,
|
createGlobal,
|
||||||
createGlobalVersion,
|
createGlobalVersion,
|
||||||
@@ -114,6 +116,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
|||||||
convertPathToJSONTraversal,
|
convertPathToJSONTraversal,
|
||||||
count,
|
count,
|
||||||
countDistinct,
|
countDistinct,
|
||||||
|
countGlobalVersions,
|
||||||
|
countVersions,
|
||||||
create,
|
create,
|
||||||
createGlobal,
|
createGlobal,
|
||||||
createGlobalVersion,
|
createGlobalVersion,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
beginTransaction,
|
beginTransaction,
|
||||||
commitTransaction,
|
commitTransaction,
|
||||||
count,
|
count,
|
||||||
|
countGlobalVersions,
|
||||||
|
countVersions,
|
||||||
create,
|
create,
|
||||||
createGlobal,
|
createGlobal,
|
||||||
createGlobalVersion,
|
createGlobalVersion,
|
||||||
@@ -127,6 +129,8 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
|||||||
convertPathToJSONTraversal,
|
convertPathToJSONTraversal,
|
||||||
count,
|
count,
|
||||||
countDistinct,
|
countDistinct,
|
||||||
|
countGlobalVersions,
|
||||||
|
countVersions,
|
||||||
create,
|
create,
|
||||||
createGlobal,
|
createGlobal,
|
||||||
createGlobalVersion,
|
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 { count } from './count.js'
|
||||||
|
export { countGlobalVersions } from './countGlobalVersions.js'
|
||||||
|
export { countVersions } from './countVersions.js'
|
||||||
export { create } from './create.js'
|
export { create } from './create.js'
|
||||||
export { createGlobal } from './createGlobal.js'
|
export { createGlobal } from './createGlobal.js'
|
||||||
export { createGlobalVersion } from './createGlobalVersion.js'
|
export { createGlobalVersion } from './createGlobalVersion.js'
|
||||||
|
|||||||
@@ -391,8 +391,8 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
|||||||
id,
|
id,
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
field: fieldName,
|
|
||||||
message: req.t('error:valueMustBeUnique'),
|
message: req.t('error:valueMustBeUnique'),
|
||||||
|
path: fieldName,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { DocumentTabConfig, DocumentTabProps } from 'payload'
|
import type { DocumentTabConfig, DocumentTabProps } from 'payload'
|
||||||
|
import type React from 'react'
|
||||||
|
|
||||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||||
import React, { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { DocumentTabLink } from './TabLink.js'
|
import { DocumentTabLink } from './TabLink.js'
|
||||||
@@ -59,17 +60,6 @@ export const DocumentTab: React.FC<
|
|||||||
})
|
})
|
||||||
: label
|
: label
|
||||||
|
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
|
||||||
importMap: payload.importMap,
|
|
||||||
serverProps: {
|
|
||||||
i18n,
|
|
||||||
payload,
|
|
||||||
permissions,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const mappedPin = createMappedComponent(Pill, undefined, Pill_Component, 'Pill')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentTabLink
|
<DocumentTabLink
|
||||||
adminRoute={routes.admin}
|
adminRoute={routes.admin}
|
||||||
@@ -82,12 +72,21 @@ export const DocumentTab: React.FC<
|
|||||||
>
|
>
|
||||||
<span className={`${baseClass}__label`}>
|
<span className={`${baseClass}__label`}>
|
||||||
{labelToRender}
|
{labelToRender}
|
||||||
{mappedPin && (
|
{Pill || Pill_Component ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|
||||||
<RenderComponent mappedComponent={mappedPin} />
|
<RenderServerComponent
|
||||||
|
Component={Pill}
|
||||||
|
Fallback={Pill_Component}
|
||||||
|
importMap={payload.importMap}
|
||||||
|
serverProps={{
|
||||||
|
i18n,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
</DocumentTabLink>
|
</DocumentTabLink>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
SanitizedGlobalConfig,
|
SanitizedGlobalConfig,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { getCustomViews } from './getCustomViews.js'
|
import { getCustomViews } from './getCustomViews.js'
|
||||||
@@ -80,33 +80,21 @@ export const DocumentTabs: React.FC<{
|
|||||||
const { path, tab } = CustomView
|
const { path, tab } = CustomView
|
||||||
|
|
||||||
if (tab.Component) {
|
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 (
|
return (
|
||||||
<RenderComponent
|
<RenderServerComponent
|
||||||
clientProps={{
|
clientProps={{
|
||||||
key: `tab-custom-${index}`,
|
|
||||||
path,
|
path,
|
||||||
}}
|
}}
|
||||||
|
Component={tab.Component}
|
||||||
|
importMap={payload.importMap}
|
||||||
key={`tab-custom-${index}`}
|
key={`tab-custom-${index}`}
|
||||||
mappedComponent={mappedTab}
|
serverProps={{
|
||||||
|
collectionConfig,
|
||||||
|
globalConfig,
|
||||||
|
i18n,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -121,6 +109,7 @@ export const DocumentTabs: React.FC<{
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -5,14 +5,11 @@ import React from 'react'
|
|||||||
import { baseClass } from '../../Tab/index.js'
|
import { baseClass } from '../../Tab/index.js'
|
||||||
|
|
||||||
export const VersionsPill: React.FC = () => {
|
export const VersionsPill: React.FC = () => {
|
||||||
const { versions } = useDocumentInfo()
|
const { versionCount } = useDocumentInfo()
|
||||||
|
|
||||||
// don't count snapshots
|
if (!versionCount) {
|
||||||
const totalVersions = versions?.docs.filter((version) => !version.snapshot).length || 0
|
|
||||||
|
|
||||||
if (!versions?.totalDocs) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span className={`${baseClass}__count`}>{totalVersions}</span>
|
return <span className={`${baseClass}__count`}>{versionCount}</span>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import { Gutter, RenderTitle } from '@payloadcms/ui'
|
import { Gutter, RenderTitle } from '@payloadcms/ui'
|
||||||
import React, { Fragment } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { DocumentTabs } from './Tabs/index.js'
|
import { DocumentTabs } from './Tabs/index.js'
|
||||||
@@ -16,32 +16,25 @@ const baseClass = `doc-header`
|
|||||||
|
|
||||||
export const DocumentHeader: React.FC<{
|
export const DocumentHeader: React.FC<{
|
||||||
collectionConfig?: SanitizedCollectionConfig
|
collectionConfig?: SanitizedCollectionConfig
|
||||||
customHeader?: React.ReactNode
|
|
||||||
globalConfig?: SanitizedGlobalConfig
|
globalConfig?: SanitizedGlobalConfig
|
||||||
hideTabs?: boolean
|
hideTabs?: boolean
|
||||||
i18n: I18n
|
i18n: I18n
|
||||||
payload: Payload
|
payload: Payload
|
||||||
permissions: Permissions
|
permissions: Permissions
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { collectionConfig, customHeader, globalConfig, hideTabs, i18n, payload, permissions } =
|
const { collectionConfig, globalConfig, hideTabs, i18n, payload, permissions } = props
|
||||||
props
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Gutter className={baseClass}>
|
<Gutter className={baseClass}>
|
||||||
{customHeader && customHeader}
|
<RenderTitle className={`${baseClass}__title`} />
|
||||||
{!customHeader && (
|
{!hideTabs && (
|
||||||
<Fragment>
|
<DocumentTabs
|
||||||
<RenderTitle className={`${baseClass}__title`} />
|
collectionConfig={collectionConfig}
|
||||||
{!hideTabs && (
|
globalConfig={globalConfig}
|
||||||
<DocumentTabs
|
i18n={i18n}
|
||||||
collectionConfig={collectionConfig}
|
payload={payload}
|
||||||
globalConfig={globalConfig}
|
permissions={permissions}
|
||||||
i18n={i18n}
|
/>
|
||||||
payload={payload}
|
|
||||||
permissions={permissions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)}
|
)}
|
||||||
</Gutter>
|
</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 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'
|
import React from 'react'
|
||||||
|
|
||||||
export const Logo: React.FC<ServerProps> = (props) => {
|
export const Logo: React.FC<ServerProps> = (props) => {
|
||||||
@@ -16,20 +17,20 @@ export const Logo: React.FC<ServerProps> = (props) => {
|
|||||||
} = {},
|
} = {},
|
||||||
} = payload.config
|
} = payload.config
|
||||||
|
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
return (
|
||||||
importMap: payload.importMap,
|
<RenderServerComponent
|
||||||
serverProps: {
|
Component={CustomLogo}
|
||||||
i18n,
|
Fallback={PayloadLogo}
|
||||||
locale,
|
importMap={payload.importMap}
|
||||||
params,
|
serverProps={{
|
||||||
payload,
|
i18n,
|
||||||
permissions,
|
locale,
|
||||||
searchParams,
|
params,
|
||||||
user,
|
payload,
|
||||||
},
|
permissions,
|
||||||
})
|
searchParams,
|
||||||
|
user,
|
||||||
const mappedCustomLogo = createMappedComponent(CustomLogo, undefined, PayloadLogo, 'CustomLogo')
|
}}
|
||||||
|
/>
|
||||||
return <RenderComponent mappedComponent={mappedCustomLogo} />
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,23 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
import type { groupNavItems } from '@payloadcms/ui/shared'
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import {
|
import { NavGroup, useConfig, useNav, useTranslation } from '@payloadcms/ui'
|
||||||
NavGroup,
|
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared'
|
||||||
useAuth,
|
|
||||||
useConfig,
|
|
||||||
useEntityVisibility,
|
|
||||||
useNav,
|
|
||||||
useTranslation,
|
|
||||||
} from '@payloadcms/ui'
|
|
||||||
import { EntityType, formatAdminURL, groupNavItems } from '@payloadcms/ui/shared'
|
|
||||||
import LinkWithDefault from 'next/link.js'
|
import LinkWithDefault from 'next/link.js'
|
||||||
import { usePathname } from 'next/navigation.js'
|
import { usePathname } from 'next/navigation.js'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
const baseClass = 'nav'
|
const baseClass = 'nav'
|
||||||
|
|
||||||
export const DefaultNavClient: React.FC = () => {
|
export const DefaultNavClient: React.FC<{
|
||||||
const { permissions } = useAuth()
|
groups: ReturnType<typeof groupNavItems>
|
||||||
const { isEntityVisible } = useEntityVisibility()
|
}> = ({ groups }) => {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
config: {
|
config: {
|
||||||
collections,
|
|
||||||
globals,
|
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
},
|
},
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
@@ -34,53 +25,23 @@ export const DefaultNavClient: React.FC = () => {
|
|||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
const { navOpen } = useNav()
|
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 (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{groups.map(({ entities, label }, key) => {
|
{groups.map(({ entities, label }, key) => {
|
||||||
return (
|
return (
|
||||||
<NavGroup key={key} label={label}>
|
<NavGroup key={key} label={label}>
|
||||||
{entities.map(({ type, entity }, i) => {
|
{entities.map(({ slug, type, label }, i) => {
|
||||||
let entityLabel: string
|
|
||||||
let href: string
|
let href: string
|
||||||
let id: string
|
let id: string
|
||||||
|
|
||||||
if (type === EntityType.collection) {
|
if (type === EntityType.collection) {
|
||||||
href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` })
|
href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })
|
||||||
entityLabel = getTranslation(entity.labels.plural, i18n)
|
id = `nav-${slug}`
|
||||||
id = `nav-${entity.slug}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === EntityType.global) {
|
if (type === EntityType.global) {
|
||||||
href = formatAdminURL({ adminRoute, path: `/globals/${entity.slug}` })
|
href = formatAdminURL({ adminRoute, path: `/globals/${slug}` })
|
||||||
entityLabel = getTranslation(entity.label, i18n)
|
id = `nav-global-${slug}`
|
||||||
id = `nav-global-${entity.slug}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Link = (LinkWithDefault.default ||
|
const Link = (LinkWithDefault.default ||
|
||||||
@@ -102,7 +63,7 @@ export const DefaultNavClient: React.FC = () => {
|
|||||||
tabIndex={!navOpen ? -1 : undefined}
|
tabIndex={!navOpen ? -1 : undefined}
|
||||||
>
|
>
|
||||||
{activeCollection && <div className={`${baseClass}__link-indicator`} />}
|
{activeCollection && <div className={`${baseClass}__link-indicator`} />}
|
||||||
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
|
<span className={`${baseClass}__link-label`}>{getTranslation(label, i18n)}</span>
|
||||||
</LinkElement>
|
</LinkElement>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||||
import type { ServerProps } from 'payload'
|
import type { ServerProps } from 'payload'
|
||||||
|
|
||||||
import { Logout } from '@payloadcms/ui'
|
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 React from 'react'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
@@ -15,7 +17,7 @@ import { DefaultNavClient } from './index.client.js'
|
|||||||
export type NavProps = ServerProps
|
export type NavProps = ServerProps
|
||||||
|
|
||||||
export const DefaultNav: React.FC<NavProps> = (props) => {
|
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) {
|
if (!payload?.config) {
|
||||||
return null
|
return null
|
||||||
@@ -23,44 +25,82 @@ export const DefaultNav: React.FC<NavProps> = (props) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
admin: {
|
admin: {
|
||||||
components: { afterNavLinks, beforeNavLinks },
|
components: { afterNavLinks, beforeNavLinks, logout },
|
||||||
},
|
},
|
||||||
|
collections,
|
||||||
|
globals,
|
||||||
} = payload.config
|
} = payload.config
|
||||||
|
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
const groups = groupNavItems(
|
||||||
importMap: payload.importMap,
|
[
|
||||||
serverProps: {
|
...collections
|
||||||
i18n,
|
.filter(({ slug }) => visibleEntities.collections.includes(slug))
|
||||||
locale,
|
.map(
|
||||||
params,
|
(collection) =>
|
||||||
payload,
|
({
|
||||||
permissions,
|
type: EntityType.collection,
|
||||||
searchParams,
|
entity: collection,
|
||||||
user,
|
}) satisfies EntityToGroup,
|
||||||
},
|
),
|
||||||
})
|
...globals
|
||||||
|
.filter(({ slug }) => visibleEntities.globals.includes(slug))
|
||||||
const mappedBeforeNavLinks = createMappedComponent(
|
.map(
|
||||||
beforeNavLinks,
|
(global) =>
|
||||||
undefined,
|
({
|
||||||
undefined,
|
type: EntityType.global,
|
||||||
'beforeNavLinks',
|
entity: global,
|
||||||
)
|
}) satisfies EntityToGroup,
|
||||||
const mappedAfterNavLinks = createMappedComponent(
|
),
|
||||||
afterNavLinks,
|
],
|
||||||
undefined,
|
permissions,
|
||||||
undefined,
|
i18n,
|
||||||
'afterNavLinks',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavWrapper baseClass={baseClass}>
|
<NavWrapper baseClass={baseClass}>
|
||||||
<nav className={`${baseClass}__wrap`}>
|
<nav className={`${baseClass}__wrap`}>
|
||||||
<RenderComponent mappedComponent={mappedBeforeNavLinks} />
|
<RenderServerComponent
|
||||||
<DefaultNavClient />
|
Component={beforeNavLinks}
|
||||||
<RenderComponent mappedComponent={mappedAfterNavLinks} />
|
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`}>
|
<div className={`${baseClass}__controls`}>
|
||||||
<Logout />
|
<RenderServerComponent
|
||||||
|
Component={logout?.Button}
|
||||||
|
Fallback={Logout}
|
||||||
|
importMap={payload.importMap}
|
||||||
|
serverProps={{
|
||||||
|
i18n,
|
||||||
|
locale,
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
searchParams,
|
||||||
|
user,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div className={`${baseClass}__header`}>
|
<div className={`${baseClass}__header`}>
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { metadata, RootLayout } from '../layouts/Root/index.js'
|
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 { addDataAndFileToRequest } from '../utilities/addDataAndFileToRequest.js'
|
||||||
export { addLocalesToRequestFromData, sanitizeLocales } from '../utilities/addLocalesToRequest.js'
|
export { addLocalesToRequestFromData, sanitizeLocales } from '../utilities/addLocalesToRequest.js'
|
||||||
export { createPayloadRequest } from '../utilities/createPayloadRequest.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 { NotFoundPage } from '../views/NotFound/index.js'
|
||||||
export { generatePageMetadata, type GenerateViewMetadata, RootPage } from '../views/Root/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 }) => {
|
export const processMultipart: ProcessMultipart = async ({ options, request }) => {
|
||||||
let parsingRequest = true
|
let parsingRequest = true
|
||||||
|
|
||||||
|
let shouldAbortProccessing = false
|
||||||
let fileCount = 0
|
let fileCount = 0
|
||||||
let filesCompleted = 0
|
let filesCompleted = 0
|
||||||
let allFilesHaveResolved: (value?: unknown) => void
|
let allFilesHaveResolved: (value?: unknown) => void
|
||||||
@@ -42,14 +43,16 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
|||||||
headersObject[name] = value
|
headersObject[name] = value
|
||||||
})
|
})
|
||||||
|
|
||||||
function abortAndDestroyFile(file: Readable, err: APIError) {
|
const reader = request.body.getReader()
|
||||||
file.destroy()
|
|
||||||
parsingRequest = false
|
|
||||||
failedResolvingFiles(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const busboy = Busboy({ ...options, headers: headersObject })
|
const busboy = Busboy({ ...options, headers: headersObject })
|
||||||
|
|
||||||
|
function abortAndDestroyFile(file: Readable, err: APIError) {
|
||||||
|
file.destroy()
|
||||||
|
shouldAbortProccessing = true
|
||||||
|
failedResolvingFiles(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Build multipart req.body fields
|
// Build multipart req.body fields
|
||||||
busboy.on('field', (field, val) => {
|
busboy.on('field', (field, val) => {
|
||||||
result.fields = buildFields(result.fields, field, val)
|
result.fields = buildFields(result.fields, field, val)
|
||||||
@@ -136,7 +139,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
|||||||
mimetype: mime,
|
mimetype: mime,
|
||||||
size,
|
size,
|
||||||
tempFilePath: getFilePath(),
|
tempFilePath: getFilePath(),
|
||||||
truncated: Boolean('truncated' in file && file.truncated),
|
truncated: Boolean('truncated' in file && file.truncated) || false,
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
),
|
),
|
||||||
@@ -164,8 +167,6 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
|||||||
uploadTimer.set()
|
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 () => {
|
busboy.on('finish', async () => {
|
||||||
debugLog(options, `Busboy finished parsing request.`)
|
debugLog(options, `Busboy finished parsing request.`)
|
||||||
if (options.parseNested) {
|
if (options.parseNested) {
|
||||||
@@ -190,14 +191,10 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
|||||||
'error',
|
'error',
|
||||||
(err = new APIError('Busboy error parsing multipart request', httpStatus.BAD_REQUEST)) => {
|
(err = new APIError('Busboy error parsing multipart request', httpStatus.BAD_REQUEST)) => {
|
||||||
debugLog(options, `Busboy error`)
|
debugLog(options, `Busboy error`)
|
||||||
parsingRequest = false
|
|
||||||
throw err
|
throw err
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const reader = request.body.getReader()
|
|
||||||
|
|
||||||
// Start parsing request
|
|
||||||
while (parsingRequest) {
|
while (parsingRequest) {
|
||||||
const { done, value } = await reader.read()
|
const { done, value } = await reader.read()
|
||||||
|
|
||||||
@@ -205,7 +202,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
|||||||
parsingRequest = false
|
parsingRequest = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value) {
|
if (value && !shouldAbortProccessing) {
|
||||||
busboy.write(value)
|
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 { 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 { rtlLanguages } from '@payloadcms/translations'
|
||||||
import { RootProvider } from '@payloadcms/ui'
|
import { RootProvider } from '@payloadcms/ui'
|
||||||
import '@payloadcms/ui/scss/app.scss'
|
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 { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
|
||||||
import { checkDependencies, parseCookies } from 'payload'
|
import { checkDependencies, parseCookies } from 'payload'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { getClientConfig } from '../../utilities/getClientConfig.js'
|
||||||
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||||
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
|
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
|
||||||
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
|
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
|
||||||
import { initReq } from '../../utilities/initReq.js'
|
import { initReq } from '../../utilities/initReq.js'
|
||||||
import { DefaultEditView } from '../../views/Edit/Default/index.js'
|
import { NestProviders } from './NestProviders.js'
|
||||||
import { DefaultListView } from '../../views/List/Default/index.js'
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
description: 'Generated by Next.js',
|
description: 'Generated by Next.js',
|
||||||
@@ -41,11 +40,12 @@ let checkedDependencies = false
|
|||||||
export const RootLayout = async ({
|
export const RootLayout = async ({
|
||||||
children,
|
children,
|
||||||
config: configPromise,
|
config: configPromise,
|
||||||
importMap,
|
serverFunction,
|
||||||
}: {
|
}: {
|
||||||
readonly children: React.ReactNode
|
readonly children: React.ReactNode
|
||||||
readonly config: Promise<SanitizedConfig>
|
readonly config: Promise<SanitizedConfig>
|
||||||
readonly importMap: ImportMap
|
readonly importMap: ImportMap
|
||||||
|
readonly serverFunction: ServerFunctionClient
|
||||||
}) => {
|
}) => {
|
||||||
if (
|
if (
|
||||||
process.env.NODE_ENV !== 'production' &&
|
process.env.NODE_ENV !== 'production' &&
|
||||||
@@ -103,16 +103,6 @@ export const RootLayout = async ({
|
|||||||
|
|
||||||
const { i18n, permissions, req, user } = await initReq(config)
|
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)
|
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
|
||||||
? 'RTL'
|
? 'RTL'
|
||||||
: 'LTR'
|
: 'LTR'
|
||||||
@@ -174,23 +164,39 @@ export const RootLayout = async ({
|
|||||||
|
|
||||||
const isNavOpen = navPreferences?.value?.open ?? true
|
const isNavOpen = navPreferences?.value?.open ?? true
|
||||||
|
|
||||||
|
const clientConfig = await getClientConfig({
|
||||||
|
config,
|
||||||
|
i18n,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html data-theme={theme} dir={dir} lang={languageCode}>
|
<html data-theme={theme} dir={dir} lang={languageCode}>
|
||||||
<body>
|
<body>
|
||||||
<RootProvider
|
<RootProvider
|
||||||
config={clientConfig}
|
config={clientConfig}
|
||||||
dateFNSKey={i18n.dateFNSKey}
|
dateFNSKey={i18n.dateFNSKey}
|
||||||
fallbackLang={clientConfig.i18n.fallbackLanguage}
|
fallbackLang={config.i18n.fallbackLanguage}
|
||||||
isNavOpen={isNavOpen}
|
isNavOpen={isNavOpen}
|
||||||
languageCode={languageCode}
|
languageCode={languageCode}
|
||||||
languageOptions={languageOptions}
|
languageOptions={languageOptions}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
|
serverFunction={serverFunction}
|
||||||
switchLanguageServerAction={switchLanguageServerAction}
|
switchLanguageServerAction={switchLanguageServerAction}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
translations={i18n.translations}
|
translations={i18n.translations}
|
||||||
user={user}
|
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>
|
</RootProvider>
|
||||||
<div id="portal" />
|
<div id="portal" />
|
||||||
</body>
|
</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 { resetPassword } from './auth/resetPassword.js'
|
||||||
import { unlock } from './auth/unlock.js'
|
import { unlock } from './auth/unlock.js'
|
||||||
import { verifyEmail } from './auth/verifyEmail.js'
|
import { verifyEmail } from './auth/verifyEmail.js'
|
||||||
import { buildFormState } from './buildFormState.js'
|
|
||||||
import { endpointsAreDisabled } from './checkEndpoints.js'
|
import { endpointsAreDisabled } from './checkEndpoints.js'
|
||||||
import { count } from './collections/count.js'
|
import { count } from './collections/count.js'
|
||||||
import { create } from './collections/create.js'
|
import { create } from './collections/create.js'
|
||||||
@@ -110,9 +109,6 @@ const endpoints = {
|
|||||||
access,
|
access,
|
||||||
og: generateOGImage,
|
og: generateOGImage,
|
||||||
},
|
},
|
||||||
POST: {
|
|
||||||
'form-state': buildFormState,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,10 +571,6 @@ export const POST =
|
|||||||
res = new Response('Route Not Found', { status: 404 })
|
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) {
|
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'
|
import React from 'react'
|
||||||
|
|
||||||
export const OGImage: React.FC<{
|
export const OGImage: React.FC<{
|
||||||
description?: string
|
description?: string
|
||||||
|
Fallback: React.ComponentType
|
||||||
fontFamily?: string
|
fontFamily?: string
|
||||||
Icon: MappedComponent
|
Icon: PayloadComponent
|
||||||
|
importMap: ImportMap
|
||||||
leader?: string
|
leader?: string
|
||||||
title?: string
|
title?: string
|
||||||
}> = ({ description, fontFamily = 'Arial, sans-serif', Icon, leader, title }) => {
|
}> = ({
|
||||||
|
description,
|
||||||
|
Fallback,
|
||||||
|
fontFamily = 'Arial, sans-serif',
|
||||||
|
Icon,
|
||||||
|
importMap,
|
||||||
|
leader,
|
||||||
|
title,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -85,11 +95,13 @@ export const OGImage: React.FC<{
|
|||||||
width: '38px',
|
width: '38px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RenderComponent
|
<RenderServerComponent
|
||||||
clientProps={{
|
clientProps={{
|
||||||
fill: 'white',
|
fill: 'white',
|
||||||
}}
|
}}
|
||||||
mappedComponent={Icon}
|
Component={Icon}
|
||||||
|
Fallback={Fallback}
|
||||||
|
importMap={importMap}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PayloadRequest } from 'payload'
|
import type { PayloadRequest } from 'payload'
|
||||||
|
|
||||||
import { getCreateMappedComponent, PayloadIcon } from '@payloadcms/ui/shared'
|
import { PayloadIcon } from '@payloadcms/ui/shared'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
import { ImageResponse } from 'next/og.js'
|
import { ImageResponse } from 'next/og.js'
|
||||||
import { NextResponse } from 'next/server.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 leader = hasLeader ? searchParams.get('leader')?.slice(0, 100).replace('-', ' ') : ''
|
||||||
const description = searchParams.has('description') ? searchParams.get('description') : ''
|
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
|
let fontData
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -62,8 +50,10 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
|
|||||||
(
|
(
|
||||||
<OGImage
|
<OGImage
|
||||||
description={description}
|
description={description}
|
||||||
|
Fallback={PayloadIcon}
|
||||||
fontFamily={fontFamily}
|
fontFamily={fontFamily}
|
||||||
Icon={mappedIcon}
|
Icon={config.admin?.components?.graphics?.Icon}
|
||||||
|
importMap={req.payload.importMap}
|
||||||
leader={leader}
|
leader={leader}
|
||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,73 +1,12 @@
|
|||||||
import type { Collection, ErrorResult, PayloadRequest, SanitizedConfig } from 'payload'
|
import type { Collection, ErrorResult, PayloadRequest, SanitizedConfig } from 'payload'
|
||||||
|
|
||||||
import httpStatus from 'http-status'
|
import httpStatus from 'http-status'
|
||||||
import { APIError, APIErrorName, ValidationErrorName } from 'payload'
|
import { APIError, formatErrors } from 'payload'
|
||||||
|
|
||||||
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||||
import { headersWithCors } from '../../utilities/headersWithCors.js'
|
import { headersWithCors } from '../../utilities/headersWithCors.js'
|
||||||
import { mergeHeaders } from '../../utilities/mergeHeaders.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 ({
|
export const routeError = async ({
|
||||||
collection,
|
collection,
|
||||||
config: configArg,
|
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 {
|
||||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
ActionsProvider,
|
||||||
|
AppHeader,
|
||||||
|
BulkUploadProvider,
|
||||||
|
EntityVisibilityProvider,
|
||||||
|
NavToggler,
|
||||||
|
} from '@payloadcms/ui'
|
||||||
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { DefaultNav } from '../../elements/Nav/index.js'
|
import { DefaultNav } from '../../elements/Nav/index.js'
|
||||||
@@ -14,6 +20,7 @@ const baseClass = 'template-default'
|
|||||||
export type DefaultTemplateProps = {
|
export type DefaultTemplateProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
|
viewActions?: CustomComponent[]
|
||||||
visibleEntities: VisibleEntities
|
visibleEntities: VisibleEntities
|
||||||
} & ServerProps
|
} & ServerProps
|
||||||
|
|
||||||
@@ -27,10 +34,13 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
|||||||
permissions,
|
permissions,
|
||||||
searchParams,
|
searchParams,
|
||||||
user,
|
user,
|
||||||
|
viewActions,
|
||||||
visibleEntities,
|
visibleEntities,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
admin: {
|
admin: {
|
||||||
|
avatar,
|
||||||
|
components,
|
||||||
components: { header: CustomHeader, Nav: CustomNav } = {
|
components: { header: CustomHeader, Nav: CustomNav } = {
|
||||||
header: undefined,
|
header: undefined,
|
||||||
Nav: undefined,
|
Nav: undefined,
|
||||||
@@ -38,54 +48,98 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
|||||||
} = {},
|
} = {},
|
||||||
} = payload.config || {}
|
} = payload.config || {}
|
||||||
|
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
const { Actions } = React.useMemo<{
|
||||||
importMap: payload.importMap,
|
Actions: Record<string, React.ReactNode>
|
||||||
serverProps: {
|
}>(() => {
|
||||||
i18n,
|
return {
|
||||||
locale,
|
Actions: viewActions
|
||||||
params,
|
? viewActions.reduce((acc, action) => {
|
||||||
payload,
|
if (action) {
|
||||||
permissions,
|
if (typeof action === 'object') {
|
||||||
searchParams,
|
acc[action.path] = (
|
||||||
user,
|
<RenderServerComponent Component={action} importMap={payload.importMap} />
|
||||||
},
|
)
|
||||||
})
|
} else {
|
||||||
|
acc[action] = (
|
||||||
|
<RenderServerComponent Component={action} importMap={payload.importMap} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const MappedDefaultNav: MappedComponent = createMappedComponent(
|
return acc
|
||||||
CustomNav,
|
}, {})
|
||||||
undefined,
|
: undefined,
|
||||||
DefaultNav,
|
}
|
||||||
'CustomNav',
|
}, [viewActions, payload])
|
||||||
)
|
|
||||||
|
|
||||||
const MappedCustomHeader = createMappedComponent(
|
|
||||||
CustomHeader,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
'CustomHeader',
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityVisibilityProvider visibleEntities={visibleEntities}>
|
<EntityVisibilityProvider visibleEntities={visibleEntities}>
|
||||||
<BulkUploadProvider>
|
<BulkUploadProvider>
|
||||||
<RenderComponent mappedComponent={MappedCustomHeader} />
|
<ActionsProvider Actions={Actions}>
|
||||||
<div style={{ position: 'relative' }}>
|
<RenderServerComponent
|
||||||
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
|
clientProps={{ clientProps: { visibleEntities } }}
|
||||||
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
|
Component={CustomHeader}
|
||||||
<NavToggler className={`${baseClass}__nav-toggler`}>
|
importMap={payload.importMap}
|
||||||
<NavHamburger />
|
serverProps={{
|
||||||
</NavToggler>
|
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>
|
</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>
|
</div>
|
||||||
<Wrapper baseClass={baseClass} className={className}>
|
</ActionsProvider>
|
||||||
<RenderComponent mappedComponent={MappedDefaultNav} />
|
|
||||||
|
|
||||||
<div className={`${baseClass}__wrap`}>
|
|
||||||
<AppHeader />
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</Wrapper>
|
|
||||||
</div>
|
|
||||||
</BulkUploadProvider>
|
</BulkUploadProvider>
|
||||||
</EntityVisibilityProvider>
|
</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 { findLocaleFromCode } from '@payloadcms/ui/shared'
|
||||||
import { headers as getHeaders } from 'next/headers.js'
|
import { headers as getHeaders } from 'next/headers.js'
|
||||||
@@ -47,13 +48,13 @@ export const initPage = async ({
|
|||||||
req: {
|
req: {
|
||||||
headers,
|
headers,
|
||||||
host: headers.get('host'),
|
host: headers.get('host'),
|
||||||
i18n,
|
i18n: i18n as I18n,
|
||||||
query: qs.parse(queryString, {
|
query: qs.parse(queryString, {
|
||||||
depth: 10,
|
depth: 10,
|
||||||
ignoreQueryPrefix: true,
|
ignoreQueryPrefix: true,
|
||||||
}),
|
}),
|
||||||
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
|
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
|
||||||
} as PayloadRequest,
|
},
|
||||||
},
|
},
|
||||||
payload,
|
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 type { PayloadRequest, Permissions, SanitizedConfig, User } from 'payload'
|
||||||
|
|
||||||
import { initI18n } from '@payloadcms/translations'
|
import { initI18n } from '@payloadcms/translations'
|
||||||
@@ -16,7 +16,10 @@ type Result = {
|
|||||||
user: User
|
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 payload = await getPayloadHMR({ config })
|
||||||
|
|
||||||
const headers = await getHeaders()
|
const headers = await getHeaders()
|
||||||
@@ -40,9 +43,9 @@ export const initReq = cache(async function (config: SanitizedConfig): Promise<R
|
|||||||
req: {
|
req: {
|
||||||
headers,
|
headers,
|
||||||
host: headers.get('host'),
|
host: headers.get('host'),
|
||||||
i18n,
|
i18n: i18n as I18n,
|
||||||
url: `${payload.config.serverURL}`,
|
url: `${payload.config.serverURL}`,
|
||||||
} as PayloadRequest,
|
},
|
||||||
},
|
},
|
||||||
payload,
|
payload,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const LocaleSelector: React.FC<{
|
|||||||
<SelectField
|
<SelectField
|
||||||
field={{
|
field={{
|
||||||
name: 'locale',
|
name: 'locale',
|
||||||
_path: 'locale',
|
|
||||||
label: t('general:locale'),
|
label: t('general:locale'),
|
||||||
options: localeOptions,
|
options: localeOptions,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Gutter,
|
Gutter,
|
||||||
MinimizeMaximizeIcon,
|
MinimizeMaximizeIcon,
|
||||||
NumberField,
|
NumberField,
|
||||||
SetViewActions,
|
SetDocumentStepNav,
|
||||||
useConfig,
|
useConfig,
|
||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
useLocale,
|
useLocale,
|
||||||
@@ -19,7 +19,6 @@ import { useSearchParams } from 'next/navigation.js'
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { LocaleSelector } from './LocaleSelector/index.js'
|
import { LocaleSelector } from './LocaleSelector/index.js'
|
||||||
import { RenderJSON } from './RenderJSON/index.js'
|
import { RenderJSON } from './RenderJSON/index.js'
|
||||||
@@ -42,8 +41,8 @@ export const APIViewClient: React.FC = () => {
|
|||||||
getEntityConfig,
|
getEntityConfig,
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
|
||||||
const collectionClientConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||||
const globalClientConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||||
|
|
||||||
const localeOptions =
|
const localeOptions =
|
||||||
localization &&
|
localization &&
|
||||||
@@ -52,13 +51,13 @@ export const APIViewClient: React.FC = () => {
|
|||||||
let draftsEnabled: boolean = false
|
let draftsEnabled: boolean = false
|
||||||
let docEndpoint: string = ''
|
let docEndpoint: string = ''
|
||||||
|
|
||||||
if (collectionClientConfig) {
|
if (collectionConfig) {
|
||||||
draftsEnabled = Boolean(collectionClientConfig.versions?.drafts)
|
draftsEnabled = Boolean(collectionConfig.versions?.drafts)
|
||||||
docEndpoint = `/${collectionSlug}/${id}`
|
docEndpoint = `/${collectionSlug}/${id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (globalClientConfig) {
|
if (globalConfig) {
|
||||||
draftsEnabled = Boolean(globalClientConfig.versions?.drafts)
|
draftsEnabled = Boolean(globalConfig.versions?.drafts)
|
||||||
docEndpoint = `/globals/${globalSlug}`
|
docEndpoint = `/globals/${globalSlug}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,19 +110,13 @@ export const APIViewClient: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<SetDocumentStepNav
|
<SetDocumentStepNav
|
||||||
collectionSlug={collectionSlug}
|
collectionSlug={collectionSlug}
|
||||||
globalLabel={globalClientConfig?.label}
|
globalLabel={globalConfig?.label}
|
||||||
globalSlug={globalSlug}
|
globalSlug={globalSlug}
|
||||||
id={id}
|
id={id}
|
||||||
pluralLabel={collectionClientConfig ? collectionClientConfig?.labels?.plural : undefined}
|
pluralLabel={collectionConfig ? collectionConfig?.labels?.plural : undefined}
|
||||||
useAsTitle={collectionClientConfig ? collectionClientConfig?.admin?.useAsTitle : undefined}
|
useAsTitle={collectionConfig ? collectionConfig?.admin?.useAsTitle : undefined}
|
||||||
view="API"
|
view="API"
|
||||||
/>
|
/>
|
||||||
<SetViewActions
|
|
||||||
actions={
|
|
||||||
(collectionClientConfig || globalClientConfig)?.admin?.components?.views?.edit?.api
|
|
||||||
?.actions
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className={`${baseClass}__configuration`}>
|
<div className={`${baseClass}__configuration`}>
|
||||||
<div className={`${baseClass}__api-url`}>
|
<div className={`${baseClass}__api-url`}>
|
||||||
<span className={`${baseClass}__label`}>
|
<span className={`${baseClass}__label`}>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const Settings: React.FC<{
|
|||||||
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||||
<h3>{i18n.t('general:payloadSettings')}</h3>
|
<h3>{i18n.t('general:payloadSettings')}</h3>
|
||||||
<div className={`${baseClass}__language`}>
|
<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} />
|
<LanguageSelector languageOptions={languageOptions} />
|
||||||
</div>
|
</div>
|
||||||
{theme === 'all' && <ToggleTheme />}
|
{theme === 'all' && <ToggleTheme />}
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import type { AdminViewProps } from 'payload'
|
import type { AdminViewProps } from 'payload'
|
||||||
|
|
||||||
import {
|
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||||
DocumentInfoProvider,
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||||
EditDepthProvider,
|
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||||
HydrateAuthProvider,
|
|
||||||
RenderComponent,
|
|
||||||
} from '@payloadcms/ui'
|
|
||||||
import { getCreateMappedComponent } from '@payloadcms/ui/shared'
|
|
||||||
import { notFound } from 'next/navigation.js'
|
import { notFound } from 'next/navigation.js'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
|
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
|
||||||
|
import { getDocPreferences } from '../Document/getDocPreferences.js'
|
||||||
import { getDocumentData } from '../Document/getDocumentData.js'
|
import { getDocumentData } from '../Document/getDocumentData.js'
|
||||||
import { getDocumentPermissions } from '../Document/getDocumentPermissions.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 { EditView } from '../Edit/index.js'
|
||||||
import { AccountClient } from './index.client.js'
|
import { AccountClient } from './index.client.js'
|
||||||
import { Settings } from './Settings/index.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)
|
const collectionConfig = config.collections.find((collection) => collection.slug === userSlug)
|
||||||
|
|
||||||
if (collectionConfig && user?.id) {
|
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 } =
|
const { docPermissions, hasPublishPermission, hasSavePermission } =
|
||||||
await getDocumentPermissions({
|
await getDocumentPermissions({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
data: user,
|
data,
|
||||||
req,
|
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,
|
id: user.id,
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
locale,
|
isEditing: true,
|
||||||
req,
|
payload: req.payload,
|
||||||
|
user,
|
||||||
})
|
})
|
||||||
|
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
// Get all versions required for UI
|
||||||
importMap: payload.importMap,
|
const { hasPublishedDoc, mostRecentVersionIsAutosaved, unpublishedVersionCount, versionCount } =
|
||||||
serverProps: {
|
await getVersions({
|
||||||
i18n,
|
id: user.id,
|
||||||
initPageResult,
|
collectionConfig,
|
||||||
locale,
|
docPermissions,
|
||||||
params,
|
locale: locale?.code,
|
||||||
payload,
|
payload,
|
||||||
permissions,
|
|
||||||
routeSegments: [],
|
|
||||||
searchParams,
|
|
||||||
user,
|
user,
|
||||||
},
|
})
|
||||||
})
|
|
||||||
|
|
||||||
const mappedAccountComponent = createMappedComponent(
|
|
||||||
CustomAccountComponent?.Component,
|
|
||||||
undefined,
|
|
||||||
EditView,
|
|
||||||
'CustomAccountComponent.Component',
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentInfoProvider
|
<DocumentInfoProvider
|
||||||
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} theme={theme} />}
|
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} theme={theme} />}
|
||||||
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
||||||
collectionSlug={userSlug}
|
collectionSlug={userSlug}
|
||||||
|
currentEditor={currentEditor}
|
||||||
docPermissions={docPermissions}
|
docPermissions={docPermissions}
|
||||||
|
hasPublishedDoc={hasPublishedDoc}
|
||||||
hasPublishPermission={hasPublishPermission}
|
hasPublishPermission={hasPublishPermission}
|
||||||
hasSavePermission={hasSavePermission}
|
hasSavePermission={hasSavePermission}
|
||||||
id={user?.id}
|
id={user?.id}
|
||||||
initialData={data}
|
initialData={data}
|
||||||
initialState={formState}
|
initialState={formState}
|
||||||
isEditing
|
isEditing
|
||||||
|
isLocked={isLocked}
|
||||||
|
lastUpdateTime={lastUpdateTime}
|
||||||
|
mostRecentVersionIsAutosaved={mostRecentVersionIsAutosaved}
|
||||||
|
unpublishedVersionCount={unpublishedVersionCount}
|
||||||
|
versionCount={versionCount}
|
||||||
>
|
>
|
||||||
<EditDepthProvider depth={1}>
|
<EditDepthProvider>
|
||||||
<DocumentHeader
|
<DocumentHeader
|
||||||
collectionConfig={collectionConfig}
|
collectionConfig={collectionConfig}
|
||||||
hideTabs
|
hideTabs
|
||||||
@@ -109,7 +142,22 @@ export const Account: React.FC<AdminViewProps> = async ({
|
|||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
/>
|
/>
|
||||||
<HydrateAuthProvider 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 />
|
<AccountClient />
|
||||||
</EditDepthProvider>
|
</EditDepthProvider>
|
||||||
</DocumentInfoProvider>
|
</DocumentInfoProvider>
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FormProps, UserWithToken } from '@payloadcms/ui'
|
import type { FormProps, UserWithToken } from '@payloadcms/ui'
|
||||||
import type { ClientCollectionConfig, FormState, LoginWithUsernameOptions } from 'payload'
|
import type {
|
||||||
|
ClientCollectionConfig,
|
||||||
|
DocumentPermissions,
|
||||||
|
DocumentPreferences,
|
||||||
|
FormState,
|
||||||
|
LoginWithUsernameOptions,
|
||||||
|
} from 'payload'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConfirmPasswordField,
|
ConfirmPasswordField,
|
||||||
|
EmailAndUsernameFields,
|
||||||
Form,
|
Form,
|
||||||
FormSubmit,
|
FormSubmit,
|
||||||
PasswordField,
|
PasswordField,
|
||||||
RenderFields,
|
RenderFields,
|
||||||
useAuth,
|
useAuth,
|
||||||
useConfig,
|
useConfig,
|
||||||
|
useServerFunctions,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import { getFormState } from '@payloadcms/ui/shared'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { RenderEmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js'
|
|
||||||
|
|
||||||
export const CreateFirstUserClient: React.FC<{
|
export const CreateFirstUserClient: React.FC<{
|
||||||
|
docPermissions: DocumentPermissions
|
||||||
|
docPreferences: DocumentPreferences
|
||||||
initialState: FormState
|
initialState: FormState
|
||||||
loginWithUsername?: false | LoginWithUsernameOptions
|
loginWithUsername?: false | LoginWithUsernameOptions
|
||||||
userSlug: string
|
userSlug: string
|
||||||
}> = ({ initialState, loginWithUsername, userSlug }) => {
|
}> = ({ docPermissions, docPreferences, initialState, loginWithUsername, userSlug }) => {
|
||||||
const {
|
const {
|
||||||
config: {
|
config: {
|
||||||
routes: { admin, api: apiRoute },
|
routes: { admin, api: apiRoute },
|
||||||
@@ -30,6 +37,8 @@ export const CreateFirstUserClient: React.FC<{
|
|||||||
getEntityConfig,
|
getEntityConfig,
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
|
||||||
|
const { getFormState } = useServerFunctions()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { setUser } = useAuth()
|
const { setUser } = useAuth()
|
||||||
|
|
||||||
@@ -38,18 +47,17 @@ export const CreateFirstUserClient: React.FC<{
|
|||||||
const onChange: FormProps['onChange'][0] = React.useCallback(
|
const onChange: FormProps['onChange'][0] = React.useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
const { state } = await getFormState({
|
const { state } = await getFormState({
|
||||||
apiRoute,
|
collectionSlug: userSlug,
|
||||||
body: {
|
docPermissions,
|
||||||
collectionSlug: userSlug,
|
docPreferences,
|
||||||
formState: prevFormState,
|
formState: prevFormState,
|
||||||
operation: 'create',
|
operation: 'create',
|
||||||
schemaPath: `_${userSlug}.auth`,
|
schemaPath: `_${userSlug}.auth`,
|
||||||
},
|
|
||||||
serverURL,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
[apiRoute, userSlug, serverURL],
|
[userSlug, getFormState, docPermissions, docPreferences],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFirstRegister = (data: UserWithToken) => {
|
const handleFirstRegister = (data: UserWithToken) => {
|
||||||
@@ -66,14 +74,15 @@ export const CreateFirstUserClient: React.FC<{
|
|||||||
redirect={admin}
|
redirect={admin}
|
||||||
validationOperation="create"
|
validationOperation="create"
|
||||||
>
|
>
|
||||||
<RenderEmailAndUsernameFields
|
<EmailAndUsernameFields
|
||||||
className="emailAndUsername"
|
className="emailAndUsername"
|
||||||
loginWithUsername={loginWithUsername}
|
loginWithUsername={loginWithUsername}
|
||||||
operation="create"
|
operation="create"
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
|
t={t}
|
||||||
/>
|
/>
|
||||||
<PasswordField
|
<PasswordField
|
||||||
autoComplete={'off'}
|
autoComplete="off"
|
||||||
field={{
|
field={{
|
||||||
name: 'password',
|
name: 'password',
|
||||||
label: t('authentication:newPassword'),
|
label: t('authentication:newPassword'),
|
||||||
@@ -84,10 +93,11 @@ export const CreateFirstUserClient: React.FC<{
|
|||||||
<RenderFields
|
<RenderFields
|
||||||
fields={collectionConfig.fields}
|
fields={collectionConfig.fields}
|
||||||
forceRender
|
forceRender
|
||||||
operation="create"
|
parentIndexPath=""
|
||||||
path=""
|
parentPath=""
|
||||||
|
parentSchemaPath={userSlug}
|
||||||
|
permissions={null}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
schemaPath={userSlug}
|
|
||||||
/>
|
/>
|
||||||
<FormSubmit size="large">{t('general:create')}</FormSubmit>
|
<FormSubmit size="large">{t('general:create')}</FormSubmit>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import type { AdminViewProps } from 'payload'
|
import type { AdminViewProps } from 'payload'
|
||||||
|
|
||||||
|
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { getDocPreferences } from '../Document/getDocPreferences.js'
|
||||||
import { getDocumentData } from '../Document/getDocumentData.js'
|
import { getDocumentData } from '../Document/getDocumentData.js'
|
||||||
|
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
||||||
import { CreateFirstUserClient } from './index.client.js'
|
import { CreateFirstUserClient } from './index.client.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
@@ -26,11 +29,39 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
|||||||
const { auth: authOptions } = collectionConfig
|
const { auth: authOptions } = collectionConfig
|
||||||
const loginWithUsername = authOptions.loginWithUsername
|
const loginWithUsername = authOptions.loginWithUsername
|
||||||
|
|
||||||
const { formState } = await getDocumentData({
|
// Fetch the data required for the view
|
||||||
collectionConfig,
|
const data = await getDocumentData({
|
||||||
|
collectionSlug: collectionConfig.slug,
|
||||||
locale,
|
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,
|
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 (
|
return (
|
||||||
@@ -38,6 +69,8 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
|||||||
<h1>{req.t('general:welcome')}</h1>
|
<h1>{req.t('general:welcome')}</h1>
|
||||||
<p>{req.t('authentication:beginCreateFirstUser')}</p>
|
<p>{req.t('authentication:beginCreateFirstUser')}</p>
|
||||||
<CreateFirstUserClient
|
<CreateFirstUserClient
|
||||||
|
docPermissions={docPermissions}
|
||||||
|
docPreferences={docPreferences}
|
||||||
initialState={formState}
|
initialState={formState}
|
||||||
loginWithUsername={loginWithUsername}
|
loginWithUsername={loginWithUsername}
|
||||||
userSlug={userSlug}
|
userSlug={userSlug}
|
||||||
|
|||||||
@@ -2,13 +2,9 @@ import type { groupNavItems } from '@payloadcms/ui/shared'
|
|||||||
import type { ClientUser, Permissions, ServerProps, VisibleEntities } from 'payload'
|
import type { ClientUser, Permissions, ServerProps, VisibleEntities } from 'payload'
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import { Button, Card, Gutter, Locked, SetStepNav, SetViewActions } from '@payloadcms/ui'
|
import { Button, Card, Gutter, Locked } from '@payloadcms/ui'
|
||||||
import {
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||||
EntityType,
|
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared'
|
||||||
formatAdminURL,
|
|
||||||
getCreateMappedComponent,
|
|
||||||
RenderComponent,
|
|
||||||
} from '@payloadcms/ui/shared'
|
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
@@ -50,41 +46,25 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
|||||||
user,
|
user,
|
||||||
} = props
|
} = 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 (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
<SetStepNav nav={[]} />
|
|
||||||
<SetViewActions actions={[]} />
|
|
||||||
<Gutter className={`${baseClass}__wrap`}>
|
<Gutter className={`${baseClass}__wrap`}>
|
||||||
<RenderComponent mappedComponent={mappedBeforeDashboards} />
|
{beforeDashboard && (
|
||||||
|
<RenderServerComponent
|
||||||
|
Component={beforeDashboard}
|
||||||
|
importMap={payload.importMap}
|
||||||
|
serverProps={{
|
||||||
|
i18n,
|
||||||
|
locale,
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
searchParams,
|
||||||
|
user,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SetViewActions actions={[]} />
|
|
||||||
{!navGroups || navGroups?.length === 0 ? (
|
{!navGroups || navGroups?.length === 0 ? (
|
||||||
<p>no nav groups....</p>
|
<p>no nav groups....</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -93,7 +73,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
|||||||
<div className={`${baseClass}__group`} key={groupIndex}>
|
<div className={`${baseClass}__group`} key={groupIndex}>
|
||||||
<h2 className={`${baseClass}__label`}>{label}</h2>
|
<h2 className={`${baseClass}__label`}>{label}</h2>
|
||||||
<ul className={`${baseClass}__card-list`}>
|
<ul className={`${baseClass}__card-list`}>
|
||||||
{entities.map(({ type, entity }, entityIndex) => {
|
{entities.map(({ slug, type, label }, entityIndex) => {
|
||||||
let title: string
|
let title: string
|
||||||
let buttonAriaLabel: string
|
let buttonAriaLabel: string
|
||||||
let createHREF: string
|
let createHREF: string
|
||||||
@@ -103,38 +83,34 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
|||||||
let userEditing = null
|
let userEditing = null
|
||||||
|
|
||||||
if (type === EntityType.collection) {
|
if (type === EntityType.collection) {
|
||||||
title = getTranslation(entity.labels.plural, i18n)
|
title = getTranslation(label, i18n)
|
||||||
|
|
||||||
buttonAriaLabel = t('general:showAllLabel', { label: title })
|
buttonAriaLabel = t('general:showAllLabel', { label: title })
|
||||||
|
|
||||||
href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` })
|
href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })
|
||||||
|
|
||||||
createHREF = formatAdminURL({
|
createHREF = formatAdminURL({
|
||||||
adminRoute,
|
adminRoute,
|
||||||
path: `/collections/${entity.slug}/create`,
|
path: `/collections/${slug}/create`,
|
||||||
})
|
})
|
||||||
|
|
||||||
hasCreatePermission =
|
hasCreatePermission = permissions?.collections?.[slug]?.create?.permission
|
||||||
permissions?.collections?.[entity.slug]?.create?.permission
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === EntityType.global) {
|
if (type === EntityType.global) {
|
||||||
title = getTranslation(entity.label, i18n)
|
title = getTranslation(label, i18n)
|
||||||
|
|
||||||
buttonAriaLabel = t('general:editLabel', {
|
buttonAriaLabel = t('general:editLabel', {
|
||||||
label: getTranslation(entity.label, i18n),
|
label: getTranslation(label, i18n),
|
||||||
})
|
})
|
||||||
|
|
||||||
href = formatAdminURL({
|
href = formatAdminURL({
|
||||||
adminRoute,
|
adminRoute,
|
||||||
path: `/globals/${entity.slug}`,
|
path: `/globals/${slug}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Find the lock status for the global
|
// Find the lock status for the global
|
||||||
const globalLockData = globalData.find(
|
const globalLockData = globalData.find((global) => global.slug === slug)
|
||||||
(global) => global.slug === entity.slug,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (globalLockData) {
|
if (globalLockData) {
|
||||||
isLocked = globalLockData.data._isLocked
|
isLocked = globalLockData.data._isLocked
|
||||||
userEditing = globalLockData.data._userEditing
|
userEditing = globalLockData.data._userEditing
|
||||||
@@ -164,7 +140,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
|||||||
) : hasCreatePermission && type === EntityType.collection ? (
|
) : hasCreatePermission && type === EntityType.collection ? (
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('general:createNewLabel', {
|
aria-label={t('general:createNewLabel', {
|
||||||
label: getTranslation(entity.labels.singular, i18n),
|
label,
|
||||||
})}
|
})}
|
||||||
buttonStyle="icon-label"
|
buttonStyle="icon-label"
|
||||||
el="link"
|
el="link"
|
||||||
@@ -178,9 +154,9 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
|||||||
}
|
}
|
||||||
buttonAriaLabel={buttonAriaLabel}
|
buttonAriaLabel={buttonAriaLabel}
|
||||||
href={href}
|
href={href}
|
||||||
id={`card-${entity.slug}`}
|
id={`card-${slug}`}
|
||||||
Link={Link}
|
Link={Link}
|
||||||
title={title}
|
title={getTranslation(label, i18n)}
|
||||||
titleAs="h3"
|
titleAs="h3"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
@@ -192,7 +168,21 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
|||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
<RenderComponent mappedComponent={mappedAfterDashboards} />
|
{afterDashboard && (
|
||||||
|
<RenderServerComponent
|
||||||
|
Component={afterDashboard}
|
||||||
|
importMap={payload.importMap}
|
||||||
|
serverProps={{
|
||||||
|
i18n,
|
||||||
|
locale,
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
searchParams,
|
||||||
|
user,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Gutter>
|
</Gutter>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||||
import type { AdminViewProps } from 'payload'
|
import type { AdminViewProps } from 'payload'
|
||||||
|
|
||||||
import { HydrateAuthProvider } from '@payloadcms/ui'
|
import { HydrateAuthProvider, SetStepNav } from '@payloadcms/ui'
|
||||||
import {
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||||
EntityType,
|
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||||
getCreateMappedComponent,
|
|
||||||
groupNavItems,
|
|
||||||
RenderComponent,
|
|
||||||
} from '@payloadcms/ui/shared'
|
|
||||||
import LinkImport from 'next/link.js'
|
import LinkImport from 'next/link.js'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
@@ -111,39 +107,31 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
|
|||||||
i18n,
|
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 (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<HydrateAuthProvider permissions={permissions} />
|
<HydrateAuthProvider permissions={permissions} />
|
||||||
<RenderComponent
|
<SetStepNav nav={[]} />
|
||||||
|
<RenderServerComponent
|
||||||
clientProps={{
|
clientProps={{
|
||||||
Link,
|
Link,
|
||||||
locale,
|
locale,
|
||||||
}}
|
}}
|
||||||
mappedComponent={mappedDashboardComponent}
|
Component={CustomDashboardComponent}
|
||||||
|
Fallback={DefaultDashboard}
|
||||||
|
importMap={payload.importMap}
|
||||||
|
serverProps={{
|
||||||
|
globalData,
|
||||||
|
i18n,
|
||||||
|
Link,
|
||||||
|
locale,
|
||||||
|
navGroups,
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
searchParams,
|
||||||
|
user,
|
||||||
|
visibleEntities,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</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'
|
} from 'payload'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
|
||||||
import { notFound } from 'next/navigation.js'
|
|
||||||
|
|
||||||
import { APIView as DefaultAPIView } from '../API/index.js'
|
import { APIView as DefaultAPIView } from '../API/index.js'
|
||||||
import { EditView as DefaultEditView } from '../Edit/index.js'
|
import { EditView as DefaultEditView } from '../Edit/index.js'
|
||||||
import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/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> = {
|
export type ViewFromConfig<TProps extends object> = {
|
||||||
Component?: React.FC<TProps>
|
Component?: React.FC<TProps>
|
||||||
payloadComponent?: PayloadComponent<TProps>
|
ComponentConfig?: PayloadComponent<TProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getViewsFromConfig = ({
|
export const getViewsFromConfig = ({
|
||||||
@@ -81,7 +79,7 @@ export const getViewsFromConfig = ({
|
|||||||
routeSegments
|
routeSegments
|
||||||
|
|
||||||
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
|
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
|
||||||
notFound()
|
throw new Error('not-found')
|
||||||
} else {
|
} else {
|
||||||
// `../:id`, or `../create`
|
// `../:id`, or `../create`
|
||||||
switch (routeSegments.length) {
|
switch (routeSegments.length) {
|
||||||
@@ -94,7 +92,7 @@ export const getViewsFromConfig = ({
|
|||||||
docPermissions?.create?.permission
|
docPermissions?.create?.permission
|
||||||
) {
|
) {
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||||
}
|
}
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
Component: DefaultEditView,
|
Component: DefaultEditView,
|
||||||
@@ -132,11 +130,11 @@ export const getViewsFromConfig = ({
|
|||||||
viewKey = customViewKey
|
viewKey = customViewKey
|
||||||
|
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: CustomViewComponent,
|
ComponentConfig: CustomViewComponent,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||||
}
|
}
|
||||||
|
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
@@ -156,7 +154,7 @@ export const getViewsFromConfig = ({
|
|||||||
case 'api': {
|
case 'api': {
|
||||||
if (collectionConfig?.admin?.hideAPIURL !== true) {
|
if (collectionConfig?.admin?.hideAPIURL !== true) {
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'api'),
|
ComponentConfig: getCustomViewByKey(views, 'api'),
|
||||||
}
|
}
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
Component: DefaultAPIView,
|
Component: DefaultAPIView,
|
||||||
@@ -171,7 +169,7 @@ export const getViewsFromConfig = ({
|
|||||||
Component: DefaultLivePreviewView,
|
Component: DefaultLivePreviewView,
|
||||||
}
|
}
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'livePreview'),
|
ComponentConfig: getCustomViewByKey(views, 'livePreview'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -180,7 +178,7 @@ export const getViewsFromConfig = ({
|
|||||||
case 'versions': {
|
case 'versions': {
|
||||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'versions'),
|
ComponentConfig: getCustomViewByKey(views, 'versions'),
|
||||||
}
|
}
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
Component: DefaultVersionsView,
|
Component: DefaultVersionsView,
|
||||||
@@ -218,7 +216,7 @@ export const getViewsFromConfig = ({
|
|||||||
viewKey = customViewKey
|
viewKey = customViewKey
|
||||||
|
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: CustomViewComponent,
|
ComponentConfig: CustomViewComponent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +231,7 @@ export const getViewsFromConfig = ({
|
|||||||
if (segment4 === 'versions') {
|
if (segment4 === 'versions') {
|
||||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'version'),
|
ComponentConfig: getCustomViewByKey(views, 'version'),
|
||||||
}
|
}
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
Component: DefaultVersionView,
|
Component: DefaultVersionView,
|
||||||
@@ -269,7 +267,7 @@ export const getViewsFromConfig = ({
|
|||||||
viewKey = customViewKey
|
viewKey = customViewKey
|
||||||
|
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: CustomViewComponent,
|
ComponentConfig: CustomViewComponent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,12 +282,12 @@ export const getViewsFromConfig = ({
|
|||||||
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
|
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
|
||||||
|
|
||||||
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
|
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
|
||||||
notFound()
|
throw new Error('not-found')
|
||||||
} else {
|
} else {
|
||||||
switch (routeSegments.length) {
|
switch (routeSegments.length) {
|
||||||
case 2: {
|
case 2: {
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||||
}
|
}
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
Component: DefaultEditView,
|
Component: DefaultEditView,
|
||||||
@@ -303,7 +301,7 @@ export const getViewsFromConfig = ({
|
|||||||
case 'api': {
|
case 'api': {
|
||||||
if (globalConfig?.admin?.hideAPIURL !== true) {
|
if (globalConfig?.admin?.hideAPIURL !== true) {
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'api'),
|
ComponentConfig: getCustomViewByKey(views, 'api'),
|
||||||
}
|
}
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
Component: DefaultAPIView,
|
Component: DefaultAPIView,
|
||||||
@@ -318,7 +316,7 @@ export const getViewsFromConfig = ({
|
|||||||
Component: DefaultLivePreviewView,
|
Component: DefaultLivePreviewView,
|
||||||
}
|
}
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'livePreview'),
|
ComponentConfig: getCustomViewByKey(views, 'livePreview'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -327,7 +325,7 @@ export const getViewsFromConfig = ({
|
|||||||
case 'versions': {
|
case 'versions': {
|
||||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'versions'),
|
ComponentConfig: getCustomViewByKey(views, 'versions'),
|
||||||
}
|
}
|
||||||
|
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
@@ -362,7 +360,7 @@ export const getViewsFromConfig = ({
|
|||||||
viewKey = customViewKey
|
viewKey = customViewKey
|
||||||
|
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: CustomViewComponent,
|
ComponentConfig: CustomViewComponent,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
@@ -385,7 +383,7 @@ export const getViewsFromConfig = ({
|
|||||||
if (segment3 === 'versions') {
|
if (segment3 === 'versions') {
|
||||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: getCustomViewByKey(views, 'version'),
|
ComponentConfig: getCustomViewByKey(views, 'version'),
|
||||||
}
|
}
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
Component: DefaultVersionView,
|
Component: DefaultVersionView,
|
||||||
@@ -416,7 +414,7 @@ export const getViewsFromConfig = ({
|
|||||||
viewKey = customViewKey
|
viewKey = customViewKey
|
||||||
|
|
||||||
CustomView = {
|
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 {
|
import type {
|
||||||
AdminViewProps,
|
AdminViewProps,
|
||||||
EditViewComponent,
|
Data,
|
||||||
MappedComponent,
|
PayloadComponent,
|
||||||
|
ServerProps,
|
||||||
ServerSideEditViewProps,
|
ServerSideEditViewProps,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||||
import {
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||||
formatAdminURL,
|
import { formatAdminURL, isEditing as getIsEditing } from '@payloadcms/ui/shared'
|
||||||
getCreateMappedComponent,
|
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||||
isEditing as getIsEditing,
|
import { isRedirectError } from 'next/dist/client/components/redirect.js'
|
||||||
RenderComponent,
|
|
||||||
} from '@payloadcms/ui/shared'
|
|
||||||
import { notFound, redirect } from 'next/navigation.js'
|
import { notFound, redirect } from 'next/navigation.js'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
|
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
|
||||||
|
import type { ViewFromConfig } from './getViewsFromConfig.js'
|
||||||
|
|
||||||
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
|
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
|
||||||
import { NotFoundView } from '../NotFound/index.js'
|
import { NotFoundView } from '../NotFound/index.js'
|
||||||
|
import { getDocPreferences } from './getDocPreferences.js'
|
||||||
import { getDocumentData } from './getDocumentData.js'
|
import { getDocumentData } from './getDocumentData.js'
|
||||||
import { getDocumentPermissions } from './getDocumentPermissions.js'
|
import { getDocumentPermissions } from './getDocumentPermissions.js'
|
||||||
|
import { getIsLocked } from './getIsLocked.js'
|
||||||
import { getMetaBySegment } from './getMetaBySegment.js'
|
import { getMetaBySegment } from './getMetaBySegment.js'
|
||||||
|
import { getVersions } from './getVersions.js'
|
||||||
import { getViewsFromConfig } from './getViewsFromConfig.js'
|
import { getViewsFromConfig } from './getViewsFromConfig.js'
|
||||||
|
import { renderDocumentSlots } from './renderDocumentSlots.js'
|
||||||
|
|
||||||
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)
|
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,
|
importMap,
|
||||||
|
initialData,
|
||||||
initPageResult,
|
initPageResult,
|
||||||
params,
|
params,
|
||||||
|
redirectAfterDelete,
|
||||||
|
redirectAfterDuplicate,
|
||||||
searchParams,
|
searchParams,
|
||||||
}) => {
|
}: AdminViewProps): Promise<{
|
||||||
|
data: Data
|
||||||
|
Document: React.ReactNode
|
||||||
|
}> => {
|
||||||
const {
|
const {
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
docID: id,
|
docID: id,
|
||||||
@@ -57,54 +72,108 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
const segments = Array.isArray(params?.segments) ? params.segments : []
|
const segments = Array.isArray(params?.segments) ? params.segments : []
|
||||||
const collectionSlug = collectionConfig?.slug || undefined
|
const collectionSlug = collectionConfig?.slug || undefined
|
||||||
const globalSlug = globalConfig?.slug || undefined
|
const globalSlug = globalConfig?.slug || undefined
|
||||||
|
|
||||||
const isEditing = getIsEditing({ id, collectionSlug, globalSlug })
|
const isEditing = getIsEditing({ id, collectionSlug, globalSlug })
|
||||||
|
|
||||||
let RootViewOverride: MappedComponent<ServerSideEditViewProps>
|
let RootViewOverride: PayloadComponent
|
||||||
let CustomView: MappedComponent<ServerSideEditViewProps>
|
let CustomView: ViewFromConfig<ServerSideEditViewProps>
|
||||||
let DefaultView: MappedComponent<ServerSideEditViewProps>
|
let DefaultView: ViewFromConfig<ServerSideEditViewProps>
|
||||||
let ErrorView: MappedComponent<AdminViewProps>
|
let ErrorView: ViewFromConfig<AdminViewProps>
|
||||||
|
|
||||||
let apiURL: string
|
let apiURL: string
|
||||||
|
|
||||||
const { data, formState } = await getDocumentData({
|
// Fetch the doc required for the view
|
||||||
id,
|
const doc =
|
||||||
collectionConfig,
|
initialData ||
|
||||||
globalConfig,
|
(await getDocumentData({
|
||||||
locale,
|
id,
|
||||||
req,
|
collectionSlug,
|
||||||
})
|
globalSlug,
|
||||||
|
locale,
|
||||||
|
payload,
|
||||||
|
user,
|
||||||
|
}))
|
||||||
|
|
||||||
if (!data) {
|
if (isEditing && !doc) {
|
||||||
notFound()
|
throw new Error('not-found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { docPermissions, hasPublishPermission, hasSavePermission } = await getDocumentPermissions({
|
const [
|
||||||
id,
|
docPreferences,
|
||||||
collectionConfig,
|
{ docPermissions, hasPublishPermission, hasSavePermission },
|
||||||
data,
|
{ currentEditor, isLocked, lastUpdateTime },
|
||||||
globalConfig,
|
] = await Promise.all([
|
||||||
req,
|
// Get document preferences
|
||||||
})
|
getDocPreferences({
|
||||||
|
id,
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
collectionSlug,
|
||||||
importMap,
|
globalSlug,
|
||||||
serverProps: {
|
|
||||||
i18n,
|
|
||||||
initPageResult,
|
|
||||||
locale,
|
|
||||||
params,
|
|
||||||
payload,
|
payload,
|
||||||
permissions,
|
|
||||||
routeSegments: segments,
|
|
||||||
searchParams,
|
|
||||||
user,
|
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 (collectionConfig) {
|
||||||
if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) {
|
if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) {
|
||||||
notFound()
|
throw new Error('not-found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
@@ -122,12 +191,7 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
RootViewOverride =
|
RootViewOverride =
|
||||||
collectionConfig?.admin?.components?.views?.edit?.root &&
|
collectionConfig?.admin?.components?.views?.edit?.root &&
|
||||||
'Component' in collectionConfig.admin.components.views.edit.root
|
'Component' in collectionConfig.admin.components.views.edit.root
|
||||||
? createMappedComponent(
|
? collectionConfig?.admin?.components?.views?.edit?.root?.Component
|
||||||
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',
|
|
||||||
)
|
|
||||||
: null
|
: null
|
||||||
|
|
||||||
if (!RootViewOverride) {
|
if (!RootViewOverride) {
|
||||||
@@ -138,36 +202,21 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
routeSegments: segments,
|
routeSegments: segments,
|
||||||
})
|
})
|
||||||
|
|
||||||
CustomView = createMappedComponent(
|
CustomView = collectionViews?.CustomView
|
||||||
collectionViews?.CustomView?.payloadComponent,
|
DefaultView = collectionViews?.DefaultView
|
||||||
undefined,
|
ErrorView = collectionViews?.ErrorView
|
||||||
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',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
|
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
|
||||||
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
|
ErrorView = {
|
||||||
|
Component: NotFoundView,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (globalConfig) {
|
if (globalConfig) {
|
||||||
if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) {
|
if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) {
|
||||||
notFound()
|
throw new Error('not-found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -189,12 +238,7 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
RootViewOverride =
|
RootViewOverride =
|
||||||
globalConfig?.admin?.components?.views?.edit?.root &&
|
globalConfig?.admin?.components?.views?.edit?.root &&
|
||||||
'Component' in globalConfig.admin.components.views.edit.root
|
'Component' in globalConfig.admin.components.views.edit.root
|
||||||
? createMappedComponent(
|
? globalConfig?.admin?.components?.views?.edit?.root?.Component
|
||||||
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',
|
|
||||||
)
|
|
||||||
: null
|
: null
|
||||||
|
|
||||||
if (!RootViewOverride) {
|
if (!RootViewOverride) {
|
||||||
@@ -205,29 +249,14 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
routeSegments: segments,
|
routeSegments: segments,
|
||||||
})
|
})
|
||||||
|
|
||||||
CustomView = createMappedComponent(
|
CustomView = globalViews?.CustomView
|
||||||
globalViews?.CustomView?.payloadComponent,
|
DefaultView = globalViews?.DefaultView
|
||||||
undefined,
|
ErrorView = globalViews?.ErrorView
|
||||||
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',
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!CustomView && !DefaultView && !RootViewOverride && !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 &&
|
hasSavePermission &&
|
||||||
((collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
|
((collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
|
||||||
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave))
|
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave))
|
||||||
|
|
||||||
const validateDraftData =
|
const validateDraftData =
|
||||||
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate
|
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate
|
||||||
|
|
||||||
if (shouldAutosave && !validateDraftData && !id && collectionSlug) {
|
if (shouldAutosave && !validateDraftData && !id && collectionSlug) {
|
||||||
const doc = await payload.create({
|
const doc = await payload.create({
|
||||||
collection: collectionSlug,
|
collection: collectionSlug,
|
||||||
data: {},
|
data: initialData || {},
|
||||||
depth: 0,
|
depth: 0,
|
||||||
draft: true,
|
draft: true,
|
||||||
fallbackLocale: null,
|
fallbackLocale: null,
|
||||||
@@ -263,57 +293,96 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
})
|
})
|
||||||
redirect(redirectURL)
|
redirect(redirectURL)
|
||||||
} else {
|
} else {
|
||||||
notFound()
|
throw new Error('not-found')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const documentSlots = renderDocumentSlots({
|
||||||
<DocumentInfoProvider
|
collectionConfig,
|
||||||
apiURL={apiURL}
|
globalConfig,
|
||||||
collectionSlug={collectionConfig?.slug}
|
hasSavePermission,
|
||||||
disableActions={false}
|
permissions: docPermissions,
|
||||||
docPermissions={docPermissions}
|
req,
|
||||||
globalSlug={globalConfig?.slug}
|
})
|
||||||
hasPublishPermission={hasPublishPermission}
|
|
||||||
hasSavePermission={hasSavePermission}
|
const clientProps = { formState, ...documentSlots }
|
||||||
id={id}
|
|
||||||
initialData={data}
|
return {
|
||||||
initialState={formState}
|
data: doc,
|
||||||
isEditing={isEditing}
|
Document: (
|
||||||
key={locale?.code}
|
<DocumentInfoProvider
|
||||||
>
|
apiURL={apiURL}
|
||||||
{!RootViewOverride && (
|
collectionSlug={collectionConfig?.slug}
|
||||||
<DocumentHeader
|
currentEditor={currentEditor}
|
||||||
collectionConfig={collectionConfig}
|
disableActions={disableActions ?? false}
|
||||||
globalConfig={globalConfig}
|
docPermissions={docPermissions}
|
||||||
i18n={i18n}
|
globalSlug={globalConfig?.slug}
|
||||||
payload={payload}
|
hasPublishedDoc={hasPublishedDoc}
|
||||||
permissions={permissions}
|
hasPublishPermission={hasPublishPermission}
|
||||||
/>
|
hasSavePermission={hasSavePermission}
|
||||||
)}
|
id={id}
|
||||||
<HydrateAuthProvider permissions={permissions} />
|
initialData={doc}
|
||||||
{/**
|
initialState={formState}
|
||||||
* 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).
|
isEditing={isEditing}
|
||||||
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
|
isLocked={isLocked}
|
||||||
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error when loading up some version views - for example a versions
|
key={locale?.code}
|
||||||
* view in the draft-posts collection of the versions test suite. RenderCustomComponent is what renders the versions view.
|
lastUpdateTime={lastUpdateTime}
|
||||||
*
|
mostRecentVersionIsAutosaved={mostRecentVersionIsAutosaved}
|
||||||
* // 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)
|
redirectAfterDelete={redirectAfterDelete}
|
||||||
*/}
|
redirectAfterDuplicate={redirectAfterDuplicate}
|
||||||
<EditDepthProvider
|
unpublishedVersionCount={unpublishedVersionCount}
|
||||||
depth={1}
|
versionCount={versionCount}
|
||||||
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
|
|
||||||
>
|
>
|
||||||
{ErrorView ? (
|
{!RootViewOverride && !drawerSlug && (
|
||||||
<RenderComponent mappedComponent={ErrorView} />
|
<DocumentHeader
|
||||||
) : (
|
collectionConfig={collectionConfig}
|
||||||
<RenderComponent
|
globalConfig={globalConfig}
|
||||||
mappedComponent={
|
i18n={i18n}
|
||||||
RootViewOverride ? RootViewOverride : CustomView ? CustomView : DefaultView
|
payload={payload}
|
||||||
}
|
permissions={permissions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</EditDepthProvider>
|
<HydrateAuthProvider permissions={permissions} />
|
||||||
</DocumentInfoProvider>
|
<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 React from 'react'
|
||||||
|
|
||||||
import { EditViewClient } from './index.client.js'
|
export const EditView: React.FC<ClientSideEditViewProps> = (props) => {
|
||||||
|
return <DefaultEditView {...props} />
|
||||||
export const EditView: PayloadServerReactComponent<EditViewComponent> = () => {
|
|
||||||
return <EditViewClient />
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,9 +105,11 @@ export const ForgotPasswordForm: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmailField
|
<EmailField
|
||||||
autoComplete="email"
|
|
||||||
field={{
|
field={{
|
||||||
name: 'email',
|
name: 'email',
|
||||||
|
admin: {
|
||||||
|
autoComplete: 'email',
|
||||||
|
},
|
||||||
label: t('general:email'),
|
label: t('general:email'),
|
||||||
required: true,
|
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 {
|
import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui'
|
||||||
HydrateAuthProvider,
|
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||||
ListInfoProvider,
|
import { renderFilters, renderTable } from '@payloadcms/ui/rsc'
|
||||||
ListQueryProvider,
|
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
|
||||||
TableColumnsProvider,
|
|
||||||
} from '@payloadcms/ui'
|
|
||||||
import { formatAdminURL, getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
|
||||||
import { createClientCollectionConfig } from '@payloadcms/ui/utilities/createClientConfig'
|
|
||||||
import { notFound } from 'next/navigation.js'
|
import { notFound } from 'next/navigation.js'
|
||||||
import { deepCopyObjectSimple, mergeListSearchAndWhere } from 'payload'
|
|
||||||
import { isNumber } from 'payload/shared'
|
import { isNumber } from 'payload/shared'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import type { ListPreferences } from './Default/types.js'
|
import { renderListViewSlots } from './renderListViewSlots.js'
|
||||||
|
|
||||||
import { DefaultEditView } from '../Edit/Default/index.js'
|
|
||||||
import { DefaultListView } from './Default/index.js'
|
|
||||||
|
|
||||||
export { generateListMetadata } from './meta.js'
|
export { generateListMetadata } from './meta.js'
|
||||||
|
|
||||||
export const ListView: React.FC<AdminViewProps> = async ({
|
type ListViewArgs = {
|
||||||
initPageResult,
|
customCellProps?: Record<string, any>
|
||||||
params,
|
disableBulkDelete?: boolean
|
||||||
searchParams,
|
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 {
|
const {
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
|
collectionConfig: {
|
||||||
|
slug: collectionSlug,
|
||||||
|
admin: { useAsTitle },
|
||||||
|
defaultSort,
|
||||||
|
fields,
|
||||||
|
},
|
||||||
locale: fullLocale,
|
locale: fullLocale,
|
||||||
permissions,
|
permissions,
|
||||||
req,
|
req,
|
||||||
@@ -35,18 +55,18 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
|||||||
locale,
|
locale,
|
||||||
payload,
|
payload,
|
||||||
payload: { config },
|
payload: { config },
|
||||||
query,
|
query: queryFromReq,
|
||||||
user,
|
user,
|
||||||
},
|
},
|
||||||
visibleEntities,
|
visibleEntities,
|
||||||
} = initPageResult
|
} = initPageResult
|
||||||
|
|
||||||
const collectionSlug = collectionConfig?.slug
|
|
||||||
|
|
||||||
if (!permissions?.collections?.[collectionSlug]?.read?.permission) {
|
if (!permissions?.collections?.[collectionSlug]?.read?.permission) {
|
||||||
notFound()
|
throw new Error('not-found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = queryFromArgs || queryFromReq
|
||||||
|
|
||||||
let listPreferences: ListPreferences
|
let listPreferences: ListPreferences
|
||||||
const preferenceKey = `${collectionSlug}-list`
|
const preferenceKey = `${collectionSlug}-list`
|
||||||
|
|
||||||
@@ -79,7 +99,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
?.then((res) => res?.docs?.[0]?.value)) as ListPreferences
|
?.then((res) => res?.docs?.[0]?.value)) as ListPreferences
|
||||||
} catch (error) {} // eslint-disable-line no-empty
|
} catch (_err) {} // eslint-disable-line no-empty
|
||||||
|
|
||||||
const {
|
const {
|
||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
@@ -87,20 +107,21 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
|||||||
|
|
||||||
if (collectionConfig) {
|
if (collectionConfig) {
|
||||||
if (!visibleEntities.collections.includes(collectionSlug)) {
|
if (!visibleEntities.collections.includes(collectionSlug)) {
|
||||||
return notFound()
|
throw new Error('not-found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = isNumber(query?.page) ? Number(query.page) : 0
|
const page = isNumber(query?.page) ? Number(query.page) : 0
|
||||||
|
|
||||||
const whereQuery = mergeListSearchAndWhere({
|
const whereQuery = mergeListSearchAndWhere({
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
query: {
|
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||||
search: typeof query?.search === 'string' ? query.search : undefined,
|
where: (query?.where as Where) || undefined,
|
||||||
where: (query?.where as Where) || undefined,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const limit = isNumber(query?.limit)
|
const limit = isNumber(query?.limit)
|
||||||
? Number(query.limit)
|
? Number(query.limit)
|
||||||
: listPreferences?.limit || collectionConfig.admin.pagination.defaultLimit
|
: listPreferences?.limit || collectionConfig.admin.pagination.defaultLimit
|
||||||
|
|
||||||
const sort =
|
const sort =
|
||||||
query?.sort && typeof query.sort === 'string'
|
query?.sort && typeof query.sort === 'string'
|
||||||
? query.sort
|
? query.sort
|
||||||
@@ -125,89 +146,104 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
|||||||
where: whereQuery || {},
|
where: whereQuery || {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
const clientCollectionConfig = clientConfig.collections.find((c) => c.slug === collectionSlug)
|
||||||
importMap: payload.importMap,
|
|
||||||
serverProps: {
|
const { columnState, Table } = renderTable({
|
||||||
collectionConfig,
|
collectionConfig: clientCollectionConfig,
|
||||||
collectionSlug,
|
columnPreferences: listPreferences?.columns,
|
||||||
data,
|
customCellProps,
|
||||||
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
|
docs: data.docs,
|
||||||
i18n,
|
drawerSlug,
|
||||||
limit,
|
enableRowSelections,
|
||||||
listPreferences,
|
fields,
|
||||||
listSearchableFields: collectionConfig.admin.listSearchableFields,
|
i18n: req.i18n,
|
||||||
locale: fullLocale,
|
payload,
|
||||||
newDocumentURL: formatAdminURL({
|
useAsTitle,
|
||||||
adminRoute,
|
|
||||||
path: `/collections/${collectionSlug}/create`,
|
|
||||||
}),
|
|
||||||
params,
|
|
||||||
payload,
|
|
||||||
permissions,
|
|
||||||
searchParams,
|
|
||||||
user,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const ListComponent = createMappedComponent(
|
const renderedFilters = renderFilters(fields, req.payload.importMap)
|
||||||
collectionConfig?.admin?.components?.views?.list?.Component,
|
|
||||||
undefined,
|
|
||||||
DefaultListView,
|
|
||||||
'collectionConfig?.admin?.components?.views?.list?.Component',
|
|
||||||
)
|
|
||||||
|
|
||||||
let clientCollectionConfig = deepCopyObjectSimple(
|
const staticDescription =
|
||||||
|
typeof collectionConfig.admin.description === 'function'
|
||||||
|
? collectionConfig.admin.description({ t: i18n.t })
|
||||||
|
: collectionConfig.admin.description
|
||||||
|
|
||||||
|
const listViewSlots = renderListViewSlots({
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
) as unknown as ClientCollectionConfig
|
description: staticDescription,
|
||||||
clientCollectionConfig = createClientCollectionConfig({
|
|
||||||
clientCollection: clientCollectionConfig,
|
|
||||||
collection: collectionConfig,
|
|
||||||
createMappedComponent,
|
|
||||||
DefaultEditView,
|
|
||||||
DefaultListView,
|
|
||||||
i18n,
|
|
||||||
importMap: payload.importMap,
|
|
||||||
payload,
|
payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
const clientProps: ListViewClientProps = {
|
||||||
<Fragment>
|
...listViewSlots,
|
||||||
<HydrateAuthProvider permissions={permissions} />
|
collectionSlug,
|
||||||
<ListInfoProvider
|
columnState,
|
||||||
collectionConfig={clientCollectionConfig}
|
disableBulkDelete,
|
||||||
collectionSlug={collectionSlug}
|
disableBulkEdit,
|
||||||
hasCreatePermission={permissions?.collections?.[collectionSlug]?.create?.permission}
|
enableRowSelections,
|
||||||
newDocumentURL={formatAdminURL({
|
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
|
||||||
adminRoute,
|
listPreferences,
|
||||||
path: `/collections/${collectionSlug}/create`,
|
newDocumentURL: formatAdminURL({
|
||||||
})}
|
adminRoute,
|
||||||
>
|
path: `/collections/${collectionSlug}/create`,
|
||||||
|
}),
|
||||||
|
renderedFilters,
|
||||||
|
Table,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInDrawer = Boolean(drawerSlug)
|
||||||
|
|
||||||
|
return {
|
||||||
|
List: (
|
||||||
|
<Fragment>
|
||||||
|
<HydrateAuthProvider permissions={permissions} />
|
||||||
<ListQueryProvider
|
<ListQueryProvider
|
||||||
|
collectionSlug={collectionSlug}
|
||||||
data={data}
|
data={data}
|
||||||
defaultLimit={limit || collectionConfig?.admin?.pagination?.defaultLimit}
|
defaultLimit={limit}
|
||||||
defaultSort={sort}
|
defaultSort={sort}
|
||||||
modifySearchParams
|
modifySearchParams={!isInDrawer}
|
||||||
preferenceKey={preferenceKey}
|
preferenceKey={preferenceKey}
|
||||||
>
|
>
|
||||||
<TableColumnsProvider
|
<RenderServerComponent
|
||||||
collectionSlug={collectionSlug}
|
clientProps={clientProps}
|
||||||
enableRowSelections
|
Component={collectionConfig?.admin?.components?.views?.list?.Component}
|
||||||
listPreferences={listPreferences}
|
Fallback={DefaultListView}
|
||||||
preferenceKey={preferenceKey}
|
importMap={payload.importMap}
|
||||||
>
|
serverProps={{
|
||||||
<RenderComponent
|
collectionConfig,
|
||||||
clientProps={{
|
collectionSlug,
|
||||||
collectionSlug,
|
data,
|
||||||
listSearchableFields: collectionConfig?.admin?.listSearchableFields,
|
i18n,
|
||||||
}}
|
limit,
|
||||||
mappedComponent={ListComponent}
|
listPreferences,
|
||||||
/>
|
listSearchableFields: collectionConfig.admin.listSearchableFields,
|
||||||
</TableColumnsProvider>
|
locale: fullLocale,
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
searchParams,
|
||||||
|
user,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ListQueryProvider>
|
</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 {
|
import {
|
||||||
DocumentControls,
|
DocumentControls,
|
||||||
DocumentFields,
|
DocumentFields,
|
||||||
|
DocumentLocked,
|
||||||
|
DocumentTakeOver,
|
||||||
Form,
|
Form,
|
||||||
|
LeaveWithoutSaving,
|
||||||
OperationProvider,
|
OperationProvider,
|
||||||
SetViewActions,
|
SetDocumentStepNav,
|
||||||
|
SetDocumentTitle,
|
||||||
useAuth,
|
useAuth,
|
||||||
useConfig,
|
useConfig,
|
||||||
|
useDocumentDrawerContext,
|
||||||
useDocumentEvents,
|
useDocumentEvents,
|
||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
|
useServerFunctions,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import {
|
import { handleBackToDashboard, handleGoBack, handleTakeOver } from '@payloadcms/ui/shared'
|
||||||
getFormState,
|
|
||||||
handleBackToDashboard,
|
|
||||||
handleGoBack,
|
|
||||||
handleTakeOver,
|
|
||||||
} from '@payloadcms/ui/shared'
|
|
||||||
import { useRouter } from 'next/navigation.js'
|
import { useRouter } from 'next/navigation.js'
|
||||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
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 { useLivePreviewContext } from './Context/context.js'
|
||||||
import { LivePreviewProvider } from './Context/index.js'
|
import { LivePreviewProvider } from './Context/index.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
@@ -55,13 +51,11 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PreviewView: React.FC<Props> = ({
|
const PreviewView: React.FC<Props> = ({
|
||||||
apiRoute,
|
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
config,
|
config,
|
||||||
fields,
|
fields,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
serverURL,
|
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@@ -69,7 +63,6 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
AfterDocument,
|
AfterDocument,
|
||||||
AfterFields,
|
AfterFields,
|
||||||
apiURL,
|
apiURL,
|
||||||
BeforeDocument,
|
|
||||||
BeforeFields,
|
BeforeFields,
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
currentEditor,
|
currentEditor,
|
||||||
@@ -86,13 +79,16 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
isEditing,
|
isEditing,
|
||||||
isInitializing,
|
isInitializing,
|
||||||
lastUpdateTime,
|
lastUpdateTime,
|
||||||
onSave: onSaveFromProps,
|
|
||||||
setCurrentEditor,
|
setCurrentEditor,
|
||||||
setDocumentIsLocked,
|
setDocumentIsLocked,
|
||||||
unlockDocument,
|
unlockDocument,
|
||||||
updateDocumentEditor,
|
updateDocumentEditor,
|
||||||
} = useDocumentInfo()
|
} = useDocumentInfo()
|
||||||
|
|
||||||
|
const { getFormState } = useServerFunctions()
|
||||||
|
|
||||||
|
const { onSave: onSaveFromProps } = useDocumentDrawerContext()
|
||||||
|
|
||||||
const operation = id ? 'update' : 'create'
|
const operation = id ? 'update' : 'create'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -120,6 +116,8 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
||||||
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
||||||
|
|
||||||
|
const abortControllerRef = useRef(new AbortController())
|
||||||
|
|
||||||
const [editSessionStartTime, setEditSessionStartTime] = useState(Date.now())
|
const [editSessionStartTime, setEditSessionStartTime] = useState(Date.now())
|
||||||
|
|
||||||
const lockExpiryTime = lastUpdateTime + lockDurationInMilliseconds
|
const lockExpiryTime = lastUpdateTime + lockDurationInMilliseconds
|
||||||
@@ -178,6 +176,17 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
|
|
||||||
const onChange: FormProps['onChange'][0] = useCallback(
|
const onChange: FormProps['onChange'][0] = useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
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 currentTime = Date.now()
|
||||||
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
||||||
|
|
||||||
@@ -190,19 +199,17 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
const docPreferences = await getDocPreferences()
|
const docPreferences = await getDocPreferences()
|
||||||
|
|
||||||
const { lockedState, state } = await getFormState({
|
const { lockedState, state } = await getFormState({
|
||||||
apiRoute,
|
id,
|
||||||
body: {
|
collectionSlug,
|
||||||
id,
|
docPermissions,
|
||||||
collectionSlug,
|
docPreferences,
|
||||||
docPreferences,
|
formState: prevFormState,
|
||||||
formState: prevFormState,
|
globalSlug,
|
||||||
globalSlug,
|
operation,
|
||||||
operation,
|
returnLockStatus: isLockingEnabled ? true : false,
|
||||||
returnLockStatus: isLockingEnabled ? true : false,
|
schemaPath,
|
||||||
schemaPath,
|
signal: abortController.signal,
|
||||||
updateLastEdited,
|
updateLastEdited,
|
||||||
},
|
|
||||||
serverURL,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setDocumentIsLocked(true)
|
setDocumentIsLocked(true)
|
||||||
@@ -214,8 +221,13 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
: documentLockStateRef.current?.user
|
: documentLockStateRef.current?.user
|
||||||
|
|
||||||
if (lockedState) {
|
if (lockedState) {
|
||||||
if (!documentLockStateRef.current || lockedState.user.id !== previousOwnerId) {
|
const lockedUserID =
|
||||||
if (previousOwnerId === user.id && lockedState.user.id !== user.id) {
|
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)
|
setShowTakeOverModal(true)
|
||||||
documentLockStateRef.current.hasShownLockedModal = true
|
documentLockStateRef.current.hasShownLockedModal = true
|
||||||
}
|
}
|
||||||
@@ -223,9 +235,10 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
documentLockStateRef.current = documentLockStateRef.current = {
|
documentLockStateRef.current = documentLockStateRef.current = {
|
||||||
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
|
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
|
||||||
isLocked: true,
|
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
|
return state
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
collectionSlug,
|
|
||||||
editSessionStartTime,
|
editSessionStartTime,
|
||||||
globalSlug,
|
|
||||||
serverURL,
|
|
||||||
apiRoute,
|
|
||||||
id,
|
|
||||||
isLockingEnabled,
|
isLockingEnabled,
|
||||||
|
getDocPreferences,
|
||||||
|
getFormState,
|
||||||
|
id,
|
||||||
|
collectionSlug,
|
||||||
|
docPermissions,
|
||||||
|
globalSlug,
|
||||||
operation,
|
operation,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
getDocPreferences,
|
|
||||||
setCurrentEditor,
|
|
||||||
setDocumentIsLocked,
|
setDocumentIsLocked,
|
||||||
user,
|
user.id,
|
||||||
|
setCurrentEditor,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Clean up when the component unmounts or when the document is unlocked
|
// Clean up when the component unmounts or when the document is unlocked
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
try {
|
||||||
|
abortControllerRef.current.abort()
|
||||||
|
} catch (_err) {
|
||||||
|
// swallow error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isLockingEnabled) {
|
if (!isLockingEnabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -415,7 +436,6 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
>
|
>
|
||||||
{BeforeDocument}
|
|
||||||
<DocumentFields
|
<DocumentFields
|
||||||
AfterFields={AfterFields}
|
AfterFields={AfterFields}
|
||||||
BeforeFields={BeforeFields}
|
BeforeFields={BeforeFields}
|
||||||
@@ -423,7 +443,7 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
forceSidebarWrap
|
forceSidebarWrap
|
||||||
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
|
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
|
||||||
schemaPath={collectionSlug || globalSlug}
|
schemaPathSegments={[collectionSlug || globalSlug]}
|
||||||
/>
|
/>
|
||||||
{AfterDocument}
|
{AfterDocument}
|
||||||
</div>
|
</div>
|
||||||
@@ -464,11 +484,6 @@ export const LivePreviewClient: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SetViewActions
|
|
||||||
actions={
|
|
||||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.livePreview?.actions
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<LivePreviewProvider
|
<LivePreviewProvider
|
||||||
breakpoints={breakpoints}
|
breakpoints={breakpoints}
|
||||||
fieldSchema={collectionConfig?.fields || globalConfig?.fields}
|
fieldSchema={collectionConfig?.fields || globalConfig?.fields}
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
|
|||||||
if (type === 'email') {
|
if (type === 'email') {
|
||||||
return (
|
return (
|
||||||
<EmailField
|
<EmailField
|
||||||
autoComplete="email"
|
|
||||||
field={{
|
field={{
|
||||||
name: 'email',
|
name: 'email',
|
||||||
|
admin: {
|
||||||
|
autoComplete: 'email',
|
||||||
|
},
|
||||||
label: t('general:email'),
|
label: t('general:email'),
|
||||||
required,
|
required,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { AdminViewProps } from 'payload'
|
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 { redirect } from 'next/navigation.js'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
@@ -28,23 +28,6 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
|||||||
routes: { admin },
|
routes: { admin },
|
||||||
} = config
|
} = 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) {
|
if (user) {
|
||||||
redirect((searchParams.redirect as string) || admin)
|
redirect((searchParams.redirect as string) || admin)
|
||||||
}
|
}
|
||||||
@@ -82,7 +65,19 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
|||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<RenderComponent mappedComponent={mappedBeforeLogins} />
|
<RenderServerComponent
|
||||||
|
Component={beforeLogin}
|
||||||
|
importMap={payload.importMap}
|
||||||
|
serverProps={{
|
||||||
|
i18n,
|
||||||
|
locale,
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
searchParams,
|
||||||
|
user,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{!collectionConfig?.auth?.disableLocalStrategy && (
|
{!collectionConfig?.auth?.disableLocalStrategy && (
|
||||||
<LoginForm
|
<LoginForm
|
||||||
prefillEmail={prefillEmail}
|
prefillEmail={prefillEmail}
|
||||||
@@ -91,7 +86,19 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
|||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<RenderComponent mappedComponent={mappedAfterLogins} />
|
<RenderServerComponent
|
||||||
|
Component={afterLogin}
|
||||||
|
importMap={payload.importMap}
|
||||||
|
serverProps={{
|
||||||
|
i18n,
|
||||||
|
locale,
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
searchParams,
|
||||||
|
user,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,13 +75,26 @@ export const ResetPasswordForm: React.FC<Args> = ({ token }) => {
|
|||||||
label: i18n.t('authentication:newPassword'),
|
label: i18n.t('authentication:newPassword'),
|
||||||
required: true,
|
required: true,
|
||||||
}}
|
}}
|
||||||
|
indexPath=""
|
||||||
|
parentPath=""
|
||||||
|
parentSchemaPath=""
|
||||||
|
path="password"
|
||||||
|
schemaPath={`${userSlug}.password`}
|
||||||
/>
|
/>
|
||||||
<ConfirmPasswordField />
|
<ConfirmPasswordField />
|
||||||
<HiddenField
|
<HiddenField
|
||||||
field={{
|
field={{
|
||||||
name: 'token',
|
name: 'token',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
forceUsePathFromProps
|
indexPath=""
|
||||||
|
parentPath={userSlug}
|
||||||
|
parentSchemaPath={userSlug}
|
||||||
|
path="token"
|
||||||
|
schemaPath={`${userSlug}.token`}
|
||||||
value={token}
|
value={token}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 type React from 'react'
|
||||||
|
|
||||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||||
@@ -46,6 +53,20 @@ const oneSegmentViews: OneSegmentViews = {
|
|||||||
unauthorized: UnauthorizedView,
|
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 = ({
|
export const getViewFromConfig = ({
|
||||||
adminRoute,
|
adminRoute,
|
||||||
config,
|
config,
|
||||||
@@ -65,8 +86,10 @@ export const getViewFromConfig = ({
|
|||||||
}): {
|
}): {
|
||||||
DefaultView: ViewFromConfig
|
DefaultView: ViewFromConfig
|
||||||
initPageOptions: Parameters<typeof initPage>[0]
|
initPageOptions: Parameters<typeof initPage>[0]
|
||||||
|
serverProps: Record<string, unknown>
|
||||||
templateClassName: string
|
templateClassName: string
|
||||||
templateType: 'default' | 'minimal'
|
templateType: 'default' | 'minimal'
|
||||||
|
viewActions?: CustomComponent[]
|
||||||
} => {
|
} => {
|
||||||
let ViewToRender: ViewFromConfig = null
|
let ViewToRender: ViewFromConfig = null
|
||||||
let templateClassName: string
|
let templateClassName: string
|
||||||
@@ -79,10 +102,30 @@ export const getViewFromConfig = ({
|
|||||||
searchParams,
|
searchParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
const [segmentOne, segmentTwo] = segments
|
let viewActions: CustomComponent[] = config?.admin?.components?.actions || []
|
||||||
|
|
||||||
|
const [segmentOne, segmentTwo, segmentThree, segmentFour, segmentFive] = segments
|
||||||
|
|
||||||
const isGlobal = segmentOne === 'globals'
|
const isGlobal = segmentOne === 'globals'
|
||||||
const isCollection = segmentOne === 'collections'
|
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) {
|
switch (segments.length) {
|
||||||
case 0: {
|
case 0: {
|
||||||
@@ -146,7 +189,7 @@ export const getViewFromConfig = ({
|
|||||||
templateType = 'minimal'
|
templateType = 'minimal'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCollection) {
|
if (isCollection && matchedCollection) {
|
||||||
// --> /collections/:collectionSlug
|
// --> /collections/:collectionSlug
|
||||||
|
|
||||||
ViewToRender = {
|
ViewToRender = {
|
||||||
@@ -155,7 +198,8 @@ export const getViewFromConfig = ({
|
|||||||
|
|
||||||
templateClassName = `${segmentTwo}-list`
|
templateClassName = `${segmentTwo}-list`
|
||||||
templateType = 'default'
|
templateType = 'default'
|
||||||
} else if (isGlobal) {
|
viewActions = viewActions.concat(matchedCollection.admin.components?.views?.list?.actions)
|
||||||
|
} else if (isGlobal && matchedGlobal) {
|
||||||
// --> /globals/:globalSlug
|
// --> /globals/:globalSlug
|
||||||
|
|
||||||
ViewToRender = {
|
ViewToRender = {
|
||||||
@@ -164,6 +208,14 @@ export const getViewFromConfig = ({
|
|||||||
|
|
||||||
templateClassName = 'global-edit'
|
templateClassName = 'global-edit'
|
||||||
templateType = 'default'
|
templateType = 'default'
|
||||||
|
|
||||||
|
// add default view actions
|
||||||
|
viewActions = viewActions.concat(
|
||||||
|
getViewActions({
|
||||||
|
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||||
|
viewKey: 'default',
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -176,13 +228,13 @@ export const getViewFromConfig = ({
|
|||||||
|
|
||||||
templateClassName = 'verify'
|
templateClassName = 'verify'
|
||||||
templateType = 'minimal'
|
templateType = 'minimal'
|
||||||
} else if (isCollection) {
|
} else if (isCollection && matchedCollection) {
|
||||||
// Custom Views
|
// Custom Views
|
||||||
// --> /collections/:collectionSlug/:id
|
// --> /collections/:collectionSlug/:id
|
||||||
|
// --> /collections/:collectionSlug/:id/api
|
||||||
// --> /collections/:collectionSlug/:id/preview
|
// --> /collections/:collectionSlug/:id/preview
|
||||||
// --> /collections/:collectionSlug/:id/versions
|
// --> /collections/:collectionSlug/:id/versions
|
||||||
// --> /collections/:collectionSlug/:id/versions/:versionId
|
// --> /collections/:collectionSlug/:id/versions/:versionId
|
||||||
// --> /collections/:collectionSlug/:id/api
|
|
||||||
|
|
||||||
ViewToRender = {
|
ViewToRender = {
|
||||||
Component: DocumentView,
|
Component: DocumentView,
|
||||||
@@ -190,7 +242,65 @@ export const getViewFromConfig = ({
|
|||||||
|
|
||||||
templateClassName = `collection-default-edit`
|
templateClassName = `collection-default-edit`
|
||||||
templateType = 'default'
|
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
|
// Custom Views
|
||||||
// --> /globals/:globalSlug/versions
|
// --> /globals/:globalSlug/versions
|
||||||
// --> /globals/:globalSlug/preview
|
// --> /globals/:globalSlug/preview
|
||||||
@@ -203,6 +313,56 @@ export const getViewFromConfig = ({
|
|||||||
|
|
||||||
templateClassName = `global-edit`
|
templateClassName = `global-edit`
|
||||||
templateType = 'default'
|
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
|
break
|
||||||
}
|
}
|
||||||
@@ -214,7 +374,9 @@ export const getViewFromConfig = ({
|
|||||||
return {
|
return {
|
||||||
DefaultView: ViewToRender,
|
DefaultView: ViewToRender,
|
||||||
initPageOptions,
|
initPageOptions,
|
||||||
|
serverProps,
|
||||||
templateClassName,
|
templateClassName,
|
||||||
templateType,
|
templateType,
|
||||||
|
viewActions: viewActions.reverse(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import type { I18nClient } from '@payloadcms/translations'
|
import type { I18nClient } from '@payloadcms/translations'
|
||||||
import type { Metadata } from 'next'
|
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 { notFound, redirect } from 'next/navigation.js'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import { DefaultTemplate } from '../../templates/Default/index.js'
|
import { DefaultTemplate } from '../../templates/Default/index.js'
|
||||||
import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
||||||
|
import { getClientConfig } from '../../utilities/getClientConfig.js'
|
||||||
import { initPage } from '../../utilities/initPage/index.js'
|
import { initPage } from '../../utilities/initPage/index.js'
|
||||||
import { getViewFromConfig } from './getViewFromConfig.js'
|
import { getViewFromConfig } from './getViewFromConfig.js'
|
||||||
|
|
||||||
@@ -55,7 +57,14 @@ export const RootPage = async ({
|
|||||||
|
|
||||||
const searchParams = await searchParamsPromise
|
const searchParams = await searchParamsPromise
|
||||||
|
|
||||||
const { DefaultView, initPageOptions, templateClassName, templateType } = getViewFromConfig({
|
const {
|
||||||
|
DefaultView,
|
||||||
|
initPageOptions,
|
||||||
|
serverProps,
|
||||||
|
templateClassName,
|
||||||
|
templateType,
|
||||||
|
viewActions,
|
||||||
|
} = getViewFromConfig({
|
||||||
adminRoute,
|
adminRoute,
|
||||||
config,
|
config,
|
||||||
currentRoute,
|
currentRoute,
|
||||||
@@ -64,21 +73,22 @@ export const RootPage = async ({
|
|||||||
segments,
|
segments,
|
||||||
})
|
})
|
||||||
|
|
||||||
let dbHasUser = false
|
|
||||||
|
|
||||||
const initPageResult = await initPage(initPageOptions)
|
const initPageResult = await initPage(initPageOptions)
|
||||||
|
|
||||||
dbHasUser = await initPageResult?.req.payload.db
|
const dbHasUser =
|
||||||
.findOne({
|
initPageResult.req.user ||
|
||||||
collection: userSlug,
|
(await initPageResult?.req.payload.db
|
||||||
req: initPageResult?.req,
|
.findOne({
|
||||||
})
|
collection: userSlug,
|
||||||
?.then((doc) => !!doc)
|
req: initPageResult?.req,
|
||||||
|
})
|
||||||
|
?.then((doc) => !!doc))
|
||||||
|
|
||||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
||||||
if (initPageResult?.req?.user) {
|
if (initPageResult?.req?.user) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dbHasUser) {
|
if (dbHasUser) {
|
||||||
redirect(adminRoute)
|
redirect(adminRoute)
|
||||||
}
|
}
|
||||||
@@ -111,27 +121,30 @@ export const RootPage = async ({
|
|||||||
redirect(adminRoute)
|
redirect(adminRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createMappedView = getCreateMappedComponent({
|
const clientConfig = await getClientConfig({
|
||||||
importMap,
|
config,
|
||||||
serverProps: {
|
i18n: initPageResult?.req.i18n,
|
||||||
i18n: initPageResult?.req.i18n,
|
|
||||||
importMap,
|
|
||||||
initPageResult,
|
|
||||||
params,
|
|
||||||
payload: initPageResult?.req.payload,
|
|
||||||
searchParams,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const MappedView: MappedComponent = createMappedView(
|
const RenderedView = (
|
||||||
DefaultView.payloadComponent,
|
<RenderServerComponent
|
||||||
undefined,
|
clientProps={{ clientConfig }}
|
||||||
DefaultView.Component,
|
Component={DefaultView.payloadComponent}
|
||||||
'createMappedView',
|
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 (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{!templateType && <Fragment>{RenderedView}</Fragment>}
|
{!templateType && <Fragment>{RenderedView}</Fragment>}
|
||||||
@@ -147,6 +160,7 @@ export const RootPage = async ({
|
|||||||
permissions={initPageResult?.permissions}
|
permissions={initPageResult?.permissions}
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
user={initPageResult?.req.user}
|
user={initPageResult?.req.user}
|
||||||
|
viewActions={viewActions}
|
||||||
visibleEntities={{
|
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
|
// 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
|
// which this caused as soon as initPageResult.visibleEntities is passed in
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { ClientCollectionConfig, ClientGlobalConfig, OptionObject } from 'payload'
|
import type { ClientCollectionConfig, ClientGlobalConfig, OptionObject } from 'payload'
|
||||||
|
|
||||||
import {
|
import { Gutter, useConfig, useDocumentInfo, usePayloadAPI, useTranslation } from '@payloadcms/ui'
|
||||||
Gutter,
|
|
||||||
SetViewActions,
|
|
||||||
useConfig,
|
|
||||||
useDocumentInfo,
|
|
||||||
usePayloadAPI,
|
|
||||||
useTranslation,
|
|
||||||
} from '@payloadcms/ui'
|
|
||||||
import { formatDate } from '@payloadcms/ui/shared'
|
import { formatDate } from '@payloadcms/ui/shared'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
@@ -80,11 +73,6 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={baseClass}>
|
<main className={baseClass}>
|
||||||
<SetViewActions
|
|
||||||
actions={
|
|
||||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.version?.actions
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<SetStepNav
|
<SetStepNav
|
||||||
collectionConfig={collectionConfig}
|
collectionConfig={collectionConfig}
|
||||||
collectionSlug={collectionSlug}
|
collectionSlug={collectionSlug}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { I18n } from '@payloadcms/translations'
|
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 { type Column, SortColumn } from '@payloadcms/ui'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -11,6 +17,7 @@ import { IDCell } from './cells/ID/index.js'
|
|||||||
export const buildVersionColumns = ({
|
export const buildVersionColumns = ({
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
docID,
|
docID,
|
||||||
|
docs,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
i18n: { t },
|
i18n: { t },
|
||||||
latestDraftVersion,
|
latestDraftVersion,
|
||||||
@@ -19,6 +26,7 @@ export const buildVersionColumns = ({
|
|||||||
collectionConfig?: SanitizedCollectionConfig
|
collectionConfig?: SanitizedCollectionConfig
|
||||||
config: SanitizedConfig
|
config: SanitizedConfig
|
||||||
docID?: number | string
|
docID?: number | string
|
||||||
|
docs: PaginatedDocs<TypeWithVersion<any>>['docs']
|
||||||
globalConfig?: SanitizedGlobalConfig
|
globalConfig?: SanitizedGlobalConfig
|
||||||
i18n: I18n
|
i18n: I18n
|
||||||
latestDraftVersion?: string
|
latestDraftVersion?: string
|
||||||
@@ -30,56 +38,37 @@ export const buildVersionColumns = ({
|
|||||||
{
|
{
|
||||||
accessor: 'updatedAt',
|
accessor: 'updatedAt',
|
||||||
active: true,
|
active: true,
|
||||||
cellProps: {
|
field: {
|
||||||
field: {
|
name: '',
|
||||||
name: '',
|
type: 'date',
|
||||||
type: 'date',
|
|
||||||
admin: {
|
|
||||||
components: {
|
|
||||||
Cell: {
|
|
||||||
type: 'client',
|
|
||||||
Component: null,
|
|
||||||
RenderedComponent: (
|
|
||||||
<CreatedAtCell
|
|
||||||
collectionSlug={collectionConfig?.slug}
|
|
||||||
docID={docID}
|
|
||||||
globalSlug={globalConfig?.slug}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
Label: {
|
|
||||||
type: 'client',
|
|
||||||
Component: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Heading: <SortColumn Label={t('general:updatedAt')} name="updatedAt" />,
|
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',
|
accessor: 'id',
|
||||||
active: true,
|
active: true,
|
||||||
cellProps: {
|
field: {
|
||||||
field: {
|
name: '',
|
||||||
name: '',
|
type: 'text',
|
||||||
type: 'text',
|
|
||||||
admin: {
|
|
||||||
components: {
|
|
||||||
Cell: {
|
|
||||||
type: 'client',
|
|
||||||
Component: null,
|
|
||||||
RenderedComponent: <IDCell />,
|
|
||||||
},
|
|
||||||
Label: {
|
|
||||||
type: 'client',
|
|
||||||
Component: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Heading: <SortColumn disable Label={t('version:versionID')} name="id" />,
|
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({
|
columns.push({
|
||||||
accessor: '_status',
|
accessor: '_status',
|
||||||
active: true,
|
active: true,
|
||||||
cellProps: {
|
field: {
|
||||||
field: {
|
name: '',
|
||||||
name: '',
|
type: 'checkbox',
|
||||||
type: 'checkbox',
|
|
||||||
admin: {
|
|
||||||
components: {
|
|
||||||
Cell: {
|
|
||||||
type: 'client',
|
|
||||||
Component: null,
|
|
||||||
RenderedComponent: (
|
|
||||||
<AutosaveCell
|
|
||||||
latestDraftVersion={latestDraftVersion}
|
|
||||||
latestPublishedVersion={latestPublishedVersion}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
Label: {
|
|
||||||
type: 'client',
|
|
||||||
Component: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Heading: <SortColumn disable Label={t('version:status')} name="status" />,
|
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'
|
'use client'
|
||||||
import { Pill, useConfig, useTableCell, useTranslation } from '@payloadcms/ui'
|
import { Pill, useConfig, useTranslation } from '@payloadcms/ui'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
type AutosaveCellProps = {
|
type AutosaveCellProps = {
|
||||||
latestDraftVersion?: string
|
latestDraftVersion?: string
|
||||||
latestPublishedVersion?: string
|
latestPublishedVersion?: string
|
||||||
|
rowData?: {
|
||||||
|
autosave?: boolean
|
||||||
|
publishedLocale?: string
|
||||||
|
version: {
|
||||||
|
_status?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const renderPill = (data, latestVersion, currentLabel, previousLabel, pillStyle) => {
|
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> = ({
|
export const AutosaveCell: React.FC<AutosaveCellProps> = ({
|
||||||
latestDraftVersion,
|
latestDraftVersion,
|
||||||
latestPublishedVersion,
|
latestPublishedVersion,
|
||||||
|
rowData = { autosave: undefined, publishedLocale: undefined, version: undefined },
|
||||||
}) => {
|
}) => {
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
const { rowData } = useTableCell()
|
|
||||||
const {
|
const {
|
||||||
config: { localization },
|
config: { localization },
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useConfig, useTableCell, useTranslation } from '@payloadcms/ui'
|
import { useConfig, useTranslation } from '@payloadcms/ui'
|
||||||
import { formatAdminURL, formatDate } from '@payloadcms/ui/shared'
|
import { formatAdminURL, formatDate } from '@payloadcms/ui/shared'
|
||||||
import LinkImport from 'next/link.js'
|
import LinkImport from 'next/link.js'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -10,12 +10,17 @@ type CreatedAtCellProps = {
|
|||||||
collectionSlug?: string
|
collectionSlug?: string
|
||||||
docID?: number | string
|
docID?: number | string
|
||||||
globalSlug?: string
|
globalSlug?: string
|
||||||
|
rowData?: {
|
||||||
|
id: number | string
|
||||||
|
updatedAt: Date | number | string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
docID,
|
docID,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
|
rowData: { id, updatedAt } = {},
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
config: {
|
config: {
|
||||||
@@ -26,30 +31,25 @@ export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
|||||||
|
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
const { cellData, rowData } = useTableCell()
|
|
||||||
|
|
||||||
const versionID = rowData.id
|
|
||||||
|
|
||||||
let to: string
|
let to: string
|
||||||
|
|
||||||
if (collectionSlug) {
|
if (collectionSlug) {
|
||||||
to = formatAdminURL({
|
to = formatAdminURL({
|
||||||
adminRoute,
|
adminRoute,
|
||||||
path: `/collections/${collectionSlug}/${docID}/versions/${versionID}`,
|
path: `/collections/${collectionSlug}/${docID}/versions/${id}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (globalSlug) {
|
if (globalSlug) {
|
||||||
to = formatAdminURL({
|
to = formatAdminURL({
|
||||||
adminRoute,
|
adminRoute,
|
||||||
path: `/globals/${globalSlug}/versions/${versionID}`,
|
path: `/globals/${globalSlug}/versions/${id}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={to} prefetch={false}>
|
<Link href={to} prefetch={false}>
|
||||||
{cellData &&
|
{formatDate({ date: updatedAt, i18n, pattern: dateFormat })}
|
||||||
formatDate({ date: cellData as Date | number | string, i18n, pattern: dateFormat })}
|
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useTableCell } from '@payloadcms/ui'
|
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
export const IDCell: React.FC = () => {
|
export function IDCell({ id }: { id: number | string }) {
|
||||||
const { cellData } = useTableCell()
|
return <Fragment>{id}</Fragment>
|
||||||
return <Fragment>{cellData as number | string}</Fragment>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { ClientCollectionConfig, ClientGlobalConfig, SanitizedCollectionConfig } from 'payload'
|
import type { SanitizedCollectionConfig } from 'payload'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type Column,
|
type Column,
|
||||||
LoadingOverlayToggle,
|
LoadingOverlayToggle,
|
||||||
Pagination,
|
Pagination,
|
||||||
PerPage,
|
PerPage,
|
||||||
SetViewActions,
|
|
||||||
Table,
|
Table,
|
||||||
useConfig,
|
|
||||||
useDocumentInfo,
|
|
||||||
useListQuery,
|
useListQuery,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
@@ -24,14 +21,8 @@ export const VersionsViewClient: React.FC<{
|
|||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { baseClass, columns, paginationLimits } = props
|
const { baseClass, columns, paginationLimits } = props
|
||||||
|
|
||||||
const { collectionSlug, globalSlug } = useDocumentInfo()
|
|
||||||
const { data, handlePageChange, handlePerPageChange } = useListQuery()
|
const { data, handlePageChange, handlePerPageChange } = useListQuery()
|
||||||
|
|
||||||
const { getEntityConfig } = useConfig()
|
|
||||||
|
|
||||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
|
||||||
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
|
||||||
|
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const limit = searchParams.get('limit')
|
const limit = searchParams.get('limit')
|
||||||
|
|
||||||
@@ -41,11 +32,6 @@ export const VersionsViewClient: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<SetViewActions
|
|
||||||
actions={
|
|
||||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.versions?.actions
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<LoadingOverlayToggle name="versions" show={!data} />
|
<LoadingOverlayToggle name="versions" show={!data} />
|
||||||
{versionCount === 0 && (
|
{versionCount === 0 && (
|
||||||
<div className={`${baseClass}__no-versions`}>
|
<div className={`${baseClass}__no-versions`}>
|
||||||
@@ -54,11 +40,7 @@ export const VersionsViewClient: React.FC<{
|
|||||||
)}
|
)}
|
||||||
{versionCount > 0 && (
|
{versionCount > 0 && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Table
|
<Table columns={columns} data={data?.docs} />
|
||||||
columns={columns}
|
|
||||||
data={data?.docs}
|
|
||||||
fields={(collectionConfig || globalConfig)?.fields}
|
|
||||||
/>
|
|
||||||
<div className={`${baseClass}__page-controls`}>
|
<div className={`${baseClass}__page-controls`}>
|
||||||
<Pagination
|
<Pagination
|
||||||
hasNextPage={data.hasNextPage}
|
hasNextPage={data.hasNextPage}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { EditViewComponent, PaginatedDocs, PayloadServerReactComponent } from 'payload'
|
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 { notFound } from 'next/navigation.js'
|
||||||
import { isNumber } from 'payload/shared'
|
import { isNumber } from 'payload/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
|
||||||
import { buildVersionColumns } from './buildColumns.js'
|
import { buildVersionColumns } from './buildColumns.js'
|
||||||
import { getLatestVersion } from './getLatestVersion.js'
|
import { getLatestVersion } from './getLatestVersion.js'
|
||||||
import { VersionsViewClient } from './index.client.js'
|
import { VersionsViewClient } from './index.client.js'
|
||||||
@@ -165,6 +164,7 @@ export const VersionsView: PayloadServerReactComponent<EditViewComponent> = asyn
|
|||||||
collectionConfig,
|
collectionConfig,
|
||||||
config,
|
config,
|
||||||
docID: id,
|
docID: id,
|
||||||
|
docs: versionsData?.docs,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
i18n,
|
i18n,
|
||||||
latestDraftVersion: latestDraftVersion?.id,
|
latestDraftVersion: latestDraftVersion?.id,
|
||||||
@@ -190,6 +190,7 @@ export const VersionsView: PayloadServerReactComponent<EditViewComponent> = asyn
|
|||||||
<main className={baseClass}>
|
<main className={baseClass}>
|
||||||
<Gutter className={`${baseClass}__wrap`}>
|
<Gutter className={`${baseClass}__wrap`}>
|
||||||
<ListQueryProvider
|
<ListQueryProvider
|
||||||
|
collectionSlug={collectionSlug}
|
||||||
data={versionsData}
|
data={versionsData}
|
||||||
defaultLimit={limitToUse}
|
defaultLimit={limitToUse}
|
||||||
defaultSort={sort as string}
|
defaultSort={sort as string}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"src/**/*.tsx",
|
"src/**/*.tsx",
|
||||||
"src/withPayload.js" /* Include the withPayload.js file in the build */
|
"src/withPayload.js" /* Include the withPayload.js file in the build */
|
||||||
],
|
, "../ui/src/utilities/renderFields.tsx" ],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../payload" },
|
{ "path": "../payload" },
|
||||||
{ "path": "../ui" },
|
{ "path": "../ui" },
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import type { JSONSchema4 } from 'json-schema'
|
|||||||
import type { ImportMap } from '../bin/generateImportMap/index.js'
|
import type { ImportMap } from '../bin/generateImportMap/index.js'
|
||||||
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||||
import type { Config, PayloadComponent, SanitizedConfig } from '../config/types.js'
|
import type { Config, PayloadComponent, SanitizedConfig } from '../config/types.js'
|
||||||
|
import type { ValidationFieldError } from '../errors/ValidationError.js'
|
||||||
import type {
|
import type {
|
||||||
Field,
|
|
||||||
FieldAffectingData,
|
FieldAffectingData,
|
||||||
RichTextField,
|
RichTextField,
|
||||||
RichTextFieldClient,
|
RichTextFieldClient,
|
||||||
@@ -15,7 +15,7 @@ import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
|||||||
import type { RequestContext } from '../index.js'
|
import type { RequestContext } from '../index.js'
|
||||||
import type { JsonObject, Payload, PayloadRequest, PopulateType } from '../types/index.js'
|
import type { JsonObject, Payload, PayloadRequest, PopulateType } from '../types/index.js'
|
||||||
import type { RichTextFieldClientProps } from './fields/RichText.js'
|
import type { RichTextFieldClientProps } from './fields/RichText.js'
|
||||||
import type { CreateMappedComponent } from './types.js'
|
import type { FieldSchemaMap } from './types.js'
|
||||||
|
|
||||||
export type AfterReadRichTextHookArgs<
|
export type AfterReadRichTextHookArgs<
|
||||||
TData extends TypeWithID = any,
|
TData extends TypeWithID = any,
|
||||||
@@ -91,7 +91,7 @@ export type BeforeChangeRichTextHookArgs<
|
|||||||
|
|
||||||
duplicate?: boolean
|
duplicate?: boolean
|
||||||
|
|
||||||
errors?: { field: string; message: string }[]
|
errors?: ValidationFieldError[]
|
||||||
/** Only available in `beforeChange` field hooks */
|
/** Only available in `beforeChange` field hooks */
|
||||||
mergeLocaleActions?: (() => Promise<void>)[]
|
mergeLocaleActions?: (() => Promise<void>)[]
|
||||||
/** A string relating to which operation the field type is currently executing within. */
|
/** A string relating to which operation the field type is currently executing within. */
|
||||||
@@ -184,32 +184,19 @@ export type RichTextHooks = {
|
|||||||
beforeChange?: BeforeChangeRichTextHook[]
|
beforeChange?: BeforeChangeRichTextHook[]
|
||||||
beforeValidate?: BeforeValidateRichTextHook[]
|
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<
|
type RichTextAdapterBase<
|
||||||
Value extends object = object,
|
Value extends object = object,
|
||||||
AdapterProps = any,
|
AdapterProps = any,
|
||||||
ExtraFieldProperties = {},
|
ExtraFieldProperties = {},
|
||||||
> = {
|
> = {
|
||||||
generateComponentMap: PayloadComponent<any, never>
|
|
||||||
generateImportMap?: Config['admin']['importMap']['generators'][0]
|
generateImportMap?: Config['admin']['importMap']['generators'][0]
|
||||||
generateSchemaMap?: (args: {
|
generateSchemaMap?: (args: {
|
||||||
config: SanitizedConfig
|
config: SanitizedConfig
|
||||||
field: RichTextField
|
field: RichTextField
|
||||||
i18n: I18n<any, any>
|
i18n: I18n<any, any>
|
||||||
schemaMap: Map<string, Field[]>
|
schemaMap: FieldSchemaMap
|
||||||
schemaPath: string
|
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.
|
* 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 { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||||
import type { ClientField } from '../../fields/config/client.js'
|
import type { ClientField } from '../../fields/config/client.js'
|
||||||
|
|
||||||
export type RowData = Record<string, any>
|
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 className?: string
|
||||||
|
readonly collectionConfig: ClientCollectionConfig
|
||||||
|
readonly columnIndex?: number
|
||||||
|
readonly customCellProps?: Record<string, any>
|
||||||
readonly field: TField
|
readonly field: TField
|
||||||
readonly link?: boolean
|
readonly link?: boolean
|
||||||
readonly onClick?: (args: {
|
readonly onClick?: (args: {
|
||||||
@@ -12,13 +17,5 @@ export type CellComponentProps<TField extends ClientField = ClientField> = {
|
|||||||
collectionSlug: SanitizedCollectionConfig['slug']
|
collectionSlug: SanitizedCollectionConfig['slug']
|
||||||
rowData: RowData
|
rowData: RowData
|
||||||
}) => void
|
}) => void
|
||||||
}
|
|
||||||
|
|
||||||
export type DefaultCellComponentProps<TCellData = any, TField extends ClientField = ClientField> = {
|
|
||||||
readonly cellData: TCellData
|
|
||||||
readonly customCellContext?: {
|
|
||||||
collectionSlug?: SanitizedCollectionConfig['slug']
|
|
||||||
uploadConfig?: SanitizedCollectionConfig['upload']
|
|
||||||
}
|
|
||||||
readonly rowData: RowData
|
readonly rowData: RowData
|
||||||
} & CellComponentProps<TField>
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { MarkOptional } from 'ts-essentials'
|
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 { ArrayFieldValidation } from '../../fields/validations.js'
|
||||||
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
|
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
|
||||||
import type {
|
import type {
|
||||||
@@ -14,15 +14,14 @@ import type {
|
|||||||
FieldDescriptionServerComponent,
|
FieldDescriptionServerComponent,
|
||||||
FieldLabelClientComponent,
|
FieldLabelClientComponent,
|
||||||
FieldLabelServerComponent,
|
FieldLabelServerComponent,
|
||||||
MappedComponent,
|
|
||||||
} from '../types.js'
|
} from '../types.js'
|
||||||
|
|
||||||
type ArrayFieldClientWithoutType = MarkOptional<ArrayFieldClient, 'type'>
|
type ArrayFieldClientWithoutType = MarkOptional<ArrayFieldClient, 'type'>
|
||||||
|
|
||||||
type ArrayFieldBaseClientProps = {
|
type ArrayFieldBaseClientProps = {
|
||||||
readonly CustomRowLabel?: MappedComponent
|
readonly path?: string
|
||||||
readonly validate?: ArrayFieldValidation
|
readonly validate?: ArrayFieldValidation
|
||||||
}
|
} & Pick<ServerFieldBase, 'permissions'>
|
||||||
|
|
||||||
export type ArrayFieldClientProps = ArrayFieldBaseClientProps &
|
export type ArrayFieldClientProps = ArrayFieldBaseClientProps &
|
||||||
ClientFieldBase<ArrayFieldClientWithoutType>
|
ClientFieldBase<ArrayFieldClientWithoutType>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { MarkOptional } from 'ts-essentials'
|
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 { BlocksFieldValidation } from '../../fields/validations.js'
|
||||||
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
|
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
|
||||||
import type {
|
import type {
|
||||||
@@ -19,8 +19,9 @@ import type {
|
|||||||
type BlocksFieldClientWithoutType = MarkOptional<BlocksFieldClient, 'type'>
|
type BlocksFieldClientWithoutType = MarkOptional<BlocksFieldClient, 'type'>
|
||||||
|
|
||||||
type BlocksFieldBaseClientProps = {
|
type BlocksFieldBaseClientProps = {
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: BlocksFieldValidation
|
readonly validate?: BlocksFieldValidation
|
||||||
}
|
} & Pick<ServerFieldBase, 'permissions'>
|
||||||
|
|
||||||
export type BlocksFieldClientProps = BlocksFieldBaseClientProps &
|
export type BlocksFieldClientProps = BlocksFieldBaseClientProps &
|
||||||
ClientFieldBase<BlocksFieldClientWithoutType>
|
ClientFieldBase<BlocksFieldClientWithoutType>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type CheckboxFieldBaseClientProps = {
|
|||||||
readonly id?: string
|
readonly id?: string
|
||||||
readonly onChange?: (value: boolean) => void
|
readonly onChange?: (value: boolean) => void
|
||||||
readonly partialChecked?: boolean
|
readonly partialChecked?: boolean
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: CheckboxFieldValidation
|
readonly validate?: CheckboxFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ type CodeFieldClientWithoutType = MarkOptional<CodeFieldClient, 'type'>
|
|||||||
|
|
||||||
type CodeFieldBaseClientProps = {
|
type CodeFieldBaseClientProps = {
|
||||||
readonly autoComplete?: string
|
readonly autoComplete?: string
|
||||||
readonly valiCode?: CodeFieldValidation
|
readonly path?: string
|
||||||
|
readonly validate?: CodeFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CodeFieldClientProps = ClientFieldBase<CodeFieldClientWithoutType> &
|
export type CodeFieldClientProps = ClientFieldBase<CodeFieldClientWithoutType> &
|
||||||
|
|||||||
@@ -15,9 +15,14 @@ import type {
|
|||||||
FieldLabelServerComponent,
|
FieldLabelServerComponent,
|
||||||
} from '../types.js'
|
} from '../types.js'
|
||||||
|
|
||||||
|
type CollapsibleFieldBaseClientProps = {
|
||||||
|
readonly path?: string
|
||||||
|
} & Pick<ServerFieldBase, 'permissions'>
|
||||||
|
|
||||||
type CollapsibleFieldClientWithoutType = MarkOptional<CollapsibleFieldClient, 'type'>
|
type CollapsibleFieldClientWithoutType = MarkOptional<CollapsibleFieldClient, 'type'>
|
||||||
|
|
||||||
export type CollapsibleFieldClientProps = ClientFieldBase<CollapsibleFieldClientWithoutType>
|
export type CollapsibleFieldClientProps = ClientFieldBase<CollapsibleFieldClientWithoutType> &
|
||||||
|
CollapsibleFieldBaseClientProps
|
||||||
|
|
||||||
export type CollapsibleFieldServerProps = ServerFieldBase<
|
export type CollapsibleFieldServerProps = ServerFieldBase<
|
||||||
CollapsibleField,
|
CollapsibleField,
|
||||||
@@ -29,8 +34,10 @@ export type CollapsibleFieldServerComponent = FieldServerComponent<
|
|||||||
CollapsibleFieldClientWithoutType
|
CollapsibleFieldClientWithoutType
|
||||||
>
|
>
|
||||||
|
|
||||||
export type CollapsibleFieldClientComponent =
|
export type CollapsibleFieldClientComponent = FieldClientComponent<
|
||||||
FieldClientComponent<CollapsibleFieldClientWithoutType>
|
CollapsibleFieldClientWithoutType,
|
||||||
|
CollapsibleFieldBaseClientProps
|
||||||
|
>
|
||||||
|
|
||||||
export type CollapsibleFieldLabelServerComponent = FieldLabelServerComponent<
|
export type CollapsibleFieldLabelServerComponent = FieldLabelServerComponent<
|
||||||
CollapsibleField,
|
CollapsibleField,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
type DateFieldClientWithoutType = MarkOptional<DateFieldClient, 'type'>
|
type DateFieldClientWithoutType = MarkOptional<DateFieldClient, 'type'>
|
||||||
|
|
||||||
type DateFieldBaseClientProps = {
|
type DateFieldBaseClientProps = {
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: DateFieldValidation
|
readonly validate?: DateFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import type {
|
|||||||
type EmailFieldClientWithoutType = MarkOptional<EmailFieldClient, 'type'>
|
type EmailFieldClientWithoutType = MarkOptional<EmailFieldClient, 'type'>
|
||||||
|
|
||||||
type EmailFieldBaseClientProps = {
|
type EmailFieldBaseClientProps = {
|
||||||
readonly autoComplete?: string
|
readonly path?: string
|
||||||
readonly validate?: EmailFieldValidation
|
readonly validate?: EmailFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ import type {
|
|||||||
|
|
||||||
type GroupFieldClientWithoutType = MarkOptional<GroupFieldClient, '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>
|
export type GroupFieldServerProps = ServerFieldBase<GroupField, GroupFieldClientWithoutType>
|
||||||
|
|
||||||
@@ -26,7 +31,10 @@ export type GroupFieldServerComponent = FieldServerComponent<
|
|||||||
GroupFieldClientWithoutType
|
GroupFieldClientWithoutType
|
||||||
>
|
>
|
||||||
|
|
||||||
export type GroupFieldClientComponent = FieldClientComponent<GroupFieldClientWithoutType>
|
export type GroupFieldClientComponent = FieldClientComponent<
|
||||||
|
GroupFieldClientWithoutType,
|
||||||
|
GroupFieldBaseClientProps
|
||||||
|
>
|
||||||
|
|
||||||
export type GroupFieldLabelServerComponent = FieldLabelServerComponent<
|
export type GroupFieldLabelServerComponent = FieldLabelServerComponent<
|
||||||
GroupField,
|
GroupField,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { ClientField } from '../../fields/config/client.js'
|
import type { ClientField } from '../../fields/config/types.js'
|
||||||
import type { FormFieldBase } from '../types.js'
|
import type { ClientFieldBase } from '../types.js'
|
||||||
|
|
||||||
export type HiddenFieldProps = {
|
type HiddenFieldBaseClientProps = {
|
||||||
readonly disableModifyingForm?: false
|
readonly disableModifyingForm?: false
|
||||||
readonly field?: {
|
readonly field?: {
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
} & Pick<ClientField, '_path'>
|
} & ClientField
|
||||||
readonly forceUsePathFromProps?: boolean
|
|
||||||
readonly value?: unknown
|
readonly value?: unknown
|
||||||
} & FormFieldBase
|
}
|
||||||
|
|
||||||
|
export type HiddenFieldProps = ClientFieldBase & HiddenFieldBaseClientProps
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
type JSONFieldClientWithoutType = MarkOptional<JSONFieldClient, 'type'>
|
type JSONFieldClientWithoutType = MarkOptional<JSONFieldClient, 'type'>
|
||||||
|
|
||||||
type JSONFieldBaseClientProps = {
|
type JSONFieldBaseClientProps = {
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: JSONFieldValidation
|
readonly validate?: JSONFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import type {
|
|||||||
|
|
||||||
type JoinFieldClientWithoutType = MarkOptional<JoinFieldClient, 'type'>
|
type JoinFieldClientWithoutType = MarkOptional<JoinFieldClient, 'type'>
|
||||||
|
|
||||||
export type JoinFieldClientProps = ClientFieldBase<JoinFieldClientWithoutType>
|
export type JoinFieldClientProps = {
|
||||||
|
path?: string
|
||||||
|
} & ClientFieldBase<JoinFieldClientWithoutType>
|
||||||
|
|
||||||
export type JoinFieldServerProps = ServerFieldBase<JoinField>
|
export type JoinFieldServerProps = ServerFieldBase<JoinField>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type NumberFieldClientWithoutType = MarkOptional<NumberFieldClient, 'type'>
|
|||||||
|
|
||||||
type NumberFieldBaseClientProps = {
|
type NumberFieldBaseClientProps = {
|
||||||
readonly onChange?: (e: number) => void
|
readonly onChange?: (e: number) => void
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: NumberFieldValidation
|
readonly validate?: NumberFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
type PointFieldClientWithoutType = MarkOptional<PointFieldClient, 'type'>
|
type PointFieldClientWithoutType = MarkOptional<PointFieldClient, 'type'>
|
||||||
|
|
||||||
type PointFieldBaseClientProps = {
|
type PointFieldBaseClientProps = {
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: PointFieldValidation
|
readonly validate?: PointFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type RadioFieldBaseClientProps = {
|
|||||||
*/
|
*/
|
||||||
readonly disableModifyingForm?: boolean
|
readonly disableModifyingForm?: boolean
|
||||||
readonly onChange?: OnChange
|
readonly onChange?: OnChange
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: RadioFieldValidation
|
readonly validate?: RadioFieldValidation
|
||||||
readonly value?: string
|
readonly value?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
type RelationshipFieldClientWithoutType = MarkOptional<RelationshipFieldClient, 'type'>
|
type RelationshipFieldClientWithoutType = MarkOptional<RelationshipFieldClient, 'type'>
|
||||||
|
|
||||||
type RelationshipFieldBaseClientProps = {
|
type RelationshipFieldBaseClientProps = {
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: RelationshipFieldValidation
|
readonly validate?: RelationshipFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,18 @@ import type {
|
|||||||
FieldLabelServerComponent,
|
FieldLabelServerComponent,
|
||||||
} from '../types.js'
|
} 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<
|
type RichTextFieldBaseClientProps<
|
||||||
TValue extends object = any,
|
TValue extends object = any,
|
||||||
TAdapterProps = any,
|
TAdapterProps = any,
|
||||||
TExtraProperties = object,
|
TExtraProperties = object,
|
||||||
> = {
|
> = {
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: RichTextFieldValidation
|
readonly validate?: RichTextFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +35,7 @@ export type RichTextFieldClientProps<
|
|||||||
TValue extends object = any,
|
TValue extends object = any,
|
||||||
TAdapterProps = any,
|
TAdapterProps = any,
|
||||||
TExtraProperties = object,
|
TExtraProperties = object,
|
||||||
> = ClientFieldBase<RichTextFieldClientWithoutType> &
|
> = ClientFieldBase<RichTextFieldClientWithoutType<TValue, TAdapterProps, TExtraProperties>> &
|
||||||
RichTextFieldBaseClientProps<TValue, TAdapterProps, TExtraProperties>
|
RichTextFieldBaseClientProps<TValue, TAdapterProps, TExtraProperties>
|
||||||
|
|
||||||
export type RichTextFieldServerProps = ServerFieldBase<
|
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