Compare commits
213 Commits
fix/postgr
...
fix/SetVie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6f0aee553 | ||
|
|
f6c0dcacee | ||
|
|
4a639ccc19 | ||
|
|
d7c57aeeb9 | ||
|
|
cb2771d3a9 | ||
|
|
3176e87a95 | ||
|
|
76f3102d07 | ||
|
|
53f05ad303 | ||
|
|
8e1ef2222d | ||
|
|
40d5ae3165 | ||
|
|
fe69f9ec35 | ||
|
|
a56c6a5757 | ||
|
|
186f886c1d | ||
|
|
2e9af8d258 | ||
|
|
7162ce9ef5 | ||
|
|
b6b58550e1 | ||
|
|
bfd08bbeaf | ||
|
|
290594a232 | ||
|
|
c6325c13cf | ||
|
|
c7cd6e3fbb | ||
|
|
414e68e463 | ||
|
|
a6df86d596 | ||
|
|
78ffe523f5 | ||
|
|
03645d172d | ||
|
|
9f7924b930 | ||
|
|
9f5f909094 | ||
|
|
997ddb4654 | ||
|
|
73ee8b5549 | ||
|
|
8195bd804b | ||
|
|
4395dc8901 | ||
|
|
7668e0907e | ||
|
|
c46946a77c | ||
|
|
4194b8bc61 | ||
|
|
807500d55f | ||
|
|
456eea1344 | ||
|
|
af8fed9ab3 | ||
|
|
38dc313051 | ||
|
|
5a53c6b130 | ||
|
|
d3dd8aef53 | ||
|
|
7b39acc54d | ||
|
|
e7b69ce70f | ||
|
|
91b65b4cc6 | ||
|
|
42f78480ba | ||
|
|
bbe0fa38ae | ||
|
|
d32798a2a0 | ||
|
|
11c505930d | ||
|
|
0dcb109101 | ||
|
|
95a2bc4d1e | ||
|
|
3f9c7e2acd | ||
|
|
50a1770d7e | ||
|
|
9bf24c0379 | ||
|
|
2429f64f3a | ||
|
|
5adcff3e76 | ||
|
|
bc155c4a87 | ||
|
|
cb03d5d197 | ||
|
|
b3021b559a | ||
|
|
1bc7d91c4f | ||
|
|
42dd173986 | ||
|
|
d9b188061d | ||
|
|
8e5ec02037 | ||
|
|
1d370e0d69 | ||
|
|
e51067ccaf | ||
|
|
d48cb1b8eb | ||
|
|
acc4432a99 | ||
|
|
be5772b71c | ||
|
|
c1222b5e06 | ||
|
|
24d5bc88f6 | ||
|
|
06750e1f4a | ||
|
|
0aaa4fe643 | ||
|
|
cb6a0249fb | ||
|
|
3383bf3479 | ||
|
|
06536eb275 | ||
|
|
e881dcd4b4 | ||
|
|
67acde45e6 | ||
|
|
d6498e442f | ||
|
|
7e50fc51f1 | ||
|
|
b49f9f92be | ||
|
|
9ffbb3f7f9 | ||
|
|
54301c9088 | ||
|
|
809df54cf0 | ||
|
|
d254a6e622 | ||
|
|
f92dcf68c6 | ||
|
|
c8cba855a2 | ||
|
|
4e5121f6d3 | ||
|
|
c3eefec31f | ||
|
|
7dbd1d861f | ||
|
|
3ff9e34199 | ||
|
|
e7c1f98b50 | ||
|
|
7aa604c0d7 | ||
|
|
fbfa0fd5d6 | ||
|
|
be149b362a | ||
|
|
0e05c5d60d | ||
|
|
91cd7672e3 | ||
|
|
ca8e8becd2 | ||
|
|
b09bd65020 | ||
|
|
7882c83f03 | ||
|
|
7498099ede | ||
|
|
f800cb8dc5 | ||
|
|
f6360d055f | ||
|
|
a597579354 | ||
|
|
7d2fc41f19 | ||
|
|
01ba2c0114 | ||
|
|
f0b431e799 | ||
|
|
b68b625899 | ||
|
|
57f8475780 | ||
|
|
e14a8876ab | ||
|
|
e9cd82bc81 | ||
|
|
054d183a96 | ||
|
|
e03a330fd3 | ||
|
|
9038020dd9 | ||
|
|
1f0e551578 | ||
|
|
f2c5750e78 | ||
|
|
23f136ab82 | ||
|
|
aa38ac9bd8 | ||
|
|
5d3294b341 | ||
|
|
7fddc5fbd9 | ||
|
|
d056b0b964 | ||
|
|
57e9109f93 | ||
|
|
515629b51d | ||
|
|
6ec779fb6c | ||
|
|
84fd8d06f0 | ||
|
|
01b580cb24 | ||
|
|
4d60f9c7da | ||
|
|
fde05840fe | ||
|
|
8ed1766d5c | ||
|
|
6fa47bf854 | ||
|
|
362cd1712d | ||
|
|
e669368149 | ||
|
|
068d7eec52 | ||
|
|
fb861a53ec | ||
|
|
f16e55fff2 | ||
|
|
47c19224c2 | ||
|
|
fd2444e614 | ||
|
|
31ffe3bc43 | ||
|
|
4cd89cd5d7 | ||
|
|
1d7bcb365d | ||
|
|
8ffc090cac | ||
|
|
039bd0f76d | ||
|
|
5235ce819d | ||
|
|
c1d2736e8b | ||
|
|
eca0a25063 | ||
|
|
e737c8db32 | ||
|
|
bc84def8d8 | ||
|
|
2cf4a58e89 | ||
|
|
9c8f623068 | ||
|
|
60edd35671 | ||
|
|
bca9aece06 | ||
|
|
e43a03d0ff | ||
|
|
4424904f58 | ||
|
|
5150a5a30f | ||
|
|
b6d829acd8 | ||
|
|
41d622f613 | ||
|
|
c32c7d50ef | ||
|
|
d59c1c01c9 | ||
|
|
e5ce24eafb | ||
|
|
0de81afa92 | ||
|
|
0f5fe98a1b | ||
|
|
e330f1756f | ||
|
|
c353a0f296 | ||
|
|
a7c1dd057d | ||
|
|
3cbf7b2603 | ||
|
|
a4135e5975 | ||
|
|
7fb860f15b | ||
|
|
e684f3ac2e | ||
|
|
ac25118945 | ||
|
|
9a859a453e | ||
|
|
dc46f18af9 | ||
|
|
271a8c7191 | ||
|
|
0dbc3bad57 | ||
|
|
4f0cb93204 | ||
|
|
b035afe4e3 | ||
|
|
d33f9f5a1c | ||
|
|
ccba668dc1 | ||
|
|
9188fbe396 | ||
|
|
4d66c65958 | ||
|
|
66c767f201 | ||
|
|
4daf22c03c | ||
|
|
30cc5a018c | ||
|
|
71eb66b393 | ||
|
|
b88fabf148 | ||
|
|
25385a4923 | ||
|
|
911d93c207 | ||
|
|
afe19b3c53 | ||
|
|
95c3eb3313 | ||
|
|
b5ea0a787d | ||
|
|
8849655afc | ||
|
|
868698ed47 | ||
|
|
7291adc3c2 | ||
|
|
3c71e2880e | ||
|
|
b840bea4cf | ||
|
|
a5f82d8a16 | ||
|
|
0d109be224 | ||
|
|
c36b6a43a4 | ||
|
|
a154a86350 | ||
|
|
e9815e6ec7 | ||
|
|
cdde8d729d | ||
|
|
487599e2ee | ||
|
|
c7f3278d93 | ||
|
|
4d8159e9aa | ||
|
|
e5956051f2 | ||
|
|
1155c0aa22 | ||
|
|
6ca2f1d28b | ||
|
|
5d3193a164 | ||
|
|
d0af4f2271 | ||
|
|
69c74ecbbc | ||
|
|
76cc178d36 | ||
|
|
b63e18573e | ||
|
|
b61d271bd5 | ||
|
|
ddc57dd5cf | ||
|
|
f53ef13f4b | ||
|
|
168a8c5317 | ||
|
|
5d496c60fa | ||
|
|
72c206551b |
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
180
docs/admin/server-functions.mdx
Normal file
180
docs/admin/server-functions.mdx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
title: Server Functions
|
||||||
|
label: Server Functions
|
||||||
|
order: 100
|
||||||
|
desc: Execute custom server-side logic from client-side code using Server Functions in Payload.
|
||||||
|
keywords: server functions, server-side functions, server-side logic, server-side code, server-side, functions, Payload, headless, Content Management System, cms, javascript, react, node, nextjs
|
||||||
|
---
|
||||||
|
|
||||||
|
The Payload [Admin Panel] supports [React Server Functions](https://react.dev/reference/rsc/server-actions) directly through the Payload Config. Server Functions are functions that are defined on the server, which may use server-only modules, but are called by the client. This is a way to execute server-side logic through a client-side action.
|
||||||
|
|
||||||
|
Server Functions are a good alternative to traditional [REST API Endpoints](../rest-api#custom-endpoints), but with a few key differences. While they behave similarly, Server Functions:
|
||||||
|
|
||||||
|
1. are simpler to define, not requiring a specified route or method
|
||||||
|
2. are easier to consume, not requiring the Fetch API
|
||||||
|
3. are able to return React and/or JSX
|
||||||
|
|
||||||
|
Server Functions do not necessarily need to be defined in the Payload Config. It is possible to write your own Server Functions and thread them to your client accordingly. You will, however, be responsible for authenticating those requests yourself. All Server Functions defined through the Payload Config will automatically receive a `req` argument, containing the `user`, `payload`, and more.
|
||||||
|
|
||||||
|
<Banner type="info">
|
||||||
|
<strong>Note:</strong>
|
||||||
|
Server Functions defined through the Payload Config are only available within the Admin Panel, not your public-facing application. For public-facing server-side logic, you can write your own Server Functions directly into your application.
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
## Admin Options
|
||||||
|
|
||||||
|
To add a new Server Function, use the `admin.serverFunctions` property in your Payload config:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { buildConfig } from 'payload'
|
||||||
|
|
||||||
|
const config = buildConfig({
|
||||||
|
// ...
|
||||||
|
admin: {
|
||||||
|
// highlight-start
|
||||||
|
serverFunctions: [
|
||||||
|
{
|
||||||
|
name: 'my-server-action',
|
||||||
|
fn: ({ req, value }) => `The value is: "${value}"`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
// highlight-end
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The following options are available:
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **name** | `string` | The name of the Server Function. |
|
||||||
|
| **fn** | `Function` | The function to execute. [More details](#function-arguments) |
|
||||||
|
|
||||||
|
### Function Arguments
|
||||||
|
|
||||||
|
The function receives an object with the following properties:
|
||||||
|
|
||||||
|
| Property | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **req** | `PayloadRequest` | The request object, containing `payload`, `user`, and `config` properties. |
|
||||||
|
| **importMap** | `Record<string, any>` | The import map object. |
|
||||||
|
|
||||||
|
## Client-side Usage
|
||||||
|
|
||||||
|
To execute a Server Function from the client, use the `useServerFunctions` hook, passing the `name` of your Server Function:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
'use client'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { useServerFunctions } from '@payloadcms/ui'
|
||||||
|
|
||||||
|
const MyComponent = () => {
|
||||||
|
const { serverFunction } = useServerFunctions()
|
||||||
|
const [result, setResult] = React.useState<string | null>(null)
|
||||||
|
|
||||||
|
const callServerAction = useCallback(async () => {
|
||||||
|
const result = await serverFunction({
|
||||||
|
name: 'my-server-action',
|
||||||
|
args: {
|
||||||
|
value: 'Hello, world!'
|
||||||
|
}
|
||||||
|
}) as string
|
||||||
|
|
||||||
|
setResult(result)
|
||||||
|
}, [serverFunction])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={callServerAction} type="button">
|
||||||
|
{result || 'Call Server Action'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
In order for Payload to support Sever Functions through the Payload Config, a single handler is placed at the root of the application:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/* 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' // highlight-line
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { importMap } from './admin/importMap.js'
|
||||||
|
|
||||||
|
import './custom.scss'
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlight-start
|
||||||
|
const serverFunction: ServerFunctionClient = async function (args) {
|
||||||
|
'use server'
|
||||||
|
return handleServerFunctions({
|
||||||
|
...args,
|
||||||
|
config,
|
||||||
|
importMap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// highlight-end
|
||||||
|
|
||||||
|
const Layout = ({ children }: Args) => (
|
||||||
|
<RootLayout
|
||||||
|
config={config}
|
||||||
|
importMap={importMap}
|
||||||
|
serverFunction={serverFunction} // highlight-line
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RootLayout>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Layout
|
||||||
|
```
|
||||||
|
|
||||||
|
The Server Function Handler is a necessary pattern for Server Functions to have access to the Payload Config, as well as any other server-only modules that may be required. This is because all server-only modules _must_ be imported in the closure as the Server Function, wherever the `use server` directive is used. [More details](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#closures-and-encryption).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
#### "Unknown Server Function: 'my-server-function'"
|
||||||
|
|
||||||
|
Ensure the `name` property of your Server Function matches the name you are passing through the `serverFunction` args.
|
||||||
|
|
||||||
|
#### "Error: Client Functions cannot be passed directly to Server Functions. Only Functions passed from the Server can be passed back again"
|
||||||
|
|
||||||
|
Non-serializable values cannot cross the server / client boundary. Ensure that the args your sending through your Server Function are serializable, i.e. not containing any functions, classes, etc.
|
||||||
|
|
||||||
|
#### "Body exceeded _n_ limit"
|
||||||
|
|
||||||
|
By default, Next.js places a 1mb limit on the body size of incoming requests. However, this can be increased by setting the `bodySizeLimit` option in your `next.config.ts` file. [More details](https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
// ...
|
||||||
|
experimental: {
|
||||||
|
serverActions: {
|
||||||
|
bodySizeLimit: '2mb',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
You can import the Payload `ServerFunction` type as well as other common types from the `payload` package. [More details](../typescript/overview).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type {
|
||||||
|
ServerFunction,
|
||||||
|
ServerFunctionArgs,
|
||||||
|
ServerFunctionClient,
|
||||||
|
ServerFunctionClientArgs,
|
||||||
|
ServerFunctionConfig,
|
||||||
|
DefaultServerFunctionArgs,
|
||||||
|
} from 'payload'
|
||||||
|
```
|
||||||
@@ -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),
|
||||||
|
|||||||
73
packages/next/src/elements/DocumentDrawerHeader/index.scss
Normal file
73
packages/next/src/elements/DocumentDrawerHeader/index.scss
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
@import '../../scss/styles.scss';
|
||||||
|
|
||||||
|
@layer payload-default {
|
||||||
|
.doc-drawer {
|
||||||
|
&__header {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: base(2.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: base(0.5);
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header-text {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toggler {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header-close {
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
width: base(2);
|
||||||
|
height: base(2);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: base(2);
|
||||||
|
height: base(2);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.stroke {
|
||||||
|
stroke-width: 2px;
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include mid-break {
|
||||||
|
&__header {
|
||||||
|
margin-top: base(1.5);
|
||||||
|
margin-bottom: base(0.5);
|
||||||
|
padding-left: var(--gutter-h);
|
||||||
|
padding-right: var(--gutter-h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/next/src/elements/DocumentDrawerHeader/index.tsx
Normal file
34
packages/next/src/elements/DocumentDrawerHeader/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
'use client'
|
||||||
|
import { Gutter, RenderTitle, useModal, useTranslation, XIcon } from '@payloadcms/ui'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
const baseClass = 'doc-drawer'
|
||||||
|
|
||||||
|
export const DocumentDrawerHeader: React.FC<{
|
||||||
|
drawerSlug?: string
|
||||||
|
Header?: React.ReactNode
|
||||||
|
}> = ({ drawerSlug, Header }) => {
|
||||||
|
const { toggleModal } = useModal()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Gutter className={`${baseClass}__header`}>
|
||||||
|
<div className={`${baseClass}__header-content`}>
|
||||||
|
<h2 className={`${baseClass}__header-text`}>{Header || <RenderTitle element="span" />}</h2>
|
||||||
|
{/* TODO: the `button` HTML element breaks CSS transitions on the drawer for some reason...
|
||||||
|
i.e. changing to a `div` element will fix the animation issue but will break accessibility
|
||||||
|
*/}
|
||||||
|
<button
|
||||||
|
aria-label={t('general:close')}
|
||||||
|
className={`${baseClass}__header-close`}
|
||||||
|
onClick={() => toggleModal(drawerSlug)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* <DocumentTitle /> */}
|
||||||
|
</Gutter>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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 { Fragment } from 'react'
|
||||||
import React, { Fragment } from 'react'
|
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { DocumentTabLink } from './TabLink.js'
|
import { DocumentTabLink } from './TabLink.js'
|
||||||
@@ -59,17 +59,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 +71,19 @@ export const DocumentTab: React.FC<
|
|||||||
>
|
>
|
||||||
<span className={`${baseClass}__label`}>
|
<span className={`${baseClass}__label`}>
|
||||||
{labelToRender}
|
{labelToRender}
|
||||||
{mappedPin && (
|
<Fragment>
|
||||||
<Fragment>
|
|
||||||
|
{/* <RenderServerComponent
|
||||||
<RenderComponent mappedComponent={mappedPin} />
|
Component={Pill}
|
||||||
</Fragment>
|
Fallback={Pill_Component}
|
||||||
)}
|
importMap={payload.importMap}
|
||||||
|
serverProps={{
|
||||||
|
i18n,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
</Fragment>
|
||||||
</span>
|
</span>
|
||||||
</DocumentTabLink>
|
</DocumentTabLink>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import type {
|
|||||||
SanitizedGlobalConfig,
|
SanitizedGlobalConfig,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { RenderServerComponent } from '../../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||||
import { getCustomViews } from './getCustomViews.js'
|
import { getCustomViews } from './getCustomViews.js'
|
||||||
import { getViewConfig } from './getViewConfig.js'
|
import { getViewConfig } from './getViewConfig.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
@@ -80,33 +80,24 @@ 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}`,
|
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,
|
||||||
|
key: `tab-custom-${index}`,
|
||||||
|
path,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -121,6 +112,7 @@ export const DocumentTabs: React.FC<{
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const VersionsPill: React.FC = () => {
|
|||||||
const { versions } = useDocumentInfo()
|
const { versions } = useDocumentInfo()
|
||||||
|
|
||||||
// don't count snapshots
|
// don't count snapshots
|
||||||
const totalVersions = versions?.docs.filter((version) => !version.snapshot).length || 0
|
const totalVersions = versions?.docs?.filter((version) => !version.snapshot).length || 0
|
||||||
|
|
||||||
if (!versions?.totalDocs) {
|
if (!versions?.totalDocs) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import type { FieldPermissions, LoginWithUsernameOptions } from 'payload'
|
|
||||||
|
|
||||||
import { EmailField, RenderFields, TextField, useTranslation } from '@payloadcms/ui'
|
|
||||||
import { email, username } from 'payload/shared'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
readonly loginWithUsername?: false | LoginWithUsernameOptions
|
|
||||||
}
|
|
||||||
function EmailFieldComponent(props: Props) {
|
|
||||||
const { loginWithUsername } = props
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const requireEmail = !loginWithUsername || (loginWithUsername && loginWithUsername.requireEmail)
|
|
||||||
const showEmailField =
|
|
||||||
!loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin
|
|
||||||
|
|
||||||
if (showEmailField) {
|
|
||||||
return (
|
|
||||||
<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=""
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -39,6 +39,11 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__create-new-button {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&__toggler {
|
&__toggler {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -90,16 +95,6 @@
|
|||||||
margin-top: base(1);
|
margin-top: base(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__first-cell {
|
|
||||||
border: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mid-break {
|
@include mid-break {
|
||||||
.collection-list__header {
|
.collection-list__header {
|
||||||
margin-bottom: base(0.5);
|
margin-bottom: base(0.5);
|
||||||
89
packages/next/src/elements/ListDrawerHeader/index.tsx
Normal file
89
packages/next/src/elements/ListDrawerHeader/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
'use client'
|
||||||
|
import type { StaticDescription, StaticLabel } from 'payload'
|
||||||
|
|
||||||
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
import {
|
||||||
|
FieldLabel,
|
||||||
|
Pill,
|
||||||
|
ReactSelect,
|
||||||
|
useModal,
|
||||||
|
useTranslation,
|
||||||
|
ViewDescription,
|
||||||
|
XIcon,
|
||||||
|
} from '@payloadcms/ui'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
const baseClass = 'list-drawer'
|
||||||
|
|
||||||
|
export const ListDrawerHeader: React.FC<{
|
||||||
|
CustomDescription?: React.ReactNode
|
||||||
|
customHeader?: string
|
||||||
|
description?: StaticDescription
|
||||||
|
documentDrawerSlug: string
|
||||||
|
drawerSlug: string
|
||||||
|
hasCreatePermission: boolean
|
||||||
|
pluralLabel: StaticLabel
|
||||||
|
}> = ({
|
||||||
|
CustomDescription,
|
||||||
|
customHeader,
|
||||||
|
description,
|
||||||
|
documentDrawerSlug,
|
||||||
|
drawerSlug,
|
||||||
|
hasCreatePermission,
|
||||||
|
pluralLabel,
|
||||||
|
}) => {
|
||||||
|
const { i18n, t } = useTranslation()
|
||||||
|
const { closeModal, openModal } = useModal()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className={`${baseClass}__header`}>
|
||||||
|
<div className={`${baseClass}__header-wrap`}>
|
||||||
|
<div className={`${baseClass}__header-content`}>
|
||||||
|
<h2 className={`${baseClass}__header-text`}>
|
||||||
|
{!customHeader ? getTranslation(pluralLabel, i18n) : customHeader}
|
||||||
|
</h2>
|
||||||
|
{hasCreatePermission && (
|
||||||
|
<button
|
||||||
|
className={`${baseClass}__create-new-button`}
|
||||||
|
onClick={() => openModal(documentDrawerSlug)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Pill>{t('general:createNew')}</Pill>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label={t('general:close')}
|
||||||
|
className={`${baseClass}__header-close`}
|
||||||
|
onClick={() => {
|
||||||
|
closeModal(drawerSlug)
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{description || CustomDescription ? (
|
||||||
|
<div className={`${baseClass}__sub-header`}>
|
||||||
|
{CustomDescription ?? <ViewDescription description={description} />}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{/* {moreThanOneAvailableCollection && (
|
||||||
|
<div className={`${baseClass}__select-collection-wrap`}>
|
||||||
|
<FieldLabel label={t('upload:selectCollectionToBrowse')} />
|
||||||
|
<ReactSelect
|
||||||
|
className={`${baseClass}__select-collection`}
|
||||||
|
onChange={setSelectedOption} // this is only changing the options which is not rerunning my effect
|
||||||
|
options={enabledCollectionConfigs.map((coll) => ({
|
||||||
|
label: getTranslation(coll.labels.singular, i18n),
|
||||||
|
value: coll.slug,
|
||||||
|
}))}
|
||||||
|
value={selectedOption}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { ServerProps } from 'payload'
|
import type { ServerProps } from 'payload'
|
||||||
|
|
||||||
import { getCreateMappedComponent, PayloadLogo, RenderComponent } from '@payloadcms/ui/shared'
|
import { PayloadLogo } from '@payloadcms/ui/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||||
|
|
||||||
export const Logo: React.FC<ServerProps> = (props) => {
|
export const Logo: React.FC<ServerProps> = (props) => {
|
||||||
const { i18n, locale, params, payload, permissions, searchParams, user } = props
|
const { i18n, locale, params, payload, permissions, searchParams, user } = props
|
||||||
|
|
||||||
@@ -16,20 +18,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 ||
|
||||||
@@ -101,7 +62,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,9 +1,11 @@
|
|||||||
|
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 { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { NavHamburger } from './NavHamburger/index.js'
|
import { NavHamburger } from './NavHamburger/index.js'
|
||||||
import { NavWrapper } from './NavWrapper/index.js'
|
import { NavWrapper } from './NavWrapper/index.js'
|
||||||
@@ -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
|
||||||
@@ -25,40 +27,65 @@ export const DefaultNav: React.FC<NavProps> = (props) => {
|
|||||||
admin: {
|
admin: {
|
||||||
components: { afterNavLinks, beforeNavLinks },
|
components: { afterNavLinks, beforeNavLinks },
|
||||||
},
|
},
|
||||||
|
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 />
|
<Logout />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
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 { 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 +39,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 +102,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 +163,29 @@ 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}
|
{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,26 @@
|
|||||||
import type { MappedComponent } from 'payload'
|
import type { ImportMap, PayloadComponent } from 'payload'
|
||||||
|
|
||||||
import { RenderComponent } from '@payloadcms/ui/shared'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { RenderServerComponent } from '../../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||||
|
|
||||||
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 +96,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'
|
||||||
@@ -8,6 +8,7 @@ import path from 'path'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import { RenderServerComponent } from '../../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||||
import { OGImage } from './image.js'
|
import { OGImage } from './image.js'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
@@ -33,18 +34,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 +51,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,6 +34,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
|||||||
permissions,
|
permissions,
|
||||||
searchParams,
|
searchParams,
|
||||||
user,
|
user,
|
||||||
|
viewActions,
|
||||||
visibleEntities,
|
visibleEntities,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@@ -38,54 +46,77 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
|||||||
} = {},
|
} = {},
|
||||||
} = payload.config || {}
|
} = payload.config || {}
|
||||||
|
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
|
||||||
importMap: payload.importMap,
|
|
||||||
serverProps: {
|
|
||||||
i18n,
|
|
||||||
locale,
|
|
||||||
params,
|
|
||||||
payload,
|
|
||||||
permissions,
|
|
||||||
searchParams,
|
|
||||||
user,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const MappedDefaultNav: MappedComponent = createMappedComponent(
|
|
||||||
CustomNav,
|
|
||||||
undefined,
|
|
||||||
DefaultNav,
|
|
||||||
'CustomNav',
|
|
||||||
)
|
|
||||||
|
|
||||||
const MappedCustomHeader = createMappedComponent(
|
|
||||||
CustomHeader,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
'CustomHeader',
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntityVisibilityProvider visibleEntities={visibleEntities}>
|
<EntityVisibilityProvider visibleEntities={visibleEntities}>
|
||||||
<BulkUploadProvider>
|
<BulkUploadProvider>
|
||||||
<RenderComponent mappedComponent={MappedCustomHeader} />
|
<ActionsProvider
|
||||||
<div style={{ position: 'relative' }}>
|
Actions={
|
||||||
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
|
viewActions
|
||||||
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
|
? viewActions.reduce((acc, action, i) => {
|
||||||
<NavToggler className={`${baseClass}__nav-toggler`}>
|
if (action) {
|
||||||
<NavHamburger />
|
if (typeof action === 'object') {
|
||||||
</NavToggler>
|
acc[action.path] = (
|
||||||
</div>
|
<RenderServerComponent Component={action} importMap={payload.importMap} />
|
||||||
</div>
|
)
|
||||||
<Wrapper baseClass={baseClass} className={className}>
|
} else {
|
||||||
<RenderComponent mappedComponent={MappedDefaultNav} />
|
acc[action] = (
|
||||||
|
<RenderServerComponent Component={action} importMap={payload.importMap} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<div className={`${baseClass}__wrap`}>
|
return acc
|
||||||
<AppHeader />
|
}, {})
|
||||||
{children}
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RenderServerComponent
|
||||||
|
clientProps={{ clientProps: { visibleEntities } }}
|
||||||
|
Component={CustomHeader}
|
||||||
|
importMap={payload.importMap}
|
||||||
|
serverProps={{
|
||||||
|
i18n,
|
||||||
|
locale,
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
searchParams,
|
||||||
|
user,
|
||||||
|
visibleEntities,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
|
||||||
|
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
|
||||||
|
<NavToggler className={`${baseClass}__nav-toggler`}>
|
||||||
|
<NavHamburger />
|
||||||
|
</NavToggler>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Wrapper>
|
<Wrapper baseClass={baseClass} className={className}>
|
||||||
</div>
|
<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 />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Wrapper>
|
||||||
|
</div>
|
||||||
|
</ActionsProvider>
|
||||||
</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 clientConfig
|
||||||
|
},
|
||||||
|
)
|
||||||
45
packages/next/src/utilities/handleServerFunctions.ts
Normal file
45
packages/next/src/utilities/handleServerFunctions.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { ServerFunction, ServerFunctionHandler } from 'payload'
|
||||||
|
|
||||||
|
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||||
|
import { buildTableState } from '@payloadcms/ui/utilities/buildTableState'
|
||||||
|
|
||||||
|
import { initReq } from './initReq.js'
|
||||||
|
import { renderDocumentFn } from './renderDocument.js'
|
||||||
|
import { renderListFn } from './renderList.js'
|
||||||
|
|
||||||
|
const defaultFunctions = {
|
||||||
|
'form-state': buildFormState as any as ServerFunction,
|
||||||
|
'render-document': renderDocumentFn as any as ServerFunction,
|
||||||
|
'render-list': renderListFn as any as ServerFunction,
|
||||||
|
'table-state': buildTableState as any as ServerFunction,
|
||||||
|
}
|
||||||
|
|
||||||
|
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: {
|
||||||
|
[key: string]: ServerFunction
|
||||||
|
} = {
|
||||||
|
...defaultFunctions,
|
||||||
|
...req.payload?.config?.admin?.serverFunctions?.reduce((acc, fnConfig) => {
|
||||||
|
acc[fnConfig.name] = fnConfig.fn
|
||||||
|
return acc
|
||||||
|
}, {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
@@ -46,13 +47,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,
|
||||||
)
|
)
|
||||||
|
|||||||
190
packages/next/src/utilities/renderDocument.tsx
Normal file
190
packages/next/src/utilities/renderDocument.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import type { I18nClient } from '@payloadcms/translations'
|
||||||
|
import type {
|
||||||
|
ClientConfig,
|
||||||
|
Data,
|
||||||
|
DocumentPreferences,
|
||||||
|
PayloadRequest,
|
||||||
|
SanitizedConfig,
|
||||||
|
VisibleEntities,
|
||||||
|
} from 'payload'
|
||||||
|
|
||||||
|
import { headers as getHeaders } from 'next/headers.js'
|
||||||
|
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
|
||||||
|
|
||||||
|
import { renderDocument } from '../views/Document/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 = {
|
||||||
|
docID: string
|
||||||
|
Document: React.ReactNode
|
||||||
|
preferences: DocumentPreferences
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderDocumentFn = async (args: {
|
||||||
|
collectionSlug: string
|
||||||
|
disableActions?: boolean
|
||||||
|
docID: string
|
||||||
|
drawerSlug?: string
|
||||||
|
initialData?: Data
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
// get prefs, then set update them using the columns that we just received
|
||||||
|
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 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 {
|
||||||
|
docID: data.id,
|
||||||
|
Document,
|
||||||
|
preferences,
|
||||||
|
}
|
||||||
|
}
|
||||||
97
packages/next/src/utilities/renderDocumentSlots.tsx
Normal file
97
packages/next/src/utilities/renderDocumentSlots.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import type {
|
||||||
|
DocumentSlots,
|
||||||
|
ImportMap,
|
||||||
|
Payload,
|
||||||
|
Permissions,
|
||||||
|
SanitizedCollectionConfig,
|
||||||
|
SanitizedGlobalConfig,
|
||||||
|
} from 'payload'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { RenderServerComponent } from '../../../ui/src/elements/RenderServerComponent/index.js'
|
||||||
|
|
||||||
|
export const renderDocumentSlots: (args: {
|
||||||
|
collectionConfig?: SanitizedCollectionConfig
|
||||||
|
globalConfig?: SanitizedGlobalConfig
|
||||||
|
hasSavePermission: boolean
|
||||||
|
importMap: ImportMap
|
||||||
|
payload: Payload
|
||||||
|
permissions: Permissions
|
||||||
|
}) => DocumentSlots = (args) => {
|
||||||
|
const { collectionConfig, globalConfig, hasSavePermission, importMap } = args
|
||||||
|
|
||||||
|
const components: DocumentSlots = {} as DocumentSlots
|
||||||
|
|
||||||
|
const unsavedDraftWithValidations = undefined
|
||||||
|
|
||||||
|
if (
|
||||||
|
(collectionConfig?.admin?.preview || globalConfig?.admin?.preview) &&
|
||||||
|
(collectionConfig?.admin?.components?.edit?.PreviewButton ||
|
||||||
|
globalConfig?.admin?.components?.elements?.PreviewButton)
|
||||||
|
) {
|
||||||
|
components.PreviewButton = (
|
||||||
|
<RenderServerComponent
|
||||||
|
Component={
|
||||||
|
collectionConfig?.admin?.components?.edit?.PreviewButton ||
|
||||||
|
globalConfig?.admin?.components?.elements?.PreviewButton
|
||||||
|
}
|
||||||
|
importMap={importMap}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSavePermission) {
|
||||||
|
if (collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts) {
|
||||||
|
if (
|
||||||
|
collectionConfig?.admin?.components?.edit?.PublishButton ||
|
||||||
|
globalConfig?.admin?.components?.elements?.PublishButton
|
||||||
|
) {
|
||||||
|
components.PublishButton = (
|
||||||
|
<RenderServerComponent
|
||||||
|
Component={
|
||||||
|
collectionConfig?.admin?.components?.edit?.PublishButton ||
|
||||||
|
globalConfig?.admin?.components?.elements?.PublishButton
|
||||||
|
}
|
||||||
|
importMap={importMap}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
((collectionConfig?.versions?.drafts && !collectionConfig?.versions?.drafts?.autosave) ||
|
||||||
|
unsavedDraftWithValidations ||
|
||||||
|
(globalConfig?.versions?.drafts && !globalConfig?.versions?.drafts?.autosave)) &&
|
||||||
|
(collectionConfig?.admin?.components?.edit?.SaveDraftButton ||
|
||||||
|
globalConfig?.admin?.components?.elements?.SaveDraftButton)
|
||||||
|
) {
|
||||||
|
components.SaveDraftButton = (
|
||||||
|
<RenderServerComponent
|
||||||
|
Component={
|
||||||
|
collectionConfig?.admin?.components?.edit?.SaveDraftButton ||
|
||||||
|
globalConfig?.admin?.components?.elements?.SaveDraftButton
|
||||||
|
}
|
||||||
|
importMap={importMap}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
collectionConfig?.admin?.components?.edit?.SaveButton ||
|
||||||
|
globalConfig?.admin?.components?.elements?.SaveButton
|
||||||
|
) {
|
||||||
|
components.SaveButton = (
|
||||||
|
<RenderServerComponent
|
||||||
|
Component={
|
||||||
|
collectionConfig?.admin?.components?.edit?.SaveButton ||
|
||||||
|
globalConfig?.admin?.components?.elements?.SaveButton
|
||||||
|
}
|
||||||
|
importMap={importMap}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return components
|
||||||
|
}
|
||||||
194
packages/next/src/utilities/renderList.tsx
Normal file
194
packages/next/src/utilities/renderList.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import type { I18nClient } from '@payloadcms/translations'
|
||||||
|
import type { ListPreferences } from '@payloadcms/ui'
|
||||||
|
import type {
|
||||||
|
ClientConfig,
|
||||||
|
Data,
|
||||||
|
DocumentPreferences,
|
||||||
|
PayloadRequest,
|
||||||
|
SanitizedConfig,
|
||||||
|
VisibleEntities,
|
||||||
|
} from 'payload'
|
||||||
|
|
||||||
|
import { headers as getHeaders } from 'next/headers.js'
|
||||||
|
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
|
||||||
|
|
||||||
|
import { renderListView } from '../views/List/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 renderListFn = async (args: {
|
||||||
|
collectionSlug: string
|
||||||
|
disableActions?: boolean
|
||||||
|
disableBulkDelete?: boolean
|
||||||
|
disableBulkEdit?: boolean
|
||||||
|
documentDrawerSlug: string
|
||||||
|
drawerSlug?: string
|
||||||
|
enableRowSelections: boolean
|
||||||
|
redirectAfterDelete: boolean
|
||||||
|
redirectAfterDuplicate: boolean
|
||||||
|
req: PayloadRequest
|
||||||
|
}): Promise<RenderListResult> => {
|
||||||
|
const {
|
||||||
|
collectionSlug,
|
||||||
|
disableActions,
|
||||||
|
disableBulkDelete,
|
||||||
|
disableBulkEdit,
|
||||||
|
documentDrawerSlug,
|
||||||
|
drawerSlug,
|
||||||
|
enableRowSelections,
|
||||||
|
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,
|
||||||
|
documentDrawerSlug,
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
redirectAfterDelete,
|
||||||
|
redirectAfterDuplicate,
|
||||||
|
searchParams: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
List,
|
||||||
|
preferences,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,12 +1,7 @@
|
|||||||
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,
|
|
||||||
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'
|
||||||
|
|
||||||
@@ -20,6 +15,7 @@ import { Settings } from './Settings/index.js'
|
|||||||
export { generateAccountMetadata } from './meta.js'
|
export { generateAccountMetadata } from './meta.js'
|
||||||
|
|
||||||
export const Account: React.FC<AdminViewProps> = async ({
|
export const Account: React.FC<AdminViewProps> = async ({
|
||||||
|
importMap,
|
||||||
initPageResult,
|
initPageResult,
|
||||||
params,
|
params,
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -61,32 +57,11 @@ export const Account: React.FC<AdminViewProps> = async ({
|
|||||||
const { data, formState } = await getDocumentData({
|
const { data, formState } = await getDocumentData({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
|
importMap,
|
||||||
locale,
|
locale,
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
|
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
|
||||||
importMap: payload.importMap,
|
|
||||||
serverProps: {
|
|
||||||
i18n,
|
|
||||||
initPageResult,
|
|
||||||
locale,
|
|
||||||
params,
|
|
||||||
payload,
|
|
||||||
permissions,
|
|
||||||
routeSegments: [],
|
|
||||||
searchParams,
|
|
||||||
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} />}
|
||||||
@@ -100,7 +75,7 @@ export const Account: React.FC<AdminViewProps> = async ({
|
|||||||
initialState={formState}
|
initialState={formState}
|
||||||
isEditing
|
isEditing
|
||||||
>
|
>
|
||||||
<EditDepthProvider depth={1}>
|
<EditDepthProvider>
|
||||||
<DocumentHeader
|
<DocumentHeader
|
||||||
collectionConfig={collectionConfig}
|
collectionConfig={collectionConfig}
|
||||||
hideTabs
|
hideTabs
|
||||||
@@ -109,7 +84,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>
|
||||||
|
|||||||
@@ -4,19 +4,17 @@ import type { ClientCollectionConfig, FormState, LoginWithUsernameOptions } from
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ConfirmPasswordField,
|
ConfirmPasswordField,
|
||||||
|
EmailAndUsernameFields,
|
||||||
Form,
|
Form,
|
||||||
FormSubmit,
|
FormSubmit,
|
||||||
PasswordField,
|
PasswordField,
|
||||||
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<{
|
||||||
initialState: FormState
|
initialState: FormState
|
||||||
loginWithUsername?: false | LoginWithUsernameOptions
|
loginWithUsername?: false | LoginWithUsernameOptions
|
||||||
@@ -30,6 +28,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 +38,15 @@ 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: {
|
formState: prevFormState,
|
||||||
collectionSlug: userSlug,
|
operation: 'create',
|
||||||
formState: prevFormState,
|
schemaPath: [`_${userSlug}`, 'auth'],
|
||||||
operation: 'create',
|
|
||||||
schemaPath: `_${userSlug}.auth`,
|
|
||||||
},
|
|
||||||
serverURL,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
[apiRoute, userSlug, serverURL],
|
[userSlug, getFormState],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFirstRegister = (data: UserWithToken) => {
|
const handleFirstRegister = (data: UserWithToken) => {
|
||||||
@@ -66,11 +63,12 @@ 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'}
|
||||||
@@ -81,14 +79,7 @@ export const CreateFirstUserClient: React.FC<{
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ConfirmPasswordField />
|
<ConfirmPasswordField />
|
||||||
<RenderFields
|
{/* Fields Here */}
|
||||||
fields={collectionConfig.fields}
|
|
||||||
forceRender
|
|
||||||
operation="create"
|
|
||||||
path=""
|
|
||||||
readOnly={false}
|
|
||||||
schemaPath={userSlug}
|
|
||||||
/>
|
|
||||||
<FormSubmit size="large">{t('general:create')}</FormSubmit>
|
<FormSubmit size="large">{t('general:create')}</FormSubmit>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import './index.scss'
|
|||||||
|
|
||||||
export { generateCreateFirstUserMetadata } from './meta.js'
|
export { generateCreateFirstUserMetadata } from './meta.js'
|
||||||
|
|
||||||
export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageResult }) => {
|
export const CreateFirstUserView: React.FC<AdminViewProps> = async ({
|
||||||
|
importMap,
|
||||||
|
initPageResult,
|
||||||
|
}) => {
|
||||||
const {
|
const {
|
||||||
locale,
|
locale,
|
||||||
req,
|
req,
|
||||||
@@ -28,9 +31,10 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
|||||||
|
|
||||||
const { formState } = await getDocumentData({
|
const { formState } = await getDocumentData({
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
|
importMap,
|
||||||
locale,
|
locale,
|
||||||
req,
|
req,
|
||||||
schemaPath: `_${collectionConfig.slug}.auth`,
|
schemaPath: [`_${collectionConfig.slug}`, 'auth'],
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -46,41 +42,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>
|
||||||
) : (
|
) : (
|
||||||
@@ -89,8 +69,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 buttonAriaLabel: string
|
let buttonAriaLabel: string
|
||||||
let createHREF: string
|
let createHREF: string
|
||||||
let href: string
|
let href: string
|
||||||
@@ -99,37 +78,30 @@ 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)
|
buttonAriaLabel = t('general:showAllLabel', { label })
|
||||||
|
|
||||||
buttonAriaLabel = t('general:showAllLabel', { label: title })
|
href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })
|
||||||
|
|
||||||
href = formatAdminURL({ adminRoute, path: `/collections/${entity.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)
|
|
||||||
|
|
||||||
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) {
|
||||||
lockStatus = globalLockData.data._isLocked
|
lockStatus = globalLockData.data._isLocked
|
||||||
userEditing = globalLockData.data._userEditing
|
userEditing = globalLockData.data._userEditing
|
||||||
@@ -145,7 +117,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"
|
||||||
@@ -159,9 +131,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>
|
||||||
@@ -173,7 +145,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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,15 +2,11 @@ import type { EntityToGroup } from '@payloadcms/ui/shared'
|
|||||||
import type { AdminViewProps } from 'payload'
|
import type { AdminViewProps } from 'payload'
|
||||||
|
|
||||||
import { HydrateAuthProvider } from '@payloadcms/ui'
|
import { HydrateAuthProvider } from '@payloadcms/ui'
|
||||||
import {
|
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||||
EntityType,
|
|
||||||
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'
|
||||||
|
|
||||||
|
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||||
import { DefaultDashboard } from './Default/index.js'
|
import { DefaultDashboard } from './Default/index.js'
|
||||||
|
|
||||||
export { generateDashboardMetadata } from './meta.js'
|
export { generateDashboardMetadata } from './meta.js'
|
||||||
@@ -94,39 +90,30 @@ 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
|
<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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,58 +1,63 @@
|
|||||||
import type {
|
import type {
|
||||||
Data,
|
Data,
|
||||||
FormState,
|
FormState,
|
||||||
|
ImportMap,
|
||||||
Locale,
|
Locale,
|
||||||
PayloadRequest,
|
PayloadRequest,
|
||||||
SanitizedCollectionConfig,
|
SanitizedCollectionConfig,
|
||||||
SanitizedGlobalConfig,
|
SanitizedGlobalConfig,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
import { buildFormStateFn as buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||||
import { reduceFieldsToValues } from 'payload/shared'
|
import { reduceFieldsToValues } from 'payload/shared'
|
||||||
|
|
||||||
export const getDocumentData = async (args: {
|
export const getDocumentData = async (args: {
|
||||||
collectionConfig?: SanitizedCollectionConfig
|
collectionConfig?: SanitizedCollectionConfig
|
||||||
globalConfig?: SanitizedGlobalConfig
|
globalConfig?: SanitizedGlobalConfig
|
||||||
id?: number | string
|
id?: number | string
|
||||||
|
importMap: ImportMap
|
||||||
locale: Locale
|
locale: Locale
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
schemaPath?: string
|
schemaPath?: string[]
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
data: Data
|
data: Data
|
||||||
formState: FormState
|
formState: FormState
|
||||||
}> => {
|
}> => {
|
||||||
const { id, collectionConfig, globalConfig, locale, req, schemaPath: schemaPathFromProps } = args
|
const { id, collectionConfig, globalConfig, locale, req, schemaPath: schemaPathFromProps } = args
|
||||||
|
|
||||||
const schemaPath = schemaPathFromProps || collectionConfig?.slug || globalConfig?.slug
|
const schemaPath = schemaPathFromProps?.length
|
||||||
|
? schemaPathFromProps
|
||||||
|
: collectionConfig?.slug
|
||||||
|
? [collectionConfig.slug]
|
||||||
|
: [globalConfig?.slug]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { state: formState } = await buildFormState({
|
const result = await buildFormState({
|
||||||
req: {
|
id,
|
||||||
...req,
|
collectionSlug: collectionConfig?.slug,
|
||||||
data: {
|
globalSlug: globalConfig?.slug,
|
||||||
id,
|
locale: locale?.code,
|
||||||
collectionSlug: collectionConfig?.slug,
|
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
|
||||||
globalSlug: globalConfig?.slug,
|
renderFields: true,
|
||||||
locale: locale?.code,
|
req,
|
||||||
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
|
schemaPath,
|
||||||
schemaPath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = reduceFieldsToValues(formState, true)
|
const data = reduceFieldsToValues(result.state, true)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
formState,
|
formState: result.state,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting document data', error) // eslint-disable-line no-console
|
console.error('Error getting document data', error) // eslint-disable-line no-console
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
formState: {
|
formState: {
|
||||||
fields: {
|
fields: {
|
||||||
initialValue: undefined,
|
initialValue: undefined,
|
||||||
|
schemaPath: [],
|
||||||
valid: false,
|
valid: false,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,7 +23,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 = ({
|
||||||
@@ -94,7 +94,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 +132,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 +156,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,
|
||||||
@@ -177,7 +177,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,
|
||||||
@@ -215,7 +215,7 @@ export const getViewsFromConfig = ({
|
|||||||
viewKey = customViewKey
|
viewKey = customViewKey
|
||||||
|
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: CustomViewComponent,
|
ComponentConfig: CustomViewComponent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +230,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,
|
||||||
@@ -266,7 +266,7 @@ export const getViewsFromConfig = ({
|
|||||||
viewKey = customViewKey
|
viewKey = customViewKey
|
||||||
|
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: CustomViewComponent,
|
ComponentConfig: CustomViewComponent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,7 +286,7 @@ export const getViewsFromConfig = ({
|
|||||||
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,
|
||||||
@@ -300,7 +300,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,
|
||||||
@@ -321,7 +321,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 = {
|
||||||
@@ -356,7 +356,7 @@ export const getViewsFromConfig = ({
|
|||||||
viewKey = customViewKey
|
viewKey = customViewKey
|
||||||
|
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: CustomViewComponent,
|
ComponentConfig: CustomViewComponent,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DefaultView = {
|
DefaultView = {
|
||||||
@@ -379,7 +379,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,
|
||||||
@@ -410,7 +410,7 @@ export const getViewsFromConfig = ({
|
|||||||
viewKey = customViewKey
|
viewKey = customViewKey
|
||||||
|
|
||||||
CustomView = {
|
CustomView = {
|
||||||
payloadComponent: CustomViewComponent,
|
ComponentConfig: CustomViewComponent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
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,
|
|
||||||
isEditing as getIsEditing,
|
|
||||||
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 { DocumentDrawerHeader } from '../../elements/DocumentDrawerHeader/index.js'
|
||||||
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
|
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
|
||||||
|
import { renderDocumentSlots } from '../../utilities/renderDocumentSlots.js'
|
||||||
import { NotFoundView } from '../NotFound/index.js'
|
import { NotFoundView } from '../NotFound/index.js'
|
||||||
import { getDocumentData } from './getDocumentData.js'
|
import { getDocumentData } from './getDocumentData.js'
|
||||||
import { getDocumentPermissions } from './getDocumentPermissions.js'
|
import { getDocumentPermissions } from './getDocumentPermissions.js'
|
||||||
@@ -26,12 +26,20 @@ import { getViewsFromConfig } from './getViewsFromConfig.js'
|
|||||||
|
|
||||||
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)
|
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)
|
||||||
|
|
||||||
export const Document: React.FC<AdminViewProps> = async ({
|
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,
|
||||||
@@ -55,15 +63,17 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
} = initPageResult
|
} = initPageResult
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -71,12 +81,13 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
id,
|
id,
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
|
importMap,
|
||||||
locale,
|
locale,
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
notFound()
|
throw new Error('not-found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { docPermissions, hasPublishPermission, hasSavePermission } = await getDocumentPermissions({
|
const { docPermissions, hasPublishPermission, hasSavePermission } = await getDocumentPermissions({
|
||||||
@@ -87,24 +98,21 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
|
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
const serverProps: ServerProps = {
|
||||||
importMap,
|
i18n,
|
||||||
serverProps: {
|
initPageResult,
|
||||||
i18n,
|
locale,
|
||||||
initPageResult,
|
params,
|
||||||
locale,
|
payload,
|
||||||
params,
|
permissions,
|
||||||
payload,
|
routeSegments: segments,
|
||||||
permissions,
|
searchParams,
|
||||||
routeSegments: segments,
|
user,
|
||||||
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 +130,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 +141,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 +177,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 +188,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 +208,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 +232,93 @@ 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}
|
importMap,
|
||||||
docPermissions={docPermissions}
|
payload,
|
||||||
globalSlug={globalConfig?.slug}
|
permissions,
|
||||||
hasPublishPermission={hasPublishPermission}
|
})
|
||||||
hasSavePermission={hasSavePermission}
|
|
||||||
id={id}
|
const clientProps = { formState, ...documentSlots }
|
||||||
initialData={data}
|
|
||||||
initialState={formState}
|
return {
|
||||||
isEditing={isEditing}
|
data,
|
||||||
key={locale?.code}
|
Document: (
|
||||||
>
|
<DocumentInfoProvider
|
||||||
{!RootViewOverride && (
|
apiURL={apiURL}
|
||||||
<DocumentHeader
|
BeforeDocument={
|
||||||
collectionConfig={collectionConfig}
|
drawerSlug ? (
|
||||||
globalConfig={globalConfig}
|
<DocumentDrawerHeader
|
||||||
i18n={i18n}
|
drawerSlug={drawerSlug}
|
||||||
payload={payload}
|
Header={null} // TODO
|
||||||
permissions={permissions}
|
/>
|
||||||
/>
|
) : undefined
|
||||||
)}
|
}
|
||||||
<HydrateAuthProvider permissions={permissions} />
|
collectionSlug={collectionConfig?.slug}
|
||||||
{/**
|
disableActions={disableActions ?? false}
|
||||||
* 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).
|
docPermissions={docPermissions}
|
||||||
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
|
globalSlug={globalConfig?.slug}
|
||||||
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error when loading up some version views - for example a versions
|
hasPublishPermission={hasPublishPermission}
|
||||||
* view in the draft-posts collection of the versions test suite. RenderCustomComponent is what renders the versions view.
|
hasSavePermission={hasSavePermission}
|
||||||
*
|
id={id}
|
||||||
* // 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)
|
initialData={data}
|
||||||
*/}
|
initialState={formState}
|
||||||
<EditDepthProvider
|
isEditing={isEditing}
|
||||||
depth={1}
|
key={locale?.code}
|
||||||
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
|
redirectAfterDelete={redirectAfterDelete}
|
||||||
|
redirectAfterDuplicate={redirectAfterDuplicate}
|
||||||
>
|
>
|
||||||
{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 (error.message === 'not-found') {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,58 @@
|
|||||||
import type { AdminViewProps, ClientCollectionConfig, Where } from 'payload'
|
import type { ListPreferences, ListViewClientProps } from '@payloadcms/ui'
|
||||||
|
import type { AdminViewProps, Where } from 'payload'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DefaultListView,
|
||||||
HydrateAuthProvider,
|
HydrateAuthProvider,
|
||||||
ListInfoProvider,
|
ListInfoProvider,
|
||||||
ListQueryProvider,
|
ListQueryProvider,
|
||||||
TableColumnsProvider,
|
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import { formatAdminURL, getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
import { formatAdminURL } 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 { filterFields } from 'packages/ui/src/elements/TableColumns/filterFields.js'
|
||||||
|
import { getInitialColumns } from 'packages/ui/src/elements/TableColumns/getInitialColumns.js'
|
||||||
|
import { renderFilters, renderTable } from 'packages/ui/src/utilities/renderTable.js'
|
||||||
|
import { 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 { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||||
|
import { ListDrawerHeader } from '../../elements/ListDrawerHeader/index.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,
|
disableBulkDelete?: boolean
|
||||||
params,
|
disableBulkEdit?: boolean
|
||||||
searchParams,
|
documentDrawerSlug: string
|
||||||
}) => {
|
enableRowSelections: boolean
|
||||||
|
} & AdminViewProps
|
||||||
|
|
||||||
|
export const renderListView = async (
|
||||||
|
args: ListViewArgs,
|
||||||
|
): Promise<{
|
||||||
|
List: React.ReactNode
|
||||||
|
}> => {
|
||||||
|
const {
|
||||||
|
clientConfig,
|
||||||
|
disableBulkDelete,
|
||||||
|
disableBulkEdit,
|
||||||
|
documentDrawerSlug,
|
||||||
|
drawerSlug,
|
||||||
|
enableRowSelections,
|
||||||
|
initPageResult,
|
||||||
|
params,
|
||||||
|
searchParams,
|
||||||
|
} = args
|
||||||
|
|
||||||
const {
|
const {
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
|
collectionConfig: {
|
||||||
|
slug: collectionSlug,
|
||||||
|
admin: { defaultColumns, useAsTitle },
|
||||||
|
defaultSort,
|
||||||
|
fields,
|
||||||
|
},
|
||||||
locale: fullLocale,
|
locale: fullLocale,
|
||||||
permissions,
|
permissions,
|
||||||
req,
|
req,
|
||||||
@@ -41,10 +67,8 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
|||||||
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')
|
||||||
}
|
}
|
||||||
|
|
||||||
let listPreferences: ListPreferences
|
let listPreferences: ListPreferences
|
||||||
@@ -79,7 +103,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,7 +111,7 @@ 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
|
||||||
@@ -98,9 +122,11 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
|||||||
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 +151,124 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
|||||||
where: whereQuery || {},
|
where: whereQuery || {},
|
||||||
})
|
})
|
||||||
|
|
||||||
const createMappedComponent = getCreateMappedComponent({
|
const initialColumns = getInitialColumns(filterFields(fields), useAsTitle, defaultColumns)
|
||||||
|
|
||||||
|
const clientCollectionConfig = clientConfig.collections.find((c) => c.slug === collectionSlug)
|
||||||
|
|
||||||
|
const { columnState, Table } = renderTable({
|
||||||
|
clientFields: clientCollectionConfig?.fields,
|
||||||
|
collectionSlug,
|
||||||
|
columnPreferences: listPreferences?.columns,
|
||||||
|
columns: initialColumns,
|
||||||
|
docs: data.docs,
|
||||||
|
drawerSlug,
|
||||||
|
enableRowSelections,
|
||||||
|
fields,
|
||||||
importMap: payload.importMap,
|
importMap: payload.importMap,
|
||||||
serverProps: {
|
useAsTitle,
|
||||||
collectionConfig,
|
|
||||||
collectionSlug,
|
|
||||||
data,
|
|
||||||
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
|
|
||||||
i18n,
|
|
||||||
limit,
|
|
||||||
listPreferences,
|
|
||||||
listSearchableFields: collectionConfig.admin.listSearchableFields,
|
|
||||||
locale: fullLocale,
|
|
||||||
newDocumentURL: formatAdminURL({
|
|
||||||
adminRoute,
|
|
||||||
path: `/collections/${collectionSlug}/create`,
|
|
||||||
}),
|
|
||||||
params,
|
|
||||||
payload,
|
|
||||||
permissions,
|
|
||||||
searchParams,
|
|
||||||
user,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const 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 clientProps: ListViewClientProps = {
|
||||||
collectionConfig,
|
collectionSlug,
|
||||||
) as unknown as ClientCollectionConfig
|
columnState,
|
||||||
clientCollectionConfig = createClientCollectionConfig({
|
listPreferences,
|
||||||
clientCollection: clientCollectionConfig,
|
renderedFilters,
|
||||||
collection: collectionConfig,
|
Table,
|
||||||
createMappedComponent,
|
}
|
||||||
DefaultEditView,
|
|
||||||
DefaultListView,
|
|
||||||
i18n,
|
|
||||||
importMap: payload.importMap,
|
|
||||||
payload,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
const hasCreatePermission = permissions?.collections?.[collectionSlug]?.create?.permission
|
||||||
<Fragment>
|
|
||||||
<HydrateAuthProvider permissions={permissions} />
|
return {
|
||||||
<ListInfoProvider
|
List: (
|
||||||
collectionConfig={clientCollectionConfig}
|
<Fragment>
|
||||||
collectionSlug={collectionSlug}
|
<HydrateAuthProvider permissions={permissions} />
|
||||||
hasCreatePermission={permissions?.collections?.[collectionSlug]?.create?.permission}
|
<ListInfoProvider
|
||||||
newDocumentURL={formatAdminURL({
|
// beforeActions={
|
||||||
adminRoute,
|
// enableRowSelections
|
||||||
path: `/collections/${collectionSlug}/create`,
|
// ? [<SelectMany key="select-many" onClick={onBulkSelect} />]
|
||||||
})}
|
// : undefined
|
||||||
>
|
// }
|
||||||
<ListQueryProvider
|
collectionSlug={collectionSlug}
|
||||||
data={data}
|
disableBulkDelete={disableBulkDelete}
|
||||||
defaultLimit={limit || collectionConfig?.admin?.pagination?.defaultLimit}
|
disableBulkEdit={disableBulkEdit}
|
||||||
defaultSort={sort}
|
hasCreatePermission={hasCreatePermission}
|
||||||
modifySearchParams
|
Header={
|
||||||
preferenceKey={preferenceKey}
|
drawerSlug ? (
|
||||||
|
<ListDrawerHeader
|
||||||
|
CustomDescription={
|
||||||
|
collectionConfig?.admin?.components?.Description ? (
|
||||||
|
<RenderServerComponent
|
||||||
|
Component={collectionConfig.admin.components.Description}
|
||||||
|
importMap={payload.importMap}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
description={clientCollectionConfig?.admin?.description}
|
||||||
|
documentDrawerSlug={documentDrawerSlug}
|
||||||
|
drawerSlug={drawerSlug}
|
||||||
|
hasCreatePermission={hasCreatePermission}
|
||||||
|
pluralLabel={clientCollectionConfig?.labels?.plural}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
newDocumentURL={formatAdminURL({
|
||||||
|
adminRoute,
|
||||||
|
path: `/collections/${collectionSlug}/create`,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<TableColumnsProvider
|
<ListQueryProvider
|
||||||
collectionSlug={collectionSlug}
|
data={data}
|
||||||
enableRowSelections
|
defaultLimit={limit || collectionConfig?.admin?.pagination?.defaultLimit}
|
||||||
listPreferences={listPreferences}
|
defaultSort={sort}
|
||||||
|
modifySearchParams
|
||||||
preferenceKey={preferenceKey}
|
preferenceKey={preferenceKey}
|
||||||
>
|
>
|
||||||
<RenderComponent
|
<RenderServerComponent
|
||||||
clientProps={{
|
clientProps={clientProps}
|
||||||
|
Component={collectionConfig?.admin?.components?.views?.list.Component}
|
||||||
|
Fallback={DefaultListView}
|
||||||
|
importMap={payload.importMap}
|
||||||
|
serverProps={{
|
||||||
|
collectionConfig,
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
listSearchableFields: collectionConfig?.admin?.listSearchableFields,
|
data,
|
||||||
|
hasCreatePermission:
|
||||||
|
permissions?.collections?.[collectionSlug]?.create?.permission,
|
||||||
|
i18n,
|
||||||
|
limit,
|
||||||
|
listPreferences,
|
||||||
|
listSearchableFields: collectionConfig.admin.listSearchableFields,
|
||||||
|
locale: fullLocale,
|
||||||
|
newDocumentURL: formatAdminURL({
|
||||||
|
adminRoute,
|
||||||
|
path: `/collections/${collectionSlug}/create`,
|
||||||
|
}),
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
searchParams,
|
||||||
|
user,
|
||||||
}}
|
}}
|
||||||
mappedComponent={ListComponent}
|
|
||||||
/>
|
/>
|
||||||
</TableColumnsProvider>
|
</ListQueryProvider>
|
||||||
</ListQueryProvider>
|
</ListInfoProvider>
|
||||||
</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)
|
||||||
|
return RenderedList
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message === 'not-found') {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,27 +15,22 @@ import {
|
|||||||
DocumentFields,
|
DocumentFields,
|
||||||
Form,
|
Form,
|
||||||
OperationProvider,
|
OperationProvider,
|
||||||
SetViewActions,
|
SetDocumentStepNav,
|
||||||
|
SetDocumentTitle,
|
||||||
useAuth,
|
useAuth,
|
||||||
useConfig,
|
useConfig,
|
||||||
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 { DocumentLocked } from '../../elements/DocumentLocked/index.js'
|
||||||
import { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js'
|
import { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js'
|
||||||
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/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 +50,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,
|
||||||
@@ -92,6 +85,8 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
updateDocumentEditor,
|
updateDocumentEditor,
|
||||||
} = useDocumentInfo()
|
} = useDocumentInfo()
|
||||||
|
|
||||||
|
const { getFormState } = useServerFunctions()
|
||||||
|
|
||||||
const operation = id ? 'update' : 'create'
|
const operation = id ? 'update' : 'create'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -115,6 +110,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 documentLockStateRef = useRef<{
|
const documentLockStateRef = useRef<{
|
||||||
hasShownLockedModal: boolean
|
hasShownLockedModal: boolean
|
||||||
isLocked: boolean
|
isLocked: boolean
|
||||||
@@ -169,6 +166,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 - lastUpdateTime
|
const timeSinceLastUpdate = currentTime - lastUpdateTime
|
||||||
|
|
||||||
@@ -181,19 +189,16 @@ 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,
|
docPreferences,
|
||||||
collectionSlug,
|
formState: prevFormState,
|
||||||
docPreferences,
|
globalSlug,
|
||||||
formState: prevFormState,
|
operation,
|
||||||
globalSlug,
|
returnLockStatus: isLockingEnabled ? true : false,
|
||||||
operation,
|
schemaPath: schemaPath ? schemaPath.split('.') : [],
|
||||||
returnLockStatus: isLockingEnabled ? true : false,
|
signal: abortController.signal,
|
||||||
schemaPath,
|
updateLastEdited,
|
||||||
updateLastEdited,
|
|
||||||
},
|
|
||||||
serverURL,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setDocumentIsLocked(true)
|
setDocumentIsLocked(true)
|
||||||
@@ -202,8 +207,13 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
const previousOwnerId = documentLockStateRef.current?.user?.id
|
const previousOwnerId = documentLockStateRef.current?.user?.id
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -211,9 +221,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,8 +234,6 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
[
|
[
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
serverURL,
|
|
||||||
apiRoute,
|
|
||||||
id,
|
id,
|
||||||
isLockingEnabled,
|
isLockingEnabled,
|
||||||
lastUpdateTime,
|
lastUpdateTime,
|
||||||
@@ -234,12 +243,21 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
setCurrentEditor,
|
setCurrentEditor,
|
||||||
setDocumentIsLocked,
|
setDocumentIsLocked,
|
||||||
user,
|
user,
|
||||||
|
getFormState,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@@ -404,7 +422,6 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
forceSidebarWrap
|
forceSidebarWrap
|
||||||
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
|
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
|
||||||
schemaPath={collectionSlug || globalSlug}
|
|
||||||
/>
|
/>
|
||||||
{AfterDocument}
|
{AfterDocument}
|
||||||
</div>
|
</div>
|
||||||
@@ -445,11 +462,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}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { AdminViewProps } from 'payload'
|
import type { AdminViewProps } from 'payload'
|
||||||
|
|
||||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
|
||||||
import { redirect } from 'next/navigation.js'
|
import { redirect } from 'next/navigation.js'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import { Logo } from '../../elements/Logo/index.js'
|
import { Logo } from '../../elements/Logo/index.js'
|
||||||
|
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { LoginForm } from './LoginForm/index.js'
|
import { LoginForm } from './LoginForm/index.js'
|
||||||
|
|
||||||
@@ -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(admin)
|
redirect(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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (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
|
const 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.unshift(...(matchedCollection.admin.components?.views?.list?.actions || []))
|
||||||
|
} else if (isGlobal && matchedGlobal) {
|
||||||
// --> /globals/:globalSlug
|
// --> /globals/:globalSlug
|
||||||
|
|
||||||
ViewToRender = {
|
ViewToRender = {
|
||||||
@@ -176,13 +220,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 +234,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.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedCollection.admin.components.views.edit,
|
||||||
|
viewKey: 'root',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (segmentFive) {
|
||||||
|
if (segmentFour === 'versions') {
|
||||||
|
// add version view actions
|
||||||
|
viewActions.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedCollection.admin.components.views.edit,
|
||||||
|
viewKey: 'version',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (segmentFour) {
|
||||||
|
if (segmentFour === 'versions') {
|
||||||
|
// add versions view actions
|
||||||
|
viewActions.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedCollection.admin.components.views.edit,
|
||||||
|
viewKey: 'versions',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else if (segmentFour === 'preview') {
|
||||||
|
// add livePreview view actions
|
||||||
|
viewActions.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedCollection.admin.components.views.edit,
|
||||||
|
viewKey: 'livePreview',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else if (segmentFour === 'api') {
|
||||||
|
// add api view actions
|
||||||
|
viewActions.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedCollection.admin.components.views.edit,
|
||||||
|
viewKey: 'api',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (segmentThree) {
|
||||||
|
// add default view actions
|
||||||
|
viewActions.unshift(
|
||||||
|
...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 +305,64 @@ 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.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedGlobal.admin.components.views.edit,
|
||||||
|
viewKey: 'root',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (segmentFour) {
|
||||||
|
if (segmentThree === 'versions') {
|
||||||
|
// add version view actions
|
||||||
|
viewActions.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedGlobal.admin.components.views.edit,
|
||||||
|
viewKey: 'version',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (segmentThree) {
|
||||||
|
if (segmentThree === 'versions') {
|
||||||
|
// add versions view actions
|
||||||
|
viewActions.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedGlobal.admin.components.views.edit,
|
||||||
|
viewKey: 'versions',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else if (segmentThree === 'preview') {
|
||||||
|
// add livePreview view actions
|
||||||
|
viewActions.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedGlobal.admin.components.views.edit,
|
||||||
|
viewKey: 'livePreview',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else if (segmentThree === 'api') {
|
||||||
|
// add api view actions
|
||||||
|
viewActions.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedGlobal.admin.components.views.edit,
|
||||||
|
viewKey: 'api',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else if (segmentTwo) {
|
||||||
|
// add default view actions
|
||||||
|
viewActions.unshift(
|
||||||
|
...getViewActions({
|
||||||
|
editConfig: matchedGlobal.admin.components.views.edit,
|
||||||
|
viewKey: 'default',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -214,7 +374,9 @@ export const getViewFromConfig = ({
|
|||||||
return {
|
return {
|
||||||
DefaultView: ViewToRender,
|
DefaultView: ViewToRender,
|
||||||
initPageOptions,
|
initPageOptions,
|
||||||
|
serverProps,
|
||||||
templateClassName,
|
templateClassName,
|
||||||
templateType,
|
templateType,
|
||||||
|
viewActions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -66,7 +75,7 @@ export const RootPage = async ({
|
|||||||
|
|
||||||
let dbHasUser = false
|
let dbHasUser = false
|
||||||
|
|
||||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
if (!DefaultView) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,27 +111,30 @@ export const RootPage = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>}
|
||||||
@@ -138,6 +150,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}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
LoadingOverlayToggle,
|
LoadingOverlayToggle,
|
||||||
Pagination,
|
Pagination,
|
||||||
PerPage,
|
PerPage,
|
||||||
SetViewActions,
|
|
||||||
Table,
|
Table,
|
||||||
useConfig,
|
useConfig,
|
||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
@@ -41,11 +40,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`}>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||||
import type { JsonObject, Payload, PayloadRequest, RequestContext } from '../types/index.js'
|
import type { JsonObject, Payload, PayloadRequest, RequestContext } 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 { CreateMappedComponent, FieldSchemaMap } from './types.js'
|
||||||
|
|
||||||
export type AfterReadRichTextHookArgs<
|
export type AfterReadRichTextHookArgs<
|
||||||
TData extends TypeWithID = any,
|
TData extends TypeWithID = any,
|
||||||
@@ -89,7 +89,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. */
|
||||||
@@ -205,9 +205,9 @@ type RichTextAdapterBase<
|
|||||||
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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,8 +3,15 @@ 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 cellProps?: Partial<CellComponentProps>
|
||||||
readonly className?: string
|
readonly className?: string
|
||||||
|
readonly columnIndex?: number
|
||||||
|
readonly customCellContext?: {
|
||||||
|
collectionSlug?: SanitizedCollectionConfig['slug']
|
||||||
|
uploadConfig?: SanitizedCollectionConfig['upload']
|
||||||
|
}
|
||||||
readonly field: TField
|
readonly field: TField
|
||||||
readonly link?: boolean
|
readonly link?: boolean
|
||||||
readonly onClick?: (args: {
|
readonly onClick?: (args: {
|
||||||
@@ -12,13 +19,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,13 +14,13 @@ 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 CustomRowLabel?: React.ReactNode
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: ArrayFieldValidation
|
readonly validate?: ArrayFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +19,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type EmailFieldClientWithoutType = MarkOptional<EmailFieldClient, 'type'>
|
|||||||
|
|
||||||
type EmailFieldBaseClientProps = {
|
type EmailFieldBaseClientProps = {
|
||||||
readonly autoComplete?: string
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export type HiddenFieldProps = {
|
|||||||
readonly disableModifyingForm?: false
|
readonly disableModifyingForm?: false
|
||||||
readonly field?: {
|
readonly field?: {
|
||||||
readonly name?: string
|
readonly name?: string
|
||||||
} & Pick<ClientField, '_path'>
|
} & ClientField
|
||||||
readonly forceUsePathFromProps?: boolean
|
readonly forceUsePathFromProps?: boolean
|
||||||
readonly value?: unknown
|
readonly value?: unknown
|
||||||
} & FormFieldBase
|
} & FormFieldBase
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type RichTextFieldBaseClientProps<
|
|||||||
TAdapterProps = any,
|
TAdapterProps = any,
|
||||||
TExtraProperties = object,
|
TExtraProperties = object,
|
||||||
> = {
|
> = {
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: RichTextFieldValidation
|
readonly validate?: RichTextFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type RowFieldClientWithoutType = MarkOptional<RowFieldClient, 'type'>
|
|||||||
|
|
||||||
type RowFieldBaseClientProps = {
|
type RowFieldBaseClientProps = {
|
||||||
readonly forceRender?: boolean
|
readonly forceRender?: boolean
|
||||||
readonly indexPath: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RowFieldClientProps = ClientFieldBase<RowFieldClientWithoutType> &
|
export type RowFieldClientProps = ClientFieldBase<RowFieldClientWithoutType> &
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type SelectFieldClientWithoutType = MarkOptional<SelectFieldClient, 'type'>
|
|||||||
|
|
||||||
type SelectFieldBaseClientProps = {
|
type SelectFieldBaseClientProps = {
|
||||||
readonly onChange?: (e: string | string[]) => void
|
readonly onChange?: (e: string | string[]) => void
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: SelectFieldValidation
|
readonly validate?: SelectFieldValidation
|
||||||
readonly value?: string
|
readonly value?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import type {
|
|||||||
} from '../types.js'
|
} from '../types.js'
|
||||||
|
|
||||||
export type ClientTab =
|
export type ClientTab =
|
||||||
| ({ fields: ClientField[] } & Omit<NamedTab, 'fields'>)
|
|
||||||
| ({ fields: ClientField[] } & Omit<UnnamedTab, 'fields'>)
|
| ({ fields: ClientField[] } & Omit<UnnamedTab, 'fields'>)
|
||||||
|
| ({ fields: ClientField[]; readonly path?: string } & Omit<NamedTab, 'fields'>)
|
||||||
|
|
||||||
type TabsFieldClientWithoutType = MarkOptional<TabsFieldClient, 'type'>
|
type TabsFieldClientWithoutType = MarkOptional<TabsFieldClient, 'type'>
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type TextFieldClientWithoutType = MarkOptional<TextFieldClient, 'type'>
|
|||||||
type TextFieldBaseClientProps = {
|
type TextFieldBaseClientProps = {
|
||||||
readonly inputRef?: React.RefObject<HTMLInputElement>
|
readonly inputRef?: React.RefObject<HTMLInputElement>
|
||||||
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: TextFieldValidation
|
readonly validate?: TextFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type TextareaFieldClientWithoutType = MarkOptional<TextareaFieldClient, 'type'>
|
|||||||
type TextareaFieldBaseClientProps = {
|
type TextareaFieldBaseClientProps = {
|
||||||
readonly inputRef?: React.Ref<HTMLInputElement>
|
readonly inputRef?: React.Ref<HTMLInputElement>
|
||||||
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: TextareaFieldValidation
|
readonly validate?: TextareaFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
type UploadFieldClientWithoutType = MarkOptional<UploadFieldClient, 'type'>
|
type UploadFieldClientWithoutType = MarkOptional<UploadFieldClient, 'type'>
|
||||||
|
|
||||||
type UploadFieldBaseClientProps = {
|
type UploadFieldBaseClientProps = {
|
||||||
|
readonly path?: string
|
||||||
readonly validate?: UploadFieldValidation
|
readonly validate?: UploadFieldValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { LabelFunction, ServerProps } from '../../config/types.js'
|
import type { LabelFunction, ServerProps } from '../../config/types.js'
|
||||||
import type { Field } from '../../fields/config/types.js'
|
import type { Field } from '../../fields/config/types.js'
|
||||||
import type { MappedComponent } from '../types.js'
|
|
||||||
import type { ClientFieldWithOptionalType } from './Field.js'
|
import type { ClientFieldWithOptionalType } from './Field.js'
|
||||||
|
|
||||||
export type DescriptionFunction = LabelFunction
|
export type DescriptionFunction = LabelFunction
|
||||||
@@ -20,9 +19,9 @@ export type Description = DescriptionFunction | StaticDescription
|
|||||||
|
|
||||||
export type GenericDescriptionProps = {
|
export type GenericDescriptionProps = {
|
||||||
readonly className?: string
|
readonly className?: string
|
||||||
readonly Description?: MappedComponent
|
|
||||||
readonly description?: StaticDescription
|
readonly description?: StaticDescription
|
||||||
readonly marginPlacement?: 'bottom' | 'top'
|
readonly marginPlacement?: 'bottom' | 'top'
|
||||||
|
readonly path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FieldDescriptionServerProps<
|
export type FieldDescriptionServerProps<
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import type { ServerProps } from '../../config/types.js'
|
import type { ServerProps } from '../../config/types.js'
|
||||||
import type { Field } from '../../fields/config/types.js'
|
import type { Field } from '../../fields/config/types.js'
|
||||||
import type { MappedComponent } from '../types.js'
|
|
||||||
import type { ClientFieldWithOptionalType } from './Field.js'
|
import type { ClientFieldWithOptionalType } from './Field.js'
|
||||||
|
|
||||||
export type GenericErrorProps = {
|
export type GenericErrorProps = {
|
||||||
readonly alignCaret?: 'center' | 'left' | 'right'
|
readonly alignCaret?: 'center' | 'left' | 'right'
|
||||||
readonly CustomError?: MappedComponent
|
|
||||||
readonly message?: string
|
readonly message?: string
|
||||||
readonly path?: string
|
readonly path?: string
|
||||||
readonly showError?: boolean
|
readonly showError?: boolean
|
||||||
|
|||||||
@@ -1,50 +1,48 @@
|
|||||||
|
import type { I18nClient } from '@payloadcms/translations'
|
||||||
import type { MarkOptional } from 'ts-essentials'
|
import type { MarkOptional } from 'ts-essentials'
|
||||||
|
|
||||||
import type { User } from '../../auth/types.js'
|
import type { FieldPermissions } from '../../auth/types.js'
|
||||||
import type { Locale, ServerProps } from '../../config/types.js'
|
import type { SanitizedConfig } from '../../config/types.js'
|
||||||
import type { ClientField, Field, Validate } from '../../fields/config/types.js'
|
import type { ClientBlock, ClientField, Field } from '../../fields/config/types.js'
|
||||||
import type { DocumentPreferences } from '../../preferences/types.js'
|
import type { Payload } from '../../types/index.js'
|
||||||
import type { FieldDescriptionClientProps, FieldDescriptionServerProps } from './Description.js'
|
import type { ClientTab, FormField, RenderedField } from '../types.js'
|
||||||
import type { FieldErrorClientProps, FieldErrorServerProps } from './Error.js'
|
|
||||||
import type { FieldLabelClientProps, FieldLabelServerProps } from './Label.js'
|
|
||||||
|
|
||||||
export type ClientFieldWithOptionalType = MarkOptional<ClientField, 'type'>
|
export type ClientFieldWithOptionalType = MarkOptional<ClientField, 'type'>
|
||||||
|
|
||||||
|
export type ClientComponentProps = {
|
||||||
|
field: ClientBlock | ClientField | ClientTab
|
||||||
|
fieldState: FormField
|
||||||
|
forceRender?: boolean
|
||||||
|
path: string
|
||||||
|
permissions: FieldPermissions
|
||||||
|
readOnly?: boolean
|
||||||
|
renderedBlocks?: RenderedField[]
|
||||||
|
rowLabels?: React.ReactNode[]
|
||||||
|
schemaPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerComponentProps = {
|
||||||
|
clientField: ClientBlock | ClientField | ClientTab
|
||||||
|
config: SanitizedConfig
|
||||||
|
field: Field
|
||||||
|
i18n: I18nClient
|
||||||
|
payload: Payload
|
||||||
|
}
|
||||||
|
|
||||||
export type ClientFieldBase<
|
export type ClientFieldBase<
|
||||||
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
||||||
> = {
|
> = {
|
||||||
readonly descriptionProps?: FieldDescriptionClientProps<TFieldClient>
|
|
||||||
readonly errorProps?: FieldErrorClientProps<TFieldClient>
|
|
||||||
readonly field: TFieldClient
|
readonly field: TFieldClient
|
||||||
readonly labelProps?: FieldLabelClientProps<TFieldClient>
|
} & Omit<ClientComponentProps, 'field'>
|
||||||
} & FormFieldBase
|
|
||||||
|
|
||||||
export type ServerFieldBase<
|
export type ServerFieldBase<
|
||||||
TFieldServer extends Field = Field,
|
TFieldServer extends Field = Field,
|
||||||
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
||||||
> = {
|
> = {
|
||||||
readonly clientField: TFieldClient
|
readonly clientField: TFieldClient
|
||||||
readonly descriptionProps?: FieldDescriptionServerProps<TFieldServer, TFieldClient>
|
|
||||||
readonly errorProps?: FieldErrorServerProps<TFieldServer, TFieldClient>
|
|
||||||
readonly field: TFieldServer
|
readonly field: TFieldServer
|
||||||
readonly labelProps?: FieldLabelServerProps<TFieldServer, TFieldClient>
|
} & Omit<ClientComponentProps, 'field'> &
|
||||||
} & FormFieldBase &
|
Omit<ServerComponentProps, 'clientField' | 'field'>
|
||||||
Partial<ServerProps>
|
|
||||||
|
|
||||||
export type FormFieldBase = {
|
|
||||||
readonly docPreferences?: DocumentPreferences
|
|
||||||
/**
|
|
||||||
* `forceRender` is added by RenderField automatically.
|
|
||||||
*/
|
|
||||||
readonly forceRender?: boolean
|
|
||||||
readonly locale?: Locale
|
|
||||||
/**
|
|
||||||
* `readOnly` is added by RenderField automatically. This should be used instead of `field.admin.readOnly`.
|
|
||||||
*/
|
|
||||||
readonly readOnly?: boolean
|
|
||||||
readonly user?: User
|
|
||||||
readonly validate?: Validate
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FieldClientComponent<
|
export type FieldClientComponent<
|
||||||
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { Field, Validate } from '../../fields/config/types.js'
|
import { type SupportedLanguages } from '@payloadcms/translations'
|
||||||
import type { Where } from '../../types/index.js'
|
|
||||||
|
import type { Field } from '../../fields/config/types.js'
|
||||||
|
import type { DocumentPreferences } from '../../preferences/types.js'
|
||||||
|
import type { PayloadRequest, Where } from '../../types/index.js'
|
||||||
|
|
||||||
export type Data = {
|
export type Data = {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
@@ -15,20 +18,71 @@ export type FilterOptionsResult = {
|
|||||||
[relation: string]: boolean | Where
|
[relation: string]: boolean | Where
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FormField = {
|
export type FieldState = {
|
||||||
|
customComponents?: {
|
||||||
|
AfterInput?: React.ReactNode
|
||||||
|
BeforeInput?: React.ReactNode
|
||||||
|
Description?: React.ReactNode
|
||||||
|
Error?: React.ReactNode
|
||||||
|
Field?: React.ReactNode
|
||||||
|
Label?: React.ReactNode
|
||||||
|
}
|
||||||
disableFormData?: boolean
|
disableFormData?: boolean
|
||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
errorPaths?: string[]
|
errorPaths?: string[]
|
||||||
fieldSchema?: Field
|
fieldSchema?: Field
|
||||||
filterOptions?: FilterOptionsResult
|
filterOptions?: FilterOptionsResult
|
||||||
initialValue: unknown
|
initialValue: unknown
|
||||||
|
isSidebar?: boolean
|
||||||
passesCondition?: boolean
|
passesCondition?: boolean
|
||||||
rows?: Row[]
|
rows?: Row[]
|
||||||
|
schemaPath: string[]
|
||||||
valid: boolean
|
valid: boolean
|
||||||
validate?: Validate
|
|
||||||
value: unknown
|
value: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FieldStateWithoutComponents = Omit<FieldState, 'customComponents'>
|
||||||
|
|
||||||
export type FormState = {
|
export type FormState = {
|
||||||
[path: string]: FormField
|
[path: string]: FieldState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FormStateWithoutComponents = {
|
||||||
|
[path: string]: FieldStateWithoutComponents
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BuildFormStateArgs = {
|
||||||
|
data?: Data
|
||||||
|
docPreferences?: DocumentPreferences
|
||||||
|
formState?: FormState
|
||||||
|
id?: number | string
|
||||||
|
/*
|
||||||
|
If not i18n was passed, the language can be passed to init i18n
|
||||||
|
*/
|
||||||
|
language?: keyof SupportedLanguages
|
||||||
|
locale?: string
|
||||||
|
operation?: 'create' | 'update'
|
||||||
|
/*
|
||||||
|
Used as a "base path" when adding form state to nested fields
|
||||||
|
*/
|
||||||
|
path?: (number | string)[]
|
||||||
|
/*
|
||||||
|
If true, will render field components within their state object
|
||||||
|
*/
|
||||||
|
renderFields?: boolean
|
||||||
|
req: PayloadRequest
|
||||||
|
returnLockStatus?: boolean
|
||||||
|
schemaPath: string[]
|
||||||
|
updateLastEdited?: boolean
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
collectionSlug: string
|
||||||
|
// Do not type it as never. This still makes it so that either collectionSlug or globalSlug is required, but makes it easier to provide both collectionSlug and globalSlug if it's
|
||||||
|
// unclear which one is actually available.
|
||||||
|
globalSlug?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
collectionSlug?: string
|
||||||
|
globalSlug: string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import type { ServerProps, StaticLabel } from '../../config/types.js'
|
import type { ServerProps, StaticLabel } from '../../config/types.js'
|
||||||
import type { Field } from '../../fields/config/types.js'
|
import type { Field } from '../../fields/config/types.js'
|
||||||
import type { MappedComponent } from '../types.js'
|
|
||||||
import type { ClientFieldWithOptionalType } from './Field.js'
|
import type { ClientFieldWithOptionalType } from './Field.js'
|
||||||
|
|
||||||
export type GenericLabelProps = {
|
export type GenericLabelProps = {
|
||||||
readonly as?: 'label' | 'span'
|
readonly as?: 'label' | 'span'
|
||||||
readonly hideLocale?: boolean
|
readonly hideLocale?: boolean
|
||||||
readonly htmlFor?: string
|
readonly htmlFor?: string
|
||||||
readonly Label?: MappedComponent
|
|
||||||
readonly label?: StaticLabel
|
readonly label?: StaticLabel
|
||||||
readonly localized?: boolean
|
readonly localized?: boolean
|
||||||
|
readonly path?: string
|
||||||
readonly required?: boolean
|
readonly required?: boolean
|
||||||
readonly unstyled?: boolean
|
readonly unstyled?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
44
packages/payload/src/admin/functions/index.ts
Normal file
44
packages/payload/src/admin/functions/index.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import type { ImportMap } from '../../bin/generateImportMap/index.js'
|
||||||
|
import type { SanitizedConfig } from '../../config/types.js'
|
||||||
|
import { PaginatedDocs } from '../../database/types.js'
|
||||||
|
import type { PayloadRequest } from '../../types/index.js'
|
||||||
|
|
||||||
|
export type DefaultServerFunctionArgs = {
|
||||||
|
importMap: ImportMap
|
||||||
|
req: PayloadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerFunctionArgs = {
|
||||||
|
args: Record<string, unknown>
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerFunctionClientArgs = {
|
||||||
|
args: Record<string, unknown>
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerFunctionClient = (args: ServerFunctionClientArgs) => Promise<unknown> | unknown
|
||||||
|
|
||||||
|
export type ServerFunction = (
|
||||||
|
args: DefaultServerFunctionArgs & ServerFunctionClientArgs['args'],
|
||||||
|
) => Promise<unknown> | unknown
|
||||||
|
|
||||||
|
export type ServerFunctionConfig = {
|
||||||
|
fn: ServerFunction
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerFunctionHandler = (
|
||||||
|
args: {
|
||||||
|
config: Promise<SanitizedConfig> | SanitizedConfig
|
||||||
|
importMap: ImportMap
|
||||||
|
} & ServerFunctionClientArgs,
|
||||||
|
) => Promise<unknown>
|
||||||
|
|
||||||
|
export type BuildTableStateArgs = {
|
||||||
|
collectionSlug: string
|
||||||
|
columns?: any[] // TODO: type this (comes from ui pkg)
|
||||||
|
docs: PaginatedDocs['docs']
|
||||||
|
req: PayloadRequest
|
||||||
|
}
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
|
import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
|
||||||
import type { PayloadComponent } from '../config/types.js'
|
import type { ImportMap } from '../bin/generateImportMap/index.js'
|
||||||
|
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
|
||||||
|
import type { SanitizedConfig } from '../config/types.js'
|
||||||
|
import type { Block, ClientField, Field, FieldTypes, Tab } from '../fields/config/types.js'
|
||||||
|
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||||
import type { JsonObject } from '../types/index.js'
|
import type { JsonObject } from '../types/index.js'
|
||||||
|
import type {
|
||||||
|
BuildFormStateArgs,
|
||||||
|
Data,
|
||||||
|
FieldState,
|
||||||
|
FieldStateWithoutComponents,
|
||||||
|
FilterOptionsResult,
|
||||||
|
FormState,
|
||||||
|
FormStateWithoutComponents,
|
||||||
|
Row,
|
||||||
|
} from './forms/Form.js'
|
||||||
|
|
||||||
export type { CellComponentProps, DefaultCellComponentProps } from './elements/Cell.js'
|
export type { DefaultCellComponentProps } from './elements/Cell.js'
|
||||||
export type { ConditionalDateProps } from './elements/DatePicker.js'
|
export type { ConditionalDateProps } from './elements/DatePicker.js'
|
||||||
export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js'
|
export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js'
|
||||||
export type { CustomPreviewButton } from './elements/PreviewButton.js'
|
export type { CustomPreviewButton } from './elements/PreviewButton.js'
|
||||||
@@ -318,9 +333,26 @@ export type {
|
|||||||
GenericErrorProps,
|
GenericErrorProps,
|
||||||
} from './forms/Error.js'
|
} from './forms/Error.js'
|
||||||
|
|
||||||
export type { FormFieldBase, ServerFieldBase } from './forms/Field.js'
|
export type {
|
||||||
|
ClientComponentProps,
|
||||||
|
ClientFieldBase,
|
||||||
|
ClientFieldWithOptionalType,
|
||||||
|
FieldClientComponent,
|
||||||
|
FieldServerComponent,
|
||||||
|
ServerComponentProps,
|
||||||
|
ServerFieldBase,
|
||||||
|
} from './forms/Field.js'
|
||||||
|
|
||||||
export type { Data, FilterOptionsResult, FormField, FormState, Row } from './forms/Form.js'
|
export type {
|
||||||
|
BuildFormStateArgs,
|
||||||
|
Data,
|
||||||
|
FieldState as FormField,
|
||||||
|
FieldStateWithoutComponents as FormFieldWithoutComponents,
|
||||||
|
FilterOptionsResult,
|
||||||
|
FormState,
|
||||||
|
FormStateWithoutComponents,
|
||||||
|
Row,
|
||||||
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
FieldLabelClientComponent,
|
FieldLabelClientComponent,
|
||||||
@@ -333,25 +365,19 @@ export type {
|
|||||||
|
|
||||||
export type { RowLabel, RowLabelComponent } from './forms/RowLabel.js'
|
export type { RowLabel, RowLabelComponent } from './forms/RowLabel.js'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
BuildTableStateArgs,
|
||||||
|
DefaultServerFunctionArgs,
|
||||||
|
ServerFunction,
|
||||||
|
ServerFunctionArgs,
|
||||||
|
ServerFunctionClient,
|
||||||
|
ServerFunctionClientArgs,
|
||||||
|
ServerFunctionConfig,
|
||||||
|
ServerFunctionHandler,
|
||||||
|
} from './functions/index.js'
|
||||||
|
|
||||||
export type { LanguageOptions } from './LanguageOptions.js'
|
export type { LanguageOptions } from './LanguageOptions.js'
|
||||||
|
|
||||||
export type {
|
|
||||||
RichTextAdapter,
|
|
||||||
RichTextAdapterProvider,
|
|
||||||
RichTextGenerateComponentMap,
|
|
||||||
RichTextHooks,
|
|
||||||
} from './RichText.js'
|
|
||||||
|
|
||||||
export type {
|
|
||||||
AdminViewComponent,
|
|
||||||
AdminViewConfig,
|
|
||||||
AdminViewProps,
|
|
||||||
EditViewProps,
|
|
||||||
InitPageResult,
|
|
||||||
ServerSideEditViewProps,
|
|
||||||
VisibleEntities,
|
|
||||||
} from './views/types.js'
|
|
||||||
|
|
||||||
export type MappedServerComponent<TComponentClientProps extends JsonObject = JsonObject> = {
|
export type MappedServerComponent<TComponentClientProps extends JsonObject = JsonObject> = {
|
||||||
Component?: React.ComponentType<TComponentClientProps>
|
Component?: React.ComponentType<TComponentClientProps>
|
||||||
props?: Partial<any>
|
props?: Partial<any>
|
||||||
@@ -370,30 +396,91 @@ export type MappedEmptyComponent = {
|
|||||||
type: 'empty'
|
type: 'empty'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MappedComponent<TComponentClientProps extends JsonObject = JsonObject> =
|
export enum Action {
|
||||||
| MappedClientComponent<TComponentClientProps>
|
RenderConfig = 'render-config',
|
||||||
| MappedEmptyComponent
|
|
||||||
| MappedServerComponent<TComponentClientProps>
|
|
||||||
| undefined
|
|
||||||
|
|
||||||
export type CreateMappedComponent = {
|
|
||||||
<T extends JsonObject>(
|
|
||||||
component: { Component: React.FC<T> } | null | PayloadComponent<T>,
|
|
||||||
props: {
|
|
||||||
clientProps?: JsonObject
|
|
||||||
serverProps?: object
|
|
||||||
},
|
|
||||||
fallback: React.FC,
|
|
||||||
identifier: string,
|
|
||||||
): MappedComponent<T>
|
|
||||||
|
|
||||||
<T extends JsonObject>(
|
|
||||||
components: ({ Component: React.FC<T> } | PayloadComponent<T>)[],
|
|
||||||
props: {
|
|
||||||
clientProps?: JsonObject
|
|
||||||
serverProps?: object
|
|
||||||
},
|
|
||||||
fallback: React.FC,
|
|
||||||
identifier: string,
|
|
||||||
): MappedComponent<T>[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RenderEntityConfigArgs = {
|
||||||
|
collectionSlug?: string
|
||||||
|
data?: Data
|
||||||
|
globalSlug?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RenderRootConfigArgs = {}
|
||||||
|
|
||||||
|
export type RenderFieldConfigArgs = {
|
||||||
|
collectionSlug?: string
|
||||||
|
formState?: FormState
|
||||||
|
globalSlug?: string
|
||||||
|
schemaPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RenderConfigArgs = {
|
||||||
|
action: Action.RenderConfig
|
||||||
|
config: Promise<SanitizedConfig> | SanitizedConfig
|
||||||
|
i18n: I18nClient
|
||||||
|
importMap: ImportMap
|
||||||
|
languageCode: AcceptedLanguages
|
||||||
|
serverProps?: any
|
||||||
|
} & (RenderEntityConfigArgs | RenderFieldConfigArgs | RenderRootConfigArgs)
|
||||||
|
|
||||||
|
export type PayloadServerAction = (
|
||||||
|
args:
|
||||||
|
| {
|
||||||
|
[key: string]: any
|
||||||
|
action: Action
|
||||||
|
i18n: I18nClient
|
||||||
|
}
|
||||||
|
| RenderConfigArgs,
|
||||||
|
) => Promise<string>
|
||||||
|
|
||||||
|
export type RenderedField = {
|
||||||
|
Field: React.ReactNode
|
||||||
|
indexPath?: string
|
||||||
|
initialSchemaPath?: string
|
||||||
|
isSidebar: boolean
|
||||||
|
path: string
|
||||||
|
schemaPath: string
|
||||||
|
type: FieldTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldRow = {
|
||||||
|
RowLabel?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DocumentSlots = {
|
||||||
|
PreviewButton?: React.ReactNode
|
||||||
|
PublishButton?: React.ReactNode
|
||||||
|
SaveButton?: React.ReactNode
|
||||||
|
SaveDraftButton?: React.ReactNode
|
||||||
|
Upload?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
RichTextAdapter,
|
||||||
|
RichTextAdapterProvider,
|
||||||
|
RichTextGenerateComponentMap,
|
||||||
|
RichTextHooks,
|
||||||
|
} from './RichText.js'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AdminViewComponent,
|
||||||
|
AdminViewConfig,
|
||||||
|
AdminViewProps,
|
||||||
|
ClientSideEditViewProps,
|
||||||
|
EditViewProps,
|
||||||
|
InitPageResult,
|
||||||
|
ServerSideEditViewProps,
|
||||||
|
VisibleEntities,
|
||||||
|
} from './views/types.js'
|
||||||
|
|
||||||
|
type SchemaPath = {} & string
|
||||||
|
export type FieldSchemaMap = Map<
|
||||||
|
SchemaPath,
|
||||||
|
| {
|
||||||
|
fields: Field[]
|
||||||
|
}
|
||||||
|
| Block
|
||||||
|
| Field
|
||||||
|
| Tab
|
||||||
|
>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { Locale, MetaConfig, PayloadComponent } from '../../config/types.js
|
|||||||
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
|
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
|
||||||
import type { PayloadRequest } from '../../types/index.js'
|
import type { PayloadRequest } from '../../types/index.js'
|
||||||
import type { LanguageOptions } from '../LanguageOptions.js'
|
import type { LanguageOptions } from '../LanguageOptions.js'
|
||||||
import type { MappedComponent } from '../types.js'
|
import type { Data, DocumentSlots, PayloadServerAction } from '../types.js'
|
||||||
|
|
||||||
export type AdminViewConfig = {
|
export type AdminViewConfig = {
|
||||||
Component: AdminViewComponent
|
Component: AdminViewComponent
|
||||||
@@ -20,17 +20,17 @@ export type AdminViewConfig = {
|
|||||||
strict?: boolean
|
strict?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MappedView = {
|
|
||||||
actions?: MappedComponent[]
|
|
||||||
Component: MappedComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AdminViewProps = {
|
export type AdminViewProps = {
|
||||||
readonly clientConfig: ClientConfig
|
readonly clientConfig: ClientConfig
|
||||||
|
readonly disableActions?: boolean
|
||||||
|
readonly drawerSlug?: string
|
||||||
readonly importMap: ImportMap
|
readonly importMap: ImportMap
|
||||||
|
readonly initialData?: Data
|
||||||
readonly initPageResult: InitPageResult
|
readonly initPageResult: InitPageResult
|
||||||
readonly params?: { [key: string]: string | string[] | undefined }
|
readonly params?: { [key: string]: string | string[] | undefined }
|
||||||
readonly searchParams: { [key: string]: string | string[] | undefined }
|
readonly searchParams: { [key: string]: string | string[] | undefined }
|
||||||
|
readonly redirectAfterDelete?: boolean
|
||||||
|
readonly redirectAfterDuplicate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminViewComponent = PayloadComponent<AdminViewProps>
|
export type AdminViewComponent = PayloadComponent<AdminViewProps>
|
||||||
@@ -62,6 +62,9 @@ export type InitPageResult = {
|
|||||||
export type ServerSideEditViewProps = {
|
export type ServerSideEditViewProps = {
|
||||||
readonly initPageResult: InitPageResult
|
readonly initPageResult: InitPageResult
|
||||||
readonly params: { [key: string]: string | string[] | undefined }
|
readonly params: { [key: string]: string | string[] | undefined }
|
||||||
|
readonly payloadServerAction: PayloadServerAction
|
||||||
readonly routeSegments: string[]
|
readonly routeSegments: string[]
|
||||||
readonly searchParams: { [key: string]: string | string[] | undefined }
|
readonly searchParams: { [key: string]: string | string[] | undefined }
|
||||||
}
|
} & ClientSideEditViewProps
|
||||||
|
|
||||||
|
export type ClientSideEditViewProps = {} & DocumentSlots
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import type { Payload } from '../../../index.js'
|
import type { Payload } from '../../../index.js'
|
||||||
import type { PayloadRequest } from '../../../types/index.js'
|
|
||||||
import type { AuthArgs, AuthResult } from '../auth.js'
|
import type { AuthArgs, AuthResult } from '../auth.js'
|
||||||
|
|
||||||
import { createLocalReq } from '../../../utilities/createLocalReq.js'
|
import { createLocalReq } from '../../../utilities/createLocalReq.js'
|
||||||
import { auth as authOperation } from '../auth.js'
|
import { auth as authOperation } from '../auth.js'
|
||||||
|
|
||||||
export const auth = async (payload: Payload, options: AuthArgs): Promise<AuthResult> => {
|
export const auth = async (payload: Payload, options: AuthArgs): Promise<AuthResult> => {
|
||||||
const { headers } = options
|
const { headers, req } = options
|
||||||
|
|
||||||
return await authOperation({
|
return await authOperation({
|
||||||
headers,
|
headers,
|
||||||
req: await createLocalReq({ req: options.req as PayloadRequest }, payload),
|
req: await createLocalReq({ req }, payload),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { PayloadComponent } from '../../config/types.js'
|
import type { PayloadComponent } from '../../config/types.js'
|
||||||
|
|
||||||
export function parsePayloadComponent(payloadComponent: PayloadComponent): {
|
export function parsePayloadComponent(PayloadComponent: PayloadComponent): {
|
||||||
exportName: string
|
exportName: string
|
||||||
path: string
|
path: string
|
||||||
} {
|
} {
|
||||||
if (!payloadComponent) {
|
if (!PayloadComponent) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const pathAndMaybeExport =
|
const pathAndMaybeExport =
|
||||||
typeof payloadComponent === 'string' ? payloadComponent : payloadComponent.path
|
typeof PayloadComponent === 'string' ? PayloadComponent : PayloadComponent.path
|
||||||
|
|
||||||
let path = ''
|
let path = ''
|
||||||
let exportName = 'default'
|
let exportName = 'default'
|
||||||
@@ -19,8 +19,8 @@ export function parsePayloadComponent(payloadComponent: PayloadComponent): {
|
|||||||
path = pathAndMaybeExport
|
path = pathAndMaybeExport
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof payloadComponent === 'object' && payloadComponent.exportName) {
|
if (typeof PayloadComponent === 'object' && PayloadComponent.exportName) {
|
||||||
exportName = payloadComponent.exportName
|
exportName = PayloadComponent.exportName
|
||||||
}
|
}
|
||||||
|
|
||||||
return { exportName, path }
|
return { exportName, path }
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import type { MappedComponent, StaticDescription } from '../../admin/types.js'
|
import type { I18nClient } from '@payloadcms/translations'
|
||||||
import type { MappedView } from '../../admin/views/types.js'
|
|
||||||
import type { LivePreviewConfig, ServerOnlyLivePreviewProperties } from '../../config/types.js'
|
import type { StaticDescription } from '../../admin/types.js'
|
||||||
|
import type {
|
||||||
|
LivePreviewConfig,
|
||||||
|
ServerOnlyLivePreviewProperties,
|
||||||
|
StaticLabel,
|
||||||
|
} from '../../config/types.js'
|
||||||
import type { ClientField } from '../../fields/config/client.js'
|
import type { ClientField } from '../../fields/config/client.js'
|
||||||
|
import type { Payload } from '../../types/index.js'
|
||||||
import type { SanitizedCollectionConfig } from './types.js'
|
import type { SanitizedCollectionConfig } from './types.js'
|
||||||
|
|
||||||
|
import { createClientFields } from '../../fields/config/client.js'
|
||||||
|
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
|
||||||
|
|
||||||
export type ServerOnlyCollectionProperties = keyof Pick<
|
export type ServerOnlyCollectionProperties = keyof Pick<
|
||||||
SanitizedCollectionConfig,
|
SanitizedCollectionConfig,
|
||||||
'access' | 'custom' | 'endpoints' | 'hooks' | 'joins'
|
'access' | 'custom' | 'endpoints' | 'hooks' | 'joins'
|
||||||
@@ -26,34 +35,7 @@ export type ServerOnlyUploadProperties = keyof Pick<
|
|||||||
export type ClientCollectionConfig = {
|
export type ClientCollectionConfig = {
|
||||||
_isPreviewEnabled?: true
|
_isPreviewEnabled?: true
|
||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: null
|
||||||
afterList: MappedComponent[]
|
|
||||||
afterListTable: MappedComponent[]
|
|
||||||
beforeList: MappedComponent[]
|
|
||||||
beforeListTable: MappedComponent[]
|
|
||||||
Description: MappedComponent
|
|
||||||
edit: {
|
|
||||||
PreviewButton: MappedComponent
|
|
||||||
PublishButton: MappedComponent
|
|
||||||
SaveButton: MappedComponent
|
|
||||||
SaveDraftButton: MappedComponent
|
|
||||||
Upload: MappedComponent
|
|
||||||
}
|
|
||||||
views: {
|
|
||||||
edit: {
|
|
||||||
[key: string]: MappedView
|
|
||||||
api: MappedView
|
|
||||||
default: MappedView
|
|
||||||
livePreview: MappedView
|
|
||||||
version: MappedView
|
|
||||||
versions: MappedView
|
|
||||||
}
|
|
||||||
list: {
|
|
||||||
actions: MappedComponent[]
|
|
||||||
Component: MappedComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
description?: StaticDescription
|
description?: StaticDescription
|
||||||
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
||||||
} & Omit<
|
} & Omit<
|
||||||
@@ -61,4 +43,156 @@ export type ClientCollectionConfig = {
|
|||||||
'components' | 'description' | 'joins' | 'livePreview' | ServerOnlyCollectionAdminProperties
|
'components' | 'description' | 'joins' | 'livePreview' | ServerOnlyCollectionAdminProperties
|
||||||
>
|
>
|
||||||
fields: ClientField[]
|
fields: ClientField[]
|
||||||
} & Omit<SanitizedCollectionConfig, 'admin' | 'fields' | ServerOnlyCollectionProperties>
|
labels?: {
|
||||||
|
plural: StaticLabel
|
||||||
|
singular: StaticLabel
|
||||||
|
}
|
||||||
|
} & Omit<SanitizedCollectionConfig, 'admin' | 'fields' | 'labels' | ServerOnlyCollectionProperties>
|
||||||
|
|
||||||
|
const serverOnlyCollectionProperties: Partial<ServerOnlyCollectionProperties>[] = [
|
||||||
|
'hooks',
|
||||||
|
'access',
|
||||||
|
'endpoints',
|
||||||
|
'custom',
|
||||||
|
'joins',
|
||||||
|
// `upload`
|
||||||
|
// `admin`
|
||||||
|
// are all handled separately
|
||||||
|
]
|
||||||
|
|
||||||
|
const serverOnlyUploadProperties: Partial<ServerOnlyUploadProperties>[] = [
|
||||||
|
'adminThumbnail',
|
||||||
|
'externalFileHeaderFilter',
|
||||||
|
'handlers',
|
||||||
|
'modifyResponseHeaders',
|
||||||
|
'withMetadata',
|
||||||
|
]
|
||||||
|
|
||||||
|
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
|
||||||
|
'hidden',
|
||||||
|
'preview',
|
||||||
|
// `livePreview` is handled separately
|
||||||
|
]
|
||||||
|
|
||||||
|
export const createClientCollectionConfig = ({
|
||||||
|
collection,
|
||||||
|
defaultIDType,
|
||||||
|
i18n,
|
||||||
|
}: {
|
||||||
|
collection: SanitizedCollectionConfig
|
||||||
|
defaultIDType: Payload['config']['db']['defaultIDType']
|
||||||
|
i18n: I18nClient
|
||||||
|
}): ClientCollectionConfig => {
|
||||||
|
const clientCollection = deepCopyObjectSimple(collection) as unknown as ClientCollectionConfig
|
||||||
|
|
||||||
|
clientCollection.fields = createClientFields({
|
||||||
|
clientFields: clientCollection?.fields || [],
|
||||||
|
defaultIDType,
|
||||||
|
fields: collection.fields,
|
||||||
|
i18n,
|
||||||
|
parentSchemaPath: [collection.slug],
|
||||||
|
})
|
||||||
|
|
||||||
|
serverOnlyCollectionProperties.forEach((key) => {
|
||||||
|
if (key in clientCollection) {
|
||||||
|
delete clientCollection[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if ('upload' in clientCollection && typeof clientCollection.upload === 'object') {
|
||||||
|
serverOnlyUploadProperties.forEach((key) => {
|
||||||
|
if (key in clientCollection.upload) {
|
||||||
|
delete clientCollection.upload[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if ('imageSizes' in clientCollection.upload && clientCollection.upload.imageSizes.length) {
|
||||||
|
clientCollection.upload.imageSizes = clientCollection.upload.imageSizes.map((size) => {
|
||||||
|
const sanitizedSize = { ...size }
|
||||||
|
if ('generateImageName' in sanitizedSize) {
|
||||||
|
delete sanitizedSize.generateImageName
|
||||||
|
}
|
||||||
|
return sanitizedSize
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('auth' in clientCollection && typeof clientCollection.auth === 'object') {
|
||||||
|
delete clientCollection.auth.strategies
|
||||||
|
delete clientCollection.auth.forgotPassword
|
||||||
|
delete clientCollection.auth.verify
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection.labels) {
|
||||||
|
Object.entries(collection.labels).forEach(([labelType, collectionLabel]) => {
|
||||||
|
if (typeof collectionLabel === 'function') {
|
||||||
|
clientCollection.labels[labelType] = collectionLabel({ t: i18n.t })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collection.admin.preview) {
|
||||||
|
clientCollection._isPreviewEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientCollection.admin) {
|
||||||
|
clientCollection.admin = {} as ClientCollectionConfig['admin']
|
||||||
|
}
|
||||||
|
|
||||||
|
serverOnlyCollectionAdminProperties.forEach((key) => {
|
||||||
|
if (key in clientCollection.admin) {
|
||||||
|
delete clientCollection.admin[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
clientCollection.admin.components = null
|
||||||
|
|
||||||
|
let description = undefined
|
||||||
|
|
||||||
|
if (collection.admin?.description) {
|
||||||
|
if (
|
||||||
|
typeof collection.admin?.description === 'string' ||
|
||||||
|
typeof collection.admin?.description === 'object'
|
||||||
|
) {
|
||||||
|
description = collection.admin.description
|
||||||
|
} else if (typeof collection.admin?.description === 'function') {
|
||||||
|
description = collection.admin?.description({ t: i18n.t })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientCollection.admin.description = description
|
||||||
|
|
||||||
|
if (
|
||||||
|
'livePreview' in clientCollection.admin &&
|
||||||
|
clientCollection.admin.livePreview &&
|
||||||
|
'url' in clientCollection.admin.livePreview
|
||||||
|
) {
|
||||||
|
delete clientCollection.admin.livePreview.url
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createClientCollectionConfigs = ({
|
||||||
|
collections,
|
||||||
|
defaultIDType,
|
||||||
|
i18n,
|
||||||
|
}: {
|
||||||
|
collections: SanitizedCollectionConfig[]
|
||||||
|
defaultIDType: Payload['config']['db']['defaultIDType']
|
||||||
|
i18n: I18nClient
|
||||||
|
}): ClientCollectionConfig[] => {
|
||||||
|
const clientCollections = new Array(collections.length)
|
||||||
|
|
||||||
|
for (let i = 0; i < collections.length; i++) {
|
||||||
|
const collection = collections[i]
|
||||||
|
|
||||||
|
clientCollections[i] = createClientCollectionConfig({
|
||||||
|
collection,
|
||||||
|
defaultIDType,
|
||||||
|
i18n,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientCollections
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import type { MappedComponent } from '../admin/types.js'
|
import type { I18nClient } from '@payloadcms/translations'
|
||||||
import type { ClientCollectionConfig } from '../collections/config/client.js'
|
|
||||||
import type { ClientGlobalConfig } from '../globals/config/client.js'
|
|
||||||
import type {
|
import type {
|
||||||
LivePreviewConfig,
|
LivePreviewConfig,
|
||||||
SanitizedConfig,
|
SanitizedConfig,
|
||||||
ServerOnlyLivePreviewProperties,
|
ServerOnlyLivePreviewProperties,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ClientCollectionConfig,
|
||||||
|
createClientCollectionConfigs,
|
||||||
|
} from '../collections/config/client.js'
|
||||||
|
import { type ClientGlobalConfig, createClientGlobalConfigs } from '../globals/config/client.js'
|
||||||
|
import { deepCopyObjectSimple } from '../utilities/deepCopyObject.js'
|
||||||
|
|
||||||
export type ServerOnlyRootProperties = keyof Pick<
|
export type ServerOnlyRootProperties = keyof Pick<
|
||||||
SanitizedConfig,
|
SanitizedConfig,
|
||||||
| 'bin'
|
| 'bin'
|
||||||
@@ -27,20 +33,15 @@ export type ServerOnlyRootProperties = keyof Pick<
|
|||||||
| 'typescript'
|
| 'typescript'
|
||||||
>
|
>
|
||||||
|
|
||||||
export type ServerOnlyRootAdminProperties = keyof Pick<SanitizedConfig['admin'], 'components'>
|
export type ServerOnlyRootAdminProperties = keyof Pick<
|
||||||
|
SanitizedConfig['admin'],
|
||||||
|
'components' | 'serverFunctions'
|
||||||
|
>
|
||||||
|
|
||||||
export type ClientConfig = {
|
export type ClientConfig = {
|
||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: null
|
||||||
actions?: MappedComponent[]
|
dependencies?: Record<string, React.ReactNode>
|
||||||
Avatar: MappedComponent
|
|
||||||
graphics: {
|
|
||||||
Icon: MappedComponent
|
|
||||||
Logo: MappedComponent
|
|
||||||
}
|
|
||||||
LogoutButton?: MappedComponent
|
|
||||||
}
|
|
||||||
dependencies?: Record<string, MappedComponent>
|
|
||||||
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
||||||
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
|
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
|
||||||
collections: ClientCollectionConfig[]
|
collections: ClientCollectionConfig[]
|
||||||
@@ -48,6 +49,10 @@ export type ClientConfig = {
|
|||||||
globals: ClientGlobalConfig[]
|
globals: ClientGlobalConfig[]
|
||||||
} & Omit<SanitizedConfig, 'admin' | 'collections' | 'globals' | ServerOnlyRootProperties>
|
} & Omit<SanitizedConfig, 'admin' | 'collections' | 'globals' | ServerOnlyRootProperties>
|
||||||
|
|
||||||
|
export const serverOnlyAdminConfigProperties: readonly Partial<ServerOnlyRootAdminProperties>[] = [
|
||||||
|
'serverFunctions',
|
||||||
|
]
|
||||||
|
|
||||||
export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperties>[] = [
|
export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperties>[] = [
|
||||||
'endpoints',
|
'endpoints',
|
||||||
'db',
|
'db',
|
||||||
@@ -64,6 +69,57 @@ export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperti
|
|||||||
'email',
|
'email',
|
||||||
'custom',
|
'custom',
|
||||||
'graphQL',
|
'graphQL',
|
||||||
'logger'
|
'logger',
|
||||||
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
|
// `admin`, `onInit`, `localization`, `collections`, and `globals` are all handled separately
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const createClientConfig = ({
|
||||||
|
config,
|
||||||
|
i18n,
|
||||||
|
}: {
|
||||||
|
config: SanitizedConfig
|
||||||
|
i18n: I18nClient
|
||||||
|
}): ClientConfig => {
|
||||||
|
// We can use deepCopySimple here, as the clientConfig should be JSON serializable anyways, since it will be sent from server => client
|
||||||
|
const clientConfig = deepCopyObjectSimple(config) as unknown as ClientConfig
|
||||||
|
|
||||||
|
for (const key of serverOnlyConfigProperties) {
|
||||||
|
if (key in clientConfig) {
|
||||||
|
delete clientConfig[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('localization' in clientConfig && clientConfig.localization) {
|
||||||
|
for (const locale of clientConfig.localization.locales) {
|
||||||
|
delete locale.toString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientConfig.admin) {
|
||||||
|
clientConfig.admin = {} as ClientConfig['admin']
|
||||||
|
}
|
||||||
|
|
||||||
|
clientConfig.admin.components = null
|
||||||
|
|
||||||
|
if (
|
||||||
|
'livePreview' in clientConfig.admin &&
|
||||||
|
clientConfig.admin.livePreview &&
|
||||||
|
'url' in clientConfig.admin.livePreview
|
||||||
|
) {
|
||||||
|
delete clientConfig.admin.livePreview.url
|
||||||
|
}
|
||||||
|
|
||||||
|
clientConfig.collections = createClientCollectionConfigs({
|
||||||
|
collections: config.collections,
|
||||||
|
defaultIDType: config.db.defaultIDType,
|
||||||
|
i18n,
|
||||||
|
})
|
||||||
|
|
||||||
|
clientConfig.globals = createClientGlobalConfigs({
|
||||||
|
defaultIDType: config.db.defaultIDType,
|
||||||
|
globals: config.globals,
|
||||||
|
i18n,
|
||||||
|
})
|
||||||
|
|
||||||
|
return clientConfig
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ import type { default as sharp } from 'sharp'
|
|||||||
import type { DeepRequired } from 'ts-essentials'
|
import type { DeepRequired } from 'ts-essentials'
|
||||||
|
|
||||||
import type { RichTextAdapterProvider } from '../admin/RichText.js'
|
import type { RichTextAdapterProvider } from '../admin/RichText.js'
|
||||||
import type { DocumentTabConfig, RichTextAdapter } from '../admin/types.js'
|
import type { DocumentTabConfig, RichTextAdapter, ServerFunctionConfig } from '../admin/types.js'
|
||||||
import type { AdminViewConfig, ServerSideEditViewProps } from '../admin/views/types.js'
|
import type {
|
||||||
|
AdminViewConfig,
|
||||||
|
ServerSideEditViewProps,
|
||||||
|
VisibleEntities,
|
||||||
|
} from '../admin/views/types.js'
|
||||||
import type { Permissions } from '../auth/index.js'
|
import type { Permissions } from '../auth/index.js'
|
||||||
import type {
|
import type {
|
||||||
AddToImportMap,
|
AddToImportMap,
|
||||||
@@ -384,16 +388,20 @@ export type EditViewConfig = {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ClientProps = {
|
||||||
|
readonly [key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
export type ServerProps = {
|
export type ServerProps = {
|
||||||
readonly i18n: I18nClient
|
readonly i18n: I18nClient
|
||||||
readonly locale?: Locale
|
readonly locale?: Locale
|
||||||
readonly params?: { [key: string]: string | string[] | undefined }
|
readonly params?: { [key: string]: string | string[] | undefined }
|
||||||
readonly payload: Payload
|
readonly payload: Payload
|
||||||
readonly permissions?: Permissions
|
readonly permissions?: Permissions
|
||||||
readonly [key: string]: unknown
|
|
||||||
readonly searchParams?: { [key: string]: string | string[] | undefined }
|
readonly searchParams?: { [key: string]: string | string[] | undefined }
|
||||||
readonly user?: TypedUser
|
readonly user?: TypedUser
|
||||||
}
|
readonly visibleEntities?: VisibleEntities
|
||||||
|
} & ClientProps
|
||||||
|
|
||||||
export const serverProps: (keyof ServerProps)[] = [
|
export const serverProps: (keyof ServerProps)[] = [
|
||||||
'payload',
|
'payload',
|
||||||
@@ -818,6 +826,7 @@ export type Config = {
|
|||||||
/** The route for the unauthorized page. */
|
/** The route for the unauthorized page. */
|
||||||
unauthorized?: string
|
unauthorized?: string
|
||||||
}
|
}
|
||||||
|
serverFunctions?: ServerFunctionConfig[]
|
||||||
/**
|
/**
|
||||||
* Restrict the Admin Panel theme to use only one of your choice
|
* Restrict the Admin Panel theme to use only one of your choice
|
||||||
*
|
*
|
||||||
@@ -1078,44 +1087,43 @@ export type SanitizedConfig = {
|
|||||||
'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload'
|
'collections' | 'editor' | 'endpoint' | 'globals' | 'i18n' | 'localization' | 'upload'
|
||||||
>
|
>
|
||||||
|
|
||||||
export type EditConfig =
|
export type EditConfig = EditConfigWithoutRoot | EditConfigWithRoot
|
||||||
| {
|
|
||||||
[key: string]: EditViewConfig
|
export type EditConfigWithRoot = {
|
||||||
/**
|
api?: never
|
||||||
* Replace or modify individual nested routes, or add new ones:
|
default?: never
|
||||||
* + `default` - `/admin/collections/:collection/:id`
|
livePreview?: never
|
||||||
* + `api` - `/admin/collections/:collection/:id/api`
|
/**
|
||||||
* + `livePreview` - `/admin/collections/:collection/:id/preview`
|
* Replace or modify _all_ nested document views and routes, including the document header, controls, and tabs. This cannot be used in conjunction with other nested views.
|
||||||
* + `references` - `/admin/collections/:collection/:id/references`
|
* + `root` - `/admin/collections/:collection/:id/**\/*`
|
||||||
* + `relationships` - `/admin/collections/:collection/:id/relationships`
|
*/
|
||||||
* + `versions` - `/admin/collections/:collection/:id/versions`
|
root: Partial<EditViewConfig>
|
||||||
* + `version` - `/admin/collections/:collection/:id/versions/:version`
|
version?: never
|
||||||
* + `customView` - `/admin/collections/:collection/:id/:path`
|
versions?: never
|
||||||
*
|
}
|
||||||
* To override the entire Edit View including all nested views, use the `root` key.
|
|
||||||
*/
|
export type EditConfigWithoutRoot = {
|
||||||
api?: Partial<EditViewConfig>
|
[key: string]: EditViewConfig
|
||||||
default?: Partial<EditViewConfig>
|
/**
|
||||||
livePreview?: Partial<EditViewConfig>
|
* Replace or modify individual nested routes, or add new ones:
|
||||||
root?: never
|
* + `default` - `/admin/collections/:collection/:id`
|
||||||
version?: Partial<EditViewConfig>
|
* + `api` - `/admin/collections/:collection/:id/api`
|
||||||
versions?: Partial<EditViewConfig>
|
* + `livePreview` - `/admin/collections/:collection/:id/preview`
|
||||||
// TODO: uncomment these as they are built
|
* + `references` - `/admin/collections/:collection/:id/references`
|
||||||
// references?: EditView
|
* + `relationships` - `/admin/collections/:collection/:id/relationships`
|
||||||
// relationships?: EditView
|
* + `versions` - `/admin/collections/:collection/:id/versions`
|
||||||
}
|
* + `version` - `/admin/collections/:collection/:id/versions/:version`
|
||||||
| {
|
* + `customView` - `/admin/collections/:collection/:id/:path`
|
||||||
api?: never
|
*
|
||||||
default?: never
|
* To override the entire Edit View including all nested views, use the `root` key.
|
||||||
livePreview?: never
|
*/
|
||||||
/**
|
api?: Partial<EditViewConfig>
|
||||||
* Replace or modify _all_ nested document views and routes, including the document header, controls, and tabs. This cannot be used in conjunction with other nested views.
|
default?: Partial<EditViewConfig>
|
||||||
* + `root` - `/admin/collections/:collection/:id/**\/*`
|
livePreview?: Partial<EditViewConfig>
|
||||||
*/
|
root?: never
|
||||||
root: Partial<EditViewConfig>
|
version?: Partial<EditViewConfig>
|
||||||
version?: never
|
versions?: Partial<EditViewConfig>
|
||||||
versions?: never
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export type EntityDescriptionComponent = CustomComponent
|
export type EntityDescriptionComponent = CustomComponent
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { APIError } from './APIError.js'
|
|||||||
export let ValidationErrorName = 'ValidationError'
|
export let ValidationErrorName = 'ValidationError'
|
||||||
|
|
||||||
export type ValidationFieldError = {
|
export type ValidationFieldError = {
|
||||||
// The field path, i.e. "textField", "groupField.subTextField", etc.
|
fieldPath: (number | string)[]
|
||||||
field: string
|
fieldSchemaPath: string[]
|
||||||
// The error message to display for this field
|
// The error message to display for this field
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ export class ValidationError extends APIError<{
|
|||||||
: en.translations.error.followingFieldsInvalid_other
|
: en.translations.error.followingFieldsInvalid_other
|
||||||
|
|
||||||
super(
|
super(
|
||||||
`${message} ${results.errors.map((f) => f.field).join(', ')}`,
|
`${message} ${results.errors.map((f) => f.fieldPath.join('.')).join(', ')}`,
|
||||||
httpStatus.BAD_REQUEST,
|
httpStatus.BAD_REQUEST,
|
||||||
results,
|
results,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export {
|
|||||||
tabHasName,
|
tabHasName,
|
||||||
valueIsValueWithRelation,
|
valueIsValueWithRelation,
|
||||||
} from '../fields/config/types.js'
|
} from '../fields/config/types.js'
|
||||||
|
|
||||||
|
export { getFieldPaths } from '../fields/getFieldPaths.js'
|
||||||
|
|
||||||
export * from '../fields/validations.js'
|
export * from '../fields/validations.js'
|
||||||
|
|
||||||
export { validOperators } from '../types/constants.js'
|
export { validOperators } from '../types/constants.js'
|
||||||
|
|||||||
@@ -1,4 +1,24 @@
|
|||||||
import type { ClientField, FieldBase } from '../../fields/config/types.js'
|
import type { I18nClient } from '@payloadcms/translations'
|
||||||
|
|
||||||
|
import type { Payload } from '../../types/index.js'
|
||||||
|
|
||||||
|
import { MissingEditorProp } from '../../errors/MissingEditorProp.js'
|
||||||
|
import {
|
||||||
|
type AdminClient,
|
||||||
|
type BlocksFieldClient,
|
||||||
|
type ClientBlock,
|
||||||
|
type ClientField,
|
||||||
|
type Field,
|
||||||
|
fieldAffectsData,
|
||||||
|
type FieldBase,
|
||||||
|
fieldIsPresentationalOnly,
|
||||||
|
type LabelsClient,
|
||||||
|
type RadioFieldClient,
|
||||||
|
type RowFieldClient,
|
||||||
|
type SelectFieldClient,
|
||||||
|
type TabsFieldClient,
|
||||||
|
} from '../../fields/config/types.js'
|
||||||
|
import { getFieldPaths } from '../getFieldPaths.js'
|
||||||
|
|
||||||
// Should not be used - ClientField should be used instead. This is why we don't export ClientField, we don't want people
|
// Should not be used - ClientField should be used instead. This is why we don't export ClientField, we don't want people
|
||||||
// to accidentally use it instead of ClientField and get confused
|
// to accidentally use it instead of ClientField and get confused
|
||||||
@@ -16,3 +36,332 @@ export type ServerOnlyFieldProperties =
|
|||||||
| keyof Pick<FieldBase, 'access' | 'custom' | 'defaultValue' | 'hooks'>
|
| keyof Pick<FieldBase, 'access' | 'custom' | 'defaultValue' | 'hooks'>
|
||||||
|
|
||||||
export type ServerOnlyFieldAdminProperties = keyof Pick<FieldBase['admin'], 'condition'>
|
export type ServerOnlyFieldAdminProperties = keyof Pick<FieldBase['admin'], 'condition'>
|
||||||
|
|
||||||
|
export const createClientField = ({
|
||||||
|
clientField = {} as ClientField,
|
||||||
|
defaultIDType,
|
||||||
|
field: incomingField,
|
||||||
|
i18n,
|
||||||
|
schemaPath,
|
||||||
|
}: {
|
||||||
|
clientField?: ClientField
|
||||||
|
defaultIDType: Payload['config']['db']['defaultIDType']
|
||||||
|
field: Field
|
||||||
|
i18n: I18nClient
|
||||||
|
schemaPath: string[]
|
||||||
|
}): ClientField => {
|
||||||
|
const serverOnlyFieldProperties: Partial<ServerOnlyFieldProperties>[] = [
|
||||||
|
'hooks',
|
||||||
|
'access',
|
||||||
|
'validate',
|
||||||
|
'defaultValue',
|
||||||
|
'filterOptions', // This is a `relationship` and `upload` only property
|
||||||
|
'editor', // This is a `richText` only property
|
||||||
|
'custom',
|
||||||
|
'typescriptSchema',
|
||||||
|
'dbName', // can be a function
|
||||||
|
'enumName', // can be a function
|
||||||
|
// the following props are handled separately (see below):
|
||||||
|
// `label`
|
||||||
|
// `fields`
|
||||||
|
// `blocks`
|
||||||
|
// `tabs`
|
||||||
|
// `admin`
|
||||||
|
]
|
||||||
|
|
||||||
|
clientField._schemaPath = schemaPath
|
||||||
|
clientField.admin = clientField.admin || {}
|
||||||
|
// clientField.admin.readOnly = true
|
||||||
|
|
||||||
|
serverOnlyFieldProperties.forEach((key) => {
|
||||||
|
if (key in clientField) {
|
||||||
|
delete clientField[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (fieldIsPresentationalOnly(incomingField)) {
|
||||||
|
clientField._isPresentational = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHidden = 'hidden' in incomingField && incomingField?.hidden
|
||||||
|
const disabledFromAdmin =
|
||||||
|
incomingField?.admin && 'disabled' in incomingField.admin && incomingField.admin.disabled
|
||||||
|
|
||||||
|
if (fieldAffectsData(clientField) && (isHidden || disabledFromAdmin)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
'label' in clientField &&
|
||||||
|
'label' in incomingField &&
|
||||||
|
typeof incomingField.label === 'function'
|
||||||
|
) {
|
||||||
|
clientField.label = incomingField.label({ t: i18n.t })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(clientField.admin instanceof Object)) {
|
||||||
|
clientField.admin = {} as AdminClient
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('admin' in incomingField && 'width' in incomingField.admin) {
|
||||||
|
clientField.admin.style = {
|
||||||
|
...clientField.admin.style,
|
||||||
|
'--field-width': clientField.admin.width,
|
||||||
|
width: undefined, // avoid needlessly adding this to the element's style attribute
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!(clientField.admin.style instanceof Object)) {
|
||||||
|
clientField.admin.style = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientField.admin.style.flex = '1 1 auto'
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (incomingField.type) {
|
||||||
|
case 'array':
|
||||||
|
case 'group':
|
||||||
|
case 'collapsible':
|
||||||
|
case 'row': {
|
||||||
|
const field = clientField as unknown as RowFieldClient
|
||||||
|
|
||||||
|
if (!field.fields) {
|
||||||
|
field.fields = []
|
||||||
|
}
|
||||||
|
|
||||||
|
field.fields = createClientFields({
|
||||||
|
clientFields: field.fields,
|
||||||
|
defaultIDType,
|
||||||
|
disableAddingID: incomingField.type !== 'array',
|
||||||
|
fields: incomingField.fields,
|
||||||
|
i18n,
|
||||||
|
parentSchemaPath: schemaPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'blocks': {
|
||||||
|
const field = clientField as unknown as BlocksFieldClient
|
||||||
|
|
||||||
|
if (incomingField.blocks?.length) {
|
||||||
|
for (let i = 0; i < incomingField.blocks.length; i++) {
|
||||||
|
const block = incomingField.blocks[i]
|
||||||
|
const clientBlock: ClientBlock = {
|
||||||
|
slug: block.slug,
|
||||||
|
admin: {
|
||||||
|
components: {},
|
||||||
|
custom: block.admin?.custom,
|
||||||
|
},
|
||||||
|
fields: field.blocks?.[i]?.fields || [],
|
||||||
|
imageAltText: block.imageAltText,
|
||||||
|
imageURL: block.imageURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.labels) {
|
||||||
|
clientBlock.labels = {} as unknown as LabelsClient
|
||||||
|
if (block.labels.singular) {
|
||||||
|
if (typeof block.labels.singular === 'function') {
|
||||||
|
clientBlock.labels.singular = block.labels.singular({ t: i18n.t })
|
||||||
|
} else {
|
||||||
|
clientBlock.labels.singular = block.labels.singular
|
||||||
|
}
|
||||||
|
if (typeof block.labels.plural === 'function') {
|
||||||
|
clientBlock.labels.plural = block.labels.plural({ t: i18n.t })
|
||||||
|
} else {
|
||||||
|
clientBlock.labels.plural = block.labels.plural
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientBlock.fields = createClientFields({
|
||||||
|
clientFields: clientBlock.fields,
|
||||||
|
defaultIDType,
|
||||||
|
fields: block.fields,
|
||||||
|
i18n,
|
||||||
|
parentSchemaPath: [...schemaPath, block.slug],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!field.blocks) {
|
||||||
|
field.blocks = []
|
||||||
|
}
|
||||||
|
|
||||||
|
field.blocks[i] = clientBlock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'richText': {
|
||||||
|
if (!incomingField?.editor) {
|
||||||
|
throw new MissingEditorProp(incomingField) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof incomingField?.editor === 'function') {
|
||||||
|
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'tabs': {
|
||||||
|
const field = clientField as unknown as TabsFieldClient
|
||||||
|
|
||||||
|
if (incomingField.tabs?.length) {
|
||||||
|
for (let i = 0; i < incomingField.tabs.length; i++) {
|
||||||
|
const tab = incomingField.tabs[i]
|
||||||
|
const clientTab = field.tabs[i]
|
||||||
|
|
||||||
|
serverOnlyFieldProperties.forEach((key) => {
|
||||||
|
if (key in clientTab) {
|
||||||
|
delete clientTab[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
clientTab.fields = createClientFields({
|
||||||
|
clientFields: clientTab.fields,
|
||||||
|
defaultIDType,
|
||||||
|
disableAddingID: true,
|
||||||
|
fields: tab.fields,
|
||||||
|
i18n,
|
||||||
|
parentSchemaPath: getFieldPaths({
|
||||||
|
field: {
|
||||||
|
...tab,
|
||||||
|
type: 'tab',
|
||||||
|
},
|
||||||
|
parentPath: [],
|
||||||
|
parentSchemaPath: schemaPath,
|
||||||
|
schemaIndex: i,
|
||||||
|
}).schemaPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'select':
|
||||||
|
case 'radio': {
|
||||||
|
const field = clientField as RadioFieldClient | SelectFieldClient
|
||||||
|
|
||||||
|
if (incomingField.options?.length) {
|
||||||
|
for (let i = 0; i < incomingField.options.length; i++) {
|
||||||
|
const option = incomingField.options[i]
|
||||||
|
|
||||||
|
if (typeof option === 'object' && typeof option.label === 'function') {
|
||||||
|
if (!field.options) {
|
||||||
|
field.options = []
|
||||||
|
}
|
||||||
|
|
||||||
|
field.options[i] = {
|
||||||
|
label: option.label({ t: i18n.t }),
|
||||||
|
value: option.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverOnlyFieldAdminProperties: Partial<ServerOnlyFieldAdminProperties>[] = ['condition']
|
||||||
|
|
||||||
|
if (!clientField.admin) {
|
||||||
|
clientField.admin = {} as AdminClient
|
||||||
|
}
|
||||||
|
|
||||||
|
serverOnlyFieldAdminProperties.forEach((key) => {
|
||||||
|
if (key in clientField.admin) {
|
||||||
|
delete clientField.admin[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
type FieldWithDescription = {
|
||||||
|
admin: AdminClient
|
||||||
|
} & ClientField
|
||||||
|
|
||||||
|
if (incomingField.admin && 'description' in incomingField.admin) {
|
||||||
|
if (
|
||||||
|
typeof incomingField.admin?.description === 'string' ||
|
||||||
|
typeof incomingField.admin?.description === 'object'
|
||||||
|
) {
|
||||||
|
;(clientField as FieldWithDescription).admin.description = incomingField.admin.description
|
||||||
|
} else if (typeof incomingField.admin?.description === 'function') {
|
||||||
|
;(clientField as FieldWithDescription).admin.description = incomingField.admin?.description({
|
||||||
|
t: i18n.t,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientField
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createClientFields = ({
|
||||||
|
clientFields,
|
||||||
|
defaultIDType,
|
||||||
|
disableAddingID,
|
||||||
|
fields,
|
||||||
|
i18n,
|
||||||
|
parentSchemaPath = [],
|
||||||
|
}: {
|
||||||
|
clientFields: ClientField[]
|
||||||
|
defaultIDType: Payload['config']['db']['defaultIDType']
|
||||||
|
disableAddingID?: boolean
|
||||||
|
fields: Field[]
|
||||||
|
i18n: I18nClient
|
||||||
|
parentSchemaPath?: string[]
|
||||||
|
}): ClientField[] => {
|
||||||
|
const newClientFields: ClientField[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < fields.length; i++) {
|
||||||
|
const field = fields[i]
|
||||||
|
|
||||||
|
const { schemaPath } = getFieldPaths({
|
||||||
|
field,
|
||||||
|
parentPath: [],
|
||||||
|
parentSchemaPath,
|
||||||
|
schemaIndex: i,
|
||||||
|
})
|
||||||
|
|
||||||
|
const newField = createClientField({
|
||||||
|
clientField: clientFields[i],
|
||||||
|
defaultIDType,
|
||||||
|
field,
|
||||||
|
i18n,
|
||||||
|
schemaPath,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (newField) {
|
||||||
|
newClientFields.push(newField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasID = newClientFields.findIndex((f) => fieldAffectsData(f) && f.name === 'id') > -1
|
||||||
|
|
||||||
|
if (!disableAddingID && !hasID) {
|
||||||
|
newClientFields.push({
|
||||||
|
name: 'id',
|
||||||
|
type: defaultIDType,
|
||||||
|
_schemaPath: getFieldPaths({
|
||||||
|
field: { name: 'id', type: 'text' },
|
||||||
|
parentPath: [],
|
||||||
|
parentSchemaPath,
|
||||||
|
schemaIndex: 0,
|
||||||
|
}).schemaPath,
|
||||||
|
admin: {
|
||||||
|
description: 'The unique identifier for this document',
|
||||||
|
disableBulkEdit: true,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
hidden: true,
|
||||||
|
label: 'ID',
|
||||||
|
localized: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return newClientFields
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
||||||
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
|
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
|
||||||
import { baseIDField } from '../baseFields/baseIDField.js'
|
import { baseIDField } from '../baseFields/baseIDField.js'
|
||||||
|
import { getFieldPaths } from '../getFieldPaths.js'
|
||||||
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
|
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
|
||||||
import validations from '../validations.js'
|
import validations from '../validations.js'
|
||||||
import { sanitizeJoinField } from './sanitizeJoinField.js'
|
import { sanitizeJoinField } from './sanitizeJoinField.js'
|
||||||
@@ -41,7 +42,7 @@ type Args = {
|
|||||||
* so that you can sanitize them together, after the config has been sanitized.
|
* so that you can sanitize them together, after the config has been sanitized.
|
||||||
*/
|
*/
|
||||||
richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>>
|
richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>>
|
||||||
schemaPath?: string
|
schemaPath?: string[]
|
||||||
/**
|
/**
|
||||||
* If not null, will validate that upload and relationship fields do not relate to a collection that is not in this array.
|
* If not null, will validate that upload and relationship fields do not relate to a collection that is not in this array.
|
||||||
* This validation will be skipped if validRelationships is null.
|
* This validation will be skipped if validRelationships is null.
|
||||||
@@ -64,7 +65,7 @@ export const sanitizeFields = async ({
|
|||||||
parentIsLocalized,
|
parentIsLocalized,
|
||||||
requireFieldLevelRichTextEditor = false,
|
requireFieldLevelRichTextEditor = false,
|
||||||
richTextSanitizationPromises,
|
richTextSanitizationPromises,
|
||||||
schemaPath = '',
|
schemaPath = [],
|
||||||
validRelationships,
|
validRelationships,
|
||||||
}: Args): Promise<Field[]> => {
|
}: Args): Promise<Field[]> => {
|
||||||
if (!fields) {
|
if (!fields) {
|
||||||
@@ -260,10 +261,12 @@ export const sanitizeFields = async ({
|
|||||||
parentIsLocalized: parentIsLocalized || field.localized,
|
parentIsLocalized: parentIsLocalized || field.localized,
|
||||||
requireFieldLevelRichTextEditor,
|
requireFieldLevelRichTextEditor,
|
||||||
richTextSanitizationPromises,
|
richTextSanitizationPromises,
|
||||||
schemaPath: generateSchemaPath({
|
schemaPath: getFieldPaths({
|
||||||
name: 'name' in field ? field.name : undefined,
|
field,
|
||||||
path: schemaPath,
|
parentPath: [],
|
||||||
}),
|
parentSchemaPath: schemaPath,
|
||||||
|
schemaIndex: i,
|
||||||
|
}).schemaPath,
|
||||||
validRelationships,
|
validRelationships,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -271,10 +274,8 @@ export const sanitizeFields = async ({
|
|||||||
if (field.type === 'tabs') {
|
if (field.type === 'tabs') {
|
||||||
for (let j = 0; j < field.tabs.length; j++) {
|
for (let j = 0; j < field.tabs.length; j++) {
|
||||||
const tab = field.tabs[j]
|
const tab = field.tabs[j]
|
||||||
if (tabHasName(tab)) {
|
if (tabHasName(tab) && typeof tab.label === 'undefined') {
|
||||||
if (typeof tab.label === 'undefined') {
|
tab.label = toWords(tab.name)
|
||||||
tab.label = toWords(tab.name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tab.fields = await sanitizeFields({
|
tab.fields = await sanitizeFields({
|
||||||
@@ -285,10 +286,15 @@ export const sanitizeFields = async ({
|
|||||||
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
|
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
|
||||||
requireFieldLevelRichTextEditor,
|
requireFieldLevelRichTextEditor,
|
||||||
richTextSanitizationPromises,
|
richTextSanitizationPromises,
|
||||||
schemaPath: generateSchemaPath({
|
schemaPath: getFieldPaths({
|
||||||
name: 'name' in tab ? tab.name : undefined,
|
field: {
|
||||||
path: schemaPath,
|
...tab,
|
||||||
}),
|
type: 'tab',
|
||||||
|
},
|
||||||
|
parentPath: [],
|
||||||
|
parentSchemaPath: schemaPath,
|
||||||
|
schemaIndex: j,
|
||||||
|
}).schemaPath,
|
||||||
validRelationships,
|
validRelationships,
|
||||||
})
|
})
|
||||||
field.tabs[j] = tab
|
field.tabs[j] = tab
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const sanitizeJoinField = ({
|
|||||||
config: Config
|
config: Config
|
||||||
field: JoinField
|
field: JoinField
|
||||||
joins?: SanitizedJoins
|
joins?: SanitizedJoins
|
||||||
schemaPath?: string
|
schemaPath?: string[]
|
||||||
}) => {
|
}) => {
|
||||||
// the `joins` arg is not passed for globals or when recursing on fields that do not allow a join field
|
// the `joins` arg is not passed for globals or when recursing on fields that do not allow a join field
|
||||||
if (typeof joins === 'undefined') {
|
if (typeof joins === 'undefined') {
|
||||||
@@ -25,7 +25,7 @@ export const sanitizeJoinField = ({
|
|||||||
}
|
}
|
||||||
const join: SanitizedJoin = {
|
const join: SanitizedJoin = {
|
||||||
field,
|
field,
|
||||||
schemaPath: `${schemaPath || ''}${schemaPath ? '.' : ''}${field.name}`,
|
schemaPath: [...schemaPath, field.name].join('.'),
|
||||||
targetField: undefined,
|
targetField: undefined,
|
||||||
}
|
}
|
||||||
const joinCollection = config.collections.find(
|
const joinCollection = config.collections.find(
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import type {
|
|||||||
JSONFieldErrorServerComponent,
|
JSONFieldErrorServerComponent,
|
||||||
JSONFieldLabelClientComponent,
|
JSONFieldLabelClientComponent,
|
||||||
JSONFieldLabelServerComponent,
|
JSONFieldLabelServerComponent,
|
||||||
MappedComponent,
|
|
||||||
NumberFieldClientProps,
|
NumberFieldClientProps,
|
||||||
NumberFieldErrorClientComponent,
|
NumberFieldErrorClientComponent,
|
||||||
NumberFieldErrorServerComponent,
|
NumberFieldErrorServerComponent,
|
||||||
@@ -95,6 +94,7 @@ import type {
|
|||||||
TextareaFieldErrorServerComponent,
|
TextareaFieldErrorServerComponent,
|
||||||
TextareaFieldLabelClientComponent,
|
TextareaFieldLabelClientComponent,
|
||||||
TextareaFieldLabelServerComponent,
|
TextareaFieldLabelServerComponent,
|
||||||
|
TextFieldClientProps,
|
||||||
TextFieldErrorClientComponent,
|
TextFieldErrorClientComponent,
|
||||||
TextFieldErrorServerComponent,
|
TextFieldErrorServerComponent,
|
||||||
TextFieldLabelClientComponent,
|
TextFieldLabelClientComponent,
|
||||||
@@ -299,15 +299,6 @@ type Admin = {
|
|||||||
|
|
||||||
export type AdminClient = {
|
export type AdminClient = {
|
||||||
className?: string
|
className?: string
|
||||||
components?: {
|
|
||||||
Cell?: MappedComponent
|
|
||||||
Description?: MappedComponent
|
|
||||||
Field?: MappedComponent
|
|
||||||
/**
|
|
||||||
* The Filter component has to be a client component
|
|
||||||
*/
|
|
||||||
Filter?: MappedComponent
|
|
||||||
}
|
|
||||||
/** Extension point to add your custom data. Available in server and client. */
|
/** Extension point to add your custom data. Available in server and client. */
|
||||||
custom?: Record<string, any>
|
custom?: Record<string, any>
|
||||||
description?: StaticDescription
|
description?: StaticDescription
|
||||||
@@ -431,8 +422,7 @@ export interface FieldBase {
|
|||||||
|
|
||||||
export interface FieldBaseClient {
|
export interface FieldBaseClient {
|
||||||
_isPresentational?: undefined
|
_isPresentational?: undefined
|
||||||
_path?: string
|
_schemaPath: string[]
|
||||||
_schemaPath?: string
|
|
||||||
admin?: AdminClient
|
admin?: AdminClient
|
||||||
hidden?: boolean
|
hidden?: boolean
|
||||||
index?: boolean
|
index?: boolean
|
||||||
@@ -498,15 +488,7 @@ export type NumberField = {
|
|||||||
Omit<FieldBase, 'validate'>
|
Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type NumberFieldClient = {
|
export type NumberFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<NumberField['admin'], 'autoComplete' | 'placeholder' | 'step'>
|
||||||
components?: {
|
|
||||||
afterInput?: MappedComponent[]
|
|
||||||
beforeInput?: MappedComponent[]
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<NumberField['admin'], 'autoComplete' | 'placeholder' | 'step'>
|
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<NumberField, 'hasMany' | 'max' | 'maxRows' | 'min' | 'minRows' | 'type'>
|
Pick<NumberField, 'hasMany' | 'max' | 'maxRows' | 'min' | 'minRows' | 'type'>
|
||||||
|
|
||||||
@@ -548,15 +530,7 @@ export type TextField = {
|
|||||||
Omit<FieldBase, 'validate'>
|
Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type TextFieldClient = {
|
export type TextFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<TextField['admin'], 'autoComplete' | 'placeholder' | 'rtl'>
|
||||||
components?: {
|
|
||||||
afterInput?: MappedComponent[]
|
|
||||||
beforeInput?: MappedComponent[]
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<TextField['admin'], 'autoComplete' | 'placeholder' | 'rtl'>
|
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<TextField, 'hasMany' | 'maxLength' | 'maxRows' | 'minLength' | 'minRows' | 'type'>
|
Pick<TextField, 'hasMany' | 'maxLength' | 'maxRows' | 'minLength' | 'minRows' | 'type'>
|
||||||
|
|
||||||
@@ -576,15 +550,7 @@ export type EmailField = {
|
|||||||
} & Omit<FieldBase, 'validate'>
|
} & Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type EmailFieldClient = {
|
export type EmailFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<EmailField['admin'], 'placeholder'>
|
||||||
components?: {
|
|
||||||
afterInput?: MappedComponent[]
|
|
||||||
beforeInput?: MappedComponent[]
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<EmailField['admin'], 'placeholder'>
|
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<EmailField, 'type'>
|
Pick<EmailField, 'type'>
|
||||||
|
|
||||||
@@ -607,15 +573,7 @@ export type TextareaField = {
|
|||||||
} & Omit<FieldBase, 'validate'>
|
} & Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type TextareaFieldClient = {
|
export type TextareaFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<TextareaField['admin'], 'placeholder' | 'rows' | 'rtl'>
|
||||||
components?: {
|
|
||||||
afterInput?: MappedComponent[]
|
|
||||||
beforeInput?: MappedComponent[]
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<TextareaField['admin'], 'placeholder' | 'rows' | 'rtl'>
|
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<TextareaField, 'maxLength' | 'minLength' | 'type'>
|
Pick<TextareaField, 'maxLength' | 'minLength' | 'type'>
|
||||||
|
|
||||||
@@ -633,14 +591,7 @@ export type CheckboxField = {
|
|||||||
} & Omit<FieldBase, 'validate'>
|
} & Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type CheckboxFieldClient = {
|
export type CheckboxFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient
|
||||||
components?: {
|
|
||||||
afterInput?: MappedComponent[]
|
|
||||||
beforeInput?: MappedComponent[]
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient
|
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<CheckboxField, 'type'>
|
Pick<CheckboxField, 'type'>
|
||||||
|
|
||||||
@@ -660,15 +611,7 @@ export type DateField = {
|
|||||||
} & Omit<FieldBase, 'validate'>
|
} & Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type DateFieldClient = {
|
export type DateFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<DateField['admin'], 'date' | 'placeholder'>
|
||||||
components?: {
|
|
||||||
afterInput?: MappedComponent[]
|
|
||||||
beforeInput?: MappedComponent[]
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<DateField['admin'], 'date' | 'placeholder'>
|
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<DateField, 'type'>
|
Pick<DateField, 'type'>
|
||||||
|
|
||||||
@@ -692,12 +635,7 @@ export type GroupField = {
|
|||||||
} & Omit<FieldBase, 'required' | 'validate'>
|
} & Omit<FieldBase, 'required' | 'validate'>
|
||||||
|
|
||||||
export type GroupFieldClient = {
|
export type GroupFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<GroupField['admin'], 'hideGutter'>
|
||||||
components?: {
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<GroupField['admin'], 'hideGutter'>
|
|
||||||
fields: ClientField[]
|
fields: ClientField[]
|
||||||
} & Omit<FieldBaseClient, 'required'> &
|
} & Omit<FieldBaseClient, 'required'> &
|
||||||
Pick<GroupField, 'interfaceName' | 'type'>
|
Pick<GroupField, 'interfaceName' | 'type'>
|
||||||
@@ -745,25 +683,12 @@ export type CollapsibleField = {
|
|||||||
Omit<FieldBase, 'label' | 'name' | 'validate' | 'virtual'>
|
Omit<FieldBase, 'label' | 'name' | 'validate' | 'virtual'>
|
||||||
|
|
||||||
export type CollapsibleFieldClient = {
|
export type CollapsibleFieldClient = {
|
||||||
|
admin?: {
|
||||||
|
initCollapsed?: boolean
|
||||||
|
} & AdminClient
|
||||||
fields: ClientField[]
|
fields: ClientField[]
|
||||||
} & (
|
label: StaticLabel
|
||||||
| {
|
} & Omit<FieldBaseClient, 'label' | 'name' | 'validate'> &
|
||||||
admin: {
|
|
||||||
components: {
|
|
||||||
RowLabel: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
initCollapsed?: boolean
|
|
||||||
} & AdminClient
|
|
||||||
label?: Required<FieldBaseClient['label']>
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
admin?: {
|
|
||||||
initCollapsed?: boolean
|
|
||||||
} & AdminClient
|
|
||||||
label: Required<FieldBaseClient['label']>
|
|
||||||
}
|
|
||||||
) &
|
|
||||||
Omit<FieldBaseClient, 'label' | 'name' | 'validate'> &
|
|
||||||
Pick<CollapsibleField, 'type'>
|
Pick<CollapsibleField, 'type'>
|
||||||
|
|
||||||
type TabBase = {
|
type TabBase = {
|
||||||
@@ -856,14 +781,9 @@ export type UIField = {
|
|||||||
export type UIFieldClient = {
|
export type UIFieldClient = {
|
||||||
_isPresentational?: true
|
_isPresentational?: true
|
||||||
// still include FieldBaseClient.admin (even if it's undefinable) so that we don't need constant type checks (e.g. if('xy' in field))
|
// still include FieldBaseClient.admin (even if it's undefinable) so that we don't need constant type checks (e.g. if('xy' in field))
|
||||||
// eslint-disable-next-line perfectionist/sort-intersection-types
|
|
||||||
admin: DeepUndefinable<FieldBaseClient['admin']> & {
|
admin: DeepUndefinable<FieldBaseClient['admin']> &
|
||||||
components?: {
|
Pick<
|
||||||
Cell?: MappedComponent
|
|
||||||
Field: MappedComponent
|
|
||||||
Filter?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & Pick<
|
|
||||||
UIField['admin'],
|
UIField['admin'],
|
||||||
'custom' | 'disableBulkEdit' | 'disableListColumn' | 'position' | 'width'
|
'custom' | 'disableBulkEdit' | 'disableListColumn' | 'position' | 'width'
|
||||||
>
|
>
|
||||||
@@ -933,13 +853,8 @@ type UploadAdmin = {
|
|||||||
} & Admin['components']
|
} & Admin['components']
|
||||||
isSortable?: boolean
|
isSortable?: boolean
|
||||||
} & Admin
|
} & Admin
|
||||||
type UploadAdminClient = {
|
|
||||||
components?: {
|
type UploadAdminClient = AdminClient & Pick<UploadAdmin, 'allowCreate' | 'isSortable'>
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<UploadAdmin, 'allowCreate' | 'isSortable'>
|
|
||||||
|
|
||||||
export type PolymorphicUploadField = {
|
export type PolymorphicUploadField = {
|
||||||
admin?: {
|
admin?: {
|
||||||
@@ -989,15 +904,7 @@ export type CodeField = {
|
|||||||
} & Omit<FieldBase, 'admin' | 'validate'>
|
} & Omit<FieldBase, 'admin' | 'validate'>
|
||||||
|
|
||||||
export type CodeFieldClient = {
|
export type CodeFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<CodeField['admin'], 'editorOptions' | 'language'>
|
||||||
components?: {
|
|
||||||
afterInput?: MappedComponent[]
|
|
||||||
beforeInput?: MappedComponent[]
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<CodeField['admin'], 'editorOptions' | 'language'>
|
|
||||||
} & Omit<FieldBaseClient, 'admin'> &
|
} & Omit<FieldBaseClient, 'admin'> &
|
||||||
Pick<CodeField, 'maxLength' | 'minLength' | 'type'>
|
Pick<CodeField, 'maxLength' | 'minLength' | 'type'>
|
||||||
|
|
||||||
@@ -1022,15 +929,7 @@ export type JSONField = {
|
|||||||
} & Omit<FieldBase, 'admin' | 'validate'>
|
} & Omit<FieldBase, 'admin' | 'validate'>
|
||||||
|
|
||||||
export type JSONFieldClient = {
|
export type JSONFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<JSONField['admin'], 'editorOptions'>
|
||||||
components?: {
|
|
||||||
afterInput?: MappedComponent[]
|
|
||||||
beforeInput?: MappedComponent[]
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<JSONField['admin'], 'editorOptions'>
|
|
||||||
} & Omit<FieldBaseClient, 'admin'> &
|
} & Omit<FieldBaseClient, 'admin'> &
|
||||||
Pick<JSONField, 'jsonSchema' | 'type'>
|
Pick<JSONField, 'jsonSchema' | 'type'>
|
||||||
|
|
||||||
@@ -1069,15 +968,7 @@ export type SelectField = {
|
|||||||
Omit<FieldBase, 'validate'>
|
Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type SelectFieldClient = {
|
export type SelectFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<SelectField['admin'], 'isClearable' | 'isSortable'>
|
||||||
components?: {
|
|
||||||
afterInput?: MappedComponent[]
|
|
||||||
beforeInput?: MappedComponent[]
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<SelectField['admin'], 'isClearable' | 'isSortable'>
|
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<SelectField, 'hasMany' | 'options' | 'type'>
|
Pick<SelectField, 'hasMany' | 'options' | 'type'>
|
||||||
|
|
||||||
@@ -1142,12 +1033,7 @@ type RelationshipAdmin = {
|
|||||||
isSortable?: boolean
|
isSortable?: boolean
|
||||||
} & Admin
|
} & Admin
|
||||||
|
|
||||||
type RelationshipAdminClient = {
|
type RelationshipAdminClient = AdminClient &
|
||||||
components?: {
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'isSortable'>
|
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'isSortable'>
|
||||||
|
|
||||||
export type PolymorphicRelationshipField = {
|
export type PolymorphicRelationshipField = {
|
||||||
@@ -1226,13 +1112,6 @@ export type RichTextFieldClient<
|
|||||||
TAdapterProps = any,
|
TAdapterProps = any,
|
||||||
TExtraProperties = object,
|
TExtraProperties = object,
|
||||||
> = {
|
> = {
|
||||||
admin?: {
|
|
||||||
components?: {
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
placeholder?: Record<string, string> | string
|
|
||||||
} & AdminClient
|
|
||||||
richTextComponentMap?: Map<string, any>
|
richTextComponentMap?: Map<string, any>
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<RichTextField<TValue, TAdapterProps, TExtraProperties>, 'maxDepth' | 'type'> &
|
Pick<RichTextField<TValue, TAdapterProps, TExtraProperties>, 'maxDepth' | 'type'> &
|
||||||
@@ -1271,14 +1150,7 @@ export type ArrayField = {
|
|||||||
} & Omit<FieldBase, 'validate'>
|
} & Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type ArrayFieldClient = {
|
export type ArrayFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<ArrayField['admin'], 'initCollapsed' | 'isSortable'>
|
||||||
components?: {
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
RowLabel?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<ArrayField['admin'], 'initCollapsed' | 'isSortable'>
|
|
||||||
fields: ClientField[]
|
fields: ClientField[]
|
||||||
labels?: LabelsClient
|
labels?: LabelsClient
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
@@ -1306,13 +1178,7 @@ export type RadioField = {
|
|||||||
} & Omit<FieldBase, 'validate'>
|
} & Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type RadioFieldClient = {
|
export type RadioFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<RadioField['admin'], 'layout'>
|
||||||
components?: {
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<RadioField['admin'], 'layout'>
|
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<RadioField, 'options' | 'type'>
|
Pick<RadioField, 'options' | 'type'>
|
||||||
|
|
||||||
@@ -1361,10 +1227,11 @@ export type Block = {
|
|||||||
labels?: Labels
|
labels?: Labels
|
||||||
slug: string
|
slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClientBlock = {
|
export type ClientBlock = {
|
||||||
admin?: {
|
admin?: {
|
||||||
components?: {
|
components?: {
|
||||||
Label?: MappedComponent
|
Label?: React.ReactNode
|
||||||
}
|
}
|
||||||
} & Pick<Block['admin'], 'custom'>
|
} & Pick<Block['admin'], 'custom'>
|
||||||
fields: ClientField[]
|
fields: ClientField[]
|
||||||
@@ -1392,12 +1259,7 @@ export type BlocksField = {
|
|||||||
} & Omit<FieldBase, 'validate'>
|
} & Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type BlocksFieldClient = {
|
export type BlocksFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<BlocksField['admin'], 'initCollapsed' | 'isSortable'>
|
||||||
components?: {
|
|
||||||
Error?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<BlocksField['admin'], 'initCollapsed' | 'isSortable'>
|
|
||||||
blocks: ClientBlock[]
|
blocks: ClientBlock[]
|
||||||
labels?: LabelsClient
|
labels?: LabelsClient
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
@@ -1419,15 +1281,7 @@ export type PointField = {
|
|||||||
} & Omit<FieldBase, 'validate'>
|
} & Omit<FieldBase, 'validate'>
|
||||||
|
|
||||||
export type PointFieldClient = {
|
export type PointFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<PointField['admin'], 'placeholder' | 'step'>
|
||||||
components?: {
|
|
||||||
afterInput?: MappedComponent[]
|
|
||||||
beforeInput?: MappedComponent[]
|
|
||||||
Error?: MappedComponent
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<PointField['admin'], 'placeholder' | 'step'>
|
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<PointField, 'type'>
|
Pick<PointField, 'type'>
|
||||||
|
|
||||||
@@ -1473,12 +1327,7 @@ export type JoinField = {
|
|||||||
} & FieldBase
|
} & FieldBase
|
||||||
|
|
||||||
export type JoinFieldClient = {
|
export type JoinFieldClient = {
|
||||||
admin?: {
|
admin?: AdminClient & Pick<JoinField['admin'], 'disableBulkEdit' | 'readOnly'>
|
||||||
components?: {
|
|
||||||
Label?: MappedComponent
|
|
||||||
} & AdminClient['components']
|
|
||||||
} & AdminClient &
|
|
||||||
Pick<JoinField['admin'], 'disableBulkEdit' | 'readOnly'>
|
|
||||||
} & FieldBaseClient &
|
} & FieldBaseClient &
|
||||||
Pick<JoinField, 'collection' | 'index' | 'maxDepth' | 'on' | 'type'>
|
Pick<JoinField, 'collection' | 'index' | 'maxDepth' | 'on' | 'type'>
|
||||||
|
|
||||||
@@ -1550,6 +1399,7 @@ export type ClientFieldProps =
|
|||||||
| SelectFieldClientProps
|
| SelectFieldClientProps
|
||||||
| TabsFieldClientProps
|
| TabsFieldClientProps
|
||||||
| TextareaFieldClientProps
|
| TextareaFieldClientProps
|
||||||
|
| TextFieldClientProps
|
||||||
| UploadFieldClientProps
|
| UploadFieldClientProps
|
||||||
|
|
||||||
type ExtractFieldTypes<T> = T extends { type: infer U } ? U : never
|
type ExtractFieldTypes<T> = T extends { type: infer U } ? U : never
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import type { Field, TabAsField } from './config/types.js'
|
import type { ClientField, Field, TabAsField } from './config/types.js'
|
||||||
|
|
||||||
import { tabHasName } from './config/types.js'
|
import { tabHasName } from './config/types.js'
|
||||||
|
|
||||||
export function getFieldPaths({
|
export function getFieldPaths({
|
||||||
field,
|
field,
|
||||||
parentPath,
|
parentPath = [],
|
||||||
parentSchemaPath,
|
parentSchemaPath = [],
|
||||||
|
schemaIndex,
|
||||||
}: {
|
}: {
|
||||||
field: Field | TabAsField
|
field: ClientField | Field | TabAsField
|
||||||
parentPath: (number | string)[]
|
parentPath: (number | string)[]
|
||||||
parentSchemaPath: string[]
|
parentSchemaPath: string[]
|
||||||
|
schemaIndex: number
|
||||||
}): {
|
}): {
|
||||||
path: (number | string)[]
|
path: (number | string)[]
|
||||||
schemaPath: string[]
|
schemaPath: string[]
|
||||||
} {
|
} {
|
||||||
if (field.type === 'tabs' || field.type === 'row' || field.type === 'collapsible') {
|
if (field.type === 'tabs' || field.type === 'row' || field.type === 'collapsible') {
|
||||||
return {
|
return {
|
||||||
path: parentPath,
|
path: [...parentPath, `_index-${schemaIndex}`],
|
||||||
schemaPath: parentSchemaPath,
|
schemaPath: [...parentSchemaPath, `_index-${schemaIndex}`],
|
||||||
}
|
}
|
||||||
} else if (field.type === 'tab') {
|
} else if (field.type === 'tab') {
|
||||||
if (tabHasName(field)) {
|
if (tabHasName(field)) {
|
||||||
@@ -27,8 +29,8 @@ export function getFieldPaths({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
path: parentPath,
|
path: [...parentPath, `_index-${schemaIndex}`],
|
||||||
schemaPath: parentSchemaPath,
|
schemaPath: [...parentSchemaPath, `_index-${schemaIndex}`],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user