Compare commits
213 Commits
docs/migra
...
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. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
@@ -12,8 +14,17 @@ type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
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. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
@@ -13,7 +13,7 @@ type Args = {
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import React from 'react'
|
||||
|
||||
import './custom.scss'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
@@ -13,7 +13,7 @@ type Args = {
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -19,6 +19,11 @@ const config = withBundleAnalyzer(
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '5mb',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
|
||||
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 React from 'react'
|
||||
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
import { Fragment } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
import { DocumentTabLink } from './TabLink.js'
|
||||
@@ -59,17 +59,6 @@ export const DocumentTab: React.FC<
|
||||
})
|
||||
: label
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedPin = createMappedComponent(Pill, undefined, Pill_Component, 'Pill')
|
||||
|
||||
return (
|
||||
<DocumentTabLink
|
||||
adminRoute={routes.admin}
|
||||
@@ -82,12 +71,19 @@ export const DocumentTab: React.FC<
|
||||
>
|
||||
<span className={`${baseClass}__label`}>
|
||||
{labelToRender}
|
||||
{mappedPin && (
|
||||
<Fragment>
|
||||
|
||||
<RenderComponent mappedComponent={mappedPin} />
|
||||
</Fragment>
|
||||
)}
|
||||
<Fragment>
|
||||
|
||||
{/* <RenderServerComponent
|
||||
Component={Pill}
|
||||
Fallback={Pill_Component}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
}}
|
||||
/> */}
|
||||
</Fragment>
|
||||
</span>
|
||||
</DocumentTabLink>
|
||||
)
|
||||
|
||||
@@ -6,9 +6,9 @@ import type {
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { RenderServerComponent } from '../../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||
import { getCustomViews } from './getCustomViews.js'
|
||||
import { getViewConfig } from './getViewConfig.js'
|
||||
import './index.scss'
|
||||
@@ -80,33 +80,24 @@ export const DocumentTabs: React.FC<{
|
||||
const { path, tab } = CustomView
|
||||
|
||||
if (tab.Component) {
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
...props,
|
||||
key: `tab-custom-${index}`,
|
||||
path,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedTab = createMappedComponent(
|
||||
tab.Component,
|
||||
undefined,
|
||||
undefined,
|
||||
'tab.Component',
|
||||
)
|
||||
|
||||
return (
|
||||
<RenderComponent
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
key: `tab-custom-${index}`,
|
||||
path,
|
||||
}}
|
||||
Component={tab.Component}
|
||||
importMap={payload.importMap}
|
||||
key={`tab-custom-${index}`}
|
||||
mappedComponent={mappedTab}
|
||||
serverProps={{
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
i18n,
|
||||
key: `tab-custom-${index}`,
|
||||
path,
|
||||
payload,
|
||||
permissions,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -121,6 +112,7 @@ export const DocumentTabs: React.FC<{
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</ul>
|
||||
|
||||
@@ -8,7 +8,7 @@ export const VersionsPill: React.FC = () => {
|
||||
const { versions } = useDocumentInfo()
|
||||
|
||||
// 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
&__create-new-button {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__toggler {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
@@ -90,16 +95,6 @@
|
||||
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 {
|
||||
.collection-list__header {
|
||||
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 { getCreateMappedComponent, PayloadLogo, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { PayloadLogo } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||
|
||||
export const Logo: React.FC<ServerProps> = (props) => {
|
||||
const { i18n, locale, params, payload, permissions, searchParams, user } = props
|
||||
|
||||
@@ -16,20 +18,20 @@ export const Logo: React.FC<ServerProps> = (props) => {
|
||||
} = {},
|
||||
} = payload.config
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedCustomLogo = createMappedComponent(CustomLogo, undefined, PayloadLogo, 'CustomLogo')
|
||||
|
||||
return <RenderComponent mappedComponent={mappedCustomLogo} />
|
||||
return (
|
||||
<RenderServerComponent
|
||||
Component={CustomLogo}
|
||||
Fallback={PayloadLogo}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||
import type { groupNavItems } from '@payloadcms/ui/shared'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
NavGroup,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useEntityVisibility,
|
||||
useNav,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { EntityType, formatAdminURL, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import { NavGroup, useConfig, useNav, useTranslation } from '@payloadcms/ui'
|
||||
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import { usePathname } from 'next/navigation.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
const baseClass = 'nav'
|
||||
|
||||
export const DefaultNavClient: React.FC = () => {
|
||||
const { permissions } = useAuth()
|
||||
const { isEntityVisible } = useEntityVisibility()
|
||||
export const DefaultNavClient: React.FC<{
|
||||
groups: ReturnType<typeof groupNavItems>
|
||||
}> = ({ groups }) => {
|
||||
const pathname = usePathname()
|
||||
|
||||
const {
|
||||
config: {
|
||||
collections,
|
||||
globals,
|
||||
routes: { admin: adminRoute },
|
||||
},
|
||||
} = useConfig()
|
||||
@@ -34,53 +25,23 @@ export const DefaultNavClient: React.FC = () => {
|
||||
const { i18n } = useTranslation()
|
||||
const { navOpen } = useNav()
|
||||
|
||||
const groups = groupNavItems(
|
||||
[
|
||||
...collections
|
||||
.filter(({ slug }) => isEntityVisible({ collectionSlug: slug }))
|
||||
.map((collection) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.collection,
|
||||
entity: collection,
|
||||
}
|
||||
|
||||
return entityToGroup
|
||||
}),
|
||||
...globals
|
||||
.filter(({ slug }) => isEntityVisible({ globalSlug: slug }))
|
||||
.map((global) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.global,
|
||||
entity: global,
|
||||
}
|
||||
|
||||
return entityToGroup
|
||||
}),
|
||||
],
|
||||
permissions,
|
||||
i18n,
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{groups.map(({ entities, label }, key) => {
|
||||
return (
|
||||
<NavGroup key={key} label={label}>
|
||||
{entities.map(({ type, entity }, i) => {
|
||||
let entityLabel: string
|
||||
{entities.map(({ slug, type, label }, i) => {
|
||||
let href: string
|
||||
let id: string
|
||||
|
||||
if (type === EntityType.collection) {
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` })
|
||||
entityLabel = getTranslation(entity.labels.plural, i18n)
|
||||
id = `nav-${entity.slug}`
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })
|
||||
id = `nav-${slug}`
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
href = formatAdminURL({ adminRoute, path: `/globals/${entity.slug}` })
|
||||
entityLabel = getTranslation(entity.label, i18n)
|
||||
id = `nav-global-${entity.slug}`
|
||||
href = formatAdminURL({ adminRoute, path: `/globals/${slug}` })
|
||||
id = `nav-global-${slug}`
|
||||
}
|
||||
|
||||
const Link = (LinkWithDefault.default ||
|
||||
@@ -101,7 +62,7 @@ export const DefaultNavClient: React.FC = () => {
|
||||
tabIndex={!navOpen ? -1 : undefined}
|
||||
>
|
||||
{activeCollection && <div className={`${baseClass}__link-indicator`} />}
|
||||
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
|
||||
<span className={`${baseClass}__link-label`}>{getTranslation(label, i18n)}</span>
|
||||
</LinkElement>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||
import type { ServerProps } from 'payload'
|
||||
|
||||
import { Logout } from '@payloadcms/ui'
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||
import './index.scss'
|
||||
import { NavHamburger } from './NavHamburger/index.js'
|
||||
import { NavWrapper } from './NavWrapper/index.js'
|
||||
@@ -15,7 +17,7 @@ import { DefaultNavClient } from './index.client.js'
|
||||
export type NavProps = ServerProps
|
||||
|
||||
export const DefaultNav: React.FC<NavProps> = (props) => {
|
||||
const { i18n, locale, params, payload, permissions, searchParams, user } = props
|
||||
const { i18n, locale, params, payload, permissions, searchParams, user, visibleEntities } = props
|
||||
|
||||
if (!payload?.config) {
|
||||
return null
|
||||
@@ -25,40 +27,65 @@ export const DefaultNav: React.FC<NavProps> = (props) => {
|
||||
admin: {
|
||||
components: { afterNavLinks, beforeNavLinks },
|
||||
},
|
||||
collections,
|
||||
globals,
|
||||
} = payload.config
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedBeforeNavLinks = createMappedComponent(
|
||||
beforeNavLinks,
|
||||
undefined,
|
||||
undefined,
|
||||
'beforeNavLinks',
|
||||
)
|
||||
const mappedAfterNavLinks = createMappedComponent(
|
||||
afterNavLinks,
|
||||
undefined,
|
||||
undefined,
|
||||
'afterNavLinks',
|
||||
const groups = groupNavItems(
|
||||
[
|
||||
...collections
|
||||
.filter(({ slug }) => visibleEntities.collections.includes(slug))
|
||||
.map(
|
||||
(collection) =>
|
||||
({
|
||||
type: EntityType.collection,
|
||||
entity: collection,
|
||||
}) satisfies EntityToGroup,
|
||||
),
|
||||
...globals
|
||||
.filter(({ slug }) => visibleEntities.globals.includes(slug))
|
||||
.map(
|
||||
(global) =>
|
||||
({
|
||||
type: EntityType.global,
|
||||
entity: global,
|
||||
}) satisfies EntityToGroup,
|
||||
),
|
||||
],
|
||||
permissions,
|
||||
i18n,
|
||||
)
|
||||
|
||||
return (
|
||||
<NavWrapper baseClass={baseClass}>
|
||||
<nav className={`${baseClass}__wrap`}>
|
||||
<RenderComponent mappedComponent={mappedBeforeNavLinks} />
|
||||
<DefaultNavClient />
|
||||
<RenderComponent mappedComponent={mappedAfterNavLinks} />
|
||||
<RenderServerComponent
|
||||
Component={beforeNavLinks}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
<DefaultNavClient groups={groups} />
|
||||
<RenderServerComponent
|
||||
Component={afterNavLinks}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Logout />
|
||||
</div>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { metadata, RootLayout } from '../layouts/Root/index.js'
|
||||
export { handleServerFunctions } from '../utilities/handleServerFunctions.js'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// NOTICE: Server-only utilities, do not import anything client-side here.
|
||||
export { addDataAndFileToRequest } from '../utilities/addDataAndFileToRequest.js'
|
||||
export { addLocalesToRequestFromData, sanitizeLocales } from '../utilities/addLocalesToRequest.js'
|
||||
export { createPayloadRequest } from '../utilities/createPayloadRequest.js'
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
export { DefaultEditView as EditView } from '../views/Edit/Default/index.js'
|
||||
export { DefaultListView as ListView } from '../views/List/Default/index.js'
|
||||
export { NotFoundPage } from '../views/NotFound/index.js'
|
||||
export { generatePageMetadata, type GenerateViewMetadata, RootPage } from '../views/Root/index.js'
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import type { AcceptedLanguages } from '@payloadcms/translations'
|
||||
import type { CustomVersionParser, ImportMap, SanitizedConfig } from 'payload'
|
||||
import type { CustomVersionParser, ImportMap, SanitizedConfig, ServerFunctionClient } from 'payload'
|
||||
|
||||
import { rtlLanguages } from '@payloadcms/translations'
|
||||
import { RootProvider } from '@payloadcms/ui'
|
||||
import '@payloadcms/ui/scss/app.scss'
|
||||
import { createClientConfig } from '@payloadcms/ui/utilities/createClientConfig'
|
||||
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
|
||||
import { checkDependencies, parseCookies } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import { getClientConfig } from '../../utilities/getClientConfig.js'
|
||||
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
|
||||
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
|
||||
import { initReq } from '../../utilities/initReq.js'
|
||||
import { DefaultEditView } from '../../views/Edit/Default/index.js'
|
||||
import { DefaultListView } from '../../views/List/Default/index.js'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Generated by Next.js',
|
||||
@@ -41,11 +39,12 @@ let checkedDependencies = false
|
||||
export const RootLayout = async ({
|
||||
children,
|
||||
config: configPromise,
|
||||
importMap,
|
||||
serverFunction,
|
||||
}: {
|
||||
readonly children: React.ReactNode
|
||||
readonly config: Promise<SanitizedConfig>
|
||||
readonly importMap: ImportMap
|
||||
readonly serverFunction: ServerFunctionClient
|
||||
}) => {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
@@ -103,16 +102,6 @@ export const RootLayout = async ({
|
||||
|
||||
const { i18n, permissions, req, user } = await initReq(config)
|
||||
|
||||
const { clientConfig, render } = await createClientConfig({
|
||||
children,
|
||||
config,
|
||||
DefaultEditView,
|
||||
DefaultListView,
|
||||
i18n,
|
||||
importMap,
|
||||
payload,
|
||||
})
|
||||
|
||||
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
|
||||
? 'RTL'
|
||||
: 'LTR'
|
||||
@@ -174,23 +163,29 @@ export const RootLayout = async ({
|
||||
|
||||
const isNavOpen = navPreferences?.value?.open ?? true
|
||||
|
||||
const clientConfig = await getClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
return (
|
||||
<html data-theme={theme} dir={dir} lang={languageCode}>
|
||||
<body>
|
||||
<RootProvider
|
||||
config={clientConfig}
|
||||
dateFNSKey={i18n.dateFNSKey}
|
||||
fallbackLang={clientConfig.i18n.fallbackLanguage}
|
||||
fallbackLang={config.i18n.fallbackLanguage}
|
||||
isNavOpen={isNavOpen}
|
||||
languageCode={languageCode}
|
||||
languageOptions={languageOptions}
|
||||
permissions={permissions}
|
||||
serverFunction={serverFunction}
|
||||
switchLanguageServerAction={switchLanguageServerAction}
|
||||
theme={theme}
|
||||
translations={i18n.translations}
|
||||
user={user}
|
||||
>
|
||||
{render}
|
||||
{children}
|
||||
</RootProvider>
|
||||
<div id="portal" />
|
||||
</body>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
import { buildFormState as buildFormStateFn } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import { headersWithCors } from '../../utilities/headersWithCors.js'
|
||||
import { routeError } from './routeError.js'
|
||||
|
||||
export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
|
||||
const headers = headersWithCors({
|
||||
headers: new Headers(),
|
||||
req,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await buildFormStateFn({ req })
|
||||
|
||||
return Response.json(result, {
|
||||
headers,
|
||||
status: httpStatus.OK,
|
||||
})
|
||||
} catch (err) {
|
||||
req.payload.logger.error({ err, msg: `There was an error building form state` })
|
||||
|
||||
if (err.message === 'Could not find field schema for given path') {
|
||||
return Response.json(
|
||||
{
|
||||
message: err.message,
|
||||
},
|
||||
{
|
||||
headers,
|
||||
status: httpStatus.BAD_REQUEST,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (err.message === 'Unauthorized') {
|
||||
return Response.json(null, {
|
||||
headers,
|
||||
status: httpStatus.UNAUTHORIZED,
|
||||
})
|
||||
}
|
||||
|
||||
return routeError({
|
||||
config: req.payload.config,
|
||||
err,
|
||||
req,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import { registerFirstUser } from './auth/registerFirstUser.js'
|
||||
import { resetPassword } from './auth/resetPassword.js'
|
||||
import { unlock } from './auth/unlock.js'
|
||||
import { verifyEmail } from './auth/verifyEmail.js'
|
||||
import { buildFormState } from './buildFormState.js'
|
||||
import { endpointsAreDisabled } from './checkEndpoints.js'
|
||||
import { count } from './collections/count.js'
|
||||
import { create } from './collections/create.js'
|
||||
@@ -110,9 +109,6 @@ const endpoints = {
|
||||
access,
|
||||
og: generateOGImage,
|
||||
},
|
||||
POST: {
|
||||
'form-state': buildFormState,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -575,10 +571,6 @@ export const POST =
|
||||
res = new Response('Route Not Found', { status: 404 })
|
||||
}
|
||||
}
|
||||
} else if (slug.length === 1 && slug1 in endpoints.root.POST) {
|
||||
await addDataAndFileToRequest(req)
|
||||
addLocalesToRequestFromData(req)
|
||||
res = await endpoints.root.POST[slug1]({ req })
|
||||
}
|
||||
|
||||
if (res instanceof Response) {
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import type { MappedComponent } from 'payload'
|
||||
import type { ImportMap, PayloadComponent } from 'payload'
|
||||
|
||||
import { RenderComponent } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { RenderServerComponent } from '../../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||
|
||||
export const OGImage: React.FC<{
|
||||
description?: string
|
||||
Fallback: React.ComponentType
|
||||
fontFamily?: string
|
||||
Icon: MappedComponent
|
||||
Icon: PayloadComponent
|
||||
importMap: ImportMap
|
||||
leader?: string
|
||||
title?: string
|
||||
}> = ({ description, fontFamily = 'Arial, sans-serif', Icon, leader, title }) => {
|
||||
}> = ({
|
||||
description,
|
||||
Fallback,
|
||||
fontFamily = 'Arial, sans-serif',
|
||||
Icon,
|
||||
importMap,
|
||||
leader,
|
||||
title,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -85,11 +96,13 @@ export const OGImage: React.FC<{
|
||||
width: '38px',
|
||||
}}
|
||||
>
|
||||
<RenderComponent
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
fill: 'white',
|
||||
}}
|
||||
mappedComponent={Icon}
|
||||
Component={Icon}
|
||||
Fallback={Fallback}
|
||||
importMap={importMap}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
import { getCreateMappedComponent, PayloadIcon } from '@payloadcms/ui/shared'
|
||||
import { PayloadIcon } from '@payloadcms/ui/shared'
|
||||
import fs from 'fs/promises'
|
||||
import { ImageResponse } from 'next/og.js'
|
||||
import { NextResponse } from 'next/server.js'
|
||||
@@ -8,6 +8,7 @@ import path from 'path'
|
||||
import React from 'react'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { RenderServerComponent } from '../../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||
import { OGImage } from './image.js'
|
||||
|
||||
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 description = searchParams.has('description') ? searchParams.get('description') : ''
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: req.payload.importMap,
|
||||
serverProps: {},
|
||||
})
|
||||
|
||||
const mappedIcon = createMappedComponent(
|
||||
config.admin?.components?.graphics?.Icon,
|
||||
undefined,
|
||||
PayloadIcon,
|
||||
'config.admin.components.graphics.Icon',
|
||||
)
|
||||
|
||||
let fontData
|
||||
|
||||
try {
|
||||
@@ -62,8 +51,10 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
|
||||
(
|
||||
<OGImage
|
||||
description={description}
|
||||
Fallback={PayloadIcon}
|
||||
fontFamily={fontFamily}
|
||||
Icon={mappedIcon}
|
||||
Icon={config.admin?.components?.graphics?.Icon}
|
||||
importMap={req.payload.importMap}
|
||||
leader={leader}
|
||||
title={title}
|
||||
/>
|
||||
|
||||
@@ -1,73 +1,12 @@
|
||||
import type { Collection, ErrorResult, PayloadRequest, SanitizedConfig } from 'payload'
|
||||
|
||||
import httpStatus from 'http-status'
|
||||
import { APIError, APIErrorName, ValidationErrorName } from 'payload'
|
||||
import { APIError, formatErrors } from 'payload'
|
||||
|
||||
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||
import { headersWithCors } from '../../utilities/headersWithCors.js'
|
||||
import { mergeHeaders } from '../../utilities/mergeHeaders.js'
|
||||
|
||||
const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResult => {
|
||||
if (incoming) {
|
||||
// Cannot use `instanceof` to check error type: https://github.com/microsoft/TypeScript/issues/13965
|
||||
// Instead, get the prototype of the incoming error and check its constructor name
|
||||
const proto = Object.getPrototypeOf(incoming)
|
||||
|
||||
// Payload 'ValidationError' and 'APIError'
|
||||
if (
|
||||
(proto.constructor.name === ValidationErrorName || proto.constructor.name === APIErrorName) &&
|
||||
incoming.data
|
||||
) {
|
||||
return {
|
||||
errors: [
|
||||
{
|
||||
name: incoming.name,
|
||||
data: incoming.data,
|
||||
message: incoming.message,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// Mongoose 'ValidationError': https://mongoosejs.com/docs/api/error.html#Error.ValidationError
|
||||
if (proto.constructor.name === ValidationErrorName && 'errors' in incoming && incoming.errors) {
|
||||
return {
|
||||
errors: Object.keys(incoming.errors).reduce((acc, key) => {
|
||||
acc.push({
|
||||
field: incoming.errors[key].path,
|
||||
message: incoming.errors[key].message,
|
||||
})
|
||||
return acc
|
||||
}, []),
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(incoming.message)) {
|
||||
return {
|
||||
errors: incoming.message,
|
||||
}
|
||||
}
|
||||
|
||||
if (incoming.name) {
|
||||
return {
|
||||
errors: [
|
||||
{
|
||||
message: incoming.message,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errors: [
|
||||
{
|
||||
message: 'An unknown error occurred.',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export const routeError = async ({
|
||||
collection,
|
||||
config: configArg,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import type { MappedComponent, ServerProps, VisibleEntities } from 'payload'
|
||||
import type { CustomComponent, ServerProps, VisibleEntities } from 'payload'
|
||||
|
||||
import { AppHeader, BulkUploadProvider, EntityVisibilityProvider, NavToggler } from '@payloadcms/ui'
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import {
|
||||
ActionsProvider,
|
||||
AppHeader,
|
||||
BulkUploadProvider,
|
||||
EntityVisibilityProvider,
|
||||
NavToggler,
|
||||
} from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import React from 'react'
|
||||
|
||||
import { DefaultNav } from '../../elements/Nav/index.js'
|
||||
@@ -14,6 +20,7 @@ const baseClass = 'template-default'
|
||||
export type DefaultTemplateProps = {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
viewActions?: CustomComponent[]
|
||||
visibleEntities: VisibleEntities
|
||||
} & ServerProps
|
||||
|
||||
@@ -27,6 +34,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
viewActions,
|
||||
visibleEntities,
|
||||
}) => {
|
||||
const {
|
||||
@@ -38,54 +46,77 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
} = {},
|
||||
} = 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 (
|
||||
<EntityVisibilityProvider visibleEntities={visibleEntities}>
|
||||
<BulkUploadProvider>
|
||||
<RenderComponent mappedComponent={MappedCustomHeader} />
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
|
||||
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
|
||||
<NavToggler className={`${baseClass}__nav-toggler`}>
|
||||
<NavHamburger />
|
||||
</NavToggler>
|
||||
</div>
|
||||
</div>
|
||||
<Wrapper baseClass={baseClass} className={className}>
|
||||
<RenderComponent mappedComponent={MappedDefaultNav} />
|
||||
<ActionsProvider
|
||||
Actions={
|
||||
viewActions
|
||||
? viewActions.reduce((acc, action, i) => {
|
||||
if (action) {
|
||||
if (typeof action === 'object') {
|
||||
acc[action.path] = (
|
||||
<RenderServerComponent Component={action} importMap={payload.importMap} />
|
||||
)
|
||||
} else {
|
||||
acc[action] = (
|
||||
<RenderServerComponent Component={action} importMap={payload.importMap} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<AppHeader />
|
||||
{children}
|
||||
return acc
|
||||
}, {})
|
||||
: 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>
|
||||
</Wrapper>
|
||||
</div>
|
||||
<Wrapper baseClass={baseClass} className={className}>
|
||||
<RenderServerComponent
|
||||
clientProps={{ clientProps: { visibleEntities } }}
|
||||
Component={CustomNav}
|
||||
Fallback={DefaultNav}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
}}
|
||||
/>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<AppHeader />
|
||||
{children}
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
</ActionsProvider>
|
||||
</BulkUploadProvider>
|
||||
</EntityVisibilityProvider>
|
||||
)
|
||||
|
||||
18
packages/next/src/utilities/getClientConfig.ts
Normal file
18
packages/next/src/utilities/getClientConfig.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ClientConfig, SanitizedConfig } from 'payload'
|
||||
|
||||
import { createClientConfig } from 'payload'
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getClientConfig = cache(
|
||||
async (args: { config: SanitizedConfig; i18n: I18nClient }): Promise<ClientConfig> => {
|
||||
const { config, i18n } = args
|
||||
|
||||
const clientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
return 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 { headers as getHeaders } from 'next/headers.js'
|
||||
@@ -46,13 +47,13 @@ export const initPage = async ({
|
||||
req: {
|
||||
headers,
|
||||
host: headers.get('host'),
|
||||
i18n,
|
||||
i18n: i18n as I18n,
|
||||
query: qs.parse(queryString, {
|
||||
depth: 10,
|
||||
ignoreQueryPrefix: true,
|
||||
}),
|
||||
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
|
||||
} as PayloadRequest,
|
||||
},
|
||||
},
|
||||
payload,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { I18n, I18nClient } from '@payloadcms/translations'
|
||||
import type { PayloadRequest, Permissions, SanitizedConfig, User } from 'payload'
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
@@ -16,7 +16,10 @@ type Result = {
|
||||
user: User
|
||||
}
|
||||
|
||||
export const initReq = cache(async function (config: SanitizedConfig): Promise<Result> {
|
||||
export const initReq = cache(async function (
|
||||
configPromise: Promise<SanitizedConfig> | SanitizedConfig,
|
||||
): Promise<Result> {
|
||||
const config = await configPromise
|
||||
const payload = await getPayloadHMR({ config })
|
||||
|
||||
const headers = await getHeaders()
|
||||
@@ -40,9 +43,9 @@ export const initReq = cache(async function (config: SanitizedConfig): Promise<R
|
||||
req: {
|
||||
headers,
|
||||
host: headers.get('host'),
|
||||
i18n,
|
||||
i18n: i18n as I18n,
|
||||
url: `${payload.config.serverURL}`,
|
||||
} as PayloadRequest,
|
||||
},
|
||||
},
|
||||
payload,
|
||||
)
|
||||
|
||||
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,
|
||||
MinimizeMaximizeIcon,
|
||||
NumberField,
|
||||
SetViewActions,
|
||||
SetDocumentStepNav,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useLocale,
|
||||
@@ -19,7 +19,6 @@ import { useSearchParams } from 'next/navigation.js'
|
||||
import * as React from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||
import './index.scss'
|
||||
import { LocaleSelector } from './LocaleSelector/index.js'
|
||||
import { RenderJSON } from './RenderJSON/index.js'
|
||||
@@ -42,8 +41,8 @@ export const APIViewClient: React.FC = () => {
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const collectionClientConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
const globalClientConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||
|
||||
const localeOptions =
|
||||
localization &&
|
||||
@@ -52,13 +51,13 @@ export const APIViewClient: React.FC = () => {
|
||||
let draftsEnabled: boolean = false
|
||||
let docEndpoint: string = ''
|
||||
|
||||
if (collectionClientConfig) {
|
||||
draftsEnabled = Boolean(collectionClientConfig.versions?.drafts)
|
||||
if (collectionConfig) {
|
||||
draftsEnabled = Boolean(collectionConfig.versions?.drafts)
|
||||
docEndpoint = `/${collectionSlug}/${id}`
|
||||
}
|
||||
|
||||
if (globalClientConfig) {
|
||||
draftsEnabled = Boolean(globalClientConfig.versions?.drafts)
|
||||
if (globalConfig) {
|
||||
draftsEnabled = Boolean(globalConfig.versions?.drafts)
|
||||
docEndpoint = `/globals/${globalSlug}`
|
||||
}
|
||||
|
||||
@@ -111,19 +110,13 @@ export const APIViewClient: React.FC = () => {
|
||||
>
|
||||
<SetDocumentStepNav
|
||||
collectionSlug={collectionSlug}
|
||||
globalLabel={globalClientConfig?.label}
|
||||
globalLabel={globalConfig?.label}
|
||||
globalSlug={globalSlug}
|
||||
id={id}
|
||||
pluralLabel={collectionClientConfig ? collectionClientConfig?.labels?.plural : undefined}
|
||||
useAsTitle={collectionClientConfig ? collectionClientConfig?.admin?.useAsTitle : undefined}
|
||||
pluralLabel={collectionConfig ? collectionConfig?.labels?.plural : undefined}
|
||||
useAsTitle={collectionConfig ? collectionConfig?.admin?.useAsTitle : undefined}
|
||||
view="API"
|
||||
/>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionClientConfig || globalClientConfig)?.admin?.components?.views?.edit?.api
|
||||
?.actions
|
||||
}
|
||||
/>
|
||||
<div className={`${baseClass}__configuration`}>
|
||||
<div className={`${baseClass}__api-url`}>
|
||||
<span className={`${baseClass}__label`}>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const Settings: React.FC<{
|
||||
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||
<h3>{i18n.t('general:payloadSettings')}</h3>
|
||||
<div className={`${baseClass}__language`}>
|
||||
<FieldLabel field={null} htmlFor="language-select" label={i18n.t('general:language')} />
|
||||
<FieldLabel htmlFor="language-select" label={i18n.t('general:language')} />
|
||||
<LanguageSelector languageOptions={languageOptions} />
|
||||
</div>
|
||||
{theme === 'all' && <ToggleTheme />}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import {
|
||||
DocumentInfoProvider,
|
||||
EditDepthProvider,
|
||||
HydrateAuthProvider,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui'
|
||||
import { getCreateMappedComponent } from '@payloadcms/ui/shared'
|
||||
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
@@ -20,6 +15,7 @@ import { Settings } from './Settings/index.js'
|
||||
export { generateAccountMetadata } from './meta.js'
|
||||
|
||||
export const Account: React.FC<AdminViewProps> = async ({
|
||||
importMap,
|
||||
initPageResult,
|
||||
params,
|
||||
searchParams,
|
||||
@@ -61,32 +57,11 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
const { data, formState } = await getDocumentData({
|
||||
id: user.id,
|
||||
collectionConfig,
|
||||
importMap,
|
||||
locale,
|
||||
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 (
|
||||
<DocumentInfoProvider
|
||||
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} theme={theme} />}
|
||||
@@ -100,7 +75,7 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
initialState={formState}
|
||||
isEditing
|
||||
>
|
||||
<EditDepthProvider depth={1}>
|
||||
<EditDepthProvider>
|
||||
<DocumentHeader
|
||||
collectionConfig={collectionConfig}
|
||||
hideTabs
|
||||
@@ -109,7 +84,22 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
permissions={permissions}
|
||||
/>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<RenderComponent mappedComponent={mappedAccountComponent} />
|
||||
<RenderServerComponent
|
||||
Component={CustomAccountComponent}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: [],
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
<EditView />
|
||||
<AccountClient />
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
|
||||
@@ -4,19 +4,17 @@ import type { ClientCollectionConfig, FormState, LoginWithUsernameOptions } from
|
||||
|
||||
import {
|
||||
ConfirmPasswordField,
|
||||
EmailAndUsernameFields,
|
||||
Form,
|
||||
FormSubmit,
|
||||
PasswordField,
|
||||
RenderFields,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { RenderEmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js'
|
||||
|
||||
export const CreateFirstUserClient: React.FC<{
|
||||
initialState: FormState
|
||||
loginWithUsername?: false | LoginWithUsernameOptions
|
||||
@@ -30,6 +28,8 @@ export const CreateFirstUserClient: React.FC<{
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { setUser } = useAuth()
|
||||
|
||||
@@ -38,18 +38,15 @@ export const CreateFirstUserClient: React.FC<{
|
||||
const onChange: FormProps['onChange'][0] = React.useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
const { state } = await getFormState({
|
||||
apiRoute,
|
||||
body: {
|
||||
collectionSlug: userSlug,
|
||||
formState: prevFormState,
|
||||
operation: 'create',
|
||||
schemaPath: `_${userSlug}.auth`,
|
||||
},
|
||||
serverURL,
|
||||
collectionSlug: userSlug,
|
||||
formState: prevFormState,
|
||||
operation: 'create',
|
||||
schemaPath: [`_${userSlug}`, 'auth'],
|
||||
})
|
||||
|
||||
return state
|
||||
},
|
||||
[apiRoute, userSlug, serverURL],
|
||||
[userSlug, getFormState],
|
||||
)
|
||||
|
||||
const handleFirstRegister = (data: UserWithToken) => {
|
||||
@@ -66,11 +63,12 @@ export const CreateFirstUserClient: React.FC<{
|
||||
redirect={admin}
|
||||
validationOperation="create"
|
||||
>
|
||||
<RenderEmailAndUsernameFields
|
||||
<EmailAndUsernameFields
|
||||
className="emailAndUsername"
|
||||
loginWithUsername={loginWithUsername}
|
||||
operation="create"
|
||||
readOnly={false}
|
||||
t={t}
|
||||
/>
|
||||
<PasswordField
|
||||
autoComplete={'off'}
|
||||
@@ -81,14 +79,7 @@ export const CreateFirstUserClient: React.FC<{
|
||||
}}
|
||||
/>
|
||||
<ConfirmPasswordField />
|
||||
<RenderFields
|
||||
fields={collectionConfig.fields}
|
||||
forceRender
|
||||
operation="create"
|
||||
path=""
|
||||
readOnly={false}
|
||||
schemaPath={userSlug}
|
||||
/>
|
||||
{/* Fields Here */}
|
||||
<FormSubmit size="large">{t('general:create')}</FormSubmit>
|
||||
</Form>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,10 @@ import './index.scss'
|
||||
|
||||
export { generateCreateFirstUserMetadata } from './meta.js'
|
||||
|
||||
export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageResult }) => {
|
||||
export const CreateFirstUserView: React.FC<AdminViewProps> = async ({
|
||||
importMap,
|
||||
initPageResult,
|
||||
}) => {
|
||||
const {
|
||||
locale,
|
||||
req,
|
||||
@@ -28,9 +31,10 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
|
||||
const { formState } = await getDocumentData({
|
||||
collectionConfig,
|
||||
importMap,
|
||||
locale,
|
||||
req,
|
||||
schemaPath: `_${collectionConfig.slug}.auth`,
|
||||
schemaPath: [`_${collectionConfig.slug}`, 'auth'],
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,13 +2,9 @@ import type { groupNavItems } from '@payloadcms/ui/shared'
|
||||
import type { ClientUser, Permissions, ServerProps, VisibleEntities } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { Button, Card, Gutter, Locked, SetStepNav, SetViewActions } from '@payloadcms/ui'
|
||||
import {
|
||||
EntityType,
|
||||
formatAdminURL,
|
||||
getCreateMappedComponent,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { Button, Card, Gutter, Locked } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
@@ -46,41 +42,25 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
user,
|
||||
} = props
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedBeforeDashboards = createMappedComponent(
|
||||
beforeDashboard,
|
||||
undefined,
|
||||
undefined,
|
||||
'beforeDashboard',
|
||||
)
|
||||
|
||||
const mappedAfterDashboards = createMappedComponent(
|
||||
afterDashboard,
|
||||
undefined,
|
||||
undefined,
|
||||
'afterDashboard',
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SetStepNav nav={[]} />
|
||||
<SetViewActions actions={[]} />
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
<RenderComponent mappedComponent={mappedBeforeDashboards} />
|
||||
{beforeDashboard && (
|
||||
<RenderServerComponent
|
||||
Component={beforeDashboard}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Fragment>
|
||||
<SetViewActions actions={[]} />
|
||||
{!navGroups || navGroups?.length === 0 ? (
|
||||
<p>no nav groups....</p>
|
||||
) : (
|
||||
@@ -89,8 +69,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
<div className={`${baseClass}__group`} key={groupIndex}>
|
||||
<h2 className={`${baseClass}__label`}>{label}</h2>
|
||||
<ul className={`${baseClass}__card-list`}>
|
||||
{entities.map(({ type, entity }, entityIndex) => {
|
||||
let title: string
|
||||
{entities.map(({ slug, type, label }, entityIndex) => {
|
||||
let buttonAriaLabel: string
|
||||
let createHREF: string
|
||||
let href: string
|
||||
@@ -99,37 +78,30 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
let userEditing = null
|
||||
|
||||
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/${entity.slug}` })
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })
|
||||
|
||||
createHREF = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${entity.slug}/create`,
|
||||
path: `/collections/${slug}/create`,
|
||||
})
|
||||
|
||||
hasCreatePermission =
|
||||
permissions?.collections?.[entity.slug]?.create?.permission
|
||||
hasCreatePermission = permissions?.collections?.[slug]?.create?.permission
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
title = getTranslation(entity.label, i18n)
|
||||
|
||||
buttonAriaLabel = t('general:editLabel', {
|
||||
label: getTranslation(entity.label, i18n),
|
||||
label: getTranslation(label, i18n),
|
||||
})
|
||||
|
||||
href = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${entity.slug}`,
|
||||
path: `/globals/${slug}`,
|
||||
})
|
||||
|
||||
// Find the lock status for the global
|
||||
const globalLockData = globalData.find(
|
||||
(global) => global.slug === entity.slug,
|
||||
)
|
||||
const globalLockData = globalData.find((global) => global.slug === slug)
|
||||
if (globalLockData) {
|
||||
lockStatus = globalLockData.data._isLocked
|
||||
userEditing = globalLockData.data._userEditing
|
||||
@@ -145,7 +117,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
) : hasCreatePermission && type === EntityType.collection ? (
|
||||
<Button
|
||||
aria-label={t('general:createNewLabel', {
|
||||
label: getTranslation(entity.labels.singular, i18n),
|
||||
label,
|
||||
})}
|
||||
buttonStyle="icon-label"
|
||||
el="link"
|
||||
@@ -159,9 +131,9 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
}
|
||||
buttonAriaLabel={buttonAriaLabel}
|
||||
href={href}
|
||||
id={`card-${entity.slug}`}
|
||||
id={`card-${slug}`}
|
||||
Link={Link}
|
||||
title={title}
|
||||
title={getTranslation(label, i18n)}
|
||||
titleAs="h3"
|
||||
/>
|
||||
</li>
|
||||
@@ -173,7 +145,21 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
})
|
||||
)}
|
||||
</Fragment>
|
||||
<RenderComponent mappedComponent={mappedAfterDashboards} />
|
||||
{afterDashboard && (
|
||||
<RenderServerComponent
|
||||
Component={afterDashboard}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Gutter>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,15 +2,11 @@ import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import {
|
||||
EntityType,
|
||||
getCreateMappedComponent,
|
||||
groupNavItems,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||
import { DefaultDashboard } from './Default/index.js'
|
||||
|
||||
export { generateDashboardMetadata } from './meta.js'
|
||||
@@ -94,39 +90,30 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
|
||||
i18n,
|
||||
)
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
globalData,
|
||||
i18n,
|
||||
Link,
|
||||
locale,
|
||||
navGroups,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedDashboardComponent = createMappedComponent(
|
||||
CustomDashboardComponent?.Component,
|
||||
undefined,
|
||||
DefaultDashboard,
|
||||
'CustomDashboardComponent.Component',
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<RenderComponent
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
Link,
|
||||
locale,
|
||||
}}
|
||||
mappedComponent={mappedDashboardComponent}
|
||||
Component={CustomDashboardComponent}
|
||||
Fallback={DefaultDashboard}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
globalData,
|
||||
i18n,
|
||||
Link,
|
||||
locale,
|
||||
navGroups,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
@@ -1,58 +1,63 @@
|
||||
import type {
|
||||
Data,
|
||||
FormState,
|
||||
ImportMap,
|
||||
Locale,
|
||||
PayloadRequest,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { buildFormStateFn as buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { reduceFieldsToValues } from 'payload/shared'
|
||||
|
||||
export const getDocumentData = async (args: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
importMap: ImportMap
|
||||
locale: Locale
|
||||
req: PayloadRequest
|
||||
schemaPath?: string
|
||||
schemaPath?: string[]
|
||||
}): Promise<{
|
||||
data: Data
|
||||
formState: FormState
|
||||
}> => {
|
||||
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 {
|
||||
const { state: formState } = await buildFormState({
|
||||
req: {
|
||||
...req,
|
||||
data: {
|
||||
id,
|
||||
collectionSlug: collectionConfig?.slug,
|
||||
globalSlug: globalConfig?.slug,
|
||||
locale: locale?.code,
|
||||
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
|
||||
schemaPath,
|
||||
},
|
||||
},
|
||||
const result = await buildFormState({
|
||||
id,
|
||||
collectionSlug: collectionConfig?.slug,
|
||||
globalSlug: globalConfig?.slug,
|
||||
locale: locale?.code,
|
||||
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
|
||||
renderFields: true,
|
||||
req,
|
||||
schemaPath,
|
||||
})
|
||||
|
||||
const data = reduceFieldsToValues(formState, true)
|
||||
const data = reduceFieldsToValues(result.state, true)
|
||||
|
||||
return {
|
||||
data,
|
||||
formState,
|
||||
formState: result.state,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting document data', error) // eslint-disable-line no-console
|
||||
|
||||
return {
|
||||
data: null,
|
||||
formState: {
|
||||
fields: {
|
||||
initialValue: undefined,
|
||||
schemaPath: [],
|
||||
valid: false,
|
||||
value: undefined,
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
|
||||
export type ViewFromConfig<TProps extends object> = {
|
||||
Component?: React.FC<TProps>
|
||||
payloadComponent?: PayloadComponent<TProps>
|
||||
ComponentConfig?: PayloadComponent<TProps>
|
||||
}
|
||||
|
||||
export const getViewsFromConfig = ({
|
||||
@@ -94,7 +94,7 @@ export const getViewsFromConfig = ({
|
||||
docPermissions?.create?.permission
|
||||
) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultEditView,
|
||||
@@ -132,11 +132,11 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
} else {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
|
||||
DefaultView = {
|
||||
@@ -156,7 +156,7 @@ export const getViewsFromConfig = ({
|
||||
case 'api': {
|
||||
if (collectionConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'api'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'api'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultAPIView,
|
||||
@@ -177,7 +177,7 @@ export const getViewsFromConfig = ({
|
||||
case 'versions': {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'versions'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'versions'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultVersionsView,
|
||||
@@ -215,7 +215,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ export const getViewsFromConfig = ({
|
||||
if (segment4 === 'versions') {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'version'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'version'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultVersionView,
|
||||
@@ -266,7 +266,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,7 +286,7 @@ export const getViewsFromConfig = ({
|
||||
switch (routeSegments.length) {
|
||||
case 2: {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultEditView,
|
||||
@@ -300,7 +300,7 @@ export const getViewsFromConfig = ({
|
||||
case 'api': {
|
||||
if (globalConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'api'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'api'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultAPIView,
|
||||
@@ -321,7 +321,7 @@ export const getViewsFromConfig = ({
|
||||
case 'versions': {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'versions'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'versions'),
|
||||
}
|
||||
|
||||
DefaultView = {
|
||||
@@ -356,7 +356,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
} else {
|
||||
DefaultView = {
|
||||
@@ -379,7 +379,7 @@ export const getViewsFromConfig = ({
|
||||
if (segment3 === 'versions') {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'version'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'version'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultVersionView,
|
||||
@@ -410,7 +410,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import type {
|
||||
AdminViewProps,
|
||||
EditViewComponent,
|
||||
MappedComponent,
|
||||
Data,
|
||||
PayloadComponent,
|
||||
ServerProps,
|
||||
ServerSideEditViewProps,
|
||||
} from 'payload'
|
||||
|
||||
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import {
|
||||
formatAdminURL,
|
||||
getCreateMappedComponent,
|
||||
isEditing as getIsEditing,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL, isEditing as getIsEditing } from '@payloadcms/ui/shared'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
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 { renderDocumentSlots } from '../../utilities/renderDocumentSlots.js'
|
||||
import { NotFoundView } from '../NotFound/index.js'
|
||||
import { getDocumentData } from './getDocumentData.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 Document: React.FC<AdminViewProps> = async ({
|
||||
export const renderDocument = async ({
|
||||
disableActions,
|
||||
drawerSlug,
|
||||
importMap,
|
||||
initialData,
|
||||
initPageResult,
|
||||
params,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
searchParams,
|
||||
}) => {
|
||||
}: AdminViewProps): Promise<{
|
||||
data: Data
|
||||
Document: React.ReactNode
|
||||
}> => {
|
||||
const {
|
||||
collectionConfig,
|
||||
docID: id,
|
||||
@@ -55,15 +63,17 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
} = initPageResult
|
||||
|
||||
const segments = Array.isArray(params?.segments) ? params.segments : []
|
||||
|
||||
const collectionSlug = collectionConfig?.slug || undefined
|
||||
|
||||
const globalSlug = globalConfig?.slug || undefined
|
||||
|
||||
const isEditing = getIsEditing({ id, collectionSlug, globalSlug })
|
||||
|
||||
let RootViewOverride: MappedComponent<ServerSideEditViewProps>
|
||||
let CustomView: MappedComponent<ServerSideEditViewProps>
|
||||
let DefaultView: MappedComponent<ServerSideEditViewProps>
|
||||
let ErrorView: MappedComponent<AdminViewProps>
|
||||
let RootViewOverride: PayloadComponent
|
||||
let CustomView: ViewFromConfig<ServerSideEditViewProps>
|
||||
let DefaultView: ViewFromConfig<ServerSideEditViewProps>
|
||||
let ErrorView: ViewFromConfig<AdminViewProps>
|
||||
|
||||
let apiURL: string
|
||||
|
||||
@@ -71,12 +81,13 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
id,
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
importMap,
|
||||
locale,
|
||||
req,
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const { docPermissions, hasPublishPermission, hasSavePermission } = await getDocumentPermissions({
|
||||
@@ -87,24 +98,21 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
req,
|
||||
})
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
const serverProps: ServerProps = {
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
user,
|
||||
}
|
||||
|
||||
if (collectionConfig) {
|
||||
if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
@@ -122,12 +130,7 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
RootViewOverride =
|
||||
collectionConfig?.admin?.components?.views?.edit?.root &&
|
||||
'Component' in collectionConfig.admin.components.views.edit.root
|
||||
? createMappedComponent(
|
||||
collectionConfig?.admin?.components?.views?.edit?.root?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
|
||||
undefined,
|
||||
undefined,
|
||||
'collectionConfig?.admin?.components?.views?.edit?.root',
|
||||
)
|
||||
? collectionConfig?.admin?.components?.views?.edit?.root?.Component
|
||||
: null
|
||||
|
||||
if (!RootViewOverride) {
|
||||
@@ -138,36 +141,21 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
routeSegments: segments,
|
||||
})
|
||||
|
||||
CustomView = createMappedComponent(
|
||||
collectionViews?.CustomView?.payloadComponent,
|
||||
undefined,
|
||||
collectionViews?.CustomView?.Component,
|
||||
'collectionViews?.CustomView.payloadComponent',
|
||||
)
|
||||
|
||||
DefaultView = createMappedComponent(
|
||||
collectionViews?.DefaultView?.payloadComponent,
|
||||
undefined,
|
||||
collectionViews?.DefaultView?.Component,
|
||||
'collectionViews?.DefaultView.payloadComponent',
|
||||
)
|
||||
|
||||
ErrorView = createMappedComponent(
|
||||
collectionViews?.ErrorView?.payloadComponent,
|
||||
undefined,
|
||||
collectionViews?.ErrorView?.Component,
|
||||
'collectionViews?.ErrorView.payloadComponent',
|
||||
)
|
||||
CustomView = collectionViews?.CustomView
|
||||
DefaultView = collectionViews?.DefaultView
|
||||
ErrorView = collectionViews?.ErrorView
|
||||
}
|
||||
|
||||
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
|
||||
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
|
||||
ErrorView = {
|
||||
Component: NotFoundView,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
@@ -189,12 +177,7 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
RootViewOverride =
|
||||
globalConfig?.admin?.components?.views?.edit?.root &&
|
||||
'Component' in globalConfig.admin.components.views.edit.root
|
||||
? createMappedComponent(
|
||||
globalConfig?.admin?.components?.views?.edit?.root?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
|
||||
undefined,
|
||||
undefined,
|
||||
'globalConfig?.admin?.components?.views?.edit?.root',
|
||||
)
|
||||
? globalConfig?.admin?.components?.views?.edit?.root?.Component
|
||||
: null
|
||||
|
||||
if (!RootViewOverride) {
|
||||
@@ -205,29 +188,14 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
routeSegments: segments,
|
||||
})
|
||||
|
||||
CustomView = createMappedComponent(
|
||||
globalViews?.CustomView?.payloadComponent,
|
||||
undefined,
|
||||
globalViews?.CustomView?.Component,
|
||||
'globalViews?.CustomView.payloadComponent',
|
||||
)
|
||||
|
||||
DefaultView = createMappedComponent(
|
||||
globalViews?.DefaultView?.payloadComponent,
|
||||
undefined,
|
||||
globalViews?.DefaultView?.Component,
|
||||
'globalViews?.DefaultView.payloadComponent',
|
||||
)
|
||||
|
||||
ErrorView = createMappedComponent(
|
||||
globalViews?.ErrorView?.payloadComponent,
|
||||
undefined,
|
||||
globalViews?.ErrorView?.Component,
|
||||
'globalViews?.ErrorView.payloadComponent',
|
||||
)
|
||||
CustomView = globalViews?.CustomView
|
||||
DefaultView = globalViews?.DefaultView
|
||||
ErrorView = globalViews?.ErrorView
|
||||
|
||||
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
|
||||
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
|
||||
ErrorView = {
|
||||
Component: NotFoundView,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,13 +208,14 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
hasSavePermission &&
|
||||
((collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
|
||||
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave))
|
||||
|
||||
const validateDraftData =
|
||||
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate
|
||||
|
||||
if (shouldAutosave && !validateDraftData && !id && collectionSlug) {
|
||||
const doc = await payload.create({
|
||||
collection: collectionSlug,
|
||||
data: {},
|
||||
data: initialData || {},
|
||||
depth: 0,
|
||||
draft: true,
|
||||
fallbackLocale: null,
|
||||
@@ -263,57 +232,93 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
})
|
||||
redirect(redirectURL)
|
||||
} else {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
apiURL={apiURL}
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
disableActions={false}
|
||||
docPermissions={docPermissions}
|
||||
globalSlug={globalConfig?.slug}
|
||||
hasPublishPermission={hasPublishPermission}
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={id}
|
||||
initialData={data}
|
||||
initialState={formState}
|
||||
isEditing={isEditing}
|
||||
key={locale?.code}
|
||||
>
|
||||
{!RootViewOverride && (
|
||||
<DocumentHeader
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
{/**
|
||||
* After bumping the Next.js canary to 104, and React to 19.0.0-rc-06d0b89e-20240801" we have to deepCopy the permissions object (https://github.com/payloadcms/payload/pull/7541).
|
||||
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
|
||||
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error when loading up some version views - for example a versions
|
||||
* view in the draft-posts collection of the versions test suite. RenderCustomComponent is what renders the versions view.
|
||||
*
|
||||
* // TODO: Revisit this in the future and figure out why this is happening. Might be a React/Next.js bug. We don't know why it happens, and a future React/Next version might unbreak this (keep an eye on this and remove deepCopyObjectSimple if that's the case)
|
||||
*/}
|
||||
<EditDepthProvider
|
||||
depth={1}
|
||||
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
|
||||
const documentSlots = renderDocumentSlots({
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
hasSavePermission,
|
||||
importMap,
|
||||
payload,
|
||||
permissions,
|
||||
})
|
||||
|
||||
const clientProps = { formState, ...documentSlots }
|
||||
|
||||
return {
|
||||
data,
|
||||
Document: (
|
||||
<DocumentInfoProvider
|
||||
apiURL={apiURL}
|
||||
BeforeDocument={
|
||||
drawerSlug ? (
|
||||
<DocumentDrawerHeader
|
||||
drawerSlug={drawerSlug}
|
||||
Header={null} // TODO
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
disableActions={disableActions ?? false}
|
||||
docPermissions={docPermissions}
|
||||
globalSlug={globalConfig?.slug}
|
||||
hasPublishPermission={hasPublishPermission}
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={id}
|
||||
initialData={data}
|
||||
initialState={formState}
|
||||
isEditing={isEditing}
|
||||
key={locale?.code}
|
||||
redirectAfterDelete={redirectAfterDelete}
|
||||
redirectAfterDuplicate={redirectAfterDuplicate}
|
||||
>
|
||||
{ErrorView ? (
|
||||
<RenderComponent mappedComponent={ErrorView} />
|
||||
) : (
|
||||
<RenderComponent
|
||||
mappedComponent={
|
||||
RootViewOverride ? RootViewOverride : CustomView ? CustomView : DefaultView
|
||||
}
|
||||
{!RootViewOverride && !drawerSlug && (
|
||||
<DocumentHeader
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
)
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<EditDepthProvider>
|
||||
{ErrorView ? (
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={ErrorView.ComponentConfig || ErrorView.Component}
|
||||
importMap={importMap}
|
||||
serverProps={serverProps}
|
||||
/>
|
||||
) : (
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={
|
||||
RootViewOverride
|
||||
? RootViewOverride
|
||||
: CustomView?.ComponentConfig || CustomView?.Component
|
||||
? CustomView?.ComponentConfig || CustomView?.Component
|
||||
: DefaultView?.ComponentConfig || DefaultView?.Component
|
||||
}
|
||||
importMap={importMap}
|
||||
serverProps={serverProps}
|
||||
/>
|
||||
)}
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export const Document: React.FC<AdminViewProps> = async (args) => {
|
||||
try {
|
||||
const { Document: RenderedDocument } = await renderDocument(args)
|
||||
return RenderedDocument
|
||||
} catch (error) {
|
||||
if (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 { EditViewClient } from './index.client.js'
|
||||
|
||||
export const EditView: PayloadServerReactComponent<EditViewComponent> = () => {
|
||||
return <EditViewClient />
|
||||
export const EditView: React.FC<ClientSideEditViewProps> = (props) => {
|
||||
return <DefaultEditView {...props} />
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
DefaultListView,
|
||||
HydrateAuthProvider,
|
||||
ListInfoProvider,
|
||||
ListQueryProvider,
|
||||
TableColumnsProvider,
|
||||
} from '@payloadcms/ui'
|
||||
import { formatAdminURL, getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { createClientCollectionConfig } from '@payloadcms/ui/utilities/createClientConfig'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
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 React, { Fragment } from 'react'
|
||||
|
||||
import type { ListPreferences } from './Default/types.js'
|
||||
|
||||
import { DefaultEditView } from '../Edit/Default/index.js'
|
||||
import { DefaultListView } from './Default/index.js'
|
||||
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||
import { ListDrawerHeader } from '../../elements/ListDrawerHeader/index.js'
|
||||
|
||||
export { generateListMetadata } from './meta.js'
|
||||
|
||||
export const ListView: React.FC<AdminViewProps> = async ({
|
||||
initPageResult,
|
||||
params,
|
||||
searchParams,
|
||||
}) => {
|
||||
type ListViewArgs = {
|
||||
disableBulkDelete?: boolean
|
||||
disableBulkEdit?: boolean
|
||||
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 {
|
||||
collectionConfig,
|
||||
collectionConfig: {
|
||||
slug: collectionSlug,
|
||||
admin: { defaultColumns, useAsTitle },
|
||||
defaultSort,
|
||||
fields,
|
||||
},
|
||||
locale: fullLocale,
|
||||
permissions,
|
||||
req,
|
||||
@@ -41,10 +67,8 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
visibleEntities,
|
||||
} = initPageResult
|
||||
|
||||
const collectionSlug = collectionConfig?.slug
|
||||
|
||||
if (!permissions?.collections?.[collectionSlug]?.read?.permission) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
let listPreferences: ListPreferences
|
||||
@@ -79,7 +103,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
},
|
||||
})
|
||||
?.then((res) => res?.docs?.[0]?.value)) as ListPreferences
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
} catch (_err) {} // eslint-disable-line no-empty
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
@@ -87,7 +111,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
|
||||
if (collectionConfig) {
|
||||
if (!visibleEntities.collections.includes(collectionSlug)) {
|
||||
return notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const page = isNumber(query?.page) ? Number(query.page) : 0
|
||||
@@ -98,9 +122,11 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
where: (query?.where as Where) || undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const limit = isNumber(query?.limit)
|
||||
? Number(query.limit)
|
||||
: listPreferences?.limit || collectionConfig.admin.pagination.defaultLimit
|
||||
|
||||
const sort =
|
||||
query?.sort && typeof query.sort === 'string'
|
||||
? query.sort
|
||||
@@ -125,89 +151,124 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
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,
|
||||
serverProps: {
|
||||
collectionConfig,
|
||||
collectionSlug,
|
||||
data,
|
||||
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
|
||||
i18n,
|
||||
limit,
|
||||
listPreferences,
|
||||
listSearchableFields: collectionConfig.admin.listSearchableFields,
|
||||
locale: fullLocale,
|
||||
newDocumentURL: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
}),
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
useAsTitle,
|
||||
})
|
||||
|
||||
const ListComponent = createMappedComponent(
|
||||
collectionConfig?.admin?.components?.views?.list?.Component,
|
||||
undefined,
|
||||
DefaultListView,
|
||||
'collectionConfig?.admin?.components?.views?.list?.Component',
|
||||
)
|
||||
const renderedFilters = renderFilters(fields, req.payload.importMap)
|
||||
|
||||
let clientCollectionConfig = deepCopyObjectSimple(
|
||||
collectionConfig,
|
||||
) as unknown as ClientCollectionConfig
|
||||
clientCollectionConfig = createClientCollectionConfig({
|
||||
clientCollection: clientCollectionConfig,
|
||||
collection: collectionConfig,
|
||||
createMappedComponent,
|
||||
DefaultEditView,
|
||||
DefaultListView,
|
||||
i18n,
|
||||
importMap: payload.importMap,
|
||||
payload,
|
||||
})
|
||||
const clientProps: ListViewClientProps = {
|
||||
collectionSlug,
|
||||
columnState,
|
||||
listPreferences,
|
||||
renderedFilters,
|
||||
Table,
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<ListInfoProvider
|
||||
collectionConfig={clientCollectionConfig}
|
||||
collectionSlug={collectionSlug}
|
||||
hasCreatePermission={permissions?.collections?.[collectionSlug]?.create?.permission}
|
||||
newDocumentURL={formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
})}
|
||||
>
|
||||
<ListQueryProvider
|
||||
data={data}
|
||||
defaultLimit={limit || collectionConfig?.admin?.pagination?.defaultLimit}
|
||||
defaultSort={sort}
|
||||
modifySearchParams
|
||||
preferenceKey={preferenceKey}
|
||||
const hasCreatePermission = permissions?.collections?.[collectionSlug]?.create?.permission
|
||||
|
||||
return {
|
||||
List: (
|
||||
<Fragment>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<ListInfoProvider
|
||||
// beforeActions={
|
||||
// enableRowSelections
|
||||
// ? [<SelectMany key="select-many" onClick={onBulkSelect} />]
|
||||
// : undefined
|
||||
// }
|
||||
collectionSlug={collectionSlug}
|
||||
disableBulkDelete={disableBulkDelete}
|
||||
disableBulkEdit={disableBulkEdit}
|
||||
hasCreatePermission={hasCreatePermission}
|
||||
Header={
|
||||
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
|
||||
collectionSlug={collectionSlug}
|
||||
enableRowSelections
|
||||
listPreferences={listPreferences}
|
||||
<ListQueryProvider
|
||||
data={data}
|
||||
defaultLimit={limit || collectionConfig?.admin?.pagination?.defaultLimit}
|
||||
defaultSort={sort}
|
||||
modifySearchParams
|
||||
preferenceKey={preferenceKey}
|
||||
>
|
||||
<RenderComponent
|
||||
clientProps={{
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={collectionConfig?.admin?.components?.views?.list.Component}
|
||||
Fallback={DefaultListView}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
collectionConfig,
|
||||
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>
|
||||
</ListInfoProvider>
|
||||
</Fragment>
|
||||
)
|
||||
</ListQueryProvider>
|
||||
</ListInfoProvider>
|
||||
</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,
|
||||
Form,
|
||||
OperationProvider,
|
||||
SetViewActions,
|
||||
SetDocumentStepNav,
|
||||
SetDocumentTitle,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useDocumentEvents,
|
||||
useDocumentInfo,
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import {
|
||||
getFormState,
|
||||
handleBackToDashboard,
|
||||
handleGoBack,
|
||||
handleTakeOver,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { handleBackToDashboard, handleGoBack, handleTakeOver } from '@payloadcms/ui/shared'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { DocumentLocked } from '../../elements/DocumentLocked/index.js'
|
||||
import { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js'
|
||||
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js'
|
||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||
import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle/index.js'
|
||||
import { useLivePreviewContext } from './Context/context.js'
|
||||
import { LivePreviewProvider } from './Context/index.js'
|
||||
import './index.scss'
|
||||
@@ -55,13 +50,11 @@ type Props = {
|
||||
}
|
||||
|
||||
const PreviewView: React.FC<Props> = ({
|
||||
apiRoute,
|
||||
collectionConfig,
|
||||
config,
|
||||
fields,
|
||||
globalConfig,
|
||||
schemaPath,
|
||||
serverURL,
|
||||
}) => {
|
||||
const {
|
||||
id,
|
||||
@@ -92,6 +85,8 @@ const PreviewView: React.FC<Props> = ({
|
||||
updateDocumentEditor,
|
||||
} = useDocumentInfo()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
|
||||
const operation = id ? 'update' : 'create'
|
||||
|
||||
const {
|
||||
@@ -115,6 +110,8 @@ const PreviewView: React.FC<Props> = ({
|
||||
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
||||
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
||||
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
|
||||
const documentLockStateRef = useRef<{
|
||||
hasShownLockedModal: boolean
|
||||
isLocked: boolean
|
||||
@@ -169,6 +166,17 @@ const PreviewView: React.FC<Props> = ({
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
|
||||
const currentTime = Date.now()
|
||||
const timeSinceLastUpdate = currentTime - lastUpdateTime
|
||||
|
||||
@@ -181,19 +189,16 @@ const PreviewView: React.FC<Props> = ({
|
||||
const docPreferences = await getDocPreferences()
|
||||
|
||||
const { lockedState, state } = await getFormState({
|
||||
apiRoute,
|
||||
body: {
|
||||
id,
|
||||
collectionSlug,
|
||||
docPreferences,
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
operation,
|
||||
returnLockStatus: isLockingEnabled ? true : false,
|
||||
schemaPath,
|
||||
updateLastEdited,
|
||||
},
|
||||
serverURL,
|
||||
id,
|
||||
collectionSlug,
|
||||
docPreferences,
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
operation,
|
||||
returnLockStatus: isLockingEnabled ? true : false,
|
||||
schemaPath: schemaPath ? schemaPath.split('.') : [],
|
||||
signal: abortController.signal,
|
||||
updateLastEdited,
|
||||
})
|
||||
|
||||
setDocumentIsLocked(true)
|
||||
@@ -202,8 +207,13 @@ const PreviewView: React.FC<Props> = ({
|
||||
const previousOwnerId = documentLockStateRef.current?.user?.id
|
||||
|
||||
if (lockedState) {
|
||||
if (!documentLockStateRef.current || lockedState.user.id !== previousOwnerId) {
|
||||
if (previousOwnerId === user.id && lockedState.user.id !== user.id) {
|
||||
const lockedUserID =
|
||||
typeof lockedState.user === 'string' || typeof lockedState.user === 'number'
|
||||
? lockedState.user
|
||||
: lockedState.user.id
|
||||
|
||||
if (!documentLockStateRef.current || lockedUserID !== previousOwnerId) {
|
||||
if (previousOwnerId === user.id && lockedUserID !== user.id) {
|
||||
setShowTakeOverModal(true)
|
||||
documentLockStateRef.current.hasShownLockedModal = true
|
||||
}
|
||||
@@ -211,9 +221,10 @@ const PreviewView: React.FC<Props> = ({
|
||||
documentLockStateRef.current = documentLockStateRef.current = {
|
||||
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
|
||||
isLocked: true,
|
||||
user: lockedState.user,
|
||||
user: lockedState.user as ClientUser,
|
||||
}
|
||||
setCurrentEditor(lockedState.user)
|
||||
|
||||
setCurrentEditor(lockedState.user as ClientUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,8 +234,6 @@ const PreviewView: React.FC<Props> = ({
|
||||
[
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
serverURL,
|
||||
apiRoute,
|
||||
id,
|
||||
isLockingEnabled,
|
||||
lastUpdateTime,
|
||||
@@ -234,12 +243,21 @@ const PreviewView: React.FC<Props> = ({
|
||||
setCurrentEditor,
|
||||
setDocumentIsLocked,
|
||||
user,
|
||||
getFormState,
|
||||
],
|
||||
)
|
||||
|
||||
// Clean up when the component unmounts or when the document is unlocked
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLockingEnabled) {
|
||||
return
|
||||
}
|
||||
@@ -404,7 +422,6 @@ const PreviewView: React.FC<Props> = ({
|
||||
fields={fields}
|
||||
forceSidebarWrap
|
||||
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
|
||||
schemaPath={collectionSlug || globalSlug}
|
||||
/>
|
||||
{AfterDocument}
|
||||
</div>
|
||||
@@ -445,11 +462,6 @@ export const LivePreviewClient: React.FC<{
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.livePreview?.actions
|
||||
}
|
||||
/>
|
||||
<LivePreviewProvider
|
||||
breakpoints={breakpoints}
|
||||
fieldSchema={collectionConfig?.fields || globalConfig?.fields}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { redirect } from 'next/navigation.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { Logo } from '../../elements/Logo/index.js'
|
||||
import { RenderServerComponent } from '../../../../ui/src/elements/RenderServerComponent/index.js'
|
||||
import './index.scss'
|
||||
import { LoginForm } from './LoginForm/index.js'
|
||||
|
||||
@@ -28,23 +28,6 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
routes: { admin },
|
||||
} = config
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedBeforeLogins = createMappedComponent(beforeLogin, undefined, undefined, 'beforeLogin')
|
||||
|
||||
const mappedAfterLogins = createMappedComponent(afterLogin, undefined, undefined, 'afterLogin')
|
||||
|
||||
if (user) {
|
||||
redirect(admin)
|
||||
}
|
||||
@@ -82,7 +65,19 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
<RenderComponent mappedComponent={mappedBeforeLogins} />
|
||||
<RenderServerComponent
|
||||
Component={beforeLogin}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
{!collectionConfig?.auth?.disableLocalStrategy && (
|
||||
<LoginForm
|
||||
prefillEmail={prefillEmail}
|
||||
@@ -91,7 +86,19 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
)}
|
||||
<RenderComponent mappedComponent={mappedAfterLogins} />
|
||||
<RenderServerComponent
|
||||
Component={afterLogin}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { AdminViewComponent, AdminViewProps, ImportMap, SanitizedConfig } from 'payload'
|
||||
import type {
|
||||
AdminViewComponent,
|
||||
AdminViewProps,
|
||||
CustomComponent,
|
||||
EditConfig,
|
||||
ImportMap,
|
||||
SanitizedConfig,
|
||||
} from 'payload'
|
||||
import type React from 'react'
|
||||
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
@@ -46,6 +53,20 @@ const oneSegmentViews: OneSegmentViews = {
|
||||
unauthorized: UnauthorizedView,
|
||||
}
|
||||
|
||||
function getViewActions({
|
||||
editConfig,
|
||||
viewKey,
|
||||
}: {
|
||||
editConfig: EditConfig
|
||||
viewKey: keyof EditConfig
|
||||
}): CustomComponent[] | undefined {
|
||||
if (viewKey in editConfig && 'actions' in editConfig[viewKey]) {
|
||||
return editConfig[viewKey].actions
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const getViewFromConfig = ({
|
||||
adminRoute,
|
||||
config,
|
||||
@@ -65,8 +86,10 @@ export const getViewFromConfig = ({
|
||||
}): {
|
||||
DefaultView: ViewFromConfig
|
||||
initPageOptions: Parameters<typeof initPage>[0]
|
||||
serverProps: Record<string, unknown>
|
||||
templateClassName: string
|
||||
templateType: 'default' | 'minimal'
|
||||
viewActions?: CustomComponent[]
|
||||
} => {
|
||||
let ViewToRender: ViewFromConfig = null
|
||||
let templateClassName: string
|
||||
@@ -79,10 +102,30 @@ export const getViewFromConfig = ({
|
||||
searchParams,
|
||||
}
|
||||
|
||||
const [segmentOne, segmentTwo] = segments
|
||||
const viewActions: CustomComponent[] = config?.admin?.components?.actions || []
|
||||
|
||||
const [segmentOne, segmentTwo, segmentThree, segmentFour, segmentFive] = segments
|
||||
|
||||
const isGlobal = segmentOne === 'globals'
|
||||
const isCollection = segmentOne === 'collections'
|
||||
let matchedCollection: SanitizedConfig['collections'][number] = undefined
|
||||
let matchedGlobal: SanitizedConfig['globals'][number] = undefined
|
||||
|
||||
let serverProps = {}
|
||||
|
||||
if (isCollection) {
|
||||
matchedCollection = config.collections.find(({ slug }) => slug === segmentTwo)
|
||||
serverProps = {
|
||||
collectionConfig: matchedCollection,
|
||||
}
|
||||
}
|
||||
|
||||
if (isGlobal) {
|
||||
matchedGlobal = config.globals.find(({ slug }) => slug === segmentTwo)
|
||||
serverProps = {
|
||||
globalConfig: matchedGlobal,
|
||||
}
|
||||
}
|
||||
|
||||
switch (segments.length) {
|
||||
case 0: {
|
||||
@@ -146,7 +189,7 @@ export const getViewFromConfig = ({
|
||||
templateType = 'minimal'
|
||||
}
|
||||
|
||||
if (isCollection) {
|
||||
if (isCollection && matchedCollection) {
|
||||
// --> /collections/:collectionSlug
|
||||
|
||||
ViewToRender = {
|
||||
@@ -155,7 +198,8 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = `${segmentTwo}-list`
|
||||
templateType = 'default'
|
||||
} else if (isGlobal) {
|
||||
viewActions.unshift(...(matchedCollection.admin.components?.views?.list?.actions || []))
|
||||
} else if (isGlobal && matchedGlobal) {
|
||||
// --> /globals/:globalSlug
|
||||
|
||||
ViewToRender = {
|
||||
@@ -176,13 +220,13 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = 'verify'
|
||||
templateType = 'minimal'
|
||||
} else if (isCollection) {
|
||||
} else if (isCollection && matchedCollection) {
|
||||
// Custom Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:versionId
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
@@ -190,7 +234,65 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = `collection-default-edit`
|
||||
templateType = 'default'
|
||||
} else if (isGlobal) {
|
||||
|
||||
// Adds view actions to the current collection view
|
||||
if (matchedCollection.admin?.components?.views?.edit) {
|
||||
if ('root' in matchedCollection.admin.components.views.edit) {
|
||||
viewActions.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
|
||||
// --> /globals/:globalSlug/versions
|
||||
// --> /globals/:globalSlug/preview
|
||||
@@ -203,6 +305,64 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = `global-edit`
|
||||
templateType = 'default'
|
||||
|
||||
// Adds view actions to the current global view
|
||||
if (matchedGlobal.admin?.components?.views?.edit) {
|
||||
if ('root' in matchedGlobal.admin.components.views.edit) {
|
||||
viewActions.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
|
||||
}
|
||||
@@ -214,7 +374,9 @@ export const getViewFromConfig = ({
|
||||
return {
|
||||
DefaultView: ViewToRender,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
templateClassName,
|
||||
templateType,
|
||||
viewActions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type { ImportMap, MappedComponent, SanitizedConfig } from 'payload'
|
||||
import type { ImportMap, SanitizedConfig } from 'payload'
|
||||
|
||||
import { formatAdminURL, getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { DefaultTemplate } from '../../templates/Default/index.js'
|
||||
import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
||||
import { getClientConfig } from '../../utilities/getClientConfig.js'
|
||||
import { initPage } from '../../utilities/initPage/index.js'
|
||||
import { getViewFromConfig } from './getViewFromConfig.js'
|
||||
|
||||
@@ -55,7 +57,14 @@ export const RootPage = async ({
|
||||
|
||||
const searchParams = await searchParamsPromise
|
||||
|
||||
const { DefaultView, initPageOptions, templateClassName, templateType } = getViewFromConfig({
|
||||
const {
|
||||
DefaultView,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
templateClassName,
|
||||
templateType,
|
||||
viewActions,
|
||||
} = getViewFromConfig({
|
||||
adminRoute,
|
||||
config,
|
||||
currentRoute,
|
||||
@@ -66,7 +75,7 @@ export const RootPage = async ({
|
||||
|
||||
let dbHasUser = false
|
||||
|
||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
||||
if (!DefaultView) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
@@ -102,27 +111,30 @@ export const RootPage = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const createMappedView = getCreateMappedComponent({
|
||||
importMap,
|
||||
serverProps: {
|
||||
i18n: initPageResult?.req.i18n,
|
||||
importMap,
|
||||
initPageResult,
|
||||
params,
|
||||
payload: initPageResult?.req.payload,
|
||||
searchParams,
|
||||
},
|
||||
const clientConfig = await getClientConfig({
|
||||
config,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
})
|
||||
|
||||
const MappedView: MappedComponent = createMappedView(
|
||||
DefaultView.payloadComponent,
|
||||
undefined,
|
||||
DefaultView.Component,
|
||||
'createMappedView',
|
||||
const RenderedView = (
|
||||
<RenderServerComponent
|
||||
clientProps={{ clientConfig }}
|
||||
Component={DefaultView.payloadComponent}
|
||||
Fallback={DefaultView.Component}
|
||||
importMap={importMap}
|
||||
serverProps={{
|
||||
...serverProps,
|
||||
clientConfig,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
importMap,
|
||||
initPageResult,
|
||||
params,
|
||||
payload: initPageResult?.req.payload,
|
||||
searchParams,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
const RenderedView = <RenderComponent mappedComponent={MappedView} />
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{!templateType && <Fragment>{RenderedView}</Fragment>}
|
||||
@@ -138,6 +150,7 @@ export const RootPage = async ({
|
||||
permissions={initPageResult?.permissions}
|
||||
searchParams={searchParams}
|
||||
user={initPageResult?.req.user}
|
||||
viewActions={viewActions}
|
||||
visibleEntities={{
|
||||
// The reason we are not passing in initPageResult.visibleEntities directly is due to a "Cannot assign to read only property of object '#<Object>" error introduced in React 19
|
||||
// which this caused as soon as initPageResult.visibleEntities is passed in
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
'use client'
|
||||
import type { ClientCollectionConfig, ClientGlobalConfig, OptionObject } from 'payload'
|
||||
|
||||
import {
|
||||
Gutter,
|
||||
SetViewActions,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
usePayloadAPI,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { Gutter, useConfig, useDocumentInfo, usePayloadAPI, useTranslation } from '@payloadcms/ui'
|
||||
import { formatDate } from '@payloadcms/ui/shared'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
@@ -80,11 +73,6 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
|
||||
|
||||
return (
|
||||
<main className={baseClass}>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.version?.actions
|
||||
}
|
||||
/>
|
||||
<SetStepNav
|
||||
collectionConfig={collectionConfig}
|
||||
collectionSlug={collectionSlug}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
LoadingOverlayToggle,
|
||||
Pagination,
|
||||
PerPage,
|
||||
SetViewActions,
|
||||
Table,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
@@ -41,11 +40,6 @@ export const VersionsViewClient: React.FC<{
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.versions?.actions
|
||||
}
|
||||
/>
|
||||
<LoadingOverlayToggle name="versions" show={!data} />
|
||||
{versionCount === 0 && (
|
||||
<div className={`${baseClass}__no-versions`}>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { EditViewComponent, PaginatedDocs, PayloadServerReactComponent } from 'payload'
|
||||
|
||||
import { Gutter, ListQueryProvider } from '@payloadcms/ui'
|
||||
import { Gutter, ListQueryProvider, SetDocumentStepNav } from '@payloadcms/ui'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import { isNumber } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||
import { buildVersionColumns } from './buildColumns.js'
|
||||
import { getLatestVersion } from './getLatestVersion.js'
|
||||
import { VersionsViewClient } from './index.client.js'
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/withPayload.js" /* Include the withPayload.js file in the build */
|
||||
],
|
||||
, "../ui/src/utilities/renderFields.tsx" ],
|
||||
"references": [
|
||||
{ "path": "../payload" },
|
||||
{ "path": "../ui" },
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { JSONSchema4 } from 'json-schema'
|
||||
import type { ImportMap } from '../bin/generateImportMap/index.js'
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||
import type { Config, PayloadComponent, SanitizedConfig } from '../config/types.js'
|
||||
import type { ValidationFieldError } from '../errors/ValidationError.js'
|
||||
import type {
|
||||
Field,
|
||||
FieldAffectingData,
|
||||
RichTextField,
|
||||
RichTextFieldClient,
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { JsonObject, Payload, PayloadRequest, RequestContext } from '../types/index.js'
|
||||
import type { RichTextFieldClientProps } from './fields/RichText.js'
|
||||
import type { CreateMappedComponent } from './types.js'
|
||||
import type { CreateMappedComponent, FieldSchemaMap } from './types.js'
|
||||
|
||||
export type AfterReadRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
@@ -89,7 +89,7 @@ export type BeforeChangeRichTextHookArgs<
|
||||
|
||||
duplicate?: boolean
|
||||
|
||||
errors?: { field: string; message: string }[]
|
||||
errors?: ValidationFieldError[]
|
||||
/** Only available in `beforeChange` field hooks */
|
||||
mergeLocaleActions?: (() => Promise<void>)[]
|
||||
/** A string relating to which operation the field type is currently executing within. */
|
||||
@@ -205,9 +205,9 @@ type RichTextAdapterBase<
|
||||
config: SanitizedConfig
|
||||
field: RichTextField
|
||||
i18n: I18n<any, any>
|
||||
schemaMap: Map<string, Field[]>
|
||||
schemaPath: string
|
||||
}) => Map<string, Field[]>
|
||||
schemaMap: FieldSchemaMap
|
||||
schemaPath: string[]
|
||||
}) => 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.
|
||||
*
|
||||
|
||||
@@ -3,8 +3,15 @@ import type { ClientField } from '../../fields/config/client.js'
|
||||
|
||||
export type RowData = Record<string, any>
|
||||
|
||||
export type CellComponentProps<TField extends ClientField = ClientField> = {
|
||||
export type DefaultCellComponentProps<TCellData = any, TField extends ClientField = ClientField> = {
|
||||
readonly cellData: TCellData
|
||||
// readonly cellProps?: Partial<CellComponentProps>
|
||||
readonly className?: string
|
||||
readonly columnIndex?: number
|
||||
readonly customCellContext?: {
|
||||
collectionSlug?: SanitizedCollectionConfig['slug']
|
||||
uploadConfig?: SanitizedCollectionConfig['upload']
|
||||
}
|
||||
readonly field: TField
|
||||
readonly link?: boolean
|
||||
readonly onClick?: (args: {
|
||||
@@ -12,13 +19,5 @@ export type CellComponentProps<TField extends ClientField = ClientField> = {
|
||||
collectionSlug: SanitizedCollectionConfig['slug']
|
||||
rowData: RowData
|
||||
}) => void
|
||||
}
|
||||
|
||||
export type DefaultCellComponentProps<TCellData = any, TField extends ClientField = ClientField> = {
|
||||
readonly cellData: TCellData
|
||||
readonly customCellContext?: {
|
||||
collectionSlug?: SanitizedCollectionConfig['slug']
|
||||
uploadConfig?: SanitizedCollectionConfig['upload']
|
||||
}
|
||||
readonly rowData: RowData
|
||||
} & CellComponentProps<TField>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MarkOptional } from 'ts-essentials'
|
||||
|
||||
import type { ArrayField, ArrayFieldClient } from '../../fields/config/types.js'
|
||||
import type { ArrayField, ArrayFieldClient, ClientField } from '../../fields/config/types.js'
|
||||
import type { ArrayFieldValidation } from '../../fields/validations.js'
|
||||
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
|
||||
import type {
|
||||
@@ -14,13 +14,13 @@ import type {
|
||||
FieldDescriptionServerComponent,
|
||||
FieldLabelClientComponent,
|
||||
FieldLabelServerComponent,
|
||||
MappedComponent,
|
||||
} from '../types.js'
|
||||
|
||||
type ArrayFieldClientWithoutType = MarkOptional<ArrayFieldClient, 'type'>
|
||||
|
||||
type ArrayFieldBaseClientProps = {
|
||||
readonly CustomRowLabel?: MappedComponent
|
||||
readonly CustomRowLabel?: React.ReactNode
|
||||
readonly path?: string
|
||||
readonly validate?: ArrayFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MarkOptional } from 'ts-essentials'
|
||||
|
||||
import type { BlocksField, BlocksFieldClient } from '../../fields/config/types.js'
|
||||
import type { BlocksField, BlocksFieldClient, ClientField } from '../../fields/config/types.js'
|
||||
import type { BlocksFieldValidation } from '../../fields/validations.js'
|
||||
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
|
||||
import type {
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type BlocksFieldClientWithoutType = MarkOptional<BlocksFieldClient, 'type'>
|
||||
|
||||
type BlocksFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: BlocksFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ type CheckboxFieldBaseClientProps = {
|
||||
readonly id?: string
|
||||
readonly onChange?: (value: boolean) => void
|
||||
readonly partialChecked?: boolean
|
||||
readonly path?: string
|
||||
readonly validate?: CheckboxFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ type CodeFieldClientWithoutType = MarkOptional<CodeFieldClient, 'type'>
|
||||
|
||||
type CodeFieldBaseClientProps = {
|
||||
readonly autoComplete?: string
|
||||
readonly valiCode?: CodeFieldValidation
|
||||
readonly path?: string
|
||||
readonly validate?: CodeFieldValidation
|
||||
}
|
||||
|
||||
export type CodeFieldClientProps = ClientFieldBase<CodeFieldClientWithoutType> &
|
||||
|
||||
@@ -15,9 +15,14 @@ import type {
|
||||
FieldLabelServerComponent,
|
||||
} from '../types.js'
|
||||
|
||||
type CollapsibleFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
}
|
||||
|
||||
type CollapsibleFieldClientWithoutType = MarkOptional<CollapsibleFieldClient, 'type'>
|
||||
|
||||
export type CollapsibleFieldClientProps = ClientFieldBase<CollapsibleFieldClientWithoutType>
|
||||
export type CollapsibleFieldClientProps = ClientFieldBase<CollapsibleFieldClientWithoutType> &
|
||||
CollapsibleFieldBaseClientProps
|
||||
|
||||
export type CollapsibleFieldServerProps = ServerFieldBase<
|
||||
CollapsibleField,
|
||||
@@ -29,8 +34,10 @@ export type CollapsibleFieldServerComponent = FieldServerComponent<
|
||||
CollapsibleFieldClientWithoutType
|
||||
>
|
||||
|
||||
export type CollapsibleFieldClientComponent =
|
||||
FieldClientComponent<CollapsibleFieldClientWithoutType>
|
||||
export type CollapsibleFieldClientComponent = FieldClientComponent<
|
||||
CollapsibleFieldClientWithoutType,
|
||||
CollapsibleFieldBaseClientProps
|
||||
>
|
||||
|
||||
export type CollapsibleFieldLabelServerComponent = FieldLabelServerComponent<
|
||||
CollapsibleField,
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type DateFieldClientWithoutType = MarkOptional<DateFieldClient, 'type'>
|
||||
|
||||
type DateFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: DateFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ type EmailFieldClientWithoutType = MarkOptional<EmailFieldClient, 'type'>
|
||||
|
||||
type EmailFieldBaseClientProps = {
|
||||
readonly autoComplete?: string
|
||||
readonly path?: string
|
||||
readonly validate?: EmailFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,12 @@ import type {
|
||||
|
||||
type GroupFieldClientWithoutType = MarkOptional<GroupFieldClient, 'type'>
|
||||
|
||||
export type GroupFieldClientProps = ClientFieldBase<GroupFieldClientWithoutType>
|
||||
export type GroupFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
}
|
||||
|
||||
export type GroupFieldClientProps = ClientFieldBase<GroupFieldClientWithoutType> &
|
||||
GroupFieldBaseClientProps
|
||||
|
||||
export type GroupFieldServerProps = ServerFieldBase<GroupField, GroupFieldClientWithoutType>
|
||||
|
||||
@@ -26,7 +31,10 @@ export type GroupFieldServerComponent = FieldServerComponent<
|
||||
GroupFieldClientWithoutType
|
||||
>
|
||||
|
||||
export type GroupFieldClientComponent = FieldClientComponent<GroupFieldClientWithoutType>
|
||||
export type GroupFieldClientComponent = FieldClientComponent<
|
||||
GroupFieldClientWithoutType,
|
||||
GroupFieldBaseClientProps
|
||||
>
|
||||
|
||||
export type GroupFieldLabelServerComponent = FieldLabelServerComponent<
|
||||
GroupField,
|
||||
|
||||
@@ -5,7 +5,7 @@ export type HiddenFieldProps = {
|
||||
readonly disableModifyingForm?: false
|
||||
readonly field?: {
|
||||
readonly name?: string
|
||||
} & Pick<ClientField, '_path'>
|
||||
} & ClientField
|
||||
readonly forceUsePathFromProps?: boolean
|
||||
readonly value?: unknown
|
||||
} & FormFieldBase
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type JSONFieldClientWithoutType = MarkOptional<JSONFieldClient, 'type'>
|
||||
|
||||
type JSONFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: JSONFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ import type {
|
||||
|
||||
type JoinFieldClientWithoutType = MarkOptional<JoinFieldClient, 'type'>
|
||||
|
||||
export type JoinFieldClientProps = ClientFieldBase<JoinFieldClientWithoutType>
|
||||
export type JoinFieldClientProps = {
|
||||
path?: string
|
||||
} & ClientFieldBase<JoinFieldClientWithoutType>
|
||||
|
||||
export type JoinFieldServerProps = ServerFieldBase<JoinField>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ type NumberFieldClientWithoutType = MarkOptional<NumberFieldClient, 'type'>
|
||||
|
||||
type NumberFieldBaseClientProps = {
|
||||
readonly onChange?: (e: number) => void
|
||||
readonly path?: string
|
||||
readonly validate?: NumberFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type PointFieldClientWithoutType = MarkOptional<PointFieldClient, 'type'>
|
||||
|
||||
type PointFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: PointFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ type RadioFieldBaseClientProps = {
|
||||
*/
|
||||
readonly disableModifyingForm?: boolean
|
||||
readonly onChange?: OnChange
|
||||
readonly path?: string
|
||||
readonly validate?: RadioFieldValidation
|
||||
readonly value?: string
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type RelationshipFieldClientWithoutType = MarkOptional<RelationshipFieldClient, 'type'>
|
||||
|
||||
type RelationshipFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: RelationshipFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ type RichTextFieldBaseClientProps<
|
||||
TAdapterProps = any,
|
||||
TExtraProperties = object,
|
||||
> = {
|
||||
readonly path?: string
|
||||
readonly validate?: RichTextFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ type RowFieldClientWithoutType = MarkOptional<RowFieldClient, 'type'>
|
||||
|
||||
type RowFieldBaseClientProps = {
|
||||
readonly forceRender?: boolean
|
||||
readonly indexPath: string
|
||||
}
|
||||
|
||||
export type RowFieldClientProps = ClientFieldBase<RowFieldClientWithoutType> &
|
||||
|
||||
@@ -20,6 +20,7 @@ type SelectFieldClientWithoutType = MarkOptional<SelectFieldClient, 'type'>
|
||||
|
||||
type SelectFieldBaseClientProps = {
|
||||
readonly onChange?: (e: string | string[]) => void
|
||||
readonly path?: string
|
||||
readonly validate?: SelectFieldValidation
|
||||
readonly value?: string
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import type {
|
||||
} from '../types.js'
|
||||
|
||||
export type ClientTab =
|
||||
| ({ fields: ClientField[] } & Omit<NamedTab, 'fields'>)
|
||||
| ({ fields: ClientField[] } & Omit<UnnamedTab, 'fields'>)
|
||||
| ({ fields: ClientField[]; readonly path?: string } & Omit<NamedTab, 'fields'>)
|
||||
|
||||
type TabsFieldClientWithoutType = MarkOptional<TabsFieldClient, 'type'>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ type TextFieldClientWithoutType = MarkOptional<TextFieldClient, 'type'>
|
||||
type TextFieldBaseClientProps = {
|
||||
readonly inputRef?: React.RefObject<HTMLInputElement>
|
||||
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
||||
readonly path?: string
|
||||
readonly validate?: TextFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ type TextareaFieldClientWithoutType = MarkOptional<TextareaFieldClient, 'type'>
|
||||
type TextareaFieldBaseClientProps = {
|
||||
readonly inputRef?: React.Ref<HTMLInputElement>
|
||||
readonly onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
||||
readonly path?: string
|
||||
readonly validate?: TextareaFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type UploadFieldClientWithoutType = MarkOptional<UploadFieldClient, 'type'>
|
||||
|
||||
type UploadFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: UploadFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LabelFunction, ServerProps } from '../../config/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { MappedComponent } from '../types.js'
|
||||
import type { ClientFieldWithOptionalType } from './Field.js'
|
||||
|
||||
export type DescriptionFunction = LabelFunction
|
||||
@@ -20,9 +19,9 @@ export type Description = DescriptionFunction | StaticDescription
|
||||
|
||||
export type GenericDescriptionProps = {
|
||||
readonly className?: string
|
||||
readonly Description?: MappedComponent
|
||||
readonly description?: StaticDescription
|
||||
readonly marginPlacement?: 'bottom' | 'top'
|
||||
readonly path: string
|
||||
}
|
||||
|
||||
export type FieldDescriptionServerProps<
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { ServerProps } from '../../config/types.js'
|
||||
import type { Field } from '../../fields/config/types.js'
|
||||
import type { MappedComponent } from '../types.js'
|
||||
import type { ClientFieldWithOptionalType } from './Field.js'
|
||||
|
||||
export type GenericErrorProps = {
|
||||
readonly alignCaret?: 'center' | 'left' | 'right'
|
||||
readonly CustomError?: MappedComponent
|
||||
readonly message?: string
|
||||
readonly path?: string
|
||||
readonly showError?: boolean
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { MarkOptional } from 'ts-essentials'
|
||||
|
||||
import type { User } from '../../auth/types.js'
|
||||
import type { Locale, ServerProps } from '../../config/types.js'
|
||||
import type { ClientField, Field, Validate } from '../../fields/config/types.js'
|
||||
import type { DocumentPreferences } from '../../preferences/types.js'
|
||||
import type { FieldDescriptionClientProps, FieldDescriptionServerProps } from './Description.js'
|
||||
import type { FieldErrorClientProps, FieldErrorServerProps } from './Error.js'
|
||||
import type { FieldLabelClientProps, FieldLabelServerProps } from './Label.js'
|
||||
import type { FieldPermissions } from '../../auth/types.js'
|
||||
import type { SanitizedConfig } from '../../config/types.js'
|
||||
import type { ClientBlock, ClientField, Field } from '../../fields/config/types.js'
|
||||
import type { Payload } from '../../types/index.js'
|
||||
import type { ClientTab, FormField, RenderedField } from '../types.js'
|
||||
|
||||
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<
|
||||
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
||||
> = {
|
||||
readonly descriptionProps?: FieldDescriptionClientProps<TFieldClient>
|
||||
readonly errorProps?: FieldErrorClientProps<TFieldClient>
|
||||
readonly field: TFieldClient
|
||||
readonly labelProps?: FieldLabelClientProps<TFieldClient>
|
||||
} & FormFieldBase
|
||||
} & Omit<ClientComponentProps, 'field'>
|
||||
|
||||
export type ServerFieldBase<
|
||||
TFieldServer extends Field = Field,
|
||||
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
||||
> = {
|
||||
readonly clientField: TFieldClient
|
||||
readonly descriptionProps?: FieldDescriptionServerProps<TFieldServer, TFieldClient>
|
||||
readonly errorProps?: FieldErrorServerProps<TFieldServer, TFieldClient>
|
||||
readonly field: TFieldServer
|
||||
readonly labelProps?: FieldLabelServerProps<TFieldServer, TFieldClient>
|
||||
} & FormFieldBase &
|
||||
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
|
||||
}
|
||||
} & Omit<ClientComponentProps, 'field'> &
|
||||
Omit<ServerComponentProps, 'clientField' | 'field'>
|
||||
|
||||
export type FieldClientComponent<
|
||||
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Field, Validate } from '../../fields/config/types.js'
|
||||
import type { Where } from '../../types/index.js'
|
||||
import { type SupportedLanguages } from '@payloadcms/translations'
|
||||
|
||||
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 = {
|
||||
[key: string]: any
|
||||
@@ -15,20 +18,71 @@ export type FilterOptionsResult = {
|
||||
[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
|
||||
errorMessage?: string
|
||||
errorPaths?: string[]
|
||||
fieldSchema?: Field
|
||||
filterOptions?: FilterOptionsResult
|
||||
initialValue: unknown
|
||||
isSidebar?: boolean
|
||||
passesCondition?: boolean
|
||||
rows?: Row[]
|
||||
schemaPath: string[]
|
||||
valid: boolean
|
||||
validate?: Validate
|
||||
value: unknown
|
||||
}
|
||||
|
||||
export type FieldStateWithoutComponents = Omit<FieldState, 'customComponents'>
|
||||
|
||||
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 { Field } from '../../fields/config/types.js'
|
||||
import type { MappedComponent } from '../types.js'
|
||||
import type { ClientFieldWithOptionalType } from './Field.js'
|
||||
|
||||
export type GenericLabelProps = {
|
||||
readonly as?: 'label' | 'span'
|
||||
readonly hideLocale?: boolean
|
||||
readonly htmlFor?: string
|
||||
readonly Label?: MappedComponent
|
||||
readonly label?: StaticLabel
|
||||
readonly localized?: boolean
|
||||
readonly path?: string
|
||||
readonly required?: 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 { 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 {
|
||||
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 { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js'
|
||||
export type { CustomPreviewButton } from './elements/PreviewButton.js'
|
||||
@@ -318,9 +333,26 @@ export type {
|
||||
GenericErrorProps,
|
||||
} 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 {
|
||||
FieldLabelClientComponent,
|
||||
@@ -333,25 +365,19 @@ export type {
|
||||
|
||||
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 {
|
||||
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> = {
|
||||
Component?: React.ComponentType<TComponentClientProps>
|
||||
props?: Partial<any>
|
||||
@@ -370,30 +396,91 @@ export type MappedEmptyComponent = {
|
||||
type: 'empty'
|
||||
}
|
||||
|
||||
export type MappedComponent<TComponentClientProps extends JsonObject = JsonObject> =
|
||||
| MappedClientComponent<TComponentClientProps>
|
||||
| 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 enum Action {
|
||||
RenderConfig = 'render-config',
|
||||
}
|
||||
|
||||
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 { PayloadRequest } from '../../types/index.js'
|
||||
import type { LanguageOptions } from '../LanguageOptions.js'
|
||||
import type { MappedComponent } from '../types.js'
|
||||
import type { Data, DocumentSlots, PayloadServerAction } from '../types.js'
|
||||
|
||||
export type AdminViewConfig = {
|
||||
Component: AdminViewComponent
|
||||
@@ -20,17 +20,17 @@ export type AdminViewConfig = {
|
||||
strict?: boolean
|
||||
}
|
||||
|
||||
export type MappedView = {
|
||||
actions?: MappedComponent[]
|
||||
Component: MappedComponent
|
||||
}
|
||||
|
||||
export type AdminViewProps = {
|
||||
readonly clientConfig: ClientConfig
|
||||
readonly disableActions?: boolean
|
||||
readonly drawerSlug?: string
|
||||
readonly importMap: ImportMap
|
||||
readonly initialData?: Data
|
||||
readonly initPageResult: InitPageResult
|
||||
readonly params?: { [key: string]: string | string[] | undefined }
|
||||
readonly searchParams: { [key: string]: string | string[] | undefined }
|
||||
readonly redirectAfterDelete?: boolean
|
||||
readonly redirectAfterDuplicate?: boolean
|
||||
}
|
||||
|
||||
export type AdminViewComponent = PayloadComponent<AdminViewProps>
|
||||
@@ -62,6 +62,9 @@ export type InitPageResult = {
|
||||
export type ServerSideEditViewProps = {
|
||||
readonly initPageResult: InitPageResult
|
||||
readonly params: { [key: string]: string | string[] | undefined }
|
||||
readonly payloadServerAction: PayloadServerAction
|
||||
readonly routeSegments: string[]
|
||||
readonly searchParams: { [key: string]: string | string[] | undefined }
|
||||
}
|
||||
} & ClientSideEditViewProps
|
||||
|
||||
export type ClientSideEditViewProps = {} & DocumentSlots
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { Payload } from '../../../index.js'
|
||||
import type { PayloadRequest } from '../../../types/index.js'
|
||||
import type { AuthArgs, AuthResult } from '../auth.js'
|
||||
|
||||
import { createLocalReq } from '../../../utilities/createLocalReq.js'
|
||||
import { auth as authOperation } from '../auth.js'
|
||||
|
||||
export const auth = async (payload: Payload, options: AuthArgs): Promise<AuthResult> => {
|
||||
const { headers } = options
|
||||
const { headers, req } = options
|
||||
|
||||
return await authOperation({
|
||||
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'
|
||||
|
||||
export function parsePayloadComponent(payloadComponent: PayloadComponent): {
|
||||
export function parsePayloadComponent(PayloadComponent: PayloadComponent): {
|
||||
exportName: string
|
||||
path: string
|
||||
} {
|
||||
if (!payloadComponent) {
|
||||
if (!PayloadComponent) {
|
||||
return null
|
||||
}
|
||||
const pathAndMaybeExport =
|
||||
typeof payloadComponent === 'string' ? payloadComponent : payloadComponent.path
|
||||
typeof PayloadComponent === 'string' ? PayloadComponent : PayloadComponent.path
|
||||
|
||||
let path = ''
|
||||
let exportName = 'default'
|
||||
@@ -19,8 +19,8 @@ export function parsePayloadComponent(payloadComponent: PayloadComponent): {
|
||||
path = pathAndMaybeExport
|
||||
}
|
||||
|
||||
if (typeof payloadComponent === 'object' && payloadComponent.exportName) {
|
||||
exportName = payloadComponent.exportName
|
||||
if (typeof PayloadComponent === 'object' && PayloadComponent.exportName) {
|
||||
exportName = PayloadComponent.exportName
|
||||
}
|
||||
|
||||
return { exportName, path }
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import type { MappedComponent, StaticDescription } from '../../admin/types.js'
|
||||
import type { MappedView } from '../../admin/views/types.js'
|
||||
import type { LivePreviewConfig, ServerOnlyLivePreviewProperties } from '../../config/types.js'
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
|
||||
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 { Payload } from '../../types/index.js'
|
||||
import type { SanitizedCollectionConfig } from './types.js'
|
||||
|
||||
import { createClientFields } from '../../fields/config/client.js'
|
||||
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
|
||||
|
||||
export type ServerOnlyCollectionProperties = keyof Pick<
|
||||
SanitizedCollectionConfig,
|
||||
'access' | 'custom' | 'endpoints' | 'hooks' | 'joins'
|
||||
@@ -26,34 +35,7 @@ export type ServerOnlyUploadProperties = keyof Pick<
|
||||
export type ClientCollectionConfig = {
|
||||
_isPreviewEnabled?: true
|
||||
admin: {
|
||||
components: {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
components: null
|
||||
description?: StaticDescription
|
||||
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
||||
} & Omit<
|
||||
@@ -61,4 +43,156 @@ export type ClientCollectionConfig = {
|
||||
'components' | 'description' | 'joins' | 'livePreview' | ServerOnlyCollectionAdminProperties
|
||||
>
|
||||
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 { ClientCollectionConfig } from '../collections/config/client.js'
|
||||
import type { ClientGlobalConfig } from '../globals/config/client.js'
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
|
||||
import type {
|
||||
LivePreviewConfig,
|
||||
SanitizedConfig,
|
||||
ServerOnlyLivePreviewProperties,
|
||||
} 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<
|
||||
SanitizedConfig,
|
||||
| 'bin'
|
||||
@@ -27,20 +33,15 @@ export type ServerOnlyRootProperties = keyof Pick<
|
||||
| 'typescript'
|
||||
>
|
||||
|
||||
export type ServerOnlyRootAdminProperties = keyof Pick<SanitizedConfig['admin'], 'components'>
|
||||
export type ServerOnlyRootAdminProperties = keyof Pick<
|
||||
SanitizedConfig['admin'],
|
||||
'components' | 'serverFunctions'
|
||||
>
|
||||
|
||||
export type ClientConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
actions?: MappedComponent[]
|
||||
Avatar: MappedComponent
|
||||
graphics: {
|
||||
Icon: MappedComponent
|
||||
Logo: MappedComponent
|
||||
}
|
||||
LogoutButton?: MappedComponent
|
||||
}
|
||||
dependencies?: Record<string, MappedComponent>
|
||||
components: null
|
||||
dependencies?: Record<string, React.ReactNode>
|
||||
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
|
||||
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
|
||||
collections: ClientCollectionConfig[]
|
||||
@@ -48,6 +49,10 @@ export type ClientConfig = {
|
||||
globals: ClientGlobalConfig[]
|
||||
} & Omit<SanitizedConfig, 'admin' | 'collections' | 'globals' | ServerOnlyRootProperties>
|
||||
|
||||
export const serverOnlyAdminConfigProperties: readonly Partial<ServerOnlyRootAdminProperties>[] = [
|
||||
'serverFunctions',
|
||||
]
|
||||
|
||||
export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperties>[] = [
|
||||
'endpoints',
|
||||
'db',
|
||||
@@ -64,6 +69,57 @@ export const serverOnlyConfigProperties: readonly Partial<ServerOnlyRootProperti
|
||||
'email',
|
||||
'custom',
|
||||
'graphQL',
|
||||
'logger'
|
||||
'logger',
|
||||
// `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 { RichTextAdapterProvider } from '../admin/RichText.js'
|
||||
import type { DocumentTabConfig, RichTextAdapter } from '../admin/types.js'
|
||||
import type { AdminViewConfig, ServerSideEditViewProps } from '../admin/views/types.js'
|
||||
import type { DocumentTabConfig, RichTextAdapter, ServerFunctionConfig } from '../admin/types.js'
|
||||
import type {
|
||||
AdminViewConfig,
|
||||
ServerSideEditViewProps,
|
||||
VisibleEntities,
|
||||
} from '../admin/views/types.js'
|
||||
import type { Permissions } from '../auth/index.js'
|
||||
import type {
|
||||
AddToImportMap,
|
||||
@@ -384,16 +388,20 @@ export type EditViewConfig = {
|
||||
}
|
||||
)
|
||||
|
||||
type ClientProps = {
|
||||
readonly [key: string]: unknown
|
||||
}
|
||||
|
||||
export type ServerProps = {
|
||||
readonly i18n: I18nClient
|
||||
readonly locale?: Locale
|
||||
readonly params?: { [key: string]: string | string[] | undefined }
|
||||
readonly payload: Payload
|
||||
readonly permissions?: Permissions
|
||||
readonly [key: string]: unknown
|
||||
readonly searchParams?: { [key: string]: string | string[] | undefined }
|
||||
readonly user?: TypedUser
|
||||
}
|
||||
readonly visibleEntities?: VisibleEntities
|
||||
} & ClientProps
|
||||
|
||||
export const serverProps: (keyof ServerProps)[] = [
|
||||
'payload',
|
||||
@@ -818,6 +826,7 @@ export type Config = {
|
||||
/** The route for the unauthorized page. */
|
||||
unauthorized?: string
|
||||
}
|
||||
serverFunctions?: ServerFunctionConfig[]
|
||||
/**
|
||||
* 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'
|
||||
>
|
||||
|
||||
export type EditConfig =
|
||||
| {
|
||||
[key: string]: EditViewConfig
|
||||
/**
|
||||
* Replace or modify individual nested routes, or add new ones:
|
||||
* + `default` - `/admin/collections/:collection/:id`
|
||||
* + `api` - `/admin/collections/:collection/:id/api`
|
||||
* + `livePreview` - `/admin/collections/:collection/:id/preview`
|
||||
* + `references` - `/admin/collections/:collection/:id/references`
|
||||
* + `relationships` - `/admin/collections/:collection/:id/relationships`
|
||||
* + `versions` - `/admin/collections/:collection/:id/versions`
|
||||
* + `version` - `/admin/collections/:collection/:id/versions/:version`
|
||||
* + `customView` - `/admin/collections/:collection/:id/:path`
|
||||
*
|
||||
* To override the entire Edit View including all nested views, use the `root` key.
|
||||
*/
|
||||
api?: Partial<EditViewConfig>
|
||||
default?: Partial<EditViewConfig>
|
||||
livePreview?: Partial<EditViewConfig>
|
||||
root?: never
|
||||
version?: Partial<EditViewConfig>
|
||||
versions?: Partial<EditViewConfig>
|
||||
// TODO: uncomment these as they are built
|
||||
// references?: EditView
|
||||
// relationships?: EditView
|
||||
}
|
||||
| {
|
||||
api?: never
|
||||
default?: never
|
||||
livePreview?: never
|
||||
/**
|
||||
* 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.
|
||||
* + `root` - `/admin/collections/:collection/:id/**\/*`
|
||||
*/
|
||||
root: Partial<EditViewConfig>
|
||||
version?: never
|
||||
versions?: never
|
||||
}
|
||||
export type EditConfig = EditConfigWithoutRoot | EditConfigWithRoot
|
||||
|
||||
export type EditConfigWithRoot = {
|
||||
api?: never
|
||||
default?: never
|
||||
livePreview?: never
|
||||
/**
|
||||
* 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.
|
||||
* + `root` - `/admin/collections/:collection/:id/**\/*`
|
||||
*/
|
||||
root: Partial<EditViewConfig>
|
||||
version?: never
|
||||
versions?: never
|
||||
}
|
||||
|
||||
export type EditConfigWithoutRoot = {
|
||||
[key: string]: EditViewConfig
|
||||
/**
|
||||
* Replace or modify individual nested routes, or add new ones:
|
||||
* + `default` - `/admin/collections/:collection/:id`
|
||||
* + `api` - `/admin/collections/:collection/:id/api`
|
||||
* + `livePreview` - `/admin/collections/:collection/:id/preview`
|
||||
* + `references` - `/admin/collections/:collection/:id/references`
|
||||
* + `relationships` - `/admin/collections/:collection/:id/relationships`
|
||||
* + `versions` - `/admin/collections/:collection/:id/versions`
|
||||
* + `version` - `/admin/collections/:collection/:id/versions/:version`
|
||||
* + `customView` - `/admin/collections/:collection/:id/:path`
|
||||
*
|
||||
* To override the entire Edit View including all nested views, use the `root` key.
|
||||
*/
|
||||
api?: Partial<EditViewConfig>
|
||||
default?: Partial<EditViewConfig>
|
||||
livePreview?: Partial<EditViewConfig>
|
||||
root?: never
|
||||
version?: Partial<EditViewConfig>
|
||||
versions?: Partial<EditViewConfig>
|
||||
}
|
||||
|
||||
export type EntityDescriptionComponent = CustomComponent
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import { APIError } from './APIError.js'
|
||||
export let ValidationErrorName = 'ValidationError'
|
||||
|
||||
export type ValidationFieldError = {
|
||||
// The field path, i.e. "textField", "groupField.subTextField", etc.
|
||||
field: string
|
||||
fieldPath: (number | string)[]
|
||||
fieldSchemaPath: string[]
|
||||
// The error message to display for this field
|
||||
message: string
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export class ValidationError extends APIError<{
|
||||
: en.translations.error.followingFieldsInvalid_other
|
||||
|
||||
super(
|
||||
`${message} ${results.errors.map((f) => f.field).join(', ')}`,
|
||||
`${message} ${results.errors.map((f) => f.fieldPath.join('.')).join(', ')}`,
|
||||
httpStatus.BAD_REQUEST,
|
||||
results,
|
||||
)
|
||||
|
||||
@@ -28,6 +28,9 @@ export {
|
||||
tabHasName,
|
||||
valueIsValueWithRelation,
|
||||
} from '../fields/config/types.js'
|
||||
|
||||
export { getFieldPaths } from '../fields/getFieldPaths.js'
|
||||
|
||||
export * from '../fields/validations.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
|
||||
// to accidentally use it instead of ClientField and get confused
|
||||
@@ -16,3 +36,332 @@ export type ServerOnlyFieldProperties =
|
||||
| keyof Pick<FieldBase, 'access' | 'custom' | 'defaultValue' | 'hooks'>
|
||||
|
||||
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 { baseBlockFields } from '../baseFields/baseBlockFields.js'
|
||||
import { baseIDField } from '../baseFields/baseIDField.js'
|
||||
import { getFieldPaths } from '../getFieldPaths.js'
|
||||
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
|
||||
import validations from '../validations.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.
|
||||
*/
|
||||
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.
|
||||
* This validation will be skipped if validRelationships is null.
|
||||
@@ -64,7 +65,7 @@ export const sanitizeFields = async ({
|
||||
parentIsLocalized,
|
||||
requireFieldLevelRichTextEditor = false,
|
||||
richTextSanitizationPromises,
|
||||
schemaPath = '',
|
||||
schemaPath = [],
|
||||
validRelationships,
|
||||
}: Args): Promise<Field[]> => {
|
||||
if (!fields) {
|
||||
@@ -260,10 +261,12 @@ export const sanitizeFields = async ({
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
schemaPath: generateSchemaPath({
|
||||
name: 'name' in field ? field.name : undefined,
|
||||
path: schemaPath,
|
||||
}),
|
||||
schemaPath: getFieldPaths({
|
||||
field,
|
||||
parentPath: [],
|
||||
parentSchemaPath: schemaPath,
|
||||
schemaIndex: i,
|
||||
}).schemaPath,
|
||||
validRelationships,
|
||||
})
|
||||
}
|
||||
@@ -271,10 +274,8 @@ export const sanitizeFields = async ({
|
||||
if (field.type === 'tabs') {
|
||||
for (let j = 0; j < field.tabs.length; j++) {
|
||||
const tab = field.tabs[j]
|
||||
if (tabHasName(tab)) {
|
||||
if (typeof tab.label === 'undefined') {
|
||||
tab.label = toWords(tab.name)
|
||||
}
|
||||
if (tabHasName(tab) && typeof tab.label === 'undefined') {
|
||||
tab.label = toWords(tab.name)
|
||||
}
|
||||
|
||||
tab.fields = await sanitizeFields({
|
||||
@@ -285,10 +286,15 @@ export const sanitizeFields = async ({
|
||||
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
schemaPath: generateSchemaPath({
|
||||
name: 'name' in tab ? tab.name : undefined,
|
||||
path: schemaPath,
|
||||
}),
|
||||
schemaPath: getFieldPaths({
|
||||
field: {
|
||||
...tab,
|
||||
type: 'tab',
|
||||
},
|
||||
parentPath: [],
|
||||
parentSchemaPath: schemaPath,
|
||||
schemaIndex: j,
|
||||
}).schemaPath,
|
||||
validRelationships,
|
||||
})
|
||||
field.tabs[j] = tab
|
||||
|
||||
@@ -14,7 +14,7 @@ export const sanitizeJoinField = ({
|
||||
config: Config
|
||||
field: JoinField
|
||||
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
|
||||
if (typeof joins === 'undefined') {
|
||||
@@ -25,7 +25,7 @@ export const sanitizeJoinField = ({
|
||||
}
|
||||
const join: SanitizedJoin = {
|
||||
field,
|
||||
schemaPath: `${schemaPath || ''}${schemaPath ? '.' : ''}${field.name}`,
|
||||
schemaPath: [...schemaPath, field.name].join('.'),
|
||||
targetField: undefined,
|
||||
}
|
||||
const joinCollection = config.collections.find(
|
||||
|
||||
@@ -59,7 +59,6 @@ import type {
|
||||
JSONFieldErrorServerComponent,
|
||||
JSONFieldLabelClientComponent,
|
||||
JSONFieldLabelServerComponent,
|
||||
MappedComponent,
|
||||
NumberFieldClientProps,
|
||||
NumberFieldErrorClientComponent,
|
||||
NumberFieldErrorServerComponent,
|
||||
@@ -95,6 +94,7 @@ import type {
|
||||
TextareaFieldErrorServerComponent,
|
||||
TextareaFieldLabelClientComponent,
|
||||
TextareaFieldLabelServerComponent,
|
||||
TextFieldClientProps,
|
||||
TextFieldErrorClientComponent,
|
||||
TextFieldErrorServerComponent,
|
||||
TextFieldLabelClientComponent,
|
||||
@@ -299,15 +299,6 @@ type Admin = {
|
||||
|
||||
export type AdminClient = {
|
||||
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. */
|
||||
custom?: Record<string, any>
|
||||
description?: StaticDescription
|
||||
@@ -431,8 +422,7 @@ export interface FieldBase {
|
||||
|
||||
export interface FieldBaseClient {
|
||||
_isPresentational?: undefined
|
||||
_path?: string
|
||||
_schemaPath?: string
|
||||
_schemaPath: string[]
|
||||
admin?: AdminClient
|
||||
hidden?: boolean
|
||||
index?: boolean
|
||||
@@ -498,15 +488,7 @@ export type NumberField = {
|
||||
Omit<FieldBase, 'validate'>
|
||||
|
||||
export type NumberFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: MappedComponent[]
|
||||
beforeInput?: MappedComponent[]
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<NumberField['admin'], 'autoComplete' | 'placeholder' | 'step'>
|
||||
admin?: AdminClient & Pick<NumberField['admin'], 'autoComplete' | 'placeholder' | 'step'>
|
||||
} & FieldBaseClient &
|
||||
Pick<NumberField, 'hasMany' | 'max' | 'maxRows' | 'min' | 'minRows' | 'type'>
|
||||
|
||||
@@ -548,15 +530,7 @@ export type TextField = {
|
||||
Omit<FieldBase, 'validate'>
|
||||
|
||||
export type TextFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: MappedComponent[]
|
||||
beforeInput?: MappedComponent[]
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<TextField['admin'], 'autoComplete' | 'placeholder' | 'rtl'>
|
||||
admin?: AdminClient & Pick<TextField['admin'], 'autoComplete' | 'placeholder' | 'rtl'>
|
||||
} & FieldBaseClient &
|
||||
Pick<TextField, 'hasMany' | 'maxLength' | 'maxRows' | 'minLength' | 'minRows' | 'type'>
|
||||
|
||||
@@ -576,15 +550,7 @@ export type EmailField = {
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type EmailFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: MappedComponent[]
|
||||
beforeInput?: MappedComponent[]
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<EmailField['admin'], 'placeholder'>
|
||||
admin?: AdminClient & Pick<EmailField['admin'], 'placeholder'>
|
||||
} & FieldBaseClient &
|
||||
Pick<EmailField, 'type'>
|
||||
|
||||
@@ -607,15 +573,7 @@ export type TextareaField = {
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type TextareaFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: MappedComponent[]
|
||||
beforeInput?: MappedComponent[]
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<TextareaField['admin'], 'placeholder' | 'rows' | 'rtl'>
|
||||
admin?: AdminClient & Pick<TextareaField['admin'], 'placeholder' | 'rows' | 'rtl'>
|
||||
} & FieldBaseClient &
|
||||
Pick<TextareaField, 'maxLength' | 'minLength' | 'type'>
|
||||
|
||||
@@ -633,14 +591,7 @@ export type CheckboxField = {
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type CheckboxFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: MappedComponent[]
|
||||
beforeInput?: MappedComponent[]
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient
|
||||
admin?: AdminClient
|
||||
} & FieldBaseClient &
|
||||
Pick<CheckboxField, 'type'>
|
||||
|
||||
@@ -660,15 +611,7 @@ export type DateField = {
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type DateFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: MappedComponent[]
|
||||
beforeInput?: MappedComponent[]
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<DateField['admin'], 'date' | 'placeholder'>
|
||||
admin?: AdminClient & Pick<DateField['admin'], 'date' | 'placeholder'>
|
||||
} & FieldBaseClient &
|
||||
Pick<DateField, 'type'>
|
||||
|
||||
@@ -692,12 +635,7 @@ export type GroupField = {
|
||||
} & Omit<FieldBase, 'required' | 'validate'>
|
||||
|
||||
export type GroupFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<GroupField['admin'], 'hideGutter'>
|
||||
admin?: AdminClient & Pick<GroupField['admin'], 'hideGutter'>
|
||||
fields: ClientField[]
|
||||
} & Omit<FieldBaseClient, 'required'> &
|
||||
Pick<GroupField, 'interfaceName' | 'type'>
|
||||
@@ -745,25 +683,12 @@ export type CollapsibleField = {
|
||||
Omit<FieldBase, 'label' | 'name' | 'validate' | 'virtual'>
|
||||
|
||||
export type CollapsibleFieldClient = {
|
||||
admin?: {
|
||||
initCollapsed?: boolean
|
||||
} & AdminClient
|
||||
fields: ClientField[]
|
||||
} & (
|
||||
| {
|
||||
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'> &
|
||||
label: StaticLabel
|
||||
} & Omit<FieldBaseClient, 'label' | 'name' | 'validate'> &
|
||||
Pick<CollapsibleField, 'type'>
|
||||
|
||||
type TabBase = {
|
||||
@@ -856,14 +781,9 @@ export type UIField = {
|
||||
export type UIFieldClient = {
|
||||
_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))
|
||||
// eslint-disable-next-line perfectionist/sort-intersection-types
|
||||
admin: DeepUndefinable<FieldBaseClient['admin']> & {
|
||||
components?: {
|
||||
Cell?: MappedComponent
|
||||
Field: MappedComponent
|
||||
Filter?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & Pick<
|
||||
|
||||
admin: DeepUndefinable<FieldBaseClient['admin']> &
|
||||
Pick<
|
||||
UIField['admin'],
|
||||
'custom' | 'disableBulkEdit' | 'disableListColumn' | 'position' | 'width'
|
||||
>
|
||||
@@ -933,13 +853,8 @@ type UploadAdmin = {
|
||||
} & Admin['components']
|
||||
isSortable?: boolean
|
||||
} & Admin
|
||||
type UploadAdminClient = {
|
||||
components?: {
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<UploadAdmin, 'allowCreate' | 'isSortable'>
|
||||
|
||||
type UploadAdminClient = AdminClient & Pick<UploadAdmin, 'allowCreate' | 'isSortable'>
|
||||
|
||||
export type PolymorphicUploadField = {
|
||||
admin?: {
|
||||
@@ -989,15 +904,7 @@ export type CodeField = {
|
||||
} & Omit<FieldBase, 'admin' | 'validate'>
|
||||
|
||||
export type CodeFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: MappedComponent[]
|
||||
beforeInput?: MappedComponent[]
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<CodeField['admin'], 'editorOptions' | 'language'>
|
||||
admin?: AdminClient & Pick<CodeField['admin'], 'editorOptions' | 'language'>
|
||||
} & Omit<FieldBaseClient, 'admin'> &
|
||||
Pick<CodeField, 'maxLength' | 'minLength' | 'type'>
|
||||
|
||||
@@ -1022,15 +929,7 @@ export type JSONField = {
|
||||
} & Omit<FieldBase, 'admin' | 'validate'>
|
||||
|
||||
export type JSONFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: MappedComponent[]
|
||||
beforeInput?: MappedComponent[]
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<JSONField['admin'], 'editorOptions'>
|
||||
admin?: AdminClient & Pick<JSONField['admin'], 'editorOptions'>
|
||||
} & Omit<FieldBaseClient, 'admin'> &
|
||||
Pick<JSONField, 'jsonSchema' | 'type'>
|
||||
|
||||
@@ -1069,15 +968,7 @@ export type SelectField = {
|
||||
Omit<FieldBase, 'validate'>
|
||||
|
||||
export type SelectFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: MappedComponent[]
|
||||
beforeInput?: MappedComponent[]
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<SelectField['admin'], 'isClearable' | 'isSortable'>
|
||||
admin?: AdminClient & Pick<SelectField['admin'], 'isClearable' | 'isSortable'>
|
||||
} & FieldBaseClient &
|
||||
Pick<SelectField, 'hasMany' | 'options' | 'type'>
|
||||
|
||||
@@ -1142,12 +1033,7 @@ type RelationshipAdmin = {
|
||||
isSortable?: boolean
|
||||
} & Admin
|
||||
|
||||
type RelationshipAdminClient = {
|
||||
components?: {
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
type RelationshipAdminClient = AdminClient &
|
||||
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'isSortable'>
|
||||
|
||||
export type PolymorphicRelationshipField = {
|
||||
@@ -1226,13 +1112,6 @@ export type RichTextFieldClient<
|
||||
TAdapterProps = any,
|
||||
TExtraProperties = object,
|
||||
> = {
|
||||
admin?: {
|
||||
components?: {
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
placeholder?: Record<string, string> | string
|
||||
} & AdminClient
|
||||
richTextComponentMap?: Map<string, any>
|
||||
} & FieldBaseClient &
|
||||
Pick<RichTextField<TValue, TAdapterProps, TExtraProperties>, 'maxDepth' | 'type'> &
|
||||
@@ -1271,14 +1150,7 @@ export type ArrayField = {
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type ArrayFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
RowLabel?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<ArrayField['admin'], 'initCollapsed' | 'isSortable'>
|
||||
admin?: AdminClient & Pick<ArrayField['admin'], 'initCollapsed' | 'isSortable'>
|
||||
fields: ClientField[]
|
||||
labels?: LabelsClient
|
||||
} & FieldBaseClient &
|
||||
@@ -1306,13 +1178,7 @@ export type RadioField = {
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type RadioFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<RadioField['admin'], 'layout'>
|
||||
admin?: AdminClient & Pick<RadioField['admin'], 'layout'>
|
||||
} & FieldBaseClient &
|
||||
Pick<RadioField, 'options' | 'type'>
|
||||
|
||||
@@ -1361,10 +1227,11 @@ export type Block = {
|
||||
labels?: Labels
|
||||
slug: string
|
||||
}
|
||||
|
||||
export type ClientBlock = {
|
||||
admin?: {
|
||||
components?: {
|
||||
Label?: MappedComponent
|
||||
Label?: React.ReactNode
|
||||
}
|
||||
} & Pick<Block['admin'], 'custom'>
|
||||
fields: ClientField[]
|
||||
@@ -1392,12 +1259,7 @@ export type BlocksField = {
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type BlocksFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
Error?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<BlocksField['admin'], 'initCollapsed' | 'isSortable'>
|
||||
admin?: AdminClient & Pick<BlocksField['admin'], 'initCollapsed' | 'isSortable'>
|
||||
blocks: ClientBlock[]
|
||||
labels?: LabelsClient
|
||||
} & FieldBaseClient &
|
||||
@@ -1419,15 +1281,7 @@ export type PointField = {
|
||||
} & Omit<FieldBase, 'validate'>
|
||||
|
||||
export type PointFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
afterInput?: MappedComponent[]
|
||||
beforeInput?: MappedComponent[]
|
||||
Error?: MappedComponent
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<PointField['admin'], 'placeholder' | 'step'>
|
||||
admin?: AdminClient & Pick<PointField['admin'], 'placeholder' | 'step'>
|
||||
} & FieldBaseClient &
|
||||
Pick<PointField, 'type'>
|
||||
|
||||
@@ -1473,12 +1327,7 @@ export type JoinField = {
|
||||
} & FieldBase
|
||||
|
||||
export type JoinFieldClient = {
|
||||
admin?: {
|
||||
components?: {
|
||||
Label?: MappedComponent
|
||||
} & AdminClient['components']
|
||||
} & AdminClient &
|
||||
Pick<JoinField['admin'], 'disableBulkEdit' | 'readOnly'>
|
||||
admin?: AdminClient & Pick<JoinField['admin'], 'disableBulkEdit' | 'readOnly'>
|
||||
} & FieldBaseClient &
|
||||
Pick<JoinField, 'collection' | 'index' | 'maxDepth' | 'on' | 'type'>
|
||||
|
||||
@@ -1550,6 +1399,7 @@ export type ClientFieldProps =
|
||||
| SelectFieldClientProps
|
||||
| TabsFieldClientProps
|
||||
| TextareaFieldClientProps
|
||||
| TextFieldClientProps
|
||||
| UploadFieldClientProps
|
||||
|
||||
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'
|
||||
|
||||
export function getFieldPaths({
|
||||
field,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
parentPath = [],
|
||||
parentSchemaPath = [],
|
||||
schemaIndex,
|
||||
}: {
|
||||
field: Field | TabAsField
|
||||
field: ClientField | Field | TabAsField
|
||||
parentPath: (number | string)[]
|
||||
parentSchemaPath: string[]
|
||||
schemaIndex: number
|
||||
}): {
|
||||
path: (number | string)[]
|
||||
schemaPath: string[]
|
||||
} {
|
||||
if (field.type === 'tabs' || field.type === 'row' || field.type === 'collapsible') {
|
||||
return {
|
||||
path: parentPath,
|
||||
schemaPath: parentSchemaPath,
|
||||
path: [...parentPath, `_index-${schemaIndex}`],
|
||||
schemaPath: [...parentSchemaPath, `_index-${schemaIndex}`],
|
||||
}
|
||||
} else if (field.type === 'tab') {
|
||||
if (tabHasName(field)) {
|
||||
@@ -27,8 +29,8 @@ export function getFieldPaths({
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
path: parentPath,
|
||||
schemaPath: parentSchemaPath,
|
||||
path: [...parentPath, `_index-${schemaIndex}`],
|
||||
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