Compare commits
524 Commits
v3.0.0-bet
...
feat/on-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa8d422421 | ||
|
|
707775d1ae | ||
|
|
35ca534149 | ||
|
|
12332ab5df | ||
|
|
174631340d | ||
|
|
dc8b1fe023 | ||
|
|
a24b72c530 | ||
|
|
3f74896167 | ||
|
|
b240291913 | ||
|
|
a2499352a5 | ||
|
|
774838c421 | ||
|
|
6d8fd3adeb | ||
|
|
7d627db8aa | ||
|
|
bda91d875f | ||
|
|
9eae3d685c | ||
|
|
eb82ee457c | ||
|
|
cc0b88a078 | ||
|
|
26691377d2 | ||
|
|
8201a6cacd | ||
|
|
d7fc944792 | ||
|
|
faeef5bc93 | ||
|
|
b841f01da5 | ||
|
|
2e816614ac | ||
|
|
0c4ec43460 | ||
|
|
50bab5797c | ||
|
|
de9acedaf4 | ||
|
|
6d6370ca5e | ||
|
|
a3360b5d45 | ||
|
|
e6f73e0c0d | ||
|
|
f336b2a41e | ||
|
|
7ed2859657 | ||
|
|
d3c5cf7aff | ||
|
|
bc3c949d95 | ||
|
|
ec7028ad5b | ||
|
|
6d2a5ddafe | ||
|
|
b83a7f2f37 | ||
|
|
8deab489df | ||
|
|
eab9acb35d | ||
|
|
150a31b339 | ||
|
|
6c3e27288e | ||
|
|
3f2566a191 | ||
|
|
8bb994fc6c | ||
|
|
3a48251219 | ||
|
|
694040c8bc | ||
|
|
d5c9628958 | ||
|
|
df6578183a | ||
|
|
a93da8625b | ||
|
|
859726688e | ||
|
|
7fb5b60932 | ||
|
|
46ebecfde4 | ||
|
|
6ea473aae8 | ||
|
|
e0f85b5f05 | ||
|
|
6b2c939afd | ||
|
|
20b09a8213 | ||
|
|
7c44609af4 | ||
|
|
b0f243632d | ||
|
|
4df00f4b36 | ||
|
|
7aa4a5650c | ||
|
|
1cae6cf997 | ||
|
|
8a8055a0a8 | ||
|
|
d0da6bc4e1 | ||
|
|
d9711aaa89 | ||
|
|
e26c95d430 | ||
|
|
fbb8229c54 | ||
|
|
36925f3956 | ||
|
|
50cc91ead3 | ||
|
|
58c901dfdd | ||
|
|
f599735d9d | ||
|
|
c9752918c8 | ||
|
|
0b5a294f63 | ||
|
|
7422fab2f2 | ||
|
|
3fd74dd77f | ||
|
|
0392b3d614 | ||
|
|
f1c9b2b1d0 | ||
|
|
997b9eb63d | ||
|
|
f8ab503153 | ||
|
|
3e105a20e4 | ||
|
|
906551bba4 | ||
|
|
6167bf3cca | ||
|
|
5ec26e8d54 | ||
|
|
d2bf12ac56 | ||
|
|
a33131033f | ||
|
|
b13eac201a | ||
|
|
65553af461 | ||
|
|
b212e2d989 | ||
|
|
3ee58f2211 | ||
|
|
b2dabb78bb | ||
|
|
b78b1af0af | ||
|
|
42e20bd982 | ||
|
|
b8d18a73da | ||
|
|
76ae85566e | ||
|
|
0f081a4999 | ||
|
|
36ce0438eb | ||
|
|
60fbc3e251 | ||
|
|
6d11f790c0 | ||
|
|
2add224ee5 | ||
|
|
8f3b0fd71b | ||
|
|
6a78be6b17 | ||
|
|
f8bc243144 | ||
|
|
6a6aa9a85f | ||
|
|
fac30ad95e | ||
|
|
58c792cfc9 | ||
|
|
2536107abb | ||
|
|
057a37b50e | ||
|
|
aa402bca12 | ||
|
|
64154e0818 | ||
|
|
81a904af96 | ||
|
|
0d468ffc6c | ||
|
|
c35b3164a4 | ||
|
|
f4517685df | ||
|
|
51ec6b2fa4 | ||
|
|
fdbf86d394 | ||
|
|
b9c41ee095 | ||
|
|
652e6910c9 | ||
|
|
a749617a04 | ||
|
|
f3cf304b6a | ||
|
|
9d5dd5a61a | ||
|
|
aa5abeea7e | ||
|
|
e11ad3371b | ||
|
|
d352c35c22 | ||
|
|
ac85bc4d04 | ||
|
|
4b2e86328a | ||
|
|
5e3eab7db4 | ||
|
|
cde1135106 | ||
|
|
fcecad6b2f | ||
|
|
1e696bdbde | ||
|
|
0c8debe71d | ||
|
|
2bba6c80c5 | ||
|
|
301666006c | ||
|
|
205650d26d | ||
|
|
05b83f71de | ||
|
|
f097287c8c | ||
|
|
1caf5d9327 | ||
|
|
23afac9b01 | ||
|
|
4548d30f03 | ||
|
|
b7a1f4c773 | ||
|
|
6cc18e9a21 | ||
|
|
0a61160665 | ||
|
|
3c288facda | ||
|
|
f9ea396955 | ||
|
|
9cccbba2e1 | ||
|
|
29c9e45ce6 | ||
|
|
f70ef8dac3 | ||
|
|
d6d214b745 | ||
|
|
90d9d6e17d | ||
|
|
f2d2630336 | ||
|
|
62724b54df | ||
|
|
096eefd005 | ||
|
|
4d75c56ef1 | ||
|
|
bd56af14d0 | ||
|
|
df007d42ea | ||
|
|
dbaff20b1b | ||
|
|
d400a7e77d | ||
|
|
791699146e | ||
|
|
760200c1be | ||
|
|
94f8787270 | ||
|
|
29a6ec1ee2 | ||
|
|
9030aa81c3 | ||
|
|
6dce541b33 | ||
|
|
55a9e946cc | ||
|
|
33dbcb5115 | ||
|
|
2dff78ae61 | ||
|
|
189e514ecf | ||
|
|
2e18638baf | ||
|
|
73c9a34918 | ||
|
|
223cf6348e | ||
|
|
69bcfbf2c6 | ||
|
|
4e002310e3 | ||
|
|
81948094c3 | ||
|
|
1d7dc6926f | ||
|
|
6c8de6665e | ||
|
|
3014f96113 | ||
|
|
b97c143ef8 | ||
|
|
0642127094 | ||
|
|
4d85b91afe | ||
|
|
498d849905 | ||
|
|
796c15dffc | ||
|
|
20723e2d79 | ||
|
|
fbc82d488a | ||
|
|
20e11e3fc3 | ||
|
|
4c29ab0c44 | ||
|
|
dc572a1433 | ||
|
|
da1dd11949 | ||
|
|
8a9cf9bbdf | ||
|
|
d0f31e7dc4 | ||
|
|
87ccd740ab | ||
|
|
dc6515a26e | ||
|
|
ea0fc62f5e | ||
|
|
d8a4a6f92d | ||
|
|
8fa047d3b6 | ||
|
|
27c51c5114 | ||
|
|
6dff40dc69 | ||
|
|
31c2a2a42a | ||
|
|
18d9a21bac | ||
|
|
c918991172 | ||
|
|
9bb2d97e75 | ||
|
|
aad8e9385c | ||
|
|
1807aac5c2 | ||
|
|
e50a899d92 | ||
|
|
d6d3101632 | ||
|
|
516975b5cc | ||
|
|
164020ea24 | ||
|
|
0e328372ec | ||
|
|
da6154afb4 | ||
|
|
181d8a686b | ||
|
|
5d91a2e273 | ||
|
|
fbe4295497 | ||
|
|
ca485bdc0e | ||
|
|
aa5d1f52d8 | ||
|
|
4534b74e05 | ||
|
|
d3c78deb08 | ||
|
|
a96982597b | ||
|
|
6952b751e9 | ||
|
|
52ca879722 | ||
|
|
10570936bd | ||
|
|
f5e749f62f | ||
|
|
15f485e10b | ||
|
|
83712998c5 | ||
|
|
0f1397815b | ||
|
|
7e215f570e | ||
|
|
48523e1703 | ||
|
|
560d5adf97 | ||
|
|
a19a7235f1 | ||
|
|
8fef77075c | ||
|
|
4d5f06de86 | ||
|
|
6e39d25604 | ||
|
|
9c11be879e | ||
|
|
503b94c7dc | ||
|
|
af0d3564b2 | ||
|
|
3607e4bd94 | ||
|
|
a504b08965 | ||
|
|
65d895d7f6 | ||
|
|
54509033c6 | ||
|
|
eba967df78 | ||
|
|
968269aea9 | ||
|
|
6dd09679a5 | ||
|
|
67acbfde18 | ||
|
|
f70db1b818 | ||
|
|
da0093610c | ||
|
|
f50f5d24f5 | ||
|
|
10ba80264e | ||
|
|
d3990d1108 | ||
|
|
4c146776ab | ||
|
|
cbafa720bd | ||
|
|
e81b3a8b35 | ||
|
|
3df7f6996e | ||
|
|
349686fce8 | ||
|
|
2066184187 | ||
|
|
0647d63dd9 | ||
|
|
de1591fcb5 | ||
|
|
4a11a8e723 | ||
|
|
e24ac29e80 | ||
|
|
665dba9fea | ||
|
|
2ea70f324a | ||
|
|
32df51c78e | ||
|
|
b05f888625 | ||
|
|
0b55bb4456 | ||
|
|
58321e396b | ||
|
|
c07ce89f40 | ||
|
|
1d3453c1a4 | ||
|
|
7e3e4dbb0a | ||
|
|
829a2d3c3c | ||
|
|
8a9aead49e | ||
|
|
b52fa96b4b | ||
|
|
116a03b162 | ||
|
|
f3698db5b5 | ||
|
|
ceb49e70a3 | ||
|
|
ebfcd4a53f | ||
|
|
6c2925b3b4 | ||
|
|
1ab9f2cc75 | ||
|
|
ac0dc57ac4 | ||
|
|
3f0c810569 | ||
|
|
19b44e34b9 | ||
|
|
41ceaf5ac0 | ||
|
|
ad9d18408c | ||
|
|
1da2118d6d | ||
|
|
6ad6085894 | ||
|
|
2f295f3df1 | ||
|
|
5baa2c4964 | ||
|
|
e5506a39aa | ||
|
|
6857cc08e8 | ||
|
|
a53763ca2c | ||
|
|
f883c69e11 | ||
|
|
6a680773ca | ||
|
|
02503dbd45 | ||
|
|
b7943b9f8e | ||
|
|
184325b2cf | ||
|
|
72370a1581 | ||
|
|
f80cc2089a | ||
|
|
4c1fd28ce5 | ||
|
|
b7b3ad16b7 | ||
|
|
74bc281f3d | ||
|
|
c21b555368 | ||
|
|
e53877a868 | ||
|
|
8ef1180fc1 | ||
|
|
52a006abd9 | ||
|
|
6206b46b86 | ||
|
|
e13c7057e4 | ||
|
|
3d9dd0b647 | ||
|
|
4b4ef7b4c1 | ||
|
|
34dd3ac479 | ||
|
|
f6f0aee553 | ||
|
|
5d07d40960 | ||
|
|
98f5418ac0 | ||
|
|
b63b0cf3ad | ||
|
|
2611c77211 | ||
|
|
0b948673fa | ||
|
|
f6c0dcacee | ||
|
|
4a639ccc19 | ||
|
|
2c03de6dc7 | ||
|
|
d7c57aeeb9 | ||
|
|
e8d12bc3c0 | ||
|
|
564591ed93 | ||
|
|
185764c477 | ||
|
|
b11f072ad8 | ||
|
|
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,4 +1,4 @@
|
||||
<a href="https://payloadcms.com"><img width="100%" src="https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/assets/images/github-banner-alt.jpg?raw=true" alt="Payload headless CMS Admin panel built with React" /></a>
|
||||
<a href="https://payloadcms.com"><img width="100%" src="https://github.com/payloadcms/payload/blob/beta/packages/payload/src/assets/images/github-banner-nextjs-native.jpg" alt="Payload headless CMS Admin panel built with React" /></a>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
@@ -12,8 +14,17 @@ type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -228,7 +228,6 @@ The following additional properties are also provided to the `field` prop:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`_isPresentational`** | A boolean indicating that the field is purely visual and does not directly affect data or change data shape, i.e. the [UI Field](../fields/ui). |
|
||||
| **`_path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray[0].myField`. |
|
||||
| **`_schemaPath`** | A string representing the direct, static path to the [Field Config](../fields/overview), i.e. `myGroup.myArray.myField` |
|
||||
|
||||
|
||||
@@ -370,7 +370,12 @@ const yourEditorState: SerializedEditorState // <= your current editor state her
|
||||
|
||||
// Import editor state into your headless editor
|
||||
try {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
|
||||
},
|
||||
{ discrete: true }, // This should commit the editor state immediately
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error({ err: e }, 'ERROR parsing editor state')
|
||||
}
|
||||
@@ -382,8 +387,6 @@ headlessEditor.getEditorState().read(() => {
|
||||
})
|
||||
```
|
||||
|
||||
The `.setEditorState()` function immediately updates your editor state. Thus, there's no need for the `discrete: true` flag when reading the state afterward.
|
||||
|
||||
## Lexical => Plain Text
|
||||
|
||||
Export content from the Lexical editor into plain text using these steps:
|
||||
@@ -401,8 +404,13 @@ const yourEditorState: SerializedEditorState // <= your current editor state her
|
||||
|
||||
// Import editor state into your headless editor
|
||||
try {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState)) // This should commit the editor state immediately
|
||||
} catch (e) {
|
||||
headlessEditor.update(
|
||||
() => {
|
||||
headlessEditor.setEditorState(headlessEditor.parseEditorState(yourEditorState))
|
||||
},
|
||||
{ discrete: true }, // This should commit the editor state immediately
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error({ err: e }, 'ERROR parsing editor state')
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
@@ -13,7 +13,7 @@ type Args = {
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import React from 'react'
|
||||
|
||||
import './custom.scss'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import configPromise from '@payload-config'
|
||||
import config from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
@@ -13,7 +13,7 @@ type Args = {
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
<RootLayout config={config} importMap={importMap}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
@@ -19,6 +19,11 @@ const config = withBundleAnalyzer(
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '5mb',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
|
||||
"dev:generate-types": "pnpm runts ./test/generateTypes.ts",
|
||||
"dev:postgres": "cross-env PAYLOAD_DATABASE=postgres pnpm runts ./test/dev.ts",
|
||||
"dev:prod": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/dev.ts --prod",
|
||||
"dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts",
|
||||
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
|
||||
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
|
||||
@@ -65,12 +66,12 @@
|
||||
"docker:stop": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml down",
|
||||
"force:build": "pnpm run build:core:force",
|
||||
"lint": "turbo run lint --concurrency 1 --continue",
|
||||
"lint-staged": "lint-staged",
|
||||
"lint-staged": "node ./scripts/run-lint-staged.js",
|
||||
"lint:fix": "turbo run lint:fix --concurrency 1 --continue",
|
||||
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
|
||||
"prepare": "husky",
|
||||
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"prepare-run-test-against-prod:ci": "rm -rf test/node_modules && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"prepare-run-test-against-prod:ci": "rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
|
||||
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
|
||||
|
||||
48
packages/db-mongodb/src/countGlobalVersions.ts
Normal file
48
packages/db-mongodb/src/countGlobalVersions.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { CountGlobalVersions, PayloadRequest } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
||||
this: MongooseAdapter,
|
||||
{ global, locale, req = {} as PayloadRequest, where },
|
||||
) {
|
||||
const Model = this.versions[global]
|
||||
const options: QueryOptions = await withSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
|
||||
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
|
||||
// the correct indexed field
|
||||
options.hint = {
|
||||
_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Model.countDocuments(query, options)
|
||||
|
||||
return {
|
||||
totalDocs: result,
|
||||
}
|
||||
}
|
||||
48
packages/db-mongodb/src/countVersions.ts
Normal file
48
packages/db-mongodb/src/countVersions.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { QueryOptions } from 'mongoose'
|
||||
import type { CountVersions, PayloadRequest } from 'payload'
|
||||
|
||||
import { flattenWhereToOperators } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { withSession } from './withSession.js'
|
||||
|
||||
export const countVersions: CountVersions = async function countVersions(
|
||||
this: MongooseAdapter,
|
||||
{ collection, locale, req = {} as PayloadRequest, where },
|
||||
) {
|
||||
const Model = this.versions[collection]
|
||||
const options: QueryOptions = await withSession(this, req)
|
||||
|
||||
let hasNearConstraint = false
|
||||
|
||||
if (where) {
|
||||
const constraints = flattenWhereToOperators(where)
|
||||
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
|
||||
}
|
||||
|
||||
const query = await Model.buildQuery({
|
||||
locale,
|
||||
payload: this.payload,
|
||||
where,
|
||||
})
|
||||
|
||||
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
|
||||
const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
|
||||
// the correct indexed field
|
||||
options.hint = {
|
||||
_id: 1,
|
||||
}
|
||||
}
|
||||
|
||||
const result = await Model.countDocuments(query, options)
|
||||
|
||||
return {
|
||||
totalDocs: result,
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import type { CollectionModel, GlobalModel, MigrateDownArgs, MigrateUpArgs } fro
|
||||
|
||||
import { connect } from './connect.js'
|
||||
import { count } from './count.js'
|
||||
import { countGlobalVersions } from './countGlobalVersions.js'
|
||||
import { countVersions } from './countVersions.js'
|
||||
import { create } from './create.js'
|
||||
import { createGlobal } from './createGlobal.js'
|
||||
import { createGlobalVersion } from './createGlobalVersion.js'
|
||||
@@ -154,7 +156,6 @@ export function mongooseAdapter({
|
||||
collections: {},
|
||||
connection: undefined,
|
||||
connectOptions: connectOptions || {},
|
||||
count,
|
||||
disableIndexHints,
|
||||
globals: undefined,
|
||||
mongoMemoryServer,
|
||||
@@ -166,6 +167,9 @@ export function mongooseAdapter({
|
||||
beginTransaction: transactionOptions === false ? defaultBeginTransaction() : beginTransaction,
|
||||
commitTransaction,
|
||||
connect,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { PipelineStage } from 'mongoose'
|
||||
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
|
||||
|
||||
import { combineQueries } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
import { buildSortParam } from '../queries/buildSortParam.js'
|
||||
@@ -60,11 +58,11 @@ export const buildJoinAggregation = async ({
|
||||
for (const join of joinConfig[slug]) {
|
||||
const joinModel = adapter.collections[join.field.collection]
|
||||
|
||||
if (projection && !projection[join.schemaPath]) {
|
||||
if (projection && !projection[join.joinPath]) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (joins?.[join.schemaPath] === false) {
|
||||
if (joins?.[join.joinPath] === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -72,7 +70,7 @@ export const buildJoinAggregation = async ({
|
||||
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||
where: whereJoin,
|
||||
} = joins?.[join.schemaPath] || {}
|
||||
} = joins?.[join.joinPath] || {}
|
||||
|
||||
const sort = buildSortParam({
|
||||
config: adapter.payload.config,
|
||||
@@ -105,7 +103,7 @@ export const buildJoinAggregation = async ({
|
||||
|
||||
if (adapter.payload.config.localization && locale === 'all') {
|
||||
adapter.payload.config.localization.localeCodes.forEach((code) => {
|
||||
const as = `${versions ? `version.${join.schemaPath}` : join.schemaPath}${code}`
|
||||
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
|
||||
|
||||
aggregate.push(
|
||||
{
|
||||
@@ -146,7 +144,7 @@ export const buildJoinAggregation = async ({
|
||||
} else {
|
||||
const localeSuffix =
|
||||
join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : ''
|
||||
const as = `${versions ? `version.${join.schemaPath}` : join.schemaPath}${localeSuffix}`
|
||||
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${localeSuffix}`
|
||||
|
||||
aggregate.push(
|
||||
{
|
||||
|
||||
@@ -19,8 +19,8 @@ export const handleError = ({
|
||||
collection,
|
||||
errors: [
|
||||
{
|
||||
field: Object.keys(error.keyValue)[0],
|
||||
message: req.t('error:valueMustBeUnique'),
|
||||
path: Object.keys(error.keyValue)[0],
|
||||
},
|
||||
],
|
||||
global,
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
@@ -126,6 +128,8 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
@@ -114,6 +116,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
beginTransaction,
|
||||
commitTransaction,
|
||||
count,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
@@ -127,6 +129,8 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
convertPathToJSONTraversal,
|
||||
count,
|
||||
countDistinct,
|
||||
countGlobalVersions,
|
||||
countVersions,
|
||||
create,
|
||||
createGlobal,
|
||||
createGlobalVersion,
|
||||
|
||||
42
packages/drizzle/src/countGlobalVersions.ts
Normal file
42
packages/drizzle/src/countGlobalVersions.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { CountGlobalVersions, SanitizedGlobalConfig } from 'payload'
|
||||
|
||||
import { buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
|
||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
||||
this: DrizzleAdapter,
|
||||
{ global, locale, req, where: whereArg },
|
||||
) {
|
||||
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
|
||||
({ slug }) => slug === global,
|
||||
)
|
||||
|
||||
const tableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
|
||||
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
|
||||
|
||||
const fields = buildVersionGlobalFields(this.payload.config, globalConfig)
|
||||
|
||||
const { joins, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields,
|
||||
locale,
|
||||
tableName,
|
||||
where: whereArg,
|
||||
})
|
||||
|
||||
const countResult = await this.countDistinct({
|
||||
db,
|
||||
joins,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
return { totalDocs: countResult }
|
||||
}
|
||||
40
packages/drizzle/src/countVersions.ts
Normal file
40
packages/drizzle/src/countVersions.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { CountVersions, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { buildVersionCollectionFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
|
||||
export const countVersions: CountVersions = async function countVersions(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, locale, req, where: whereArg },
|
||||
) {
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||
|
||||
const tableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
|
||||
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
|
||||
|
||||
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)
|
||||
|
||||
const { joins, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields,
|
||||
locale,
|
||||
tableName,
|
||||
where: whereArg,
|
||||
})
|
||||
|
||||
const countResult = await this.countDistinct({
|
||||
db,
|
||||
joins,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
return { totalDocs: countResult }
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export { count } from './count.js'
|
||||
export { countGlobalVersions } from './countGlobalVersions.js'
|
||||
export { countVersions } from './countVersions.js'
|
||||
export { create } from './create.js'
|
||||
export { createGlobal } from './createGlobal.js'
|
||||
export { createGlobalVersion } from './createGlobalVersion.js'
|
||||
|
||||
@@ -391,8 +391,8 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
id,
|
||||
errors: [
|
||||
{
|
||||
field: fieldName,
|
||||
message: req.t('error:valueMustBeUnique'),
|
||||
path: fieldName,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { DocumentTabConfig, DocumentTabProps } from 'payload'
|
||||
import type React from 'react'
|
||||
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { Fragment } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
import { DocumentTabLink } from './TabLink.js'
|
||||
@@ -59,17 +60,6 @@ export const DocumentTab: React.FC<
|
||||
})
|
||||
: label
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedPin = createMappedComponent(Pill, undefined, Pill_Component, 'Pill')
|
||||
|
||||
return (
|
||||
<DocumentTabLink
|
||||
adminRoute={routes.admin}
|
||||
@@ -82,12 +72,21 @@ export const DocumentTab: React.FC<
|
||||
>
|
||||
<span className={`${baseClass}__label`}>
|
||||
{labelToRender}
|
||||
{mappedPin && (
|
||||
{Pill || Pill_Component ? (
|
||||
<Fragment>
|
||||
|
||||
<RenderComponent mappedComponent={mappedPin} />
|
||||
<RenderServerComponent
|
||||
Component={Pill}
|
||||
Fallback={Pill_Component}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
) : null}
|
||||
</span>
|
||||
</DocumentTabLink>
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import React from 'react'
|
||||
|
||||
import { getCustomViews } from './getCustomViews.js'
|
||||
@@ -80,33 +80,21 @@ export const DocumentTabs: React.FC<{
|
||||
const { path, tab } = CustomView
|
||||
|
||||
if (tab.Component) {
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
...props,
|
||||
key: `tab-custom-${index}`,
|
||||
path,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedTab = createMappedComponent(
|
||||
tab.Component,
|
||||
undefined,
|
||||
undefined,
|
||||
'tab.Component',
|
||||
)
|
||||
|
||||
return (
|
||||
<RenderComponent
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
key: `tab-custom-${index}`,
|
||||
path,
|
||||
}}
|
||||
Component={tab.Component}
|
||||
importMap={payload.importMap}
|
||||
key={`tab-custom-${index}`}
|
||||
mappedComponent={mappedTab}
|
||||
serverProps={{
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -121,6 +109,7 @@ export const DocumentTabs: React.FC<{
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</ul>
|
||||
|
||||
@@ -5,14 +5,11 @@ import React from 'react'
|
||||
import { baseClass } from '../../Tab/index.js'
|
||||
|
||||
export const VersionsPill: React.FC = () => {
|
||||
const { versions } = useDocumentInfo()
|
||||
const { versionCount } = useDocumentInfo()
|
||||
|
||||
// don't count snapshots
|
||||
const totalVersions = versions?.docs.filter((version) => !version.snapshot).length || 0
|
||||
|
||||
if (!versions?.totalDocs) {
|
||||
if (!versionCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <span className={`${baseClass}__count`}>{totalVersions}</span>
|
||||
return <span className={`${baseClass}__count`}>{versionCount}</span>
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
import { Gutter, RenderTitle } from '@payloadcms/ui'
|
||||
import React, { Fragment } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
import { DocumentTabs } from './Tabs/index.js'
|
||||
@@ -16,32 +16,25 @@ const baseClass = `doc-header`
|
||||
|
||||
export const DocumentHeader: React.FC<{
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
customHeader?: React.ReactNode
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
hideTabs?: boolean
|
||||
i18n: I18n
|
||||
payload: Payload
|
||||
permissions: Permissions
|
||||
}> = (props) => {
|
||||
const { collectionConfig, customHeader, globalConfig, hideTabs, i18n, payload, permissions } =
|
||||
props
|
||||
const { collectionConfig, globalConfig, hideTabs, i18n, payload, permissions } = props
|
||||
|
||||
return (
|
||||
<Gutter className={baseClass}>
|
||||
{customHeader && customHeader}
|
||||
{!customHeader && (
|
||||
<Fragment>
|
||||
<RenderTitle className={`${baseClass}__title`} />
|
||||
{!hideTabs && (
|
||||
<DocumentTabs
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
<RenderTitle className={`${baseClass}__title`} />
|
||||
{!hideTabs && (
|
||||
<DocumentTabs
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
</Gutter>
|
||||
)
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { FieldPermissions, LoginWithUsernameOptions } from 'payload'
|
||||
|
||||
import { EmailField, RenderFields, TextField, useTranslation } from '@payloadcms/ui'
|
||||
import { email, username } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
readonly loginWithUsername?: false | LoginWithUsernameOptions
|
||||
}
|
||||
function EmailFieldComponent(props: Props) {
|
||||
const { loginWithUsername } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
const requireEmail = !loginWithUsername || (loginWithUsername && loginWithUsername.requireEmail)
|
||||
const showEmailField =
|
||||
!loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin
|
||||
|
||||
if (showEmailField) {
|
||||
return (
|
||||
<EmailField
|
||||
autoComplete="off"
|
||||
field={{
|
||||
name: 'email',
|
||||
label: t('general:email'),
|
||||
required: requireEmail,
|
||||
}}
|
||||
validate={email}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function UsernameFieldComponent(props: Props) {
|
||||
const { loginWithUsername } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
const requireUsername = loginWithUsername && loginWithUsername.requireUsername
|
||||
const showUsernameField = Boolean(loginWithUsername)
|
||||
|
||||
if (showUsernameField) {
|
||||
return (
|
||||
<TextField
|
||||
field={{
|
||||
name: 'username',
|
||||
label: t('authentication:username'),
|
||||
required: requireUsername,
|
||||
}}
|
||||
validate={username}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
type RenderEmailAndUsernameFieldsProps = {
|
||||
className?: string
|
||||
loginWithUsername?: false | LoginWithUsernameOptions
|
||||
operation?: 'create' | 'update'
|
||||
permissions?: {
|
||||
[fieldName: string]: FieldPermissions
|
||||
}
|
||||
readOnly: boolean
|
||||
}
|
||||
export function RenderEmailAndUsernameFields(props: RenderEmailAndUsernameFieldsProps) {
|
||||
const { className, loginWithUsername, operation, permissions, readOnly } = props
|
||||
|
||||
return (
|
||||
<RenderFields
|
||||
className={className}
|
||||
fields={[
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text',
|
||||
admin: {
|
||||
autoComplete: 'off',
|
||||
components: {
|
||||
Field: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: <EmailFieldComponent loginWithUsername={loginWithUsername} />,
|
||||
},
|
||||
},
|
||||
},
|
||||
localized: false,
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: <UsernameFieldComponent loginWithUsername={loginWithUsername} />,
|
||||
},
|
||||
},
|
||||
},
|
||||
localized: false,
|
||||
},
|
||||
]}
|
||||
forceRender
|
||||
operation={operation}
|
||||
path=""
|
||||
permissions={permissions}
|
||||
readOnly={readOnly}
|
||||
schemaPath=""
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ServerProps } from 'payload'
|
||||
|
||||
import { getCreateMappedComponent, PayloadLogo, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { PayloadLogo } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
export const Logo: React.FC<ServerProps> = (props) => {
|
||||
@@ -16,20 +17,20 @@ export const Logo: React.FC<ServerProps> = (props) => {
|
||||
} = {},
|
||||
} = payload.config
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedCustomLogo = createMappedComponent(CustomLogo, undefined, PayloadLogo, 'CustomLogo')
|
||||
|
||||
return <RenderComponent mappedComponent={mappedCustomLogo} />
|
||||
return (
|
||||
<RenderServerComponent
|
||||
Component={CustomLogo}
|
||||
Fallback={PayloadLogo}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||
import type { groupNavItems } from '@payloadcms/ui/shared'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
NavGroup,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useEntityVisibility,
|
||||
useNav,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { EntityType, formatAdminURL, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import { NavGroup, useConfig, useNav, useTranslation } from '@payloadcms/ui'
|
||||
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import { usePathname } from 'next/navigation.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
const baseClass = 'nav'
|
||||
|
||||
export const DefaultNavClient: React.FC = () => {
|
||||
const { permissions } = useAuth()
|
||||
const { isEntityVisible } = useEntityVisibility()
|
||||
export const DefaultNavClient: React.FC<{
|
||||
groups: ReturnType<typeof groupNavItems>
|
||||
}> = ({ groups }) => {
|
||||
const pathname = usePathname()
|
||||
|
||||
const {
|
||||
config: {
|
||||
collections,
|
||||
globals,
|
||||
routes: { admin: adminRoute },
|
||||
},
|
||||
} = useConfig()
|
||||
@@ -34,53 +25,23 @@ export const DefaultNavClient: React.FC = () => {
|
||||
const { i18n } = useTranslation()
|
||||
const { navOpen } = useNav()
|
||||
|
||||
const groups = groupNavItems(
|
||||
[
|
||||
...collections
|
||||
.filter(({ slug }) => isEntityVisible({ collectionSlug: slug }))
|
||||
.map((collection) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.collection,
|
||||
entity: collection,
|
||||
}
|
||||
|
||||
return entityToGroup
|
||||
}),
|
||||
...globals
|
||||
.filter(({ slug }) => isEntityVisible({ globalSlug: slug }))
|
||||
.map((global) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.global,
|
||||
entity: global,
|
||||
}
|
||||
|
||||
return entityToGroup
|
||||
}),
|
||||
],
|
||||
permissions,
|
||||
i18n,
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{groups.map(({ entities, label }, key) => {
|
||||
return (
|
||||
<NavGroup key={key} label={label}>
|
||||
{entities.map(({ type, entity }, i) => {
|
||||
let entityLabel: string
|
||||
{entities.map(({ slug, type, label }, i) => {
|
||||
let href: string
|
||||
let id: string
|
||||
|
||||
if (type === EntityType.collection) {
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` })
|
||||
entityLabel = getTranslation(entity.labels.plural, i18n)
|
||||
id = `nav-${entity.slug}`
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })
|
||||
id = `nav-${slug}`
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
href = formatAdminURL({ adminRoute, path: `/globals/${entity.slug}` })
|
||||
entityLabel = getTranslation(entity.label, i18n)
|
||||
id = `nav-global-${entity.slug}`
|
||||
href = formatAdminURL({ adminRoute, path: `/globals/${slug}` })
|
||||
id = `nav-global-${slug}`
|
||||
}
|
||||
|
||||
const Link = (LinkWithDefault.default ||
|
||||
@@ -102,7 +63,7 @@ export const DefaultNavClient: React.FC = () => {
|
||||
tabIndex={!navOpen ? -1 : undefined}
|
||||
>
|
||||
{activeCollection && <div className={`${baseClass}__link-indicator`} />}
|
||||
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
|
||||
<span className={`${baseClass}__link-label`}>{getTranslation(label, i18n)}</span>
|
||||
</LinkElement>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||
import type { ServerProps } from 'payload'
|
||||
|
||||
import { Logout } from '@payloadcms/ui'
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
@@ -15,7 +17,7 @@ import { DefaultNavClient } from './index.client.js'
|
||||
export type NavProps = ServerProps
|
||||
|
||||
export const DefaultNav: React.FC<NavProps> = (props) => {
|
||||
const { i18n, locale, params, payload, permissions, searchParams, user } = props
|
||||
const { i18n, locale, params, payload, permissions, searchParams, user, visibleEntities } = props
|
||||
|
||||
if (!payload?.config) {
|
||||
return null
|
||||
@@ -23,44 +25,82 @@ export const DefaultNav: React.FC<NavProps> = (props) => {
|
||||
|
||||
const {
|
||||
admin: {
|
||||
components: { afterNavLinks, beforeNavLinks },
|
||||
components: { afterNavLinks, beforeNavLinks, logout },
|
||||
},
|
||||
collections,
|
||||
globals,
|
||||
} = payload.config
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedBeforeNavLinks = createMappedComponent(
|
||||
beforeNavLinks,
|
||||
undefined,
|
||||
undefined,
|
||||
'beforeNavLinks',
|
||||
)
|
||||
const mappedAfterNavLinks = createMappedComponent(
|
||||
afterNavLinks,
|
||||
undefined,
|
||||
undefined,
|
||||
'afterNavLinks',
|
||||
const groups = groupNavItems(
|
||||
[
|
||||
...collections
|
||||
.filter(({ slug }) => visibleEntities.collections.includes(slug))
|
||||
.map(
|
||||
(collection) =>
|
||||
({
|
||||
type: EntityType.collection,
|
||||
entity: collection,
|
||||
}) satisfies EntityToGroup,
|
||||
),
|
||||
...globals
|
||||
.filter(({ slug }) => visibleEntities.globals.includes(slug))
|
||||
.map(
|
||||
(global) =>
|
||||
({
|
||||
type: EntityType.global,
|
||||
entity: global,
|
||||
}) satisfies EntityToGroup,
|
||||
),
|
||||
],
|
||||
permissions,
|
||||
i18n,
|
||||
)
|
||||
|
||||
return (
|
||||
<NavWrapper baseClass={baseClass}>
|
||||
<nav className={`${baseClass}__wrap`}>
|
||||
<RenderComponent mappedComponent={mappedBeforeNavLinks} />
|
||||
<DefaultNavClient />
|
||||
<RenderComponent mappedComponent={mappedAfterNavLinks} />
|
||||
<RenderServerComponent
|
||||
Component={beforeNavLinks}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
<DefaultNavClient groups={groups} />
|
||||
<RenderServerComponent
|
||||
Component={afterNavLinks}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Logout />
|
||||
<RenderServerComponent
|
||||
Component={logout?.Button}
|
||||
Fallback={Logout}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
<div className={`${baseClass}__header`}>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { metadata, RootLayout } from '../layouts/Root/index.js'
|
||||
export { handleServerFunctions } from '../utilities/handleServerFunctions.js'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// NOTICE: Server-only utilities, do not import anything client-side here.
|
||||
export { addDataAndFileToRequest } from '../utilities/addDataAndFileToRequest.js'
|
||||
export { addLocalesToRequestFromData, sanitizeLocales } from '../utilities/addLocalesToRequest.js'
|
||||
export { createPayloadRequest } from '../utilities/createPayloadRequest.js'
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
export { DefaultEditView as EditView } from '../views/Edit/Default/index.js'
|
||||
export { DefaultListView as ListView } from '../views/List/Default/index.js'
|
||||
export { NotFoundPage } from '../views/NotFound/index.js'
|
||||
export { generatePageMetadata, type GenerateViewMetadata, RootPage } from '../views/Root/index.js'
|
||||
|
||||
@@ -22,6 +22,7 @@ type ProcessMultipart = (args: {
|
||||
export const processMultipart: ProcessMultipart = async ({ options, request }) => {
|
||||
let parsingRequest = true
|
||||
|
||||
let shouldAbortProccessing = false
|
||||
let fileCount = 0
|
||||
let filesCompleted = 0
|
||||
let allFilesHaveResolved: (value?: unknown) => void
|
||||
@@ -42,14 +43,16 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
headersObject[name] = value
|
||||
})
|
||||
|
||||
function abortAndDestroyFile(file: Readable, err: APIError) {
|
||||
file.destroy()
|
||||
parsingRequest = false
|
||||
failedResolvingFiles(err)
|
||||
}
|
||||
const reader = request.body.getReader()
|
||||
|
||||
const busboy = Busboy({ ...options, headers: headersObject })
|
||||
|
||||
function abortAndDestroyFile(file: Readable, err: APIError) {
|
||||
file.destroy()
|
||||
shouldAbortProccessing = true
|
||||
failedResolvingFiles(err)
|
||||
}
|
||||
|
||||
// Build multipart req.body fields
|
||||
busboy.on('field', (field, val) => {
|
||||
result.fields = buildFields(result.fields, field, val)
|
||||
@@ -136,7 +139,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
mimetype: mime,
|
||||
size,
|
||||
tempFilePath: getFilePath(),
|
||||
truncated: Boolean('truncated' in file && file.truncated),
|
||||
truncated: Boolean('truncated' in file && file.truncated) || false,
|
||||
},
|
||||
options,
|
||||
),
|
||||
@@ -164,8 +167,6 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
uploadTimer.set()
|
||||
})
|
||||
|
||||
// TODO: Valid eslint error - this will likely be a floating promise. Evaluate if we need to handle this differently.
|
||||
|
||||
busboy.on('finish', async () => {
|
||||
debugLog(options, `Busboy finished parsing request.`)
|
||||
if (options.parseNested) {
|
||||
@@ -190,14 +191,10 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
'error',
|
||||
(err = new APIError('Busboy error parsing multipart request', httpStatus.BAD_REQUEST)) => {
|
||||
debugLog(options, `Busboy error`)
|
||||
parsingRequest = false
|
||||
throw err
|
||||
},
|
||||
)
|
||||
|
||||
const reader = request.body.getReader()
|
||||
|
||||
// Start parsing request
|
||||
while (parsingRequest) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
@@ -205,7 +202,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
|
||||
parsingRequest = false
|
||||
}
|
||||
|
||||
if (value) {
|
||||
if (value && !shouldAbortProccessing) {
|
||||
busboy.write(value)
|
||||
}
|
||||
}
|
||||
|
||||
30
packages/next/src/layouts/Root/NestProviders.tsx
Normal file
30
packages/next/src/layouts/Root/NestProviders.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Config, ImportMap } from 'payload'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import '@payloadcms/ui/scss/app.scss'
|
||||
import React from 'react'
|
||||
|
||||
type Args = {
|
||||
readonly children: React.ReactNode
|
||||
readonly importMap: ImportMap
|
||||
readonly providers: Config['admin']['components']['providers']
|
||||
}
|
||||
|
||||
export function NestProviders({ children, importMap, providers }: Args): React.ReactNode {
|
||||
return (
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
children:
|
||||
providers.length > 1 ? (
|
||||
<NestProviders importMap={importMap} providers={providers.slice(1)}>
|
||||
{children}
|
||||
</NestProviders>
|
||||
) : (
|
||||
children
|
||||
),
|
||||
}}
|
||||
Component={providers[0]}
|
||||
importMap={importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
import type { AcceptedLanguages } from '@payloadcms/translations'
|
||||
import type { CustomVersionParser, ImportMap, SanitizedConfig } from 'payload'
|
||||
import type { CustomVersionParser, ImportMap, SanitizedConfig, ServerFunctionClient } from 'payload'
|
||||
|
||||
import { rtlLanguages } from '@payloadcms/translations'
|
||||
import { RootProvider } from '@payloadcms/ui'
|
||||
import '@payloadcms/ui/scss/app.scss'
|
||||
import { createClientConfig } from '@payloadcms/ui/utilities/createClientConfig'
|
||||
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
|
||||
import { checkDependencies, parseCookies } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import { getClientConfig } from '../../utilities/getClientConfig.js'
|
||||
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
|
||||
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
|
||||
import { initReq } from '../../utilities/initReq.js'
|
||||
import { DefaultEditView } from '../../views/Edit/Default/index.js'
|
||||
import { DefaultListView } from '../../views/List/Default/index.js'
|
||||
import { NestProviders } from './NestProviders.js'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Generated by Next.js',
|
||||
@@ -41,11 +40,12 @@ let checkedDependencies = false
|
||||
export const RootLayout = async ({
|
||||
children,
|
||||
config: configPromise,
|
||||
importMap,
|
||||
serverFunction,
|
||||
}: {
|
||||
readonly children: React.ReactNode
|
||||
readonly config: Promise<SanitizedConfig>
|
||||
readonly importMap: ImportMap
|
||||
readonly serverFunction: ServerFunctionClient
|
||||
}) => {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
@@ -103,16 +103,6 @@ export const RootLayout = async ({
|
||||
|
||||
const { i18n, permissions, req, user } = await initReq(config)
|
||||
|
||||
const { clientConfig, render } = await createClientConfig({
|
||||
children,
|
||||
config,
|
||||
DefaultEditView,
|
||||
DefaultListView,
|
||||
i18n,
|
||||
importMap,
|
||||
payload,
|
||||
})
|
||||
|
||||
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
|
||||
? 'RTL'
|
||||
: 'LTR'
|
||||
@@ -174,23 +164,39 @@ export const RootLayout = async ({
|
||||
|
||||
const isNavOpen = navPreferences?.value?.open ?? true
|
||||
|
||||
const clientConfig = await getClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
return (
|
||||
<html data-theme={theme} dir={dir} lang={languageCode}>
|
||||
<body>
|
||||
<RootProvider
|
||||
config={clientConfig}
|
||||
dateFNSKey={i18n.dateFNSKey}
|
||||
fallbackLang={clientConfig.i18n.fallbackLanguage}
|
||||
fallbackLang={config.i18n.fallbackLanguage}
|
||||
isNavOpen={isNavOpen}
|
||||
languageCode={languageCode}
|
||||
languageOptions={languageOptions}
|
||||
permissions={permissions}
|
||||
serverFunction={serverFunction}
|
||||
switchLanguageServerAction={switchLanguageServerAction}
|
||||
theme={theme}
|
||||
translations={i18n.translations}
|
||||
user={user}
|
||||
>
|
||||
{render}
|
||||
{Array.isArray(config.admin?.components?.providers) &&
|
||||
config.admin?.components?.providers.length > 0 ? (
|
||||
<NestProviders
|
||||
importMap={payload.importMap}
|
||||
providers={config.admin?.components?.providers}
|
||||
>
|
||||
{children}
|
||||
</NestProviders>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</RootProvider>
|
||||
<div id="portal" />
|
||||
</body>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
import { buildFormState as buildFormStateFn } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import { headersWithCors } from '../../utilities/headersWithCors.js'
|
||||
import { routeError } from './routeError.js'
|
||||
|
||||
export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
|
||||
const headers = headersWithCors({
|
||||
headers: new Headers(),
|
||||
req,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await buildFormStateFn({ req })
|
||||
|
||||
return Response.json(result, {
|
||||
headers,
|
||||
status: httpStatus.OK,
|
||||
})
|
||||
} catch (err) {
|
||||
req.payload.logger.error({ err, msg: `There was an error building form state` })
|
||||
|
||||
if (err.message === 'Could not find field schema for given path') {
|
||||
return Response.json(
|
||||
{
|
||||
message: err.message,
|
||||
},
|
||||
{
|
||||
headers,
|
||||
status: httpStatus.BAD_REQUEST,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (err.message === 'Unauthorized') {
|
||||
return Response.json(null, {
|
||||
headers,
|
||||
status: httpStatus.UNAUTHORIZED,
|
||||
})
|
||||
}
|
||||
|
||||
return routeError({
|
||||
config: req.payload.config,
|
||||
err,
|
||||
req,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import { registerFirstUser } from './auth/registerFirstUser.js'
|
||||
import { resetPassword } from './auth/resetPassword.js'
|
||||
import { unlock } from './auth/unlock.js'
|
||||
import { verifyEmail } from './auth/verifyEmail.js'
|
||||
import { buildFormState } from './buildFormState.js'
|
||||
import { endpointsAreDisabled } from './checkEndpoints.js'
|
||||
import { count } from './collections/count.js'
|
||||
import { create } from './collections/create.js'
|
||||
@@ -110,9 +109,6 @@ const endpoints = {
|
||||
access,
|
||||
og: generateOGImage,
|
||||
},
|
||||
POST: {
|
||||
'form-state': buildFormState,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -575,10 +571,6 @@ export const POST =
|
||||
res = new Response('Route Not Found', { status: 404 })
|
||||
}
|
||||
}
|
||||
} else if (slug.length === 1 && slug1 in endpoints.root.POST) {
|
||||
await addDataAndFileToRequest(req)
|
||||
addLocalesToRequestFromData(req)
|
||||
res = await endpoints.root.POST[slug1]({ req })
|
||||
}
|
||||
|
||||
if (res instanceof Response) {
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import type { MappedComponent } from 'payload'
|
||||
import type { ImportMap, PayloadComponent } from 'payload'
|
||||
|
||||
import { RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import React from 'react'
|
||||
|
||||
export const OGImage: React.FC<{
|
||||
description?: string
|
||||
Fallback: React.ComponentType
|
||||
fontFamily?: string
|
||||
Icon: MappedComponent
|
||||
Icon: PayloadComponent
|
||||
importMap: ImportMap
|
||||
leader?: string
|
||||
title?: string
|
||||
}> = ({ description, fontFamily = 'Arial, sans-serif', Icon, leader, title }) => {
|
||||
}> = ({
|
||||
description,
|
||||
Fallback,
|
||||
fontFamily = 'Arial, sans-serif',
|
||||
Icon,
|
||||
importMap,
|
||||
leader,
|
||||
title,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -85,11 +95,13 @@ export const OGImage: React.FC<{
|
||||
width: '38px',
|
||||
}}
|
||||
>
|
||||
<RenderComponent
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
fill: 'white',
|
||||
}}
|
||||
mappedComponent={Icon}
|
||||
Component={Icon}
|
||||
Fallback={Fallback}
|
||||
importMap={importMap}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
import { getCreateMappedComponent, PayloadIcon } from '@payloadcms/ui/shared'
|
||||
import { PayloadIcon } from '@payloadcms/ui/shared'
|
||||
import fs from 'fs/promises'
|
||||
import { ImageResponse } from 'next/og.js'
|
||||
import { NextResponse } from 'next/server.js'
|
||||
@@ -33,18 +33,6 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
|
||||
const leader = hasLeader ? searchParams.get('leader')?.slice(0, 100).replace('-', ' ') : ''
|
||||
const description = searchParams.has('description') ? searchParams.get('description') : ''
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: req.payload.importMap,
|
||||
serverProps: {},
|
||||
})
|
||||
|
||||
const mappedIcon = createMappedComponent(
|
||||
config.admin?.components?.graphics?.Icon,
|
||||
undefined,
|
||||
PayloadIcon,
|
||||
'config.admin.components.graphics.Icon',
|
||||
)
|
||||
|
||||
let fontData
|
||||
|
||||
try {
|
||||
@@ -62,8 +50,10 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
|
||||
(
|
||||
<OGImage
|
||||
description={description}
|
||||
Fallback={PayloadIcon}
|
||||
fontFamily={fontFamily}
|
||||
Icon={mappedIcon}
|
||||
Icon={config.admin?.components?.graphics?.Icon}
|
||||
importMap={req.payload.importMap}
|
||||
leader={leader}
|
||||
title={title}
|
||||
/>
|
||||
|
||||
@@ -1,73 +1,12 @@
|
||||
import type { Collection, ErrorResult, PayloadRequest, SanitizedConfig } from 'payload'
|
||||
|
||||
import httpStatus from 'http-status'
|
||||
import { APIError, APIErrorName, ValidationErrorName } from 'payload'
|
||||
import { APIError, formatErrors } from 'payload'
|
||||
|
||||
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||
import { headersWithCors } from '../../utilities/headersWithCors.js'
|
||||
import { mergeHeaders } from '../../utilities/mergeHeaders.js'
|
||||
|
||||
const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResult => {
|
||||
if (incoming) {
|
||||
// Cannot use `instanceof` to check error type: https://github.com/microsoft/TypeScript/issues/13965
|
||||
// Instead, get the prototype of the incoming error and check its constructor name
|
||||
const proto = Object.getPrototypeOf(incoming)
|
||||
|
||||
// Payload 'ValidationError' and 'APIError'
|
||||
if (
|
||||
(proto.constructor.name === ValidationErrorName || proto.constructor.name === APIErrorName) &&
|
||||
incoming.data
|
||||
) {
|
||||
return {
|
||||
errors: [
|
||||
{
|
||||
name: incoming.name,
|
||||
data: incoming.data,
|
||||
message: incoming.message,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// Mongoose 'ValidationError': https://mongoosejs.com/docs/api/error.html#Error.ValidationError
|
||||
if (proto.constructor.name === ValidationErrorName && 'errors' in incoming && incoming.errors) {
|
||||
return {
|
||||
errors: Object.keys(incoming.errors).reduce((acc, key) => {
|
||||
acc.push({
|
||||
field: incoming.errors[key].path,
|
||||
message: incoming.errors[key].message,
|
||||
})
|
||||
return acc
|
||||
}, []),
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(incoming.message)) {
|
||||
return {
|
||||
errors: incoming.message,
|
||||
}
|
||||
}
|
||||
|
||||
if (incoming.name) {
|
||||
return {
|
||||
errors: [
|
||||
{
|
||||
message: incoming.message,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errors: [
|
||||
{
|
||||
message: 'An unknown error occurred.',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export const routeError = async ({
|
||||
collection,
|
||||
config: configArg,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import type { MappedComponent, ServerProps, VisibleEntities } from 'payload'
|
||||
import type { CustomComponent, ServerProps, VisibleEntities } from 'payload'
|
||||
|
||||
import { AppHeader, BulkUploadProvider, EntityVisibilityProvider, NavToggler } from '@payloadcms/ui'
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import {
|
||||
ActionsProvider,
|
||||
AppHeader,
|
||||
BulkUploadProvider,
|
||||
EntityVisibilityProvider,
|
||||
NavToggler,
|
||||
} from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import React from 'react'
|
||||
|
||||
import { DefaultNav } from '../../elements/Nav/index.js'
|
||||
@@ -14,6 +20,7 @@ const baseClass = 'template-default'
|
||||
export type DefaultTemplateProps = {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
viewActions?: CustomComponent[]
|
||||
visibleEntities: VisibleEntities
|
||||
} & ServerProps
|
||||
|
||||
@@ -27,10 +34,13 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
viewActions,
|
||||
visibleEntities,
|
||||
}) => {
|
||||
const {
|
||||
admin: {
|
||||
avatar,
|
||||
components,
|
||||
components: { header: CustomHeader, Nav: CustomNav } = {
|
||||
header: undefined,
|
||||
Nav: undefined,
|
||||
@@ -38,54 +48,98 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
} = {},
|
||||
} = payload.config || {}
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
const { Actions } = React.useMemo<{
|
||||
Actions: Record<string, React.ReactNode>
|
||||
}>(() => {
|
||||
return {
|
||||
Actions: viewActions
|
||||
? viewActions.reduce((acc, action) => {
|
||||
if (action) {
|
||||
if (typeof action === 'object') {
|
||||
acc[action.path] = (
|
||||
<RenderServerComponent Component={action} importMap={payload.importMap} />
|
||||
)
|
||||
} else {
|
||||
acc[action] = (
|
||||
<RenderServerComponent Component={action} importMap={payload.importMap} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const MappedDefaultNav: MappedComponent = createMappedComponent(
|
||||
CustomNav,
|
||||
undefined,
|
||||
DefaultNav,
|
||||
'CustomNav',
|
||||
)
|
||||
|
||||
const MappedCustomHeader = createMappedComponent(
|
||||
CustomHeader,
|
||||
undefined,
|
||||
undefined,
|
||||
'CustomHeader',
|
||||
)
|
||||
return acc
|
||||
}, {})
|
||||
: undefined,
|
||||
}
|
||||
}, [viewActions, payload])
|
||||
|
||||
return (
|
||||
<EntityVisibilityProvider visibleEntities={visibleEntities}>
|
||||
<BulkUploadProvider>
|
||||
<RenderComponent mappedComponent={MappedCustomHeader} />
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
|
||||
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
|
||||
<NavToggler className={`${baseClass}__nav-toggler`}>
|
||||
<NavHamburger />
|
||||
</NavToggler>
|
||||
<ActionsProvider Actions={Actions}>
|
||||
<RenderServerComponent
|
||||
clientProps={{ clientProps: { visibleEntities } }}
|
||||
Component={CustomHeader}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
}}
|
||||
/>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
|
||||
<div className={`${baseClass}__nav-toggler-container`} id="nav-toggler">
|
||||
<NavToggler className={`${baseClass}__nav-toggler`}>
|
||||
<NavHamburger />
|
||||
</NavToggler>
|
||||
</div>
|
||||
</div>
|
||||
<Wrapper baseClass={baseClass} className={className}>
|
||||
<RenderServerComponent
|
||||
clientProps={{ clientProps: { visibleEntities } }}
|
||||
Component={CustomNav}
|
||||
Fallback={DefaultNav}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
}}
|
||||
/>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<AppHeader
|
||||
CustomAvatar={
|
||||
avatar !== 'gravatar' && avatar !== 'default' ? (
|
||||
<RenderServerComponent
|
||||
Component={avatar.Component}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
CustomIcon={
|
||||
components?.graphics?.Icon ? (
|
||||
<RenderServerComponent
|
||||
Component={components.graphics.Icon}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
<Wrapper baseClass={baseClass} className={className}>
|
||||
<RenderComponent mappedComponent={MappedDefaultNav} />
|
||||
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<AppHeader />
|
||||
{children}
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
</ActionsProvider>
|
||||
</BulkUploadProvider>
|
||||
</EntityVisibilityProvider>
|
||||
)
|
||||
|
||||
18
packages/next/src/utilities/getClientConfig.ts
Normal file
18
packages/next/src/utilities/getClientConfig.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ClientConfig, SanitizedConfig } from 'payload'
|
||||
|
||||
import { createClientConfig } from 'payload'
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getClientConfig = cache(
|
||||
async (args: { config: SanitizedConfig; i18n: I18nClient }): Promise<ClientConfig> => {
|
||||
const { config, i18n } = args
|
||||
|
||||
const clientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
return Promise.resolve(clientConfig)
|
||||
},
|
||||
)
|
||||
37
packages/next/src/utilities/handleServerFunctions.ts
Normal file
37
packages/next/src/utilities/handleServerFunctions.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { ServerFunction, ServerFunctionHandler } from 'payload'
|
||||
|
||||
import { buildFormStateHandler } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { buildTableStateHandler } from '@payloadcms/ui/utilities/buildTableState'
|
||||
|
||||
import { renderDocumentHandler } from '../views/Document/handleServerFunction.js'
|
||||
import { renderDocumentSlotsHandler } from '../views/Document/renderDocumentSlots.js'
|
||||
import { renderListHandler } from '../views/List/handleServerFunction.js'
|
||||
import { initReq } from './initReq.js'
|
||||
|
||||
export const handleServerFunctions: ServerFunctionHandler = async (args) => {
|
||||
const { name: fnKey, args: fnArgs, config: configPromise, importMap } = args
|
||||
|
||||
const { req } = await initReq(configPromise)
|
||||
|
||||
const augmentedArgs: Parameters<ServerFunction>[0] = {
|
||||
...fnArgs,
|
||||
importMap,
|
||||
req,
|
||||
}
|
||||
|
||||
const serverFunctions = {
|
||||
'form-state': buildFormStateHandler as any as ServerFunction,
|
||||
'render-document': renderDocumentHandler as any as ServerFunction,
|
||||
'render-document-slots': renderDocumentSlotsHandler as any as ServerFunction,
|
||||
'render-list': renderListHandler as any as ServerFunction,
|
||||
'table-state': buildTableStateHandler as any as ServerFunction,
|
||||
}
|
||||
|
||||
const fn = serverFunctions[fnKey]
|
||||
|
||||
if (!fn) {
|
||||
throw new Error(`Unknown Server Function: ${fnKey}`)
|
||||
}
|
||||
|
||||
return fn(augmentedArgs)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { InitPageResult, Locale, PayloadRequest, VisibleEntities } from 'payload'
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { InitPageResult, Locale, VisibleEntities } from 'payload'
|
||||
|
||||
import { findLocaleFromCode } from '@payloadcms/ui/shared'
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
@@ -47,13 +48,13 @@ export const initPage = async ({
|
||||
req: {
|
||||
headers,
|
||||
host: headers.get('host'),
|
||||
i18n,
|
||||
i18n: i18n as I18n,
|
||||
query: qs.parse(queryString, {
|
||||
depth: 10,
|
||||
ignoreQueryPrefix: true,
|
||||
}),
|
||||
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
|
||||
} as PayloadRequest,
|
||||
},
|
||||
},
|
||||
payload,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { I18n, I18nClient } from '@payloadcms/translations'
|
||||
import type { PayloadRequest, Permissions, SanitizedConfig, User } from 'payload'
|
||||
|
||||
import { initI18n } from '@payloadcms/translations'
|
||||
@@ -16,7 +16,10 @@ type Result = {
|
||||
user: User
|
||||
}
|
||||
|
||||
export const initReq = cache(async function (config: SanitizedConfig): Promise<Result> {
|
||||
export const initReq = cache(async function (
|
||||
configPromise: Promise<SanitizedConfig> | SanitizedConfig,
|
||||
): Promise<Result> {
|
||||
const config = await configPromise
|
||||
const payload = await getPayloadHMR({ config })
|
||||
|
||||
const headers = await getHeaders()
|
||||
@@ -40,9 +43,9 @@ export const initReq = cache(async function (config: SanitizedConfig): Promise<R
|
||||
req: {
|
||||
headers,
|
||||
host: headers.get('host'),
|
||||
i18n,
|
||||
i18n: i18n as I18n,
|
||||
url: `${payload.config.serverURL}`,
|
||||
} as PayloadRequest,
|
||||
},
|
||||
},
|
||||
payload,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,6 @@ export const LocaleSelector: React.FC<{
|
||||
<SelectField
|
||||
field={{
|
||||
name: 'locale',
|
||||
_path: 'locale',
|
||||
label: t('general:locale'),
|
||||
options: localeOptions,
|
||||
}}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Gutter,
|
||||
MinimizeMaximizeIcon,
|
||||
NumberField,
|
||||
SetViewActions,
|
||||
SetDocumentStepNav,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useLocale,
|
||||
@@ -19,7 +19,6 @@ import { useSearchParams } from 'next/navigation.js'
|
||||
import * as React from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||
import './index.scss'
|
||||
import { LocaleSelector } from './LocaleSelector/index.js'
|
||||
import { RenderJSON } from './RenderJSON/index.js'
|
||||
@@ -42,8 +41,8 @@ export const APIViewClient: React.FC = () => {
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const collectionClientConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
const globalClientConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||
|
||||
const localeOptions =
|
||||
localization &&
|
||||
@@ -52,13 +51,13 @@ export const APIViewClient: React.FC = () => {
|
||||
let draftsEnabled: boolean = false
|
||||
let docEndpoint: string = ''
|
||||
|
||||
if (collectionClientConfig) {
|
||||
draftsEnabled = Boolean(collectionClientConfig.versions?.drafts)
|
||||
if (collectionConfig) {
|
||||
draftsEnabled = Boolean(collectionConfig.versions?.drafts)
|
||||
docEndpoint = `/${collectionSlug}/${id}`
|
||||
}
|
||||
|
||||
if (globalClientConfig) {
|
||||
draftsEnabled = Boolean(globalClientConfig.versions?.drafts)
|
||||
if (globalConfig) {
|
||||
draftsEnabled = Boolean(globalConfig.versions?.drafts)
|
||||
docEndpoint = `/globals/${globalSlug}`
|
||||
}
|
||||
|
||||
@@ -111,19 +110,13 @@ export const APIViewClient: React.FC = () => {
|
||||
>
|
||||
<SetDocumentStepNav
|
||||
collectionSlug={collectionSlug}
|
||||
globalLabel={globalClientConfig?.label}
|
||||
globalLabel={globalConfig?.label}
|
||||
globalSlug={globalSlug}
|
||||
id={id}
|
||||
pluralLabel={collectionClientConfig ? collectionClientConfig?.labels?.plural : undefined}
|
||||
useAsTitle={collectionClientConfig ? collectionClientConfig?.admin?.useAsTitle : undefined}
|
||||
pluralLabel={collectionConfig ? collectionConfig?.labels?.plural : undefined}
|
||||
useAsTitle={collectionConfig ? collectionConfig?.admin?.useAsTitle : undefined}
|
||||
view="API"
|
||||
/>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionClientConfig || globalClientConfig)?.admin?.components?.views?.edit?.api
|
||||
?.actions
|
||||
}
|
||||
/>
|
||||
<div className={`${baseClass}__configuration`}>
|
||||
<div className={`${baseClass}__api-url`}>
|
||||
<span className={`${baseClass}__label`}>
|
||||
|
||||
@@ -22,7 +22,7 @@ export const Settings: React.FC<{
|
||||
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||
<h3>{i18n.t('general:payloadSettings')}</h3>
|
||||
<div className={`${baseClass}__language`}>
|
||||
<FieldLabel field={null} htmlFor="language-select" label={i18n.t('general:language')} />
|
||||
<FieldLabel htmlFor="language-select" label={i18n.t('general:language')} />
|
||||
<LanguageSelector languageOptions={languageOptions} />
|
||||
</div>
|
||||
{theme === 'all' && <ToggleTheme />}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import {
|
||||
DocumentInfoProvider,
|
||||
EditDepthProvider,
|
||||
HydrateAuthProvider,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui'
|
||||
import { getCreateMappedComponent } from '@payloadcms/ui/shared'
|
||||
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
|
||||
import { getDocPreferences } from '../Document/getDocPreferences.js'
|
||||
import { getDocumentData } from '../Document/getDocumentData.js'
|
||||
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
||||
import { getIsLocked } from '../Document/getIsLocked.js'
|
||||
import { getVersions } from '../Document/getVersions.js'
|
||||
import { EditView } from '../Edit/index.js'
|
||||
import { AccountClient } from './index.client.js'
|
||||
import { Settings } from './Settings/index.js'
|
||||
@@ -50,57 +49,91 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
const collectionConfig = config.collections.find((collection) => collection.slug === userSlug)
|
||||
|
||||
if (collectionConfig && user?.id) {
|
||||
// Fetch the data required for the view
|
||||
const data = await getDocumentData({
|
||||
id: user.id,
|
||||
collectionSlug: collectionConfig.slug,
|
||||
locale,
|
||||
payload,
|
||||
user,
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
// Get document preferences
|
||||
const docPreferences = await getDocPreferences({
|
||||
id: user.id,
|
||||
collectionSlug: collectionConfig.slug,
|
||||
payload,
|
||||
user,
|
||||
})
|
||||
|
||||
// Get permissions
|
||||
const { docPermissions, hasPublishPermission, hasSavePermission } =
|
||||
await getDocumentPermissions({
|
||||
id: user.id,
|
||||
collectionConfig,
|
||||
data: user,
|
||||
data,
|
||||
req,
|
||||
})
|
||||
|
||||
const { data, formState } = await getDocumentData({
|
||||
// Build initial form state from data
|
||||
const { state: formState } = await buildFormState({
|
||||
id: user.id,
|
||||
collectionSlug: collectionConfig.slug,
|
||||
data,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
locale: locale?.code,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: collectionConfig.slug,
|
||||
})
|
||||
|
||||
// Fetch document lock state
|
||||
const { currentEditor, isLocked, lastUpdateTime } = await getIsLocked({
|
||||
id: user.id,
|
||||
collectionConfig,
|
||||
locale,
|
||||
req,
|
||||
isEditing: true,
|
||||
payload: req.payload,
|
||||
user,
|
||||
})
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
// Get all versions required for UI
|
||||
const { hasPublishedDoc, mostRecentVersionIsAutosaved, unpublishedVersionCount, versionCount } =
|
||||
await getVersions({
|
||||
id: user.id,
|
||||
collectionConfig,
|
||||
docPermissions,
|
||||
locale: locale?.code,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: [],
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedAccountComponent = createMappedComponent(
|
||||
CustomAccountComponent?.Component,
|
||||
undefined,
|
||||
EditView,
|
||||
'CustomAccountComponent.Component',
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} theme={theme} />}
|
||||
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
||||
collectionSlug={userSlug}
|
||||
currentEditor={currentEditor}
|
||||
docPermissions={docPermissions}
|
||||
hasPublishedDoc={hasPublishedDoc}
|
||||
hasPublishPermission={hasPublishPermission}
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={user?.id}
|
||||
initialData={data}
|
||||
initialState={formState}
|
||||
isEditing
|
||||
isLocked={isLocked}
|
||||
lastUpdateTime={lastUpdateTime}
|
||||
mostRecentVersionIsAutosaved={mostRecentVersionIsAutosaved}
|
||||
unpublishedVersionCount={unpublishedVersionCount}
|
||||
versionCount={versionCount}
|
||||
>
|
||||
<EditDepthProvider depth={1}>
|
||||
<EditDepthProvider>
|
||||
<DocumentHeader
|
||||
collectionConfig={collectionConfig}
|
||||
hideTabs
|
||||
@@ -109,7 +142,22 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
permissions={permissions}
|
||||
/>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<RenderComponent mappedComponent={mappedAccountComponent} />
|
||||
<RenderServerComponent
|
||||
Component={CustomAccountComponent}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: [],
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
<EditView />
|
||||
<AccountClient />
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
'use client'
|
||||
import type { FormProps, UserWithToken } from '@payloadcms/ui'
|
||||
import type { ClientCollectionConfig, FormState, LoginWithUsernameOptions } from 'payload'
|
||||
import type {
|
||||
ClientCollectionConfig,
|
||||
DocumentPermissions,
|
||||
DocumentPreferences,
|
||||
FormState,
|
||||
LoginWithUsernameOptions,
|
||||
} from 'payload'
|
||||
|
||||
import {
|
||||
ConfirmPasswordField,
|
||||
EmailAndUsernameFields,
|
||||
Form,
|
||||
FormSubmit,
|
||||
PasswordField,
|
||||
RenderFields,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { RenderEmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js'
|
||||
|
||||
export const CreateFirstUserClient: React.FC<{
|
||||
docPermissions: DocumentPermissions
|
||||
docPreferences: DocumentPreferences
|
||||
initialState: FormState
|
||||
loginWithUsername?: false | LoginWithUsernameOptions
|
||||
userSlug: string
|
||||
}> = ({ initialState, loginWithUsername, userSlug }) => {
|
||||
}> = ({ docPermissions, docPreferences, initialState, loginWithUsername, userSlug }) => {
|
||||
const {
|
||||
config: {
|
||||
routes: { admin, api: apiRoute },
|
||||
@@ -30,6 +37,8 @@ export const CreateFirstUserClient: React.FC<{
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { setUser } = useAuth()
|
||||
|
||||
@@ -38,18 +47,17 @@ export const CreateFirstUserClient: React.FC<{
|
||||
const onChange: FormProps['onChange'][0] = React.useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
const { state } = await getFormState({
|
||||
apiRoute,
|
||||
body: {
|
||||
collectionSlug: userSlug,
|
||||
formState: prevFormState,
|
||||
operation: 'create',
|
||||
schemaPath: `_${userSlug}.auth`,
|
||||
},
|
||||
serverURL,
|
||||
collectionSlug: userSlug,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
formState: prevFormState,
|
||||
operation: 'create',
|
||||
schemaPath: `_${userSlug}.auth`,
|
||||
})
|
||||
|
||||
return state
|
||||
},
|
||||
[apiRoute, userSlug, serverURL],
|
||||
[userSlug, getFormState, docPermissions, docPreferences],
|
||||
)
|
||||
|
||||
const handleFirstRegister = (data: UserWithToken) => {
|
||||
@@ -66,14 +74,15 @@ export const CreateFirstUserClient: React.FC<{
|
||||
redirect={admin}
|
||||
validationOperation="create"
|
||||
>
|
||||
<RenderEmailAndUsernameFields
|
||||
<EmailAndUsernameFields
|
||||
className="emailAndUsername"
|
||||
loginWithUsername={loginWithUsername}
|
||||
operation="create"
|
||||
readOnly={false}
|
||||
t={t}
|
||||
/>
|
||||
<PasswordField
|
||||
autoComplete={'off'}
|
||||
autoComplete="off"
|
||||
field={{
|
||||
name: 'password',
|
||||
label: t('authentication:newPassword'),
|
||||
@@ -84,10 +93,11 @@ export const CreateFirstUserClient: React.FC<{
|
||||
<RenderFields
|
||||
fields={collectionConfig.fields}
|
||||
forceRender
|
||||
operation="create"
|
||||
path=""
|
||||
parentIndexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath={userSlug}
|
||||
permissions={null}
|
||||
readOnly={false}
|
||||
schemaPath={userSlug}
|
||||
/>
|
||||
<FormSubmit size="large">{t('general:create')}</FormSubmit>
|
||||
</Form>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import React from 'react'
|
||||
|
||||
import { getDocPreferences } from '../Document/getDocPreferences.js'
|
||||
import { getDocumentData } from '../Document/getDocumentData.js'
|
||||
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
||||
import { CreateFirstUserClient } from './index.client.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -26,11 +29,39 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
const { auth: authOptions } = collectionConfig
|
||||
const loginWithUsername = authOptions.loginWithUsername
|
||||
|
||||
const { formState } = await getDocumentData({
|
||||
collectionConfig,
|
||||
// Fetch the data required for the view
|
||||
const data = await getDocumentData({
|
||||
collectionSlug: collectionConfig.slug,
|
||||
locale,
|
||||
payload: req.payload,
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
// Get document preferences
|
||||
const docPreferences = await getDocPreferences({
|
||||
collectionSlug: collectionConfig.slug,
|
||||
payload: req.payload,
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
// Get permissions
|
||||
const { docPermissions } = await getDocumentPermissions({
|
||||
collectionConfig,
|
||||
data,
|
||||
req,
|
||||
schemaPath: `_${collectionConfig.slug}.auth`,
|
||||
})
|
||||
|
||||
// Build initial form state from data
|
||||
const { state: formState } = await buildFormState({
|
||||
collectionSlug: collectionConfig.slug,
|
||||
data,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
locale: locale?.code,
|
||||
operation: 'create',
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: collectionConfig.slug,
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -38,6 +69,8 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
<h1>{req.t('general:welcome')}</h1>
|
||||
<p>{req.t('authentication:beginCreateFirstUser')}</p>
|
||||
<CreateFirstUserClient
|
||||
docPermissions={docPermissions}
|
||||
docPreferences={docPreferences}
|
||||
initialState={formState}
|
||||
loginWithUsername={loginWithUsername}
|
||||
userSlug={userSlug}
|
||||
|
||||
@@ -2,13 +2,9 @@ import type { groupNavItems } from '@payloadcms/ui/shared'
|
||||
import type { ClientUser, Permissions, ServerProps, VisibleEntities } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { Button, Card, Gutter, Locked, SetStepNav, SetViewActions } from '@payloadcms/ui'
|
||||
import {
|
||||
EntityType,
|
||||
formatAdminURL,
|
||||
getCreateMappedComponent,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { Button, Card, Gutter, Locked } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
@@ -50,41 +46,25 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
user,
|
||||
} = props
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedBeforeDashboards = createMappedComponent(
|
||||
beforeDashboard,
|
||||
undefined,
|
||||
undefined,
|
||||
'beforeDashboard',
|
||||
)
|
||||
|
||||
const mappedAfterDashboards = createMappedComponent(
|
||||
afterDashboard,
|
||||
undefined,
|
||||
undefined,
|
||||
'afterDashboard',
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SetStepNav nav={[]} />
|
||||
<SetViewActions actions={[]} />
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
<RenderComponent mappedComponent={mappedBeforeDashboards} />
|
||||
{beforeDashboard && (
|
||||
<RenderServerComponent
|
||||
Component={beforeDashboard}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Fragment>
|
||||
<SetViewActions actions={[]} />
|
||||
{!navGroups || navGroups?.length === 0 ? (
|
||||
<p>no nav groups....</p>
|
||||
) : (
|
||||
@@ -93,7 +73,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
<div className={`${baseClass}__group`} key={groupIndex}>
|
||||
<h2 className={`${baseClass}__label`}>{label}</h2>
|
||||
<ul className={`${baseClass}__card-list`}>
|
||||
{entities.map(({ type, entity }, entityIndex) => {
|
||||
{entities.map(({ slug, type, label }, entityIndex) => {
|
||||
let title: string
|
||||
let buttonAriaLabel: string
|
||||
let createHREF: string
|
||||
@@ -103,38 +83,34 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
let userEditing = null
|
||||
|
||||
if (type === EntityType.collection) {
|
||||
title = getTranslation(entity.labels.plural, i18n)
|
||||
title = getTranslation(label, i18n)
|
||||
|
||||
buttonAriaLabel = t('general:showAllLabel', { label: title })
|
||||
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` })
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${slug}` })
|
||||
|
||||
createHREF = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${entity.slug}/create`,
|
||||
path: `/collections/${slug}/create`,
|
||||
})
|
||||
|
||||
hasCreatePermission =
|
||||
permissions?.collections?.[entity.slug]?.create?.permission
|
||||
hasCreatePermission = permissions?.collections?.[slug]?.create?.permission
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
title = getTranslation(entity.label, i18n)
|
||||
title = getTranslation(label, i18n)
|
||||
|
||||
buttonAriaLabel = t('general:editLabel', {
|
||||
label: getTranslation(entity.label, i18n),
|
||||
label: getTranslation(label, i18n),
|
||||
})
|
||||
|
||||
href = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${entity.slug}`,
|
||||
path: `/globals/${slug}`,
|
||||
})
|
||||
|
||||
// Find the lock status for the global
|
||||
const globalLockData = globalData.find(
|
||||
(global) => global.slug === entity.slug,
|
||||
)
|
||||
|
||||
const globalLockData = globalData.find((global) => global.slug === slug)
|
||||
if (globalLockData) {
|
||||
isLocked = globalLockData.data._isLocked
|
||||
userEditing = globalLockData.data._userEditing
|
||||
@@ -164,7 +140,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
) : hasCreatePermission && type === EntityType.collection ? (
|
||||
<Button
|
||||
aria-label={t('general:createNewLabel', {
|
||||
label: getTranslation(entity.labels.singular, i18n),
|
||||
label,
|
||||
})}
|
||||
buttonStyle="icon-label"
|
||||
el="link"
|
||||
@@ -178,9 +154,9 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
}
|
||||
buttonAriaLabel={buttonAriaLabel}
|
||||
href={href}
|
||||
id={`card-${entity.slug}`}
|
||||
id={`card-${slug}`}
|
||||
Link={Link}
|
||||
title={title}
|
||||
title={getTranslation(label, i18n)}
|
||||
titleAs="h3"
|
||||
/>
|
||||
</li>
|
||||
@@ -192,7 +168,21 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
})
|
||||
)}
|
||||
</Fragment>
|
||||
<RenderComponent mappedComponent={mappedAfterDashboards} />
|
||||
{afterDashboard && (
|
||||
<RenderServerComponent
|
||||
Component={afterDashboard}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Gutter>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import {
|
||||
EntityType,
|
||||
getCreateMappedComponent,
|
||||
groupNavItems,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { HydrateAuthProvider, SetStepNav } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
@@ -111,39 +107,31 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
|
||||
i18n,
|
||||
)
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
globalData,
|
||||
i18n,
|
||||
Link,
|
||||
locale,
|
||||
navGroups,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedDashboardComponent = createMappedComponent(
|
||||
CustomDashboardComponent?.Component,
|
||||
undefined,
|
||||
DefaultDashboard,
|
||||
'CustomDashboardComponent.Component',
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<RenderComponent
|
||||
<SetStepNav nav={[]} />
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
Link,
|
||||
locale,
|
||||
}}
|
||||
mappedComponent={mappedDashboardComponent}
|
||||
Component={CustomDashboardComponent}
|
||||
Fallback={DefaultDashboard}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
globalData,
|
||||
i18n,
|
||||
Link,
|
||||
locale,
|
||||
navGroups,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
visibleEntities,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
60
packages/next/src/views/Document/getDocPreferences.ts
Normal file
60
packages/next/src/views/Document/getDocPreferences.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { DocumentPreferences, Payload, TypedUser } from 'payload'
|
||||
|
||||
type Args = {
|
||||
collectionSlug?: string
|
||||
globalSlug?: string
|
||||
id?: number | string
|
||||
payload: Payload
|
||||
user: TypedUser
|
||||
}
|
||||
|
||||
export const getDocPreferences = async ({
|
||||
id,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
payload,
|
||||
user,
|
||||
}: Args): Promise<DocumentPreferences> => {
|
||||
let preferencesKey
|
||||
|
||||
if (collectionSlug && id) {
|
||||
preferencesKey = `collection-${collectionSlug}-${id}`
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
preferencesKey = `global-${globalSlug}`
|
||||
}
|
||||
|
||||
if (preferencesKey) {
|
||||
const preferencesResult = (await payload.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: preferencesKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: user.collection,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})) as unknown as { docs: { value: DocumentPreferences }[] }
|
||||
|
||||
if (preferencesResult?.docs?.[0]?.value) {
|
||||
return preferencesResult.docs[0].value
|
||||
}
|
||||
}
|
||||
|
||||
return { fields: {} }
|
||||
}
|
||||
52
packages/next/src/views/Document/getDocumentData.ts
Normal file
52
packages/next/src/views/Document/getDocumentData.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Locale, Payload, TypedUser, TypeWithID } from 'payload'
|
||||
|
||||
type Args = {
|
||||
collectionSlug?: string
|
||||
globalSlug?: string
|
||||
id?: number | string
|
||||
locale?: Locale
|
||||
payload: Payload
|
||||
user?: TypedUser
|
||||
}
|
||||
|
||||
export const getDocumentData = async ({
|
||||
id,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
user,
|
||||
}: Args): Promise<null | Record<string, unknown> | TypeWithID> => {
|
||||
let resolvedData: Record<string, unknown> | TypeWithID = null
|
||||
|
||||
try {
|
||||
if (collectionSlug && id) {
|
||||
resolvedData = await payload.findByID({
|
||||
id,
|
||||
collection: collectionSlug,
|
||||
depth: 0,
|
||||
draft: true,
|
||||
fallbackLocale: null,
|
||||
locale: locale?.code,
|
||||
overrideAccess: false,
|
||||
user,
|
||||
})
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
resolvedData = await payload.findGlobal({
|
||||
slug: globalSlug,
|
||||
depth: 0,
|
||||
draft: true,
|
||||
fallbackLocale: null,
|
||||
locale: locale?.code,
|
||||
overrideAccess: false,
|
||||
user,
|
||||
})
|
||||
}
|
||||
} catch (_err) {
|
||||
payload.logger.error(_err)
|
||||
}
|
||||
|
||||
return resolvedData
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import type {
|
||||
Data,
|
||||
FormState,
|
||||
Locale,
|
||||
PayloadRequest,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { reduceFieldsToValues } from 'payload/shared'
|
||||
|
||||
export const getDocumentData = async (args: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
locale: Locale
|
||||
req: PayloadRequest
|
||||
schemaPath?: string
|
||||
}): Promise<{
|
||||
data: Data
|
||||
formState: FormState
|
||||
}> => {
|
||||
const { id, collectionConfig, globalConfig, locale, req, schemaPath: schemaPathFromProps } = args
|
||||
|
||||
const schemaPath = schemaPathFromProps || collectionConfig?.slug || globalConfig?.slug
|
||||
|
||||
try {
|
||||
const { state: formState } = await buildFormState({
|
||||
req: {
|
||||
...req,
|
||||
data: {
|
||||
id,
|
||||
collectionSlug: collectionConfig?.slug,
|
||||
globalSlug: globalConfig?.slug,
|
||||
locale: locale?.code,
|
||||
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
|
||||
schemaPath,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const data = reduceFieldsToValues(formState, true)
|
||||
|
||||
return {
|
||||
data,
|
||||
formState,
|
||||
}
|
||||
} catch (error) {
|
||||
req.payload.logger.error({ err: error, msg: 'Error getting document data' })
|
||||
return {
|
||||
data: null,
|
||||
formState: {
|
||||
fields: {
|
||||
initialValue: undefined,
|
||||
valid: false,
|
||||
value: undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
86
packages/next/src/views/Document/getIsLocked.ts
Normal file
86
packages/next/src/views/Document/getIsLocked.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type {
|
||||
Payload,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedGlobalConfig,
|
||||
TypedUser,
|
||||
Where,
|
||||
} from 'payload'
|
||||
|
||||
type Args = {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
isEditing: boolean
|
||||
payload: Payload
|
||||
user: TypedUser
|
||||
}
|
||||
|
||||
type Result = Promise<{
|
||||
currentEditor?: TypedUser
|
||||
isLocked: boolean
|
||||
lastUpdateTime?: number
|
||||
}>
|
||||
|
||||
export const getIsLocked = async ({
|
||||
id,
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
isEditing,
|
||||
payload,
|
||||
user,
|
||||
}: Args): Result => {
|
||||
const entityConfig = collectionConfig || globalConfig
|
||||
|
||||
const entityHasLockingEnabled =
|
||||
entityConfig?.lockDocuments !== undefined ? entityConfig?.lockDocuments : true
|
||||
|
||||
if (!entityHasLockingEnabled || !isEditing) {
|
||||
return {
|
||||
isLocked: false,
|
||||
}
|
||||
}
|
||||
|
||||
const where: Where = {}
|
||||
|
||||
if (globalConfig) {
|
||||
where.globalSlug = {
|
||||
equals: globalConfig.slug,
|
||||
}
|
||||
} else {
|
||||
where.and = [
|
||||
{
|
||||
'document.value': {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'document.relationTo': {
|
||||
equals: collectionConfig.slug,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const { docs } = await payload.find({
|
||||
collection: 'payload-locked-documents',
|
||||
depth: 1,
|
||||
where,
|
||||
})
|
||||
|
||||
if (docs.length > 0) {
|
||||
const newEditor = docs[0].user?.value
|
||||
const lastUpdateTime = new Date(docs[0].updatedAt).getTime()
|
||||
|
||||
if (newEditor?.id !== user.id) {
|
||||
return {
|
||||
currentEditor: newEditor,
|
||||
isLocked: true,
|
||||
lastUpdateTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isLocked: false,
|
||||
}
|
||||
}
|
||||
240
packages/next/src/views/Document/getVersions.ts
Normal file
240
packages/next/src/views/Document/getVersions.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import type {
|
||||
DocumentPermissions,
|
||||
Payload,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedGlobalConfig,
|
||||
TypedUser,
|
||||
} from 'payload'
|
||||
|
||||
type Args = {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
docPermissions: DocumentPermissions
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
locale?: string
|
||||
payload: Payload
|
||||
user: TypedUser
|
||||
}
|
||||
|
||||
type Result = Promise<{
|
||||
hasPublishedDoc: boolean
|
||||
mostRecentVersionIsAutosaved: boolean
|
||||
unpublishedVersionCount: number
|
||||
versionCount: number
|
||||
}>
|
||||
|
||||
// TODO: in the future, we can parallelize some of these queries
|
||||
// this will speed up the API by ~30-100ms or so
|
||||
export const getVersions = async ({
|
||||
id,
|
||||
collectionConfig,
|
||||
docPermissions,
|
||||
globalConfig,
|
||||
locale,
|
||||
payload,
|
||||
user,
|
||||
}: Args): Result => {
|
||||
let publishedQuery
|
||||
let hasPublishedDoc = false
|
||||
let mostRecentVersionIsAutosaved = false
|
||||
let unpublishedVersionCount = 0
|
||||
let versionCount = 0
|
||||
|
||||
const entityConfig = collectionConfig || globalConfig
|
||||
const versionsConfig = entityConfig?.versions
|
||||
|
||||
const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions?.permission)
|
||||
|
||||
if (!shouldFetchVersions) {
|
||||
const hasPublishedDoc = Boolean((collectionConfig && id) || globalConfig)
|
||||
|
||||
return {
|
||||
hasPublishedDoc,
|
||||
mostRecentVersionIsAutosaved,
|
||||
unpublishedVersionCount,
|
||||
versionCount,
|
||||
}
|
||||
}
|
||||
|
||||
if (collectionConfig) {
|
||||
if (!id) {
|
||||
return {
|
||||
hasPublishedDoc,
|
||||
mostRecentVersionIsAutosaved,
|
||||
unpublishedVersionCount,
|
||||
versionCount,
|
||||
}
|
||||
}
|
||||
|
||||
if (versionsConfig?.drafts) {
|
||||
publishedQuery = await payload.find({
|
||||
collection: collectionConfig.slug,
|
||||
depth: 0,
|
||||
locale: locale || undefined,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
or: [
|
||||
{
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
},
|
||||
{
|
||||
_status: {
|
||||
exists: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (publishedQuery.docs?.[0]) {
|
||||
hasPublishedDoc = true
|
||||
}
|
||||
|
||||
if (versionsConfig.drafts?.autosave) {
|
||||
const mostRecentVersion = await payload.findVersions({
|
||||
collection: collectionConfig.slug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (
|
||||
mostRecentVersion.docs[0] &&
|
||||
'autosave' in mostRecentVersion.docs[0] &&
|
||||
mostRecentVersion.docs[0].autosave
|
||||
) {
|
||||
mostRecentVersionIsAutosaved = true
|
||||
}
|
||||
}
|
||||
|
||||
if (publishedQuery.docs?.[0]?.updatedAt) {
|
||||
;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({
|
||||
collection: collectionConfig.slug,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'version._status': {
|
||||
equals: 'draft',
|
||||
},
|
||||
},
|
||||
{
|
||||
updatedAt: {
|
||||
greater_than: publishedQuery.docs[0].updatedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
;({ totalDocs: versionCount } = await payload.countVersions({
|
||||
collection: collectionConfig.slug,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
if (versionsConfig?.drafts) {
|
||||
publishedQuery = await payload.findGlobal({
|
||||
slug: globalConfig.slug,
|
||||
depth: 0,
|
||||
locale,
|
||||
user,
|
||||
})
|
||||
|
||||
if (publishedQuery?._status === 'published') {
|
||||
hasPublishedDoc = true
|
||||
}
|
||||
|
||||
if (versionsConfig.drafts?.autosave) {
|
||||
const mostRecentVersion = await payload.findGlobalVersions({
|
||||
slug: globalConfig.slug,
|
||||
limit: 1,
|
||||
select: {
|
||||
autosave: true,
|
||||
},
|
||||
user,
|
||||
})
|
||||
|
||||
if (
|
||||
mostRecentVersion.docs[0] &&
|
||||
'autosave' in mostRecentVersion.docs[0] &&
|
||||
mostRecentVersion.docs[0].autosave
|
||||
) {
|
||||
mostRecentVersionIsAutosaved = true
|
||||
}
|
||||
}
|
||||
|
||||
if (publishedQuery?.updatedAt) {
|
||||
;({ totalDocs: unpublishedVersionCount } = await payload.countGlobalVersions({
|
||||
depth: 0,
|
||||
global: globalConfig.slug,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
'version._status': {
|
||||
equals: 'draft',
|
||||
},
|
||||
},
|
||||
{
|
||||
updatedAt: {
|
||||
greater_than: publishedQuery.updatedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
;({ totalDocs: versionCount } = await payload.countGlobalVersions({
|
||||
depth: 0,
|
||||
global: globalConfig.slug,
|
||||
user,
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
hasPublishedDoc,
|
||||
mostRecentVersionIsAutosaved,
|
||||
unpublishedVersionCount,
|
||||
versionCount,
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import type {
|
||||
} from 'payload'
|
||||
import type React from 'react'
|
||||
|
||||
import { notFound } from 'next/navigation.js'
|
||||
|
||||
import { APIView as DefaultAPIView } from '../API/index.js'
|
||||
import { EditView as DefaultEditView } from '../Edit/index.js'
|
||||
import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js'
|
||||
@@ -23,7 +21,7 @@ import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
|
||||
export type ViewFromConfig<TProps extends object> = {
|
||||
Component?: React.FC<TProps>
|
||||
payloadComponent?: PayloadComponent<TProps>
|
||||
ComponentConfig?: PayloadComponent<TProps>
|
||||
}
|
||||
|
||||
export const getViewsFromConfig = ({
|
||||
@@ -81,7 +79,7 @@ export const getViewsFromConfig = ({
|
||||
routeSegments
|
||||
|
||||
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
} else {
|
||||
// `../:id`, or `../create`
|
||||
switch (routeSegments.length) {
|
||||
@@ -94,7 +92,7 @@ export const getViewsFromConfig = ({
|
||||
docPermissions?.create?.permission
|
||||
) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultEditView,
|
||||
@@ -132,11 +130,11 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
} else {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
|
||||
DefaultView = {
|
||||
@@ -156,7 +154,7 @@ export const getViewsFromConfig = ({
|
||||
case 'api': {
|
||||
if (collectionConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'api'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'api'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultAPIView,
|
||||
@@ -171,7 +169,7 @@ export const getViewsFromConfig = ({
|
||||
Component: DefaultLivePreviewView,
|
||||
}
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'livePreview'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'livePreview'),
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -180,7 +178,7 @@ export const getViewsFromConfig = ({
|
||||
case 'versions': {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'versions'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'versions'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultVersionsView,
|
||||
@@ -218,7 +216,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +231,7 @@ export const getViewsFromConfig = ({
|
||||
if (segment4 === 'versions') {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'version'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'version'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultVersionView,
|
||||
@@ -269,7 +267,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,12 +282,12 @@ export const getViewsFromConfig = ({
|
||||
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
|
||||
|
||||
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
} else {
|
||||
switch (routeSegments.length) {
|
||||
case 2: {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'default'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'default'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultEditView,
|
||||
@@ -303,7 +301,7 @@ export const getViewsFromConfig = ({
|
||||
case 'api': {
|
||||
if (globalConfig?.admin?.hideAPIURL !== true) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'api'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'api'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultAPIView,
|
||||
@@ -318,7 +316,7 @@ export const getViewsFromConfig = ({
|
||||
Component: DefaultLivePreviewView,
|
||||
}
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'livePreview'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'livePreview'),
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -327,7 +325,7 @@ export const getViewsFromConfig = ({
|
||||
case 'versions': {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'versions'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'versions'),
|
||||
}
|
||||
|
||||
DefaultView = {
|
||||
@@ -362,7 +360,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
} else {
|
||||
DefaultView = {
|
||||
@@ -385,7 +383,7 @@ export const getViewsFromConfig = ({
|
||||
if (segment3 === 'versions') {
|
||||
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
|
||||
CustomView = {
|
||||
payloadComponent: getCustomViewByKey(views, 'version'),
|
||||
ComponentConfig: getCustomViewByKey(views, 'version'),
|
||||
}
|
||||
DefaultView = {
|
||||
Component: DefaultVersionView,
|
||||
@@ -416,7 +414,7 @@ export const getViewsFromConfig = ({
|
||||
viewKey = customViewKey
|
||||
|
||||
CustomView = {
|
||||
payloadComponent: CustomViewComponent,
|
||||
ComponentConfig: CustomViewComponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
195
packages/next/src/views/Document/handleServerFunction.tsx
Normal file
195
packages/next/src/views/Document/handleServerFunction.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type {
|
||||
ClientConfig,
|
||||
Data,
|
||||
DocumentPreferences,
|
||||
FormState,
|
||||
PayloadRequest,
|
||||
SanitizedConfig,
|
||||
VisibleEntities,
|
||||
} from 'payload'
|
||||
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
|
||||
|
||||
import { renderDocument } from './index.js'
|
||||
|
||||
let cachedClientConfig = global._payload_clientConfig
|
||||
|
||||
if (!cachedClientConfig) {
|
||||
cachedClientConfig = global._payload_clientConfig = null
|
||||
}
|
||||
|
||||
export const getClientConfig = (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
}): ClientConfig => {
|
||||
const { config, i18n } = args
|
||||
|
||||
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
cachedClientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
type RenderDocumentResult = {
|
||||
data: any
|
||||
Document: React.ReactNode
|
||||
preferences: DocumentPreferences
|
||||
}
|
||||
|
||||
export const renderDocumentHandler = async (args: {
|
||||
collectionSlug: string
|
||||
disableActions?: boolean
|
||||
docID: string
|
||||
drawerSlug?: string
|
||||
initialData?: Data
|
||||
initialState?: FormState
|
||||
redirectAfterDelete: boolean
|
||||
redirectAfterDuplicate: boolean
|
||||
req: PayloadRequest
|
||||
}): Promise<RenderDocumentResult> => {
|
||||
const {
|
||||
collectionSlug,
|
||||
disableActions,
|
||||
docID,
|
||||
drawerSlug,
|
||||
initialData,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
req,
|
||||
req: {
|
||||
i18n,
|
||||
payload,
|
||||
payload: { config },
|
||||
user,
|
||||
},
|
||||
} = args
|
||||
|
||||
const headers = await getHeaders()
|
||||
|
||||
const cookies = parseCookies(headers)
|
||||
|
||||
const incomingUserSlug = user?.collection
|
||||
|
||||
const adminUserSlug = config.admin.user
|
||||
|
||||
// If we have a user slug, test it against the functions
|
||||
if (incomingUserSlug) {
|
||||
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
|
||||
|
||||
// Run the admin access function from the config if it exists
|
||||
if (adminAccessFunction) {
|
||||
const canAccessAdmin = await adminAccessFunction({ req })
|
||||
|
||||
if (!canAccessAdmin) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
// Match the user collection to the global admin config
|
||||
} else if (adminUserSlug !== incomingUserSlug) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
} else {
|
||||
const hasUsers = await payload.find({
|
||||
collection: adminUserSlug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
})
|
||||
|
||||
// If there are users, we should not allow access because of /create-first-user
|
||||
if (hasUsers.docs.length) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
}
|
||||
|
||||
const clientConfig = getClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
let preferences: DocumentPreferences
|
||||
|
||||
if (docID) {
|
||||
const preferencesKey = `${collectionSlug}-edit-${docID}`
|
||||
|
||||
preferences = await payload
|
||||
.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: preferencesKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: user.collection,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.then((res) => res.docs[0]?.value as DocumentPreferences)
|
||||
}
|
||||
|
||||
const visibleEntities: VisibleEntities = {
|
||||
collections: payload.config.collections
|
||||
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
|
||||
.filter(Boolean),
|
||||
globals: payload.config.globals
|
||||
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
|
||||
.filter(Boolean),
|
||||
}
|
||||
|
||||
const permissions = await getAccessResults({
|
||||
req,
|
||||
})
|
||||
|
||||
const { data, Document } = await renderDocument({
|
||||
clientConfig,
|
||||
disableActions,
|
||||
drawerSlug,
|
||||
importMap: payload.importMap,
|
||||
initialData,
|
||||
initPageResult: {
|
||||
collectionConfig: payload.config.collections.find(
|
||||
(collection) => collection.slug === collectionSlug,
|
||||
),
|
||||
cookies,
|
||||
docID,
|
||||
globalConfig: payload.config.globals.find((global) => global.slug === collectionSlug),
|
||||
languageOptions: undefined, // TODO
|
||||
permissions,
|
||||
req,
|
||||
translations: undefined, // TODO
|
||||
visibleEntities,
|
||||
},
|
||||
params: {
|
||||
segments: ['collections', collectionSlug, docID],
|
||||
},
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
searchParams: {},
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
Document,
|
||||
preferences,
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,52 @@
|
||||
import type {
|
||||
AdminViewProps,
|
||||
EditViewComponent,
|
||||
MappedComponent,
|
||||
Data,
|
||||
PayloadComponent,
|
||||
ServerProps,
|
||||
ServerSideEditViewProps,
|
||||
} from 'payload'
|
||||
|
||||
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import {
|
||||
formatAdminURL,
|
||||
getCreateMappedComponent,
|
||||
isEditing as getIsEditing,
|
||||
RenderComponent,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL, isEditing as getIsEditing } from '@payloadcms/ui/shared'
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect.js'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
import type { GenerateEditViewMetadata } from './getMetaBySegment.js'
|
||||
import type { ViewFromConfig } from './getViewsFromConfig.js'
|
||||
|
||||
import { DocumentHeader } from '../../elements/DocumentHeader/index.js'
|
||||
import { NotFoundView } from '../NotFound/index.js'
|
||||
import { getDocPreferences } from './getDocPreferences.js'
|
||||
import { getDocumentData } from './getDocumentData.js'
|
||||
import { getDocumentPermissions } from './getDocumentPermissions.js'
|
||||
import { getIsLocked } from './getIsLocked.js'
|
||||
import { getMetaBySegment } from './getMetaBySegment.js'
|
||||
import { getVersions } from './getVersions.js'
|
||||
import { getViewsFromConfig } from './getViewsFromConfig.js'
|
||||
import { renderDocumentSlots } from './renderDocumentSlots.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)
|
||||
|
||||
export const Document: React.FC<AdminViewProps> = async ({
|
||||
// This function will be responsible for rendering an Edit Document view
|
||||
// it will be called on the server for Edit page views as well as
|
||||
// called on-demand from document drawers
|
||||
export const renderDocument = async ({
|
||||
disableActions,
|
||||
drawerSlug,
|
||||
importMap,
|
||||
initialData,
|
||||
initPageResult,
|
||||
params,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
searchParams,
|
||||
}) => {
|
||||
}: AdminViewProps): Promise<{
|
||||
data: Data
|
||||
Document: React.ReactNode
|
||||
}> => {
|
||||
const {
|
||||
collectionConfig,
|
||||
docID: id,
|
||||
@@ -57,54 +72,108 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
const segments = Array.isArray(params?.segments) ? params.segments : []
|
||||
const collectionSlug = collectionConfig?.slug || undefined
|
||||
const globalSlug = globalConfig?.slug || undefined
|
||||
|
||||
const isEditing = getIsEditing({ id, collectionSlug, globalSlug })
|
||||
|
||||
let RootViewOverride: MappedComponent<ServerSideEditViewProps>
|
||||
let CustomView: MappedComponent<ServerSideEditViewProps>
|
||||
let DefaultView: MappedComponent<ServerSideEditViewProps>
|
||||
let ErrorView: MappedComponent<AdminViewProps>
|
||||
let RootViewOverride: PayloadComponent
|
||||
let CustomView: ViewFromConfig<ServerSideEditViewProps>
|
||||
let DefaultView: ViewFromConfig<ServerSideEditViewProps>
|
||||
let ErrorView: ViewFromConfig<AdminViewProps>
|
||||
|
||||
let apiURL: string
|
||||
|
||||
const { data, formState } = await getDocumentData({
|
||||
id,
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
locale,
|
||||
req,
|
||||
})
|
||||
// Fetch the doc required for the view
|
||||
const doc =
|
||||
initialData ||
|
||||
(await getDocumentData({
|
||||
id,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
locale,
|
||||
payload,
|
||||
user,
|
||||
}))
|
||||
|
||||
if (!data) {
|
||||
notFound()
|
||||
if (isEditing && !doc) {
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const { docPermissions, hasPublishPermission, hasSavePermission } = await getDocumentPermissions({
|
||||
id,
|
||||
collectionConfig,
|
||||
data,
|
||||
globalConfig,
|
||||
req,
|
||||
})
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
const [
|
||||
docPreferences,
|
||||
{ docPermissions, hasPublishPermission, hasSavePermission },
|
||||
{ currentEditor, isLocked, lastUpdateTime },
|
||||
] = await Promise.all([
|
||||
// Get document preferences
|
||||
getDocPreferences({
|
||||
id,
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
}),
|
||||
|
||||
// Get permissions
|
||||
getDocumentPermissions({
|
||||
id,
|
||||
collectionConfig,
|
||||
data: doc,
|
||||
globalConfig,
|
||||
req,
|
||||
}),
|
||||
|
||||
// Fetch document lock state
|
||||
getIsLocked({
|
||||
id,
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
isEditing,
|
||||
payload: req.payload,
|
||||
user,
|
||||
}),
|
||||
])
|
||||
|
||||
const [
|
||||
{ hasPublishedDoc, mostRecentVersionIsAutosaved, unpublishedVersionCount, versionCount },
|
||||
{ state: formState },
|
||||
] = await Promise.all([
|
||||
getVersions({
|
||||
id,
|
||||
collectionConfig,
|
||||
docPermissions,
|
||||
globalConfig,
|
||||
locale: locale?.code,
|
||||
payload,
|
||||
user,
|
||||
}),
|
||||
buildFormState({
|
||||
id,
|
||||
collectionSlug,
|
||||
data: doc,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
globalSlug,
|
||||
locale: locale?.code,
|
||||
operation: (collectionSlug && id) || globalSlug ? 'update' : 'create',
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: collectionSlug || globalSlug,
|
||||
}),
|
||||
])
|
||||
|
||||
const serverProps: ServerProps = {
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
user,
|
||||
}
|
||||
|
||||
if (collectionConfig) {
|
||||
if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
@@ -122,12 +191,7 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
RootViewOverride =
|
||||
collectionConfig?.admin?.components?.views?.edit?.root &&
|
||||
'Component' in collectionConfig.admin.components.views.edit.root
|
||||
? createMappedComponent(
|
||||
collectionConfig?.admin?.components?.views?.edit?.root?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
|
||||
undefined,
|
||||
undefined,
|
||||
'collectionConfig?.admin?.components?.views?.edit?.root',
|
||||
)
|
||||
? collectionConfig?.admin?.components?.views?.edit?.root?.Component
|
||||
: null
|
||||
|
||||
if (!RootViewOverride) {
|
||||
@@ -138,36 +202,21 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
routeSegments: segments,
|
||||
})
|
||||
|
||||
CustomView = createMappedComponent(
|
||||
collectionViews?.CustomView?.payloadComponent,
|
||||
undefined,
|
||||
collectionViews?.CustomView?.Component,
|
||||
'collectionViews?.CustomView.payloadComponent',
|
||||
)
|
||||
|
||||
DefaultView = createMappedComponent(
|
||||
collectionViews?.DefaultView?.payloadComponent,
|
||||
undefined,
|
||||
collectionViews?.DefaultView?.Component,
|
||||
'collectionViews?.DefaultView.payloadComponent',
|
||||
)
|
||||
|
||||
ErrorView = createMappedComponent(
|
||||
collectionViews?.ErrorView?.payloadComponent,
|
||||
undefined,
|
||||
collectionViews?.ErrorView?.Component,
|
||||
'collectionViews?.ErrorView.payloadComponent',
|
||||
)
|
||||
CustomView = collectionViews?.CustomView
|
||||
DefaultView = collectionViews?.DefaultView
|
||||
ErrorView = collectionViews?.ErrorView
|
||||
}
|
||||
|
||||
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
|
||||
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
|
||||
ErrorView = {
|
||||
Component: NotFoundView,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (globalConfig) {
|
||||
if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
@@ -189,12 +238,7 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
RootViewOverride =
|
||||
globalConfig?.admin?.components?.views?.edit?.root &&
|
||||
'Component' in globalConfig.admin.components.views.edit.root
|
||||
? createMappedComponent(
|
||||
globalConfig?.admin?.components?.views?.edit?.root?.Component as EditViewComponent, // some type info gets lost from Config => SanitizedConfig due to our usage of Deep type operations from ts-essentials. Despite .Component being defined as EditViewComponent, this info is lost and we need cast it here.
|
||||
undefined,
|
||||
undefined,
|
||||
'globalConfig?.admin?.components?.views?.edit?.root',
|
||||
)
|
||||
? globalConfig?.admin?.components?.views?.edit?.root?.Component
|
||||
: null
|
||||
|
||||
if (!RootViewOverride) {
|
||||
@@ -205,29 +249,14 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
routeSegments: segments,
|
||||
})
|
||||
|
||||
CustomView = createMappedComponent(
|
||||
globalViews?.CustomView?.payloadComponent,
|
||||
undefined,
|
||||
globalViews?.CustomView?.Component,
|
||||
'globalViews?.CustomView.payloadComponent',
|
||||
)
|
||||
|
||||
DefaultView = createMappedComponent(
|
||||
globalViews?.DefaultView?.payloadComponent,
|
||||
undefined,
|
||||
globalViews?.DefaultView?.Component,
|
||||
'globalViews?.DefaultView.payloadComponent',
|
||||
)
|
||||
|
||||
ErrorView = createMappedComponent(
|
||||
globalViews?.ErrorView?.payloadComponent,
|
||||
undefined,
|
||||
globalViews?.ErrorView?.Component,
|
||||
'globalViews?.ErrorView.payloadComponent',
|
||||
)
|
||||
CustomView = globalViews?.CustomView
|
||||
DefaultView = globalViews?.DefaultView
|
||||
ErrorView = globalViews?.ErrorView
|
||||
|
||||
if (!CustomView && !DefaultView && !RootViewOverride && !ErrorView) {
|
||||
ErrorView = createMappedComponent(undefined, undefined, NotFoundView, 'NotFoundView')
|
||||
ErrorView = {
|
||||
Component: NotFoundView,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,13 +269,14 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
hasSavePermission &&
|
||||
((collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
|
||||
(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave))
|
||||
|
||||
const validateDraftData =
|
||||
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.validate
|
||||
|
||||
if (shouldAutosave && !validateDraftData && !id && collectionSlug) {
|
||||
const doc = await payload.create({
|
||||
collection: collectionSlug,
|
||||
data: {},
|
||||
data: initialData || {},
|
||||
depth: 0,
|
||||
draft: true,
|
||||
fallbackLocale: null,
|
||||
@@ -263,57 +293,96 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
})
|
||||
redirect(redirectURL)
|
||||
} else {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
apiURL={apiURL}
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
disableActions={false}
|
||||
docPermissions={docPermissions}
|
||||
globalSlug={globalConfig?.slug}
|
||||
hasPublishPermission={hasPublishPermission}
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={id}
|
||||
initialData={data}
|
||||
initialState={formState}
|
||||
isEditing={isEditing}
|
||||
key={locale?.code}
|
||||
>
|
||||
{!RootViewOverride && (
|
||||
<DocumentHeader
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
{/**
|
||||
* After bumping the Next.js canary to 104, and React to 19.0.0-rc-06d0b89e-20240801" we have to deepCopy the permissions object (https://github.com/payloadcms/payload/pull/7541).
|
||||
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
|
||||
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error when loading up some version views - for example a versions
|
||||
* view in the draft-posts collection of the versions test suite. RenderCustomComponent is what renders the versions view.
|
||||
*
|
||||
* // TODO: Revisit this in the future and figure out why this is happening. Might be a React/Next.js bug. We don't know why it happens, and a future React/Next version might unbreak this (keep an eye on this and remove deepCopyObjectSimple if that's the case)
|
||||
*/}
|
||||
<EditDepthProvider
|
||||
depth={1}
|
||||
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
|
||||
const documentSlots = renderDocumentSlots({
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
hasSavePermission,
|
||||
permissions: docPermissions,
|
||||
req,
|
||||
})
|
||||
|
||||
const clientProps = { formState, ...documentSlots }
|
||||
|
||||
return {
|
||||
data: doc,
|
||||
Document: (
|
||||
<DocumentInfoProvider
|
||||
apiURL={apiURL}
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
currentEditor={currentEditor}
|
||||
disableActions={disableActions ?? false}
|
||||
docPermissions={docPermissions}
|
||||
globalSlug={globalConfig?.slug}
|
||||
hasPublishedDoc={hasPublishedDoc}
|
||||
hasPublishPermission={hasPublishPermission}
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={id}
|
||||
initialData={doc}
|
||||
initialState={formState}
|
||||
isEditing={isEditing}
|
||||
isLocked={isLocked}
|
||||
key={locale?.code}
|
||||
lastUpdateTime={lastUpdateTime}
|
||||
mostRecentVersionIsAutosaved={mostRecentVersionIsAutosaved}
|
||||
redirectAfterDelete={redirectAfterDelete}
|
||||
redirectAfterDuplicate={redirectAfterDuplicate}
|
||||
unpublishedVersionCount={unpublishedVersionCount}
|
||||
versionCount={versionCount}
|
||||
>
|
||||
{ErrorView ? (
|
||||
<RenderComponent mappedComponent={ErrorView} />
|
||||
) : (
|
||||
<RenderComponent
|
||||
mappedComponent={
|
||||
RootViewOverride ? RootViewOverride : CustomView ? CustomView : DefaultView
|
||||
}
|
||||
{!RootViewOverride && !drawerSlug && (
|
||||
<DocumentHeader
|
||||
collectionConfig={collectionConfig}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
)
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<EditDepthProvider>
|
||||
{ErrorView ? (
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={ErrorView.ComponentConfig || ErrorView.Component}
|
||||
importMap={importMap}
|
||||
serverProps={serverProps}
|
||||
/>
|
||||
) : (
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={
|
||||
RootViewOverride
|
||||
? RootViewOverride
|
||||
: CustomView?.ComponentConfig || CustomView?.Component
|
||||
? CustomView?.ComponentConfig || CustomView?.Component
|
||||
: DefaultView?.ComponentConfig || DefaultView?.Component
|
||||
}
|
||||
importMap={importMap}
|
||||
serverProps={serverProps}
|
||||
/>
|
||||
)}
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export const Document: React.FC<AdminViewProps> = async (args) => {
|
||||
try {
|
||||
const { Document: RenderedDocument } = await renderDocument(args)
|
||||
return RenderedDocument
|
||||
} catch (error) {
|
||||
if (isRedirectError(error)) {
|
||||
throw error
|
||||
}
|
||||
args.initPageResult.req.payload.logger.error(error)
|
||||
|
||||
if (error.message === 'not-found') {
|
||||
notFound()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
136
packages/next/src/views/Document/renderDocumentSlots.tsx
Normal file
136
packages/next/src/views/Document/renderDocumentSlots.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import type {
|
||||
DefaultServerFunctionArgs,
|
||||
DocumentPermissions,
|
||||
DocumentSlots,
|
||||
PayloadRequest,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedGlobalConfig,
|
||||
StaticDescription,
|
||||
} from 'payload'
|
||||
|
||||
import { ViewDescription } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import React from 'react'
|
||||
|
||||
import { getDocumentPermissions } from './getDocumentPermissions.js'
|
||||
|
||||
export const renderDocumentSlots: (args: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
hasSavePermission: boolean
|
||||
permissions: DocumentPermissions
|
||||
req: PayloadRequest
|
||||
}) => DocumentSlots = (args) => {
|
||||
const { collectionConfig, globalConfig, hasSavePermission, req } = args
|
||||
|
||||
const components: DocumentSlots = {} as DocumentSlots
|
||||
|
||||
const unsavedDraftWithValidations = undefined
|
||||
|
||||
const isPreviewEnabled = collectionConfig?.admin?.preview || globalConfig?.admin?.preview
|
||||
|
||||
const CustomPreviewButton =
|
||||
collectionConfig?.admin?.components?.edit?.PreviewButton ||
|
||||
globalConfig?.admin?.components?.elements?.PreviewButton
|
||||
|
||||
if (isPreviewEnabled && CustomPreviewButton) {
|
||||
components.PreviewButton = (
|
||||
<RenderServerComponent Component={CustomPreviewButton} importMap={req.payload.importMap} />
|
||||
)
|
||||
}
|
||||
|
||||
const descriptionFromConfig =
|
||||
collectionConfig?.admin?.description || globalConfig?.admin?.description
|
||||
|
||||
const staticDescription: StaticDescription =
|
||||
typeof descriptionFromConfig === 'function'
|
||||
? descriptionFromConfig({ t: req.i18n.t })
|
||||
: descriptionFromConfig
|
||||
|
||||
const CustomDescription =
|
||||
collectionConfig?.admin?.components?.Description ||
|
||||
globalConfig?.admin?.components?.elements?.Description
|
||||
|
||||
const hasDescription = CustomDescription || staticDescription
|
||||
|
||||
if (hasDescription) {
|
||||
components.Description = (
|
||||
<RenderServerComponent
|
||||
clientProps={{ description: staticDescription }}
|
||||
Component={CustomDescription}
|
||||
Fallback={ViewDescription}
|
||||
importMap={req.payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (hasSavePermission) {
|
||||
if (collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts) {
|
||||
const CustomPublishButton =
|
||||
collectionConfig?.admin?.components?.edit?.PublishButton ||
|
||||
globalConfig?.admin?.components?.elements?.PublishButton
|
||||
|
||||
if (CustomPublishButton) {
|
||||
components.PublishButton = (
|
||||
<RenderServerComponent
|
||||
Component={CustomPublishButton}
|
||||
importMap={req.payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const CustomSaveDraftButton =
|
||||
collectionConfig?.admin?.components?.edit?.SaveDraftButton ||
|
||||
globalConfig?.admin?.components?.elements?.SaveDraftButton
|
||||
|
||||
const draftsEnabled =
|
||||
(collectionConfig?.versions?.drafts && !collectionConfig?.versions?.drafts?.autosave) ||
|
||||
(globalConfig?.versions?.drafts && !globalConfig?.versions?.drafts?.autosave)
|
||||
|
||||
if ((draftsEnabled || unsavedDraftWithValidations) && CustomSaveDraftButton) {
|
||||
components.SaveDraftButton = (
|
||||
<RenderServerComponent
|
||||
Component={CustomSaveDraftButton}
|
||||
importMap={req.payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const CustomSaveButton =
|
||||
collectionConfig?.admin?.components?.edit?.SaveButton ||
|
||||
globalConfig?.admin?.components?.elements?.SaveButton
|
||||
|
||||
if (CustomSaveButton) {
|
||||
components.SaveButton = (
|
||||
<RenderServerComponent Component={CustomSaveButton} importMap={req.payload.importMap} />
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
export const renderDocumentSlotsHandler = async (
|
||||
args: { collectionSlug: string } & DefaultServerFunctionArgs,
|
||||
) => {
|
||||
const { collectionSlug, req } = args
|
||||
|
||||
const collectionConfig = req.payload.collections[collectionSlug]?.config
|
||||
|
||||
if (!collectionConfig) {
|
||||
throw new Error(req.t('error:incorrectCollection'))
|
||||
}
|
||||
|
||||
const { docPermissions, hasSavePermission } = await getDocumentPermissions({
|
||||
collectionConfig,
|
||||
data: {},
|
||||
req,
|
||||
})
|
||||
|
||||
return renderDocumentSlots({
|
||||
collectionConfig,
|
||||
hasSavePermission,
|
||||
permissions: docPermissions,
|
||||
req,
|
||||
})
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload'
|
||||
|
||||
import { RenderComponent, SetViewActions, useConfig, useDocumentInfo } from '@payloadcms/ui'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
export const EditViewClient: React.FC = () => {
|
||||
const { collectionSlug, globalSlug } = useDocumentInfo()
|
||||
|
||||
const { getEntityConfig } = useConfig()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||
|
||||
const Edit = (collectionConfig || globalConfig)?.admin?.components?.views?.edit?.default
|
||||
?.Component
|
||||
|
||||
if (!Edit) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.default?.actions
|
||||
}
|
||||
/>
|
||||
<RenderComponent mappedComponent={Edit} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { EditViewComponent, PayloadServerReactComponent } from 'payload'
|
||||
'use client'
|
||||
|
||||
import type { ClientSideEditViewProps } from 'payload'
|
||||
|
||||
import { DefaultEditView } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import { EditViewClient } from './index.client.js'
|
||||
|
||||
export const EditView: PayloadServerReactComponent<EditViewComponent> = () => {
|
||||
return <EditViewClient />
|
||||
export const EditView: React.FC<ClientSideEditViewProps> = (props) => {
|
||||
return <DefaultEditView {...props} />
|
||||
}
|
||||
|
||||
@@ -105,9 +105,11 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
/>
|
||||
) : (
|
||||
<EmailField
|
||||
autoComplete="email"
|
||||
field={{
|
||||
name: 'email',
|
||||
admin: {
|
||||
autoComplete: 'email',
|
||||
},
|
||||
label: t('general:email'),
|
||||
required: true,
|
||||
}}
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
Button,
|
||||
DeleteMany,
|
||||
EditMany,
|
||||
Gutter,
|
||||
ListControls,
|
||||
ListHeader,
|
||||
ListSelection,
|
||||
Pagination,
|
||||
PerPage,
|
||||
PublishMany,
|
||||
RelationshipProvider,
|
||||
RenderComponent,
|
||||
SelectionProvider,
|
||||
SetViewActions,
|
||||
StaggeredShimmers,
|
||||
Table,
|
||||
UnpublishMany,
|
||||
useAuth,
|
||||
useBulkUpload,
|
||||
useConfig,
|
||||
useEditDepth,
|
||||
useListInfo,
|
||||
useListQuery,
|
||||
useModal,
|
||||
useStepNav,
|
||||
useTranslation,
|
||||
useWindowInfo,
|
||||
ViewDescription,
|
||||
} from '@payloadcms/ui'
|
||||
import LinkImport from 'next/link.js'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import { formatFilesize, isNumber } from 'payload/shared'
|
||||
import React, { Fragment, useEffect } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'collection-list'
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
|
||||
export const DefaultListView: React.FC = () => {
|
||||
const { user } = useAuth()
|
||||
const {
|
||||
beforeActions,
|
||||
collectionSlug,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
hasCreatePermission,
|
||||
Header,
|
||||
newDocumentURL,
|
||||
} = useListInfo()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const { data, defaultLimit, handlePageChange, handlePerPageChange, params } = useListQuery()
|
||||
const { openModal } = useModal()
|
||||
const { setCollectionSlug, setOnSuccess } = useBulkUpload()
|
||||
const { drawerSlug } = useBulkUpload()
|
||||
|
||||
const { getEntityConfig } = useConfig()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
|
||||
const {
|
||||
admin: {
|
||||
components: {
|
||||
afterList,
|
||||
afterListTable,
|
||||
beforeList,
|
||||
beforeListTable,
|
||||
Description,
|
||||
views: {
|
||||
list: { actions },
|
||||
},
|
||||
},
|
||||
description,
|
||||
},
|
||||
fields,
|
||||
labels,
|
||||
} = collectionConfig
|
||||
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const drawerDepth = useEditDepth()
|
||||
|
||||
const { setStepNav } = useStepNav()
|
||||
|
||||
const {
|
||||
breakpoints: { s: smallBreak },
|
||||
} = useWindowInfo()
|
||||
|
||||
let docs = data.docs || []
|
||||
|
||||
const isUploadCollection = Boolean(collectionConfig.upload)
|
||||
|
||||
if (isUploadCollection) {
|
||||
docs = docs?.map((doc) => {
|
||||
return {
|
||||
...doc,
|
||||
filesize: formatFilesize(doc.filesize),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const openBulkUpload = React.useCallback(() => {
|
||||
setCollectionSlug(collectionSlug)
|
||||
openModal(drawerSlug)
|
||||
setOnSuccess(() => router.refresh())
|
||||
}, [router, collectionSlug, drawerSlug, openModal, setCollectionSlug, setOnSuccess])
|
||||
|
||||
useEffect(() => {
|
||||
if (drawerDepth <= 1) {
|
||||
setStepNav([
|
||||
{
|
||||
label: labels?.plural,
|
||||
},
|
||||
])
|
||||
}
|
||||
}, [setStepNav, labels, drawerDepth])
|
||||
|
||||
const isBulkUploadEnabled = isUploadCollection && collectionConfig.upload.bulkUpload
|
||||
|
||||
return (
|
||||
<div className={`${baseClass} ${baseClass}--${collectionSlug}`}>
|
||||
<SetViewActions actions={actions} />
|
||||
<SelectionProvider docs={data.docs} totalDocs={data.totalDocs} user={user}>
|
||||
<RenderComponent mappedComponent={beforeList} />
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
{Header || (
|
||||
<ListHeader heading={getTranslation(labels?.plural, i18n)}>
|
||||
{hasCreatePermission && (
|
||||
<>
|
||||
<Button
|
||||
aria-label={i18n.t('general:createNewLabel', {
|
||||
label: getTranslation(labels?.singular, i18n),
|
||||
})}
|
||||
buttonStyle="pill"
|
||||
el={'link'}
|
||||
Link={Link}
|
||||
size="small"
|
||||
to={newDocumentURL}
|
||||
>
|
||||
{i18n.t('general:createNew')}
|
||||
</Button>
|
||||
|
||||
{isBulkUploadEnabled && (
|
||||
<Button
|
||||
aria-label={t('upload:bulkUpload')}
|
||||
buttonStyle="pill"
|
||||
onClick={openBulkUpload}
|
||||
size="small"
|
||||
>
|
||||
{t('upload:bulkUpload')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!smallBreak && (
|
||||
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
|
||||
)}
|
||||
{(description || Description) && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription Description={Description} description={description} />
|
||||
</div>
|
||||
)}
|
||||
</ListHeader>
|
||||
)}
|
||||
<ListControls collectionConfig={collectionConfig} fields={fields} />
|
||||
<RenderComponent mappedComponent={beforeListTable} />
|
||||
{!data.docs && (
|
||||
<StaggeredShimmers
|
||||
className={[`${baseClass}__shimmer`, `${baseClass}__shimmer--rows`].join(' ')}
|
||||
count={6}
|
||||
/>
|
||||
)}
|
||||
{data.docs && data.docs.length > 0 && (
|
||||
<RelationshipProvider>
|
||||
<Table
|
||||
customCellContext={{
|
||||
collectionSlug,
|
||||
uploadConfig: collectionConfig.upload,
|
||||
}}
|
||||
data={docs}
|
||||
fields={fields}
|
||||
/>
|
||||
</RelationshipProvider>
|
||||
)}
|
||||
{data.docs && data.docs.length === 0 && (
|
||||
<div className={`${baseClass}__no-results`}>
|
||||
<p>{i18n.t('general:noResults', { label: getTranslation(labels?.plural, i18n) })}</p>
|
||||
{hasCreatePermission && newDocumentURL && (
|
||||
<Button el="link" Link={Link} to={newDocumentURL}>
|
||||
{i18n.t('general:createNewLabel', {
|
||||
label: getTranslation(labels?.singular, i18n),
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<RenderComponent mappedComponent={afterListTable} />
|
||||
{data.docs && data.docs.length > 0 && (
|
||||
<div className={`${baseClass}__page-controls`}>
|
||||
<Pagination
|
||||
hasNextPage={data.hasNextPage}
|
||||
hasPrevPage={data.hasPrevPage}
|
||||
limit={data.limit}
|
||||
nextPage={data.nextPage}
|
||||
numberOfNeighbors={1}
|
||||
onChange={(page) => void handlePageChange(page)}
|
||||
page={data.page}
|
||||
prevPage={data.prevPage}
|
||||
totalPages={data.totalPages}
|
||||
/>
|
||||
{data?.totalDocs > 0 && (
|
||||
<Fragment>
|
||||
<div className={`${baseClass}__page-info`}>
|
||||
{data.page * data.limit - (data.limit - 1)}-
|
||||
{data.totalPages > 1 && data.totalPages !== data.page
|
||||
? data.limit * data.page
|
||||
: data.totalDocs}{' '}
|
||||
{i18n.t('general:of')} {data.totalDocs}
|
||||
</div>
|
||||
<PerPage
|
||||
handleChange={(limit) => void handlePerPageChange(limit)}
|
||||
limit={isNumber(params?.limit) ? Number(params.limit) : defaultLimit}
|
||||
limits={collectionConfig?.admin?.pagination?.limits}
|
||||
resetPage={data.totalDocs <= data.pagingCounter}
|
||||
/>
|
||||
{smallBreak && (
|
||||
<div className={`${baseClass}__list-selection`}>
|
||||
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
|
||||
<div className={`${baseClass}__list-selection-actions`}>
|
||||
{beforeActions && beforeActions}
|
||||
{!disableBulkEdit && (
|
||||
<Fragment>
|
||||
<EditMany collection={collectionConfig} fields={fields} />
|
||||
<PublishMany collection={collectionConfig} />
|
||||
<UnpublishMany collection={collectionConfig} />
|
||||
</Fragment>
|
||||
)}
|
||||
{!disableBulkDelete && <DeleteMany collection={collectionConfig} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Gutter>
|
||||
<RenderComponent mappedComponent={afterList} />
|
||||
</SelectionProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
194
packages/next/src/views/List/handleServerFunction.tsx
Normal file
194
packages/next/src/views/List/handleServerFunction.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { ListPreferences } from '@payloadcms/ui'
|
||||
import type {
|
||||
ClientConfig,
|
||||
ListQuery,
|
||||
PayloadRequest,
|
||||
SanitizedConfig,
|
||||
VisibleEntities,
|
||||
} from 'payload'
|
||||
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
|
||||
|
||||
import { renderListView } from './index.js'
|
||||
|
||||
let cachedClientConfig = global._payload_clientConfig
|
||||
|
||||
if (!cachedClientConfig) {
|
||||
cachedClientConfig = global._payload_clientConfig = null
|
||||
}
|
||||
|
||||
export const getClientConfig = (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18nClient
|
||||
}): ClientConfig => {
|
||||
const { config, i18n } = args
|
||||
|
||||
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
cachedClientConfig = createClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
return cachedClientConfig
|
||||
}
|
||||
|
||||
type RenderListResult = {
|
||||
List: React.ReactNode
|
||||
preferences: ListPreferences
|
||||
}
|
||||
|
||||
export const renderListHandler = async (args: {
|
||||
collectionSlug: string
|
||||
disableActions?: boolean
|
||||
disableBulkDelete?: boolean
|
||||
disableBulkEdit?: boolean
|
||||
documentDrawerSlug: string
|
||||
drawerSlug?: string
|
||||
enableRowSelections: boolean
|
||||
query: ListQuery
|
||||
redirectAfterDelete: boolean
|
||||
redirectAfterDuplicate: boolean
|
||||
req: PayloadRequest
|
||||
}): Promise<RenderListResult> => {
|
||||
const {
|
||||
collectionSlug,
|
||||
disableActions,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
drawerSlug,
|
||||
enableRowSelections,
|
||||
query,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
req,
|
||||
req: {
|
||||
i18n,
|
||||
payload,
|
||||
payload: { config },
|
||||
user,
|
||||
},
|
||||
} = args
|
||||
|
||||
const headers = await getHeaders()
|
||||
|
||||
const cookies = parseCookies(headers)
|
||||
|
||||
const incomingUserSlug = user?.collection
|
||||
|
||||
const adminUserSlug = config.admin.user
|
||||
|
||||
// If we have a user slug, test it against the functions
|
||||
if (incomingUserSlug) {
|
||||
const adminAccessFunction = payload.collections[incomingUserSlug].config.access?.admin
|
||||
|
||||
// Run the admin access function from the config if it exists
|
||||
if (adminAccessFunction) {
|
||||
const canAccessAdmin = await adminAccessFunction({ req })
|
||||
|
||||
if (!canAccessAdmin) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
// Match the user collection to the global admin config
|
||||
} else if (adminUserSlug !== incomingUserSlug) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
} else {
|
||||
const hasUsers = await payload.find({
|
||||
collection: adminUserSlug,
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
})
|
||||
|
||||
// If there are users, we should not allow access because of /create-first-user
|
||||
if (hasUsers.docs.length) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
}
|
||||
|
||||
const clientConfig = getClientConfig({
|
||||
config,
|
||||
i18n,
|
||||
})
|
||||
|
||||
const preferencesKey = `${collectionSlug}-list`
|
||||
|
||||
const preferences = await payload
|
||||
.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: preferencesKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: user.collection,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.then((res) => res.docs[0]?.value as ListPreferences)
|
||||
|
||||
const visibleEntities: VisibleEntities = {
|
||||
collections: payload.config.collections
|
||||
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
|
||||
.filter(Boolean),
|
||||
globals: payload.config.globals
|
||||
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
|
||||
.filter(Boolean),
|
||||
}
|
||||
|
||||
const permissions = await getAccessResults({
|
||||
req,
|
||||
})
|
||||
|
||||
const { List } = await renderListView({
|
||||
clientConfig,
|
||||
disableActions,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
drawerSlug,
|
||||
enableRowSelections,
|
||||
importMap: payload.importMap,
|
||||
initPageResult: {
|
||||
collectionConfig: payload.config.collections.find(
|
||||
(collection) => collection.slug === collectionSlug,
|
||||
),
|
||||
cookies,
|
||||
globalConfig: payload.config.globals.find((global) => global.slug === collectionSlug),
|
||||
languageOptions: undefined, // TODO
|
||||
permissions,
|
||||
req,
|
||||
translations: undefined, // TODO
|
||||
visibleEntities,
|
||||
},
|
||||
params: {
|
||||
segments: ['collections', collectionSlug],
|
||||
},
|
||||
query,
|
||||
redirectAfterDelete,
|
||||
redirectAfterDuplicate,
|
||||
searchParams: {},
|
||||
})
|
||||
|
||||
return {
|
||||
List,
|
||||
preferences,
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,52 @@
|
||||
import type { AdminViewProps, ClientCollectionConfig, Where } from 'payload'
|
||||
import type { ListPreferences, ListViewClientProps } from '@payloadcms/ui'
|
||||
import type { AdminViewProps, ListQuery, Where } from 'payload'
|
||||
|
||||
import {
|
||||
HydrateAuthProvider,
|
||||
ListInfoProvider,
|
||||
ListQueryProvider,
|
||||
TableColumnsProvider,
|
||||
} from '@payloadcms/ui'
|
||||
import { formatAdminURL, getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { createClientCollectionConfig } from '@payloadcms/ui/utilities/createClientConfig'
|
||||
import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { renderFilters, renderTable } from '@payloadcms/ui/rsc'
|
||||
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import { deepCopyObjectSimple, mergeListSearchAndWhere } from 'payload'
|
||||
import { isNumber } from 'payload/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { ListPreferences } from './Default/types.js'
|
||||
|
||||
import { DefaultEditView } from '../Edit/Default/index.js'
|
||||
import { DefaultListView } from './Default/index.js'
|
||||
import { renderListViewSlots } from './renderListViewSlots.js'
|
||||
|
||||
export { generateListMetadata } from './meta.js'
|
||||
|
||||
export const ListView: React.FC<AdminViewProps> = async ({
|
||||
initPageResult,
|
||||
params,
|
||||
searchParams,
|
||||
}) => {
|
||||
type ListViewArgs = {
|
||||
customCellProps?: Record<string, any>
|
||||
disableBulkDelete?: boolean
|
||||
disableBulkEdit?: boolean
|
||||
enableRowSelections: boolean
|
||||
query: ListQuery
|
||||
} & AdminViewProps
|
||||
|
||||
export const renderListView = async (
|
||||
args: ListViewArgs,
|
||||
): Promise<{
|
||||
List: React.ReactNode
|
||||
}> => {
|
||||
const {
|
||||
clientConfig,
|
||||
customCellProps,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
drawerSlug,
|
||||
enableRowSelections,
|
||||
initPageResult,
|
||||
params,
|
||||
query: queryFromArgs,
|
||||
searchParams,
|
||||
} = args
|
||||
|
||||
const {
|
||||
collectionConfig,
|
||||
collectionConfig: {
|
||||
slug: collectionSlug,
|
||||
admin: { useAsTitle },
|
||||
defaultSort,
|
||||
fields,
|
||||
},
|
||||
locale: fullLocale,
|
||||
permissions,
|
||||
req,
|
||||
@@ -35,18 +55,18 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
locale,
|
||||
payload,
|
||||
payload: { config },
|
||||
query,
|
||||
query: queryFromReq,
|
||||
user,
|
||||
},
|
||||
visibleEntities,
|
||||
} = initPageResult
|
||||
|
||||
const collectionSlug = collectionConfig?.slug
|
||||
|
||||
if (!permissions?.collections?.[collectionSlug]?.read?.permission) {
|
||||
notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const query = queryFromArgs || queryFromReq
|
||||
|
||||
let listPreferences: ListPreferences
|
||||
const preferenceKey = `${collectionSlug}-list`
|
||||
|
||||
@@ -79,7 +99,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
},
|
||||
})
|
||||
?.then((res) => res?.docs?.[0]?.value)) as ListPreferences
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
} catch (_err) {} // eslint-disable-line no-empty
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
@@ -87,20 +107,21 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
|
||||
if (collectionConfig) {
|
||||
if (!visibleEntities.collections.includes(collectionSlug)) {
|
||||
return notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const page = isNumber(query?.page) ? Number(query.page) : 0
|
||||
|
||||
const whereQuery = mergeListSearchAndWhere({
|
||||
collectionConfig,
|
||||
query: {
|
||||
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||
where: (query?.where as Where) || undefined,
|
||||
},
|
||||
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||
where: (query?.where as Where) || undefined,
|
||||
})
|
||||
|
||||
const limit = isNumber(query?.limit)
|
||||
? Number(query.limit)
|
||||
: listPreferences?.limit || collectionConfig.admin.pagination.defaultLimit
|
||||
|
||||
const sort =
|
||||
query?.sort && typeof query.sort === 'string'
|
||||
? query.sort
|
||||
@@ -125,89 +146,104 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
where: whereQuery || {},
|
||||
})
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
collectionConfig,
|
||||
collectionSlug,
|
||||
data,
|
||||
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
|
||||
i18n,
|
||||
limit,
|
||||
listPreferences,
|
||||
listSearchableFields: collectionConfig.admin.listSearchableFields,
|
||||
locale: fullLocale,
|
||||
newDocumentURL: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
}),
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
const clientCollectionConfig = clientConfig.collections.find((c) => c.slug === collectionSlug)
|
||||
|
||||
const { columnState, Table } = renderTable({
|
||||
collectionConfig: clientCollectionConfig,
|
||||
columnPreferences: listPreferences?.columns,
|
||||
customCellProps,
|
||||
docs: data.docs,
|
||||
drawerSlug,
|
||||
enableRowSelections,
|
||||
fields,
|
||||
i18n: req.i18n,
|
||||
payload,
|
||||
useAsTitle,
|
||||
})
|
||||
|
||||
const ListComponent = createMappedComponent(
|
||||
collectionConfig?.admin?.components?.views?.list?.Component,
|
||||
undefined,
|
||||
DefaultListView,
|
||||
'collectionConfig?.admin?.components?.views?.list?.Component',
|
||||
)
|
||||
const renderedFilters = renderFilters(fields, req.payload.importMap)
|
||||
|
||||
let clientCollectionConfig = deepCopyObjectSimple(
|
||||
const staticDescription =
|
||||
typeof collectionConfig.admin.description === 'function'
|
||||
? collectionConfig.admin.description({ t: i18n.t })
|
||||
: collectionConfig.admin.description
|
||||
|
||||
const listViewSlots = renderListViewSlots({
|
||||
collectionConfig,
|
||||
) as unknown as ClientCollectionConfig
|
||||
clientCollectionConfig = createClientCollectionConfig({
|
||||
clientCollection: clientCollectionConfig,
|
||||
collection: collectionConfig,
|
||||
createMappedComponent,
|
||||
DefaultEditView,
|
||||
DefaultListView,
|
||||
i18n,
|
||||
importMap: payload.importMap,
|
||||
description: staticDescription,
|
||||
payload,
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<ListInfoProvider
|
||||
collectionConfig={clientCollectionConfig}
|
||||
collectionSlug={collectionSlug}
|
||||
hasCreatePermission={permissions?.collections?.[collectionSlug]?.create?.permission}
|
||||
newDocumentURL={formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
})}
|
||||
>
|
||||
const clientProps: ListViewClientProps = {
|
||||
...listViewSlots,
|
||||
collectionSlug,
|
||||
columnState,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
enableRowSelections,
|
||||
hasCreatePermission: permissions?.collections?.[collectionSlug]?.create?.permission,
|
||||
listPreferences,
|
||||
newDocumentURL: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
}),
|
||||
renderedFilters,
|
||||
Table,
|
||||
}
|
||||
|
||||
const isInDrawer = Boolean(drawerSlug)
|
||||
|
||||
return {
|
||||
List: (
|
||||
<Fragment>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
<ListQueryProvider
|
||||
collectionSlug={collectionSlug}
|
||||
data={data}
|
||||
defaultLimit={limit || collectionConfig?.admin?.pagination?.defaultLimit}
|
||||
defaultLimit={limit}
|
||||
defaultSort={sort}
|
||||
modifySearchParams
|
||||
modifySearchParams={!isInDrawer}
|
||||
preferenceKey={preferenceKey}
|
||||
>
|
||||
<TableColumnsProvider
|
||||
collectionSlug={collectionSlug}
|
||||
enableRowSelections
|
||||
listPreferences={listPreferences}
|
||||
preferenceKey={preferenceKey}
|
||||
>
|
||||
<RenderComponent
|
||||
clientProps={{
|
||||
collectionSlug,
|
||||
listSearchableFields: collectionConfig?.admin?.listSearchableFields,
|
||||
}}
|
||||
mappedComponent={ListComponent}
|
||||
/>
|
||||
</TableColumnsProvider>
|
||||
<RenderServerComponent
|
||||
clientProps={clientProps}
|
||||
Component={collectionConfig?.admin?.components?.views?.list?.Component}
|
||||
Fallback={DefaultListView}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
collectionConfig,
|
||||
collectionSlug,
|
||||
data,
|
||||
i18n,
|
||||
limit,
|
||||
listPreferences,
|
||||
listSearchableFields: collectionConfig.admin.listSearchableFields,
|
||||
locale: fullLocale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
</ListQueryProvider>
|
||||
</ListInfoProvider>
|
||||
</Fragment>
|
||||
)
|
||||
</Fragment>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return notFound()
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
export const ListView: React.FC<ListViewArgs> = async (args) => {
|
||||
try {
|
||||
const { List: RenderedList } = await renderListView({ ...args, enableRowSelections: true })
|
||||
return RenderedList
|
||||
} catch (error) {
|
||||
if (error.message === 'not-found') {
|
||||
notFound()
|
||||
} else {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
66
packages/next/src/views/List/renderListViewSlots.tsx
Normal file
66
packages/next/src/views/List/renderListViewSlots.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { ListViewSlots } from '@payloadcms/ui'
|
||||
import type { Payload, SanitizedCollectionConfig, StaticDescription } from 'payload'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
|
||||
export const renderListViewSlots = ({
|
||||
collectionConfig,
|
||||
description,
|
||||
payload,
|
||||
}: {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
description?: StaticDescription
|
||||
payload: Payload
|
||||
}): ListViewSlots => {
|
||||
const result: ListViewSlots = {} as ListViewSlots
|
||||
|
||||
if (collectionConfig.admin.components?.afterList) {
|
||||
result.AfterList = (
|
||||
<RenderServerComponent
|
||||
Component={collectionConfig.admin.components.afterList}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.afterListTable) {
|
||||
result.AfterListTable = (
|
||||
<RenderServerComponent
|
||||
Component={collectionConfig.admin.components.afterListTable}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.beforeList) {
|
||||
result.BeforeList = (
|
||||
<RenderServerComponent
|
||||
Component={collectionConfig.admin.components.beforeList}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.beforeListTable) {
|
||||
result.BeforeListTable = (
|
||||
<RenderServerComponent
|
||||
Component={collectionConfig.admin.components.beforeListTable}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.Description) {
|
||||
result.Description = (
|
||||
<RenderServerComponent
|
||||
clientProps={{
|
||||
description,
|
||||
}}
|
||||
Component={collectionConfig.admin.components.Description}
|
||||
importMap={payload.importMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -13,29 +13,25 @@ import type {
|
||||
import {
|
||||
DocumentControls,
|
||||
DocumentFields,
|
||||
DocumentLocked,
|
||||
DocumentTakeOver,
|
||||
Form,
|
||||
LeaveWithoutSaving,
|
||||
OperationProvider,
|
||||
SetViewActions,
|
||||
SetDocumentStepNav,
|
||||
SetDocumentTitle,
|
||||
useAuth,
|
||||
useConfig,
|
||||
useDocumentDrawerContext,
|
||||
useDocumentEvents,
|
||||
useDocumentInfo,
|
||||
useServerFunctions,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import {
|
||||
getFormState,
|
||||
handleBackToDashboard,
|
||||
handleGoBack,
|
||||
handleTakeOver,
|
||||
} from '@payloadcms/ui/shared'
|
||||
import { handleBackToDashboard, handleGoBack, handleTakeOver } from '@payloadcms/ui/shared'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { DocumentLocked } from '../../elements/DocumentLocked/index.js'
|
||||
import { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js'
|
||||
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js'
|
||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||
import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle/index.js'
|
||||
import { useLivePreviewContext } from './Context/context.js'
|
||||
import { LivePreviewProvider } from './Context/index.js'
|
||||
import './index.scss'
|
||||
@@ -55,13 +51,11 @@ type Props = {
|
||||
}
|
||||
|
||||
const PreviewView: React.FC<Props> = ({
|
||||
apiRoute,
|
||||
collectionConfig,
|
||||
config,
|
||||
fields,
|
||||
globalConfig,
|
||||
schemaPath,
|
||||
serverURL,
|
||||
}) => {
|
||||
const {
|
||||
id,
|
||||
@@ -69,7 +63,6 @@ const PreviewView: React.FC<Props> = ({
|
||||
AfterDocument,
|
||||
AfterFields,
|
||||
apiURL,
|
||||
BeforeDocument,
|
||||
BeforeFields,
|
||||
collectionSlug,
|
||||
currentEditor,
|
||||
@@ -86,13 +79,16 @@ const PreviewView: React.FC<Props> = ({
|
||||
isEditing,
|
||||
isInitializing,
|
||||
lastUpdateTime,
|
||||
onSave: onSaveFromProps,
|
||||
setCurrentEditor,
|
||||
setDocumentIsLocked,
|
||||
unlockDocument,
|
||||
updateDocumentEditor,
|
||||
} = useDocumentInfo()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
|
||||
const { onSave: onSaveFromProps } = useDocumentDrawerContext()
|
||||
|
||||
const operation = id ? 'update' : 'create'
|
||||
|
||||
const {
|
||||
@@ -120,6 +116,8 @@ const PreviewView: React.FC<Props> = ({
|
||||
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
||||
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
||||
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
|
||||
const [editSessionStartTime, setEditSessionStartTime] = useState(Date.now())
|
||||
|
||||
const lockExpiryTime = lastUpdateTime + lockDurationInMilliseconds
|
||||
@@ -178,6 +176,17 @@ const PreviewView: React.FC<Props> = ({
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
|
||||
const abortController = new AbortController()
|
||||
abortControllerRef.current = abortController
|
||||
|
||||
const currentTime = Date.now()
|
||||
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
||||
|
||||
@@ -190,19 +199,17 @@ const PreviewView: React.FC<Props> = ({
|
||||
const docPreferences = await getDocPreferences()
|
||||
|
||||
const { lockedState, state } = await getFormState({
|
||||
apiRoute,
|
||||
body: {
|
||||
id,
|
||||
collectionSlug,
|
||||
docPreferences,
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
operation,
|
||||
returnLockStatus: isLockingEnabled ? true : false,
|
||||
schemaPath,
|
||||
updateLastEdited,
|
||||
},
|
||||
serverURL,
|
||||
id,
|
||||
collectionSlug,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
operation,
|
||||
returnLockStatus: isLockingEnabled ? true : false,
|
||||
schemaPath,
|
||||
signal: abortController.signal,
|
||||
updateLastEdited,
|
||||
})
|
||||
|
||||
setDocumentIsLocked(true)
|
||||
@@ -214,8 +221,13 @@ const PreviewView: React.FC<Props> = ({
|
||||
: documentLockStateRef.current?.user
|
||||
|
||||
if (lockedState) {
|
||||
if (!documentLockStateRef.current || lockedState.user.id !== previousOwnerId) {
|
||||
if (previousOwnerId === user.id && lockedState.user.id !== user.id) {
|
||||
const lockedUserID =
|
||||
typeof lockedState.user === 'string' || typeof lockedState.user === 'number'
|
||||
? lockedState.user
|
||||
: lockedState.user.id
|
||||
|
||||
if (!documentLockStateRef.current || lockedUserID !== previousOwnerId) {
|
||||
if (previousOwnerId === user.id && lockedUserID !== user.id) {
|
||||
setShowTakeOverModal(true)
|
||||
documentLockStateRef.current.hasShownLockedModal = true
|
||||
}
|
||||
@@ -223,9 +235,10 @@ const PreviewView: React.FC<Props> = ({
|
||||
documentLockStateRef.current = documentLockStateRef.current = {
|
||||
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
|
||||
isLocked: true,
|
||||
user: lockedState.user,
|
||||
user: lockedState.user as ClientUser,
|
||||
}
|
||||
setCurrentEditor(lockedState.user)
|
||||
|
||||
setCurrentEditor(lockedState.user as ClientUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,25 +246,33 @@ const PreviewView: React.FC<Props> = ({
|
||||
return state
|
||||
},
|
||||
[
|
||||
collectionSlug,
|
||||
editSessionStartTime,
|
||||
globalSlug,
|
||||
serverURL,
|
||||
apiRoute,
|
||||
id,
|
||||
isLockingEnabled,
|
||||
getDocPreferences,
|
||||
getFormState,
|
||||
id,
|
||||
collectionSlug,
|
||||
docPermissions,
|
||||
globalSlug,
|
||||
operation,
|
||||
schemaPath,
|
||||
getDocPreferences,
|
||||
setCurrentEditor,
|
||||
setDocumentIsLocked,
|
||||
user,
|
||||
user.id,
|
||||
setCurrentEditor,
|
||||
],
|
||||
)
|
||||
|
||||
// Clean up when the component unmounts or when the document is unlocked
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
try {
|
||||
abortControllerRef.current.abort()
|
||||
} catch (_err) {
|
||||
// swallow error
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLockingEnabled) {
|
||||
return
|
||||
}
|
||||
@@ -415,7 +436,6 @@ const PreviewView: React.FC<Props> = ({
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{BeforeDocument}
|
||||
<DocumentFields
|
||||
AfterFields={AfterFields}
|
||||
BeforeFields={BeforeFields}
|
||||
@@ -423,7 +443,7 @@ const PreviewView: React.FC<Props> = ({
|
||||
fields={fields}
|
||||
forceSidebarWrap
|
||||
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
|
||||
schemaPath={collectionSlug || globalSlug}
|
||||
schemaPathSegments={[collectionSlug || globalSlug]}
|
||||
/>
|
||||
{AfterDocument}
|
||||
</div>
|
||||
@@ -464,11 +484,6 @@ export const LivePreviewClient: React.FC<{
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.livePreview?.actions
|
||||
}
|
||||
/>
|
||||
<LivePreviewProvider
|
||||
breakpoints={breakpoints}
|
||||
fieldSchema={collectionConfig?.fields || globalConfig?.fields}
|
||||
|
||||
@@ -17,9 +17,11 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
|
||||
if (type === 'email') {
|
||||
return (
|
||||
<EmailField
|
||||
autoComplete="email"
|
||||
field={{
|
||||
name: 'email',
|
||||
admin: {
|
||||
autoComplete: 'email',
|
||||
},
|
||||
label: t('general:email'),
|
||||
required,
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { redirect } from 'next/navigation.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
@@ -28,23 +28,6 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
routes: { admin },
|
||||
} = config
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedBeforeLogins = createMappedComponent(beforeLogin, undefined, undefined, 'beforeLogin')
|
||||
|
||||
const mappedAfterLogins = createMappedComponent(afterLogin, undefined, undefined, 'afterLogin')
|
||||
|
||||
if (user) {
|
||||
redirect((searchParams.redirect as string) || admin)
|
||||
}
|
||||
@@ -82,7 +65,19 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
<RenderComponent mappedComponent={mappedBeforeLogins} />
|
||||
<RenderServerComponent
|
||||
Component={beforeLogin}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
{!collectionConfig?.auth?.disableLocalStrategy && (
|
||||
<LoginForm
|
||||
prefillEmail={prefillEmail}
|
||||
@@ -91,7 +86,19 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
)}
|
||||
<RenderComponent mappedComponent={mappedAfterLogins} />
|
||||
<RenderServerComponent
|
||||
Component={afterLogin}
|
||||
importMap={payload.importMap}
|
||||
serverProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,13 +75,26 @@ export const ResetPasswordForm: React.FC<Args> = ({ token }) => {
|
||||
label: i18n.t('authentication:newPassword'),
|
||||
required: true,
|
||||
}}
|
||||
indexPath=""
|
||||
parentPath=""
|
||||
parentSchemaPath=""
|
||||
path="password"
|
||||
schemaPath={`${userSlug}.password`}
|
||||
/>
|
||||
<ConfirmPasswordField />
|
||||
<HiddenField
|
||||
field={{
|
||||
name: 'token',
|
||||
type: 'text',
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
}}
|
||||
forceUsePathFromProps
|
||||
indexPath=""
|
||||
parentPath={userSlug}
|
||||
parentSchemaPath={userSlug}
|
||||
path="token"
|
||||
schemaPath={`${userSlug}.token`}
|
||||
value={token}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { AdminViewComponent, AdminViewProps, ImportMap, SanitizedConfig } from 'payload'
|
||||
import type {
|
||||
AdminViewComponent,
|
||||
AdminViewProps,
|
||||
CustomComponent,
|
||||
EditConfig,
|
||||
ImportMap,
|
||||
SanitizedConfig,
|
||||
} from 'payload'
|
||||
import type React from 'react'
|
||||
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
@@ -46,6 +53,20 @@ const oneSegmentViews: OneSegmentViews = {
|
||||
unauthorized: UnauthorizedView,
|
||||
}
|
||||
|
||||
function getViewActions({
|
||||
editConfig,
|
||||
viewKey,
|
||||
}: {
|
||||
editConfig: EditConfig
|
||||
viewKey: keyof EditConfig
|
||||
}): CustomComponent[] | undefined {
|
||||
if (editConfig && viewKey in editConfig && 'actions' in editConfig[viewKey]) {
|
||||
return editConfig[viewKey].actions
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const getViewFromConfig = ({
|
||||
adminRoute,
|
||||
config,
|
||||
@@ -65,8 +86,10 @@ export const getViewFromConfig = ({
|
||||
}): {
|
||||
DefaultView: ViewFromConfig
|
||||
initPageOptions: Parameters<typeof initPage>[0]
|
||||
serverProps: Record<string, unknown>
|
||||
templateClassName: string
|
||||
templateType: 'default' | 'minimal'
|
||||
viewActions?: CustomComponent[]
|
||||
} => {
|
||||
let ViewToRender: ViewFromConfig = null
|
||||
let templateClassName: string
|
||||
@@ -79,10 +102,30 @@ export const getViewFromConfig = ({
|
||||
searchParams,
|
||||
}
|
||||
|
||||
const [segmentOne, segmentTwo] = segments
|
||||
let viewActions: CustomComponent[] = config?.admin?.components?.actions || []
|
||||
|
||||
const [segmentOne, segmentTwo, segmentThree, segmentFour, segmentFive] = segments
|
||||
|
||||
const isGlobal = segmentOne === 'globals'
|
||||
const isCollection = segmentOne === 'collections'
|
||||
let matchedCollection: SanitizedConfig['collections'][number] = undefined
|
||||
let matchedGlobal: SanitizedConfig['globals'][number] = undefined
|
||||
|
||||
let serverProps = {}
|
||||
|
||||
if (isCollection) {
|
||||
matchedCollection = config.collections.find(({ slug }) => slug === segmentTwo)
|
||||
serverProps = {
|
||||
collectionConfig: matchedCollection,
|
||||
}
|
||||
}
|
||||
|
||||
if (isGlobal) {
|
||||
matchedGlobal = config.globals.find(({ slug }) => slug === segmentTwo)
|
||||
serverProps = {
|
||||
globalConfig: matchedGlobal,
|
||||
}
|
||||
}
|
||||
|
||||
switch (segments.length) {
|
||||
case 0: {
|
||||
@@ -146,7 +189,7 @@ export const getViewFromConfig = ({
|
||||
templateType = 'minimal'
|
||||
}
|
||||
|
||||
if (isCollection) {
|
||||
if (isCollection && matchedCollection) {
|
||||
// --> /collections/:collectionSlug
|
||||
|
||||
ViewToRender = {
|
||||
@@ -155,7 +198,8 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = `${segmentTwo}-list`
|
||||
templateType = 'default'
|
||||
} else if (isGlobal) {
|
||||
viewActions = viewActions.concat(matchedCollection.admin.components?.views?.list?.actions)
|
||||
} else if (isGlobal && matchedGlobal) {
|
||||
// --> /globals/:globalSlug
|
||||
|
||||
ViewToRender = {
|
||||
@@ -164,6 +208,14 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = 'global-edit'
|
||||
templateType = 'default'
|
||||
|
||||
// add default view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewKey: 'default',
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -176,13 +228,13 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = 'verify'
|
||||
templateType = 'minimal'
|
||||
} else if (isCollection) {
|
||||
} else if (isCollection && matchedCollection) {
|
||||
// Custom Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:versionId
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
@@ -190,7 +242,65 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = `collection-default-edit`
|
||||
templateType = 'default'
|
||||
} else if (isGlobal) {
|
||||
|
||||
// Adds view actions to the current collection view
|
||||
if (matchedCollection.admin?.components?.views?.edit) {
|
||||
if ('root' in matchedCollection.admin.components.views.edit) {
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views?.edit,
|
||||
viewKey: 'root',
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
if (segmentFive) {
|
||||
if (segmentFour === 'versions') {
|
||||
// add version view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views?.edit,
|
||||
viewKey: 'version',
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else if (segmentFour) {
|
||||
if (segmentFour === 'versions') {
|
||||
// add versions view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views.edit,
|
||||
viewKey: 'versions',
|
||||
}),
|
||||
)
|
||||
} else if (segmentFour === 'preview') {
|
||||
// add livePreview view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views.edit,
|
||||
viewKey: 'livePreview',
|
||||
}),
|
||||
)
|
||||
} else if (segmentFour === 'api') {
|
||||
// add api view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views.edit,
|
||||
viewKey: 'api',
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else if (segmentThree) {
|
||||
// add default view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedCollection.admin?.components?.views.edit,
|
||||
viewKey: 'default',
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isGlobal && matchedGlobal) {
|
||||
// Custom Views
|
||||
// --> /globals/:globalSlug/versions
|
||||
// --> /globals/:globalSlug/preview
|
||||
@@ -203,6 +313,56 @@ export const getViewFromConfig = ({
|
||||
|
||||
templateClassName = `global-edit`
|
||||
templateType = 'default'
|
||||
|
||||
// Adds view actions to the current global view
|
||||
if (matchedGlobal.admin?.components?.views?.edit) {
|
||||
if ('root' in matchedGlobal.admin.components.views.edit) {
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin.components?.views?.edit,
|
||||
viewKey: 'root',
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
if (segmentFour) {
|
||||
if (segmentThree === 'versions') {
|
||||
// add version view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewKey: 'version',
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else if (segmentThree) {
|
||||
if (segmentThree === 'versions') {
|
||||
// add versions view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewKey: 'versions',
|
||||
}),
|
||||
)
|
||||
} else if (segmentThree === 'preview') {
|
||||
// add livePreview view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewKey: 'livePreview',
|
||||
}),
|
||||
)
|
||||
} else if (segmentThree === 'api') {
|
||||
// add api view actions
|
||||
viewActions = viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewKey: 'api',
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -214,7 +374,9 @@ export const getViewFromConfig = ({
|
||||
return {
|
||||
DefaultView: ViewToRender,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
templateClassName,
|
||||
templateType,
|
||||
viewActions: viewActions.reverse(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type { ImportMap, MappedComponent, SanitizedConfig } from 'payload'
|
||||
import type { ImportMap, SanitizedConfig } from 'payload'
|
||||
|
||||
import { formatAdminURL, getCreateMappedComponent, RenderComponent } from '@payloadcms/ui/shared'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { DefaultTemplate } from '../../templates/Default/index.js'
|
||||
import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
||||
import { getClientConfig } from '../../utilities/getClientConfig.js'
|
||||
import { initPage } from '../../utilities/initPage/index.js'
|
||||
import { getViewFromConfig } from './getViewFromConfig.js'
|
||||
|
||||
@@ -55,7 +57,14 @@ export const RootPage = async ({
|
||||
|
||||
const searchParams = await searchParamsPromise
|
||||
|
||||
const { DefaultView, initPageOptions, templateClassName, templateType } = getViewFromConfig({
|
||||
const {
|
||||
DefaultView,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
templateClassName,
|
||||
templateType,
|
||||
viewActions,
|
||||
} = getViewFromConfig({
|
||||
adminRoute,
|
||||
config,
|
||||
currentRoute,
|
||||
@@ -64,21 +73,22 @@ export const RootPage = async ({
|
||||
segments,
|
||||
})
|
||||
|
||||
let dbHasUser = false
|
||||
|
||||
const initPageResult = await initPage(initPageOptions)
|
||||
|
||||
dbHasUser = await initPageResult?.req.payload.db
|
||||
.findOne({
|
||||
collection: userSlug,
|
||||
req: initPageResult?.req,
|
||||
})
|
||||
?.then((doc) => !!doc)
|
||||
const dbHasUser =
|
||||
initPageResult.req.user ||
|
||||
(await initPageResult?.req.payload.db
|
||||
.findOne({
|
||||
collection: userSlug,
|
||||
req: initPageResult?.req,
|
||||
})
|
||||
?.then((doc) => !!doc))
|
||||
|
||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
||||
if (initPageResult?.req?.user) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
if (dbHasUser) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
@@ -111,27 +121,30 @@ export const RootPage = async ({
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
const createMappedView = getCreateMappedComponent({
|
||||
importMap,
|
||||
serverProps: {
|
||||
i18n: initPageResult?.req.i18n,
|
||||
importMap,
|
||||
initPageResult,
|
||||
params,
|
||||
payload: initPageResult?.req.payload,
|
||||
searchParams,
|
||||
},
|
||||
const clientConfig = await getClientConfig({
|
||||
config,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
})
|
||||
|
||||
const MappedView: MappedComponent = createMappedView(
|
||||
DefaultView.payloadComponent,
|
||||
undefined,
|
||||
DefaultView.Component,
|
||||
'createMappedView',
|
||||
const RenderedView = (
|
||||
<RenderServerComponent
|
||||
clientProps={{ clientConfig }}
|
||||
Component={DefaultView.payloadComponent}
|
||||
Fallback={DefaultView.Component}
|
||||
importMap={importMap}
|
||||
serverProps={{
|
||||
...serverProps,
|
||||
clientConfig,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
importMap,
|
||||
initPageResult,
|
||||
params,
|
||||
payload: initPageResult?.req.payload,
|
||||
searchParams,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
const RenderedView = <RenderComponent mappedComponent={MappedView} />
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{!templateType && <Fragment>{RenderedView}</Fragment>}
|
||||
@@ -147,6 +160,7 @@ export const RootPage = async ({
|
||||
permissions={initPageResult?.permissions}
|
||||
searchParams={searchParams}
|
||||
user={initPageResult?.req.user}
|
||||
viewActions={viewActions}
|
||||
visibleEntities={{
|
||||
// The reason we are not passing in initPageResult.visibleEntities directly is due to a "Cannot assign to read only property of object '#<Object>" error introduced in React 19
|
||||
// which this caused as soon as initPageResult.visibleEntities is passed in
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
'use client'
|
||||
import type { ClientCollectionConfig, ClientGlobalConfig, OptionObject } from 'payload'
|
||||
|
||||
import {
|
||||
Gutter,
|
||||
SetViewActions,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
usePayloadAPI,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { Gutter, useConfig, useDocumentInfo, usePayloadAPI, useTranslation } from '@payloadcms/ui'
|
||||
import { formatDate } from '@payloadcms/ui/shared'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
@@ -80,11 +73,6 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
|
||||
|
||||
return (
|
||||
<main className={baseClass}>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.version?.actions
|
||||
}
|
||||
/>
|
||||
<SetStepNav
|
||||
collectionConfig={collectionConfig}
|
||||
collectionSlug={collectionSlug}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { SanitizedCollectionConfig, SanitizedConfig, SanitizedGlobalConfig } from 'payload'
|
||||
import type {
|
||||
PaginatedDocs,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
SanitizedGlobalConfig,
|
||||
TypeWithVersion,
|
||||
} from 'payload'
|
||||
|
||||
import { type Column, SortColumn } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
@@ -11,6 +17,7 @@ import { IDCell } from './cells/ID/index.js'
|
||||
export const buildVersionColumns = ({
|
||||
collectionConfig,
|
||||
docID,
|
||||
docs,
|
||||
globalConfig,
|
||||
i18n: { t },
|
||||
latestDraftVersion,
|
||||
@@ -19,6 +26,7 @@ export const buildVersionColumns = ({
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
config: SanitizedConfig
|
||||
docID?: number | string
|
||||
docs: PaginatedDocs<TypeWithVersion<any>>['docs']
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
i18n: I18n
|
||||
latestDraftVersion?: string
|
||||
@@ -30,56 +38,37 @@ export const buildVersionColumns = ({
|
||||
{
|
||||
accessor: 'updatedAt',
|
||||
active: true,
|
||||
cellProps: {
|
||||
field: {
|
||||
name: '',
|
||||
type: 'date',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: (
|
||||
<CreatedAtCell
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
docID={docID}
|
||||
globalSlug={globalConfig?.slug}
|
||||
/>
|
||||
),
|
||||
},
|
||||
Label: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: {
|
||||
name: '',
|
||||
type: 'date',
|
||||
},
|
||||
Heading: <SortColumn Label={t('general:updatedAt')} name="updatedAt" />,
|
||||
renderedCells: docs.map((doc, i) => {
|
||||
return (
|
||||
<CreatedAtCell
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
docID={docID}
|
||||
globalSlug={globalConfig?.slug}
|
||||
key={i}
|
||||
rowData={{
|
||||
id: doc.id,
|
||||
updatedAt: doc.updatedAt,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}),
|
||||
},
|
||||
{
|
||||
accessor: 'id',
|
||||
active: true,
|
||||
cellProps: {
|
||||
field: {
|
||||
name: '',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: <IDCell />,
|
||||
},
|
||||
Label: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: {
|
||||
name: '',
|
||||
type: 'text',
|
||||
},
|
||||
Heading: <SortColumn disable Label={t('version:versionID')} name="id" />,
|
||||
renderedCells: docs.map((doc, i) => {
|
||||
return <IDCell id={doc.id} key={i} />
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -90,31 +79,21 @@ export const buildVersionColumns = ({
|
||||
columns.push({
|
||||
accessor: '_status',
|
||||
active: true,
|
||||
cellProps: {
|
||||
field: {
|
||||
name: '',
|
||||
type: 'checkbox',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: (
|
||||
<AutosaveCell
|
||||
latestDraftVersion={latestDraftVersion}
|
||||
latestPublishedVersion={latestPublishedVersion}
|
||||
/>
|
||||
),
|
||||
},
|
||||
Label: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
field: {
|
||||
name: '',
|
||||
type: 'checkbox',
|
||||
},
|
||||
Heading: <SortColumn disable Label={t('version:status')} name="status" />,
|
||||
renderedCells: docs.map((doc, i) => {
|
||||
return (
|
||||
<AutosaveCell
|
||||
key={i}
|
||||
latestDraftVersion={latestDraftVersion}
|
||||
latestPublishedVersion={latestPublishedVersion}
|
||||
rowData={doc}
|
||||
/>
|
||||
)
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
'use client'
|
||||
import { Pill, useConfig, useTableCell, useTranslation } from '@payloadcms/ui'
|
||||
import { Pill, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
type AutosaveCellProps = {
|
||||
latestDraftVersion?: string
|
||||
latestPublishedVersion?: string
|
||||
rowData?: {
|
||||
autosave?: boolean
|
||||
publishedLocale?: string
|
||||
version: {
|
||||
_status?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const renderPill = (data, latestVersion, currentLabel, previousLabel, pillStyle) => {
|
||||
@@ -23,9 +30,10 @@ export const renderPill = (data, latestVersion, currentLabel, previousLabel, pil
|
||||
export const AutosaveCell: React.FC<AutosaveCellProps> = ({
|
||||
latestDraftVersion,
|
||||
latestPublishedVersion,
|
||||
rowData = { autosave: undefined, publishedLocale: undefined, version: undefined },
|
||||
}) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
const { rowData } = useTableCell()
|
||||
|
||||
const {
|
||||
config: { localization },
|
||||
} = useConfig()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { useConfig, useTableCell, useTranslation } from '@payloadcms/ui'
|
||||
import { useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { formatAdminURL, formatDate } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React from 'react'
|
||||
@@ -10,12 +10,17 @@ type CreatedAtCellProps = {
|
||||
collectionSlug?: string
|
||||
docID?: number | string
|
||||
globalSlug?: string
|
||||
rowData?: {
|
||||
id: number | string
|
||||
updatedAt: Date | number | string
|
||||
}
|
||||
}
|
||||
|
||||
export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
||||
collectionSlug,
|
||||
docID,
|
||||
globalSlug,
|
||||
rowData: { id, updatedAt } = {},
|
||||
}) => {
|
||||
const {
|
||||
config: {
|
||||
@@ -26,30 +31,25 @@ export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const { cellData, rowData } = useTableCell()
|
||||
|
||||
const versionID = rowData.id
|
||||
|
||||
let to: string
|
||||
|
||||
if (collectionSlug) {
|
||||
to = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/${docID}/versions/${versionID}`,
|
||||
path: `/collections/${collectionSlug}/${docID}/versions/${id}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
to = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${globalSlug}/versions/${versionID}`,
|
||||
path: `/globals/${globalSlug}/versions/${id}`,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={to} prefetch={false}>
|
||||
{cellData &&
|
||||
formatDate({ date: cellData as Date | number | string, i18n, pattern: dateFormat })}
|
||||
{formatDate({ date: updatedAt, i18n, pattern: dateFormat })}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
'use client'
|
||||
import { useTableCell } from '@payloadcms/ui'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
export const IDCell: React.FC = () => {
|
||||
const { cellData } = useTableCell()
|
||||
return <Fragment>{cellData as number | string}</Fragment>
|
||||
export function IDCell({ id }: { id: number | string }) {
|
||||
return <Fragment>{id}</Fragment>
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
'use client'
|
||||
import type { ClientCollectionConfig, ClientGlobalConfig, SanitizedCollectionConfig } from 'payload'
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
type Column,
|
||||
LoadingOverlayToggle,
|
||||
Pagination,
|
||||
PerPage,
|
||||
SetViewActions,
|
||||
Table,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useListQuery,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
@@ -24,14 +21,8 @@ export const VersionsViewClient: React.FC<{
|
||||
}> = (props) => {
|
||||
const { baseClass, columns, paginationLimits } = props
|
||||
|
||||
const { collectionSlug, globalSlug } = useDocumentInfo()
|
||||
const { data, handlePageChange, handlePerPageChange } = useListQuery()
|
||||
|
||||
const { getEntityConfig } = useConfig()
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug }) as ClientCollectionConfig
|
||||
const globalConfig = getEntityConfig({ globalSlug }) as ClientGlobalConfig
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const limit = searchParams.get('limit')
|
||||
|
||||
@@ -41,11 +32,6 @@ export const VersionsViewClient: React.FC<{
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SetViewActions
|
||||
actions={
|
||||
(collectionConfig || globalConfig)?.admin?.components?.views?.edit?.versions?.actions
|
||||
}
|
||||
/>
|
||||
<LoadingOverlayToggle name="versions" show={!data} />
|
||||
{versionCount === 0 && (
|
||||
<div className={`${baseClass}__no-versions`}>
|
||||
@@ -54,11 +40,7 @@ export const VersionsViewClient: React.FC<{
|
||||
)}
|
||||
{versionCount > 0 && (
|
||||
<React.Fragment>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data?.docs}
|
||||
fields={(collectionConfig || globalConfig)?.fields}
|
||||
/>
|
||||
<Table columns={columns} data={data?.docs} />
|
||||
<div className={`${baseClass}__page-controls`}>
|
||||
<Pagination
|
||||
hasNextPage={data.hasNextPage}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { EditViewComponent, PaginatedDocs, PayloadServerReactComponent } from 'payload'
|
||||
|
||||
import { Gutter, ListQueryProvider } from '@payloadcms/ui'
|
||||
import { Gutter, ListQueryProvider, SetDocumentStepNav } from '@payloadcms/ui'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import { isNumber } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||
import { buildVersionColumns } from './buildColumns.js'
|
||||
import { getLatestVersion } from './getLatestVersion.js'
|
||||
import { VersionsViewClient } from './index.client.js'
|
||||
@@ -165,6 +164,7 @@ export const VersionsView: PayloadServerReactComponent<EditViewComponent> = asyn
|
||||
collectionConfig,
|
||||
config,
|
||||
docID: id,
|
||||
docs: versionsData?.docs,
|
||||
globalConfig,
|
||||
i18n,
|
||||
latestDraftVersion: latestDraftVersion?.id,
|
||||
@@ -190,6 +190,7 @@ export const VersionsView: PayloadServerReactComponent<EditViewComponent> = asyn
|
||||
<main className={baseClass}>
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
<ListQueryProvider
|
||||
collectionSlug={collectionSlug}
|
||||
data={versionsData}
|
||||
defaultLimit={limitToUse}
|
||||
defaultSort={sort as string}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/withPayload.js" /* Include the withPayload.js file in the build */
|
||||
],
|
||||
, "../ui/src/utilities/renderFields.tsx" ],
|
||||
"references": [
|
||||
{ "path": "../payload" },
|
||||
{ "path": "../ui" },
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { JSONSchema4 } from 'json-schema'
|
||||
import type { ImportMap } from '../bin/generateImportMap/index.js'
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||
import type { Config, PayloadComponent, SanitizedConfig } from '../config/types.js'
|
||||
import type { ValidationFieldError } from '../errors/ValidationError.js'
|
||||
import type {
|
||||
Field,
|
||||
FieldAffectingData,
|
||||
RichTextField,
|
||||
RichTextFieldClient,
|
||||
@@ -15,7 +15,7 @@ import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { RequestContext } from '../index.js'
|
||||
import type { JsonObject, Payload, PayloadRequest, PopulateType } from '../types/index.js'
|
||||
import type { RichTextFieldClientProps } from './fields/RichText.js'
|
||||
import type { CreateMappedComponent } from './types.js'
|
||||
import type { FieldSchemaMap } from './types.js'
|
||||
|
||||
export type AfterReadRichTextHookArgs<
|
||||
TData extends TypeWithID = any,
|
||||
@@ -91,7 +91,7 @@ export type BeforeChangeRichTextHookArgs<
|
||||
|
||||
duplicate?: boolean
|
||||
|
||||
errors?: { field: string; message: string }[]
|
||||
errors?: ValidationFieldError[]
|
||||
/** Only available in `beforeChange` field hooks */
|
||||
mergeLocaleActions?: (() => Promise<void>)[]
|
||||
/** A string relating to which operation the field type is currently executing within. */
|
||||
@@ -184,32 +184,19 @@ export type RichTextHooks = {
|
||||
beforeChange?: BeforeChangeRichTextHook[]
|
||||
beforeValidate?: BeforeValidateRichTextHook[]
|
||||
}
|
||||
|
||||
export type RichTextGenerateComponentMap = (args: {
|
||||
clientField: RichTextFieldClient
|
||||
createMappedComponent: CreateMappedComponent
|
||||
field: RichTextField
|
||||
i18n: I18nClient
|
||||
|
||||
importMap: ImportMap
|
||||
payload: Payload
|
||||
schemaPath: string
|
||||
}) => Map<string, unknown>
|
||||
|
||||
type RichTextAdapterBase<
|
||||
Value extends object = object,
|
||||
AdapterProps = any,
|
||||
ExtraFieldProperties = {},
|
||||
> = {
|
||||
generateComponentMap: PayloadComponent<any, never>
|
||||
generateImportMap?: Config['admin']['importMap']['generators'][0]
|
||||
generateSchemaMap?: (args: {
|
||||
config: SanitizedConfig
|
||||
field: RichTextField
|
||||
i18n: I18n<any, any>
|
||||
schemaMap: Map<string, Field[]>
|
||||
schemaMap: FieldSchemaMap
|
||||
schemaPath: string
|
||||
}) => Map<string, Field[]>
|
||||
}) => FieldSchemaMap
|
||||
/**
|
||||
* Like an afterRead hook, but runs only for the GraphQL resolver. For populating data, this should be used, as afterRead hooks do not have a depth in graphQL.
|
||||
*
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { ClientCollectionConfig } from '../../collections/config/client.js'
|
||||
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
|
||||
import type { ClientField } from '../../fields/config/client.js'
|
||||
|
||||
export type RowData = Record<string, any>
|
||||
|
||||
export type CellComponentProps<TField extends ClientField = ClientField> = {
|
||||
export type DefaultCellComponentProps<TCellData = any, TField extends ClientField = ClientField> = {
|
||||
readonly cellData: TCellData
|
||||
readonly className?: string
|
||||
readonly collectionConfig: ClientCollectionConfig
|
||||
readonly columnIndex?: number
|
||||
readonly customCellProps?: Record<string, any>
|
||||
readonly field: TField
|
||||
readonly link?: boolean
|
||||
readonly onClick?: (args: {
|
||||
@@ -12,13 +17,5 @@ export type CellComponentProps<TField extends ClientField = ClientField> = {
|
||||
collectionSlug: SanitizedCollectionConfig['slug']
|
||||
rowData: RowData
|
||||
}) => void
|
||||
}
|
||||
|
||||
export type DefaultCellComponentProps<TCellData = any, TField extends ClientField = ClientField> = {
|
||||
readonly cellData: TCellData
|
||||
readonly customCellContext?: {
|
||||
collectionSlug?: SanitizedCollectionConfig['slug']
|
||||
uploadConfig?: SanitizedCollectionConfig['upload']
|
||||
}
|
||||
readonly rowData: RowData
|
||||
} & CellComponentProps<TField>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MarkOptional } from 'ts-essentials'
|
||||
|
||||
import type { ArrayField, ArrayFieldClient } from '../../fields/config/types.js'
|
||||
import type { ArrayField, ArrayFieldClient, ClientField } from '../../fields/config/types.js'
|
||||
import type { ArrayFieldValidation } from '../../fields/validations.js'
|
||||
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
|
||||
import type {
|
||||
@@ -14,15 +14,14 @@ import type {
|
||||
FieldDescriptionServerComponent,
|
||||
FieldLabelClientComponent,
|
||||
FieldLabelServerComponent,
|
||||
MappedComponent,
|
||||
} from '../types.js'
|
||||
|
||||
type ArrayFieldClientWithoutType = MarkOptional<ArrayFieldClient, 'type'>
|
||||
|
||||
type ArrayFieldBaseClientProps = {
|
||||
readonly CustomRowLabel?: MappedComponent
|
||||
readonly path?: string
|
||||
readonly validate?: ArrayFieldValidation
|
||||
}
|
||||
} & Pick<ServerFieldBase, 'permissions'>
|
||||
|
||||
export type ArrayFieldClientProps = ArrayFieldBaseClientProps &
|
||||
ClientFieldBase<ArrayFieldClientWithoutType>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MarkOptional } from 'ts-essentials'
|
||||
|
||||
import type { BlocksField, BlocksFieldClient } from '../../fields/config/types.js'
|
||||
import type { BlocksField, BlocksFieldClient, ClientField } from '../../fields/config/types.js'
|
||||
import type { BlocksFieldValidation } from '../../fields/validations.js'
|
||||
import type { FieldErrorClientComponent, FieldErrorServerComponent } from '../forms/Error.js'
|
||||
import type {
|
||||
@@ -19,8 +19,9 @@ import type {
|
||||
type BlocksFieldClientWithoutType = MarkOptional<BlocksFieldClient, 'type'>
|
||||
|
||||
type BlocksFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: BlocksFieldValidation
|
||||
}
|
||||
} & Pick<ServerFieldBase, 'permissions'>
|
||||
|
||||
export type BlocksFieldClientProps = BlocksFieldBaseClientProps &
|
||||
ClientFieldBase<BlocksFieldClientWithoutType>
|
||||
|
||||
@@ -24,6 +24,7 @@ type CheckboxFieldBaseClientProps = {
|
||||
readonly id?: string
|
||||
readonly onChange?: (value: boolean) => void
|
||||
readonly partialChecked?: boolean
|
||||
readonly path?: string
|
||||
readonly validate?: CheckboxFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ type CodeFieldClientWithoutType = MarkOptional<CodeFieldClient, 'type'>
|
||||
|
||||
type CodeFieldBaseClientProps = {
|
||||
readonly autoComplete?: string
|
||||
readonly valiCode?: CodeFieldValidation
|
||||
readonly path?: string
|
||||
readonly validate?: CodeFieldValidation
|
||||
}
|
||||
|
||||
export type CodeFieldClientProps = ClientFieldBase<CodeFieldClientWithoutType> &
|
||||
|
||||
@@ -15,9 +15,14 @@ import type {
|
||||
FieldLabelServerComponent,
|
||||
} from '../types.js'
|
||||
|
||||
type CollapsibleFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
} & Pick<ServerFieldBase, 'permissions'>
|
||||
|
||||
type CollapsibleFieldClientWithoutType = MarkOptional<CollapsibleFieldClient, 'type'>
|
||||
|
||||
export type CollapsibleFieldClientProps = ClientFieldBase<CollapsibleFieldClientWithoutType>
|
||||
export type CollapsibleFieldClientProps = ClientFieldBase<CollapsibleFieldClientWithoutType> &
|
||||
CollapsibleFieldBaseClientProps
|
||||
|
||||
export type CollapsibleFieldServerProps = ServerFieldBase<
|
||||
CollapsibleField,
|
||||
@@ -29,8 +34,10 @@ export type CollapsibleFieldServerComponent = FieldServerComponent<
|
||||
CollapsibleFieldClientWithoutType
|
||||
>
|
||||
|
||||
export type CollapsibleFieldClientComponent =
|
||||
FieldClientComponent<CollapsibleFieldClientWithoutType>
|
||||
export type CollapsibleFieldClientComponent = FieldClientComponent<
|
||||
CollapsibleFieldClientWithoutType,
|
||||
CollapsibleFieldBaseClientProps
|
||||
>
|
||||
|
||||
export type CollapsibleFieldLabelServerComponent = FieldLabelServerComponent<
|
||||
CollapsibleField,
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type DateFieldClientWithoutType = MarkOptional<DateFieldClient, 'type'>
|
||||
|
||||
type DateFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: DateFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
type EmailFieldClientWithoutType = MarkOptional<EmailFieldClient, 'type'>
|
||||
|
||||
type EmailFieldBaseClientProps = {
|
||||
readonly autoComplete?: string
|
||||
readonly path?: string
|
||||
readonly validate?: EmailFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,12 @@ import type {
|
||||
|
||||
type GroupFieldClientWithoutType = MarkOptional<GroupFieldClient, 'type'>
|
||||
|
||||
export type GroupFieldClientProps = ClientFieldBase<GroupFieldClientWithoutType>
|
||||
export type GroupFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
} & Pick<ServerFieldBase, 'permissions'>
|
||||
|
||||
export type GroupFieldClientProps = ClientFieldBase<GroupFieldClientWithoutType> &
|
||||
GroupFieldBaseClientProps
|
||||
|
||||
export type GroupFieldServerProps = ServerFieldBase<GroupField, GroupFieldClientWithoutType>
|
||||
|
||||
@@ -26,7 +31,10 @@ export type GroupFieldServerComponent = FieldServerComponent<
|
||||
GroupFieldClientWithoutType
|
||||
>
|
||||
|
||||
export type GroupFieldClientComponent = FieldClientComponent<GroupFieldClientWithoutType>
|
||||
export type GroupFieldClientComponent = FieldClientComponent<
|
||||
GroupFieldClientWithoutType,
|
||||
GroupFieldBaseClientProps
|
||||
>
|
||||
|
||||
export type GroupFieldLabelServerComponent = FieldLabelServerComponent<
|
||||
GroupField,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ClientField } from '../../fields/config/client.js'
|
||||
import type { FormFieldBase } from '../types.js'
|
||||
import type { ClientField } from '../../fields/config/types.js'
|
||||
import type { ClientFieldBase } from '../types.js'
|
||||
|
||||
export type HiddenFieldProps = {
|
||||
type HiddenFieldBaseClientProps = {
|
||||
readonly disableModifyingForm?: false
|
||||
readonly field?: {
|
||||
readonly name?: string
|
||||
} & Pick<ClientField, '_path'>
|
||||
readonly forceUsePathFromProps?: boolean
|
||||
} & ClientField
|
||||
readonly value?: unknown
|
||||
} & FormFieldBase
|
||||
}
|
||||
|
||||
export type HiddenFieldProps = ClientFieldBase & HiddenFieldBaseClientProps
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type JSONFieldClientWithoutType = MarkOptional<JSONFieldClient, 'type'>
|
||||
|
||||
type JSONFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: JSONFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ import type {
|
||||
|
||||
type JoinFieldClientWithoutType = MarkOptional<JoinFieldClient, 'type'>
|
||||
|
||||
export type JoinFieldClientProps = ClientFieldBase<JoinFieldClientWithoutType>
|
||||
export type JoinFieldClientProps = {
|
||||
path?: string
|
||||
} & ClientFieldBase<JoinFieldClientWithoutType>
|
||||
|
||||
export type JoinFieldServerProps = ServerFieldBase<JoinField>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ type NumberFieldClientWithoutType = MarkOptional<NumberFieldClient, 'type'>
|
||||
|
||||
type NumberFieldBaseClientProps = {
|
||||
readonly onChange?: (e: number) => void
|
||||
readonly path?: string
|
||||
readonly validate?: NumberFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type PointFieldClientWithoutType = MarkOptional<PointFieldClient, 'type'>
|
||||
|
||||
type PointFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: PointFieldValidation
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ type RadioFieldBaseClientProps = {
|
||||
*/
|
||||
readonly disableModifyingForm?: boolean
|
||||
readonly onChange?: OnChange
|
||||
readonly path?: string
|
||||
readonly validate?: RadioFieldValidation
|
||||
readonly value?: string
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
type RelationshipFieldClientWithoutType = MarkOptional<RelationshipFieldClient, 'type'>
|
||||
|
||||
type RelationshipFieldBaseClientProps = {
|
||||
readonly path?: string
|
||||
readonly validate?: RelationshipFieldValidation
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user