Compare commits
391 Commits
fix/sort-v
...
@payloadcm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27589482dd | ||
|
|
d7ab4b7062 | ||
|
|
2c8fbf1be3 | ||
|
|
eec88f8f1b | ||
|
|
1481ef97b5 | ||
|
|
a89e89fb80 | ||
|
|
7e7eeb059d | ||
|
|
dc2a502dcc | ||
|
|
a9a5ba82d8 | ||
|
|
e6e8fae1c5 | ||
|
|
a05868a7f3 | ||
|
|
f27cd26575 | ||
|
|
db835ea5c8 | ||
|
|
d8f265fb94 | ||
|
|
d81d4eb075 | ||
|
|
52f89c0136 | ||
|
|
b0083b7c07 | ||
|
|
21649537a6 | ||
|
|
9be34c9599 | ||
|
|
8ca632e541 | ||
|
|
2ef79145a4 | ||
|
|
a0641a445d | ||
|
|
3a2e78f7f3 | ||
|
|
976d69d154 | ||
|
|
66018362fe | ||
|
|
647fe23d1c | ||
|
|
d7c61861f6 | ||
|
|
7caa098023 | ||
|
|
fd54c40400 | ||
|
|
e180131314 | ||
|
|
5902d4542b | ||
|
|
6bc282444e | ||
|
|
4dc6c09347 | ||
|
|
03b9ab0054 | ||
|
|
3c3c93f483 | ||
|
|
5dbfb1a335 | ||
|
|
d411874589 | ||
|
|
8358e2f2d2 | ||
|
|
2c67eff059 | ||
|
|
012b8e6f90 | ||
|
|
fcd4c8d830 | ||
|
|
81ec435363 | ||
|
|
e116fcfbf5 | ||
|
|
c47632dc1d | ||
|
|
0dab68b336 | ||
|
|
483f93bfcf | ||
|
|
4bd01df411 | ||
|
|
c956a85252 | ||
|
|
beed83b231 | ||
|
|
3b1bdcbe41 | ||
|
|
d3d0971275 | ||
|
|
1a99d66cd0 | ||
|
|
52c4a63bf1 | ||
|
|
3446d28602 | ||
|
|
2eb18771a1 | ||
|
|
f6fd5d6742 | ||
|
|
76d6c88261 | ||
|
|
10ebd76fcf | ||
|
|
36d6eb0a69 | ||
|
|
cd1f8dc332 | ||
|
|
e4275aa228 | ||
|
|
ddfcb2f12e | ||
|
|
09f33eae2c | ||
|
|
d3b7c9feec | ||
|
|
5fd3d43000 | ||
|
|
7767679caa | ||
|
|
fdf2e32005 | ||
|
|
773be8744d | ||
|
|
40d5bc0c4a | ||
|
|
fd33c790f2 | ||
|
|
ae7aac7639 | ||
|
|
05cc2873b4 | ||
|
|
171ee121e9 | ||
|
|
4af1d7d812 | ||
|
|
d229fc391a | ||
|
|
46a24a9822 | ||
|
|
06a51b3c9b | ||
|
|
1d4142ccc0 | ||
|
|
f1741beba2 | ||
|
|
9103277a10 | ||
|
|
d84673f400 | ||
|
|
15e23a3adc | ||
|
|
565929adcf | ||
|
|
8bbac60e60 | ||
|
|
15c7f0dbf3 | ||
|
|
05eba56d7d | ||
|
|
ab984b3ea9 | ||
|
|
a54638eb47 | ||
|
|
69af8d9c83 | ||
|
|
64864686c4 | ||
|
|
32c0bef05e | ||
|
|
a77513e94f | ||
|
|
aaf883909c | ||
|
|
cc56da11d6 | ||
|
|
41d9c28073 | ||
|
|
a071b97607 | ||
|
|
cfd9231403 | ||
|
|
71dce62646 | ||
|
|
db376f24ba | ||
|
|
2752483ac7 | ||
|
|
860f867c62 | ||
|
|
5b0adbe9c3 | ||
|
|
fb7d1be2f3 | ||
|
|
687a2e85d0 | ||
|
|
df80483afe | ||
|
|
8781770d83 | ||
|
|
ed1d5a60f7 | ||
|
|
50a0965561 | ||
|
|
c31fa5dd83 | ||
|
|
440eb8d9c6 | ||
|
|
1523b2be41 | ||
|
|
68f55c4064 | ||
|
|
0d3544ea04 | ||
|
|
f152f451dc | ||
|
|
5d92436e39 | ||
|
|
63edecddd8 | ||
|
|
65cdf6bfd0 | ||
|
|
9e831a6a00 | ||
|
|
72249d1ecd | ||
|
|
dc22496103 | ||
|
|
af1c2e924e | ||
|
|
1487250752 | ||
|
|
1ebb9f3915 | ||
|
|
eab04d9b4d | ||
|
|
1758b6c449 | ||
|
|
bca1be8cb6 | ||
|
|
1e197933dd | ||
|
|
4eb929d57c | ||
|
|
198209c2a4 | ||
|
|
54f19ce1e3 | ||
|
|
d32f3ade1b | ||
|
|
bf189abc91 | ||
|
|
69a379e49f | ||
|
|
493fc3ed68 | ||
|
|
138e495e1a | ||
|
|
8fe619a221 | ||
|
|
5195a80dba | ||
|
|
909cf90fa2 | ||
|
|
c1d1a00d4a | ||
|
|
ae68093f35 | ||
|
|
0f2f355a01 | ||
|
|
0101aa60d9 | ||
|
|
c823ee07cd | ||
|
|
1f12f9b480 | ||
|
|
ec31ab3a2c | ||
|
|
a7bea35d69 | ||
|
|
64a4f19539 | ||
|
|
c35661e16e | ||
|
|
69b6179521 | ||
|
|
3d2e167e78 | ||
|
|
aa1955221c | ||
|
|
7a9b11e2c4 | ||
|
|
a82c0d0e50 | ||
|
|
35a6daa10d | ||
|
|
bf5db4e44a | ||
|
|
a87e8aa82b | ||
|
|
e00d87a791 | ||
|
|
b61babca73 | ||
|
|
e403a0492e | ||
|
|
54a76e1401 | ||
|
|
91f6e36420 | ||
|
|
9e74fe558f | ||
|
|
ac8bcfac23 | ||
|
|
91b0a691ed | ||
|
|
3ced6ec2a0 | ||
|
|
650fe159ee | ||
|
|
b92657fb39 | ||
|
|
17dbe066c1 | ||
|
|
5acc88ee9a | ||
|
|
d4983af3fc | ||
|
|
6f2dcfd44e | ||
|
|
3df6435353 | ||
|
|
3ef03364be | ||
|
|
5396af9bfc | ||
|
|
56eddf3a93 | ||
|
|
6dd900e6b9 | ||
|
|
441a26b79c | ||
|
|
471d5ca17f | ||
|
|
536f995ab4 | ||
|
|
bb3c97828e | ||
|
|
c4d32d5418 | ||
|
|
8522fd9f27 | ||
|
|
ee93118688 | ||
|
|
47f9b89175 | ||
|
|
891fc55e25 | ||
|
|
988755f202 | ||
|
|
b5483a46f6 | ||
|
|
04fd2d6e82 | ||
|
|
4cdc7cf3c4 | ||
|
|
fbaa1028e6 | ||
|
|
df26e19c16 | ||
|
|
98501cf4c0 | ||
|
|
d0ac142871 | ||
|
|
d60c66ebd6 | ||
|
|
a515bdae56 | ||
|
|
cc40853903 | ||
|
|
121d69faf9 | ||
|
|
0a2d7c858a | ||
|
|
0962e1e563 | ||
|
|
a324768b3f | ||
|
|
0973ee512e | ||
|
|
f74e492448 | ||
|
|
e567627809 | ||
|
|
56965bc0ed | ||
|
|
e43d6520c5 | ||
|
|
1123909960 | ||
|
|
bc1853c2e7 | ||
|
|
318b734f96 | ||
|
|
2734b1d54a | ||
|
|
82510c1574 | ||
|
|
0a2b02f206 | ||
|
|
41ee127de8 | ||
|
|
9ddec59ddd | ||
|
|
b72b22c628 | ||
|
|
a685f30245 | ||
|
|
173ec6f0f8 | ||
|
|
349ab5343e | ||
|
|
cf97adab7c | ||
|
|
b6fc940f18 | ||
|
|
2fb685c0fe | ||
|
|
54be5847f7 | ||
|
|
bf4f37b514 | ||
|
|
9e577e7214 | ||
|
|
69185c06c2 | ||
|
|
e561016d07 | ||
|
|
9db4dadce3 | ||
|
|
fa40d511c2 | ||
|
|
ebfb86866f | ||
|
|
be853a0657 | ||
|
|
c880342099 | ||
|
|
d09bbd2171 | ||
|
|
c1823f719a | ||
|
|
39686e3f05 | ||
|
|
482973559d | ||
|
|
600dbd72f4 | ||
|
|
785337dc5d | ||
|
|
14a35f35c3 | ||
|
|
ed2e176285 | ||
|
|
2d79280999 | ||
|
|
8b5084ab43 | ||
|
|
b19356597b | ||
|
|
0bd412edbd | ||
|
|
1a6ba25e5d | ||
|
|
26d0cd18a1 | ||
|
|
31653fe76e | ||
|
|
a6d52223d5 | ||
|
|
1bf7c4084c | ||
|
|
419a3eef53 | ||
|
|
f62531bafd | ||
|
|
dc815dad14 | ||
|
|
bde2ce9b53 | ||
|
|
041af0100c | ||
|
|
2e18c5b8cf | ||
|
|
53f0c526f7 | ||
|
|
dc5c4eced0 | ||
|
|
0d8b6d03ed | ||
|
|
cfa364280f | ||
|
|
7a293563fb | ||
|
|
9359954233 | ||
|
|
9fcc05676e | ||
|
|
1e95f5de49 | ||
|
|
f8983e9e5c | ||
|
|
aab71f03b3 | ||
|
|
447d88bf82 | ||
|
|
897e94f2f4 | ||
|
|
87936e5b52 | ||
|
|
5e02762715 | ||
|
|
0785820539 | ||
|
|
bb6d545aae | ||
|
|
4cdc94d92f | ||
|
|
c28dca6fc0 | ||
|
|
95e630201a | ||
|
|
84100be7eb | ||
|
|
0c7007ae9a | ||
|
|
3e7e3669fe | ||
|
|
1d3bb9c287 | ||
|
|
cacc624f5a | ||
|
|
04dd824f0a | ||
|
|
dc929732b1 | ||
|
|
a47fd23199 | ||
|
|
d205da0aa4 | ||
|
|
c414f12527 | ||
|
|
d9418c9fe3 | ||
|
|
65e2ba9bd0 | ||
|
|
b3f808644f | ||
|
|
1e30435525 | ||
|
|
156d25741b | ||
|
|
de3ee812cd | ||
|
|
234fb33864 | ||
|
|
c168bb5201 | ||
|
|
0ce5d774cb | ||
|
|
d2c2bbd711 | ||
|
|
88193adebb | ||
|
|
eac44f9496 | ||
|
|
6400095f1f | ||
|
|
b57267e60a | ||
|
|
79541b6ba7 | ||
|
|
0420098e94 | ||
|
|
9f80634be4 | ||
|
|
25ecb27aed | ||
|
|
2ff2efd4b2 | ||
|
|
ff7a29179d | ||
|
|
8403f8ac2a | ||
|
|
df0d4fa726 | ||
|
|
2a4bb5a11d | ||
|
|
2b6c5e42b5 | ||
|
|
a1a4765a94 | ||
|
|
64d0bc7a16 | ||
|
|
b1fb43baf5 | ||
|
|
bb309ca843 | ||
|
|
760662263f | ||
|
|
bce5205cf1 | ||
|
|
0b21726af6 | ||
|
|
04a7d256c5 | ||
|
|
8a9915b58a | ||
|
|
820e867804 | ||
|
|
699314a781 | ||
|
|
86552e62ff | ||
|
|
13769d3cdc | ||
|
|
158ae0de30 | ||
|
|
04056513d7 | ||
|
|
2b7e6dda2f | ||
|
|
b11464542a | ||
|
|
b97568f394 | ||
|
|
18e8839b8c | ||
|
|
f5e5bfae81 | ||
|
|
b27ab75e07 | ||
|
|
5799e4015f | ||
|
|
2f72ed78e1 | ||
|
|
5f3f038a6b | ||
|
|
5b29852c0a | ||
|
|
9616e43035 | ||
|
|
b918425e72 | ||
|
|
9ed5f5b6fc | ||
|
|
1721d118a8 | ||
|
|
8dc400c65a | ||
|
|
a5ac793443 | ||
|
|
8d6d995d78 | ||
|
|
4652255d4f | ||
|
|
a274f2e5ca | ||
|
|
ed4528096a | ||
|
|
f7946af404 | ||
|
|
26ead270a2 | ||
|
|
c45c784c58 | ||
|
|
6b6977cc00 | ||
|
|
41a6abd2e4 | ||
|
|
d59ccc0f34 | ||
|
|
870946d01b | ||
|
|
3bf68ef9d4 | ||
|
|
60d7d51a0a | ||
|
|
61deb2c873 | ||
|
|
0ae27d4212 | ||
|
|
3c96622313 | ||
|
|
8066ce6f49 | ||
|
|
b7f9ffc51a | ||
|
|
4a873a5ae3 | ||
|
|
c080deb0b8 | ||
|
|
8cefa8181c | ||
|
|
a34dd651b1 | ||
|
|
a86041836f | ||
|
|
6dbd760a2e | ||
|
|
4181a84e9b | ||
|
|
b4d5168409 | ||
|
|
74756c0703 | ||
|
|
dce57d6fdd | ||
|
|
d55df67642 | ||
|
|
769d9063d5 | ||
|
|
8f95a23df9 | ||
|
|
9816c33015 | ||
|
|
1d14c976f2 | ||
|
|
63c436e0ac | ||
|
|
a4700d7a9d | ||
|
|
e5a1fe0771 | ||
|
|
b101ff86a9 | ||
|
|
0c5a6044a0 | ||
|
|
6fc7c0b9ad | ||
|
|
9459e82161 | ||
|
|
62501eb3b8 | ||
|
|
df000b7508 | ||
|
|
c04bde6725 | ||
|
|
1fe8ae39cb | ||
|
|
e2049b9564 | ||
|
|
49d9836ab4 | ||
|
|
4ed38575bf | ||
|
|
ef166cd70d | ||
|
|
d45665f092 | ||
|
|
10bae6dab7 | ||
|
|
ad25c86fdd | ||
|
|
f064ff35f3 | ||
|
|
55b44b41bf | ||
|
|
1f3e9b22f4 |
@@ -10,3 +10,9 @@ cdaa0acd61d3001407609915bd573b78565d5571
|
||||
|
||||
# prettier write again
|
||||
dfac7395fed95fc5d8ebca21b786ce70821942bb
|
||||
|
||||
# lint and format plugin-cloud
|
||||
fb7d1be2f3325d076b7c967b1730afcef37922c2
|
||||
|
||||
# lint and format create-payload-app
|
||||
5fd3d430001efe86515262ded5e26f00c1451181
|
||||
|
||||
36
.github/workflows/main.yml
vendored
36
.github/workflows/main.yml
vendored
@@ -170,7 +170,6 @@ jobs:
|
||||
run: pnpm dev:generate-graphql-schema graphql-schema-gen
|
||||
|
||||
build-packages:
|
||||
name: Build Packages
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
strategy:
|
||||
@@ -206,3 +205,38 @@ jobs:
|
||||
|
||||
- name: Build ${{ matrix.pkg }}
|
||||
run: pnpm turbo run build --filter=${{ matrix.pkg }}
|
||||
|
||||
plugins:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pkg:
|
||||
- plugin-cloud
|
||||
- create-payload-app
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Build ${{ matrix.pkg }}
|
||||
run: pnpm turbo run build --filter=${{ matrix.pkg }}
|
||||
|
||||
- name: Test ${{ matrix.pkg }}
|
||||
run: pnpm --filter ${{ matrix.pkg }} run test
|
||||
if: matrix.pkg != 'create-payload-app' # degit doesn't work within GitHub Actions
|
||||
|
||||
@@ -26,9 +26,8 @@
|
||||
</h4>
|
||||
<hr/>
|
||||
|
||||
<h3>
|
||||
🎉 Payload 2.0 is now available! Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>
|
||||
</h3>
|
||||
> [!IMPORTANT]
|
||||
> 🎉 <strong>Payload 2.0 is now available!<strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
|
||||
|
||||
<h3>Benefits over a regular CMS</h3>
|
||||
<ul>
|
||||
|
||||
@@ -129,7 +129,7 @@ To add a _new_ view to the Admin Panel, simply add another key to the `views` ob
|
||||
}
|
||||
```
|
||||
|
||||
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/master/test/admin/components)._
|
||||
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/main/test/admin/components)._
|
||||
|
||||
For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component).
|
||||
|
||||
@@ -399,12 +399,12 @@ Your custom view components will be given all the props that a React Router `<Ro
|
||||
|
||||
#### Example
|
||||
|
||||
You can find examples of custom views in the [Payload source code `/test/admin/components/views` folder](https://github.com/payloadcms/payload/tree/master/test/admin/components/views). There, you'll find two custom views:
|
||||
You can find examples of custom views in the [Payload source code `/test/admin/components/views` folder](https://github.com/payloadcms/payload/tree/main/test/admin/components/views). There, you'll find two custom views:
|
||||
|
||||
1. A custom view that uses the `DefaultTemplate`, which is the built-in Payload template that displays the sidebar and "eyebrow nav"
|
||||
1. A custom view that uses the `MinimalTemplate` - which is just a centered template used for things like logging in or out
|
||||
|
||||
To see how to pass in your custom views to create custom views of your own, take a look at the `admin.components.views` property of the [Payload test admin config](https://github.com/payloadcms/payload/blob/master/test/admin/config.ts).
|
||||
To see how to pass in your custom views to create custom views of your own, take a look at the `admin.components.views` property of the [Payload test admin config](https://github.com/payloadcms/payload/blob/main/test/admin/config.ts).
|
||||
|
||||
### Fields
|
||||
|
||||
|
||||
@@ -28,25 +28,25 @@ When bundled, it is code-split, highly performant (even with 100+ fields), and w
|
||||
|
||||
All options for the Admin panel are defined in your base Payload config file.
|
||||
|
||||
| Option | Description |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `user` | The `slug` of a Collection that you want be used to log in to the Admin dashboard. [More](/docs/admin/overview#the-admin-user-collection) |
|
||||
| `buildPath` | Specify an absolute path for where to store the built Admin panel bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| `meta` | Base meta data to use for the Admin panel. Included properties are `titleSuffix`, `ogImage`, and `favicon`. |
|
||||
| `disable` | If set to `true`, the entire Admin panel will be disabled. |
|
||||
| `indexHTML` | Optionally replace the entirety of the `index.html` file used by the Admin panel. Reference the [base index.html file](https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/index.html) to ensure your replacement has the appropriate HTML elements. |
|
||||
| `css` | Absolute path to a stylesheet that you can use to override / customize the Admin panel styling. [More](/docs/admin/customizing-css). |
|
||||
| `scss` | Absolute path to a Sass variables / mixins stylesheet meant to override Payload styles to make for an easy re-skinning of the Admin panel. [More](/docs/admin/customizing-css#overriding-scss-variables). |
|
||||
| `dateFormat` | Global date format that will be used for all dates in the Admin panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `autoLogin` | Used to automate admin log-in for dev and demonstration convenience. [More](/docs/authentication/config). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
|
||||
| `components` | Component overrides that affect the entirety of the Admin panel. [More](/docs/admin/components) |
|
||||
| `webpack` | Customize the Webpack config that's used to generate the Admin panel. [More](/docs/admin/webpack) |
|
||||
| `vite` | Customize the Vite config that's used to generate the Admin panel. [More](/docs/admin/vite) |
|
||||
| **`bundler`** | The bundler that you would like to use to bundle the admin panel. Officially supported bundlers: [Webpack](/docs/admin/webpack) and [Vite](/docs/admin/vite). |
|
||||
| **`logoutRoute`** | The route for the `logout` page. |
|
||||
| **`inactivityRoute`** | The route for the `logout` inactivity page. |
|
||||
| Option | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `bundler` | The bundler that you would like to use to bundle the admin panel. Officially supported bundlers: [Webpack](/docs/admin/webpack) and [Vite](/docs/admin/vite). |
|
||||
| `user` | The `slug` of a Collection that you want be used to log in to the Admin dashboard. [More](/docs/admin/overview#the-admin-user-collection) |
|
||||
| `buildPath` | Specify an absolute path for where to store the built Admin panel bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| `meta` | Base meta data to use for the Admin panel. Included properties are `titleSuffix`, `ogImage`, and `favicon`. |
|
||||
| `disable` | If set to `true`, the entire Admin panel will be disabled. |
|
||||
| `indexHTML` | Optionally replace the entirety of the `index.html` file used by the Admin panel. Reference the [base index.html file](https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/index.html) to ensure your replacement has the appropriate HTML elements. |
|
||||
| `css` | Absolute path to a stylesheet that you can use to override / customize the Admin panel styling. [More](/docs/admin/customizing-css). |
|
||||
| `scss` | Absolute path to a Sass variables / mixins stylesheet meant to override Payload styles to make for an easy re-skinning of the Admin panel. [More](/docs/admin/customizing-css#overriding-scss-variables). |
|
||||
| `dateFormat` | Global date format that will be used for all dates in the Admin panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `autoLogin` | Used to automate admin log-in for dev and demonstration convenience. [More](/docs/authentication/config). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
|
||||
| `components` | Component overrides that affect the entirety of the Admin panel. [More](/docs/admin/components) |
|
||||
| `webpack` | Customize the Webpack config that's used to generate the Admin panel. [More](/docs/admin/webpack) |
|
||||
| `vite` | Customize the Vite config that's used to generate the Admin panel. [More](/docs/admin/vite) |
|
||||
| `logoutRoute` | The route for the `logout` page. |
|
||||
| `inactivityRoute` | The route for the `logout` inactivity page. |
|
||||
|
||||
### The Admin User Collection
|
||||
|
||||
|
||||
@@ -19,48 +19,53 @@ Payload is a _config-based_, code-first CMS and application framework. The Paylo
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `serverURL` | A string used to define the absolute URL of your app including the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port |
|
||||
| `collections` | An array of all Collections that Payload will manage. To read more about how to define your collection configs, [click here](/docs/configuration/collections). |
|
||||
| `cors` | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. |
|
||||
| `globals` | An array of all Globals that Payload will manage. For more on Globals and their configs, [click here](/docs/configuration/globals). |
|
||||
| `admin` | Base Payload admin configuration. Specify custom components, control metadata, set the Admin user collection, and [more](/docs/admin/overview#admin-options). |
|
||||
| `editor` | Default richText editor which will be used by richText fields. |
|
||||
| `localization` | Opt-in and control how Payload handles the translation of your content into multiple locales. [More](/docs/configuration/localization) |
|
||||
| `graphQL` | Manage GraphQL-specific functionality here. Define your own queries and mutations, manage query complexity limits, and [more](/docs/graphql/overview#graphql-options). |
|
||||
| `cookiePrefix` | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| `csrf` | A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. [More](/docs/authentication/overview#csrf-protection) |
|
||||
| `defaultDepth` | If a user does not specify `depth` while requesting a resource, this depth will be used. [More](/docs/getting-started/concepts#depth) |
|
||||
| `maxDepth` | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. |
|
||||
| `indexSortableFields` | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
| `upload` | Base Payload upload configuration. [More](/docs/upload/overview#payload-wide-upload-options). |
|
||||
| `routes` | Control the routing structure that Payload binds itself to. Specify `admin`, `api`, `graphQL`, and `graphQLPlayground`. |
|
||||
| `email` | Base email settings to allow Payload to generate email such as Forgot Password requests and other requirements. [More](/docs/email/overview#configuration) |
|
||||
| `express` | Express-specific middleware options such as compression and JSON parsing. [More](/docs/configuration/express) |
|
||||
| `debug` | Enable to expose more detailed error information. |
|
||||
| `telemetry` | Disable Payload telemetry by passing `false`. [More](/docs/configuration/overview#telemetry) |
|
||||
| `rateLimit` | Control IP-based rate limiting for all Payload resources. Used to prevent DDoS attacks and [more](/docs/production/preventing-abuse#rate-limiting-requests). |
|
||||
| `hooks` | Tap into Payload-wide hooks. [More](/docs/hooks/overview) |
|
||||
| `plugins` | An array of Payload plugins. [More](/docs/plugins/overview) |
|
||||
| `endpoints` | An array of custom API endpoints added to the Payload router. [More](/docs/rest-api/overview#custom-endpoints) |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
| Option | Description |
|
||||
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `admin` \* | Base Payload admin configuration. Specify bundler*, custom components, control metadata, set the Admin user collection, and [more](/docs/admin/overview#admin-options). Required. |
|
||||
| `editor` \* | Rich Text Editor which will be used by richText fields. Required. |
|
||||
| `db` \* | Database Adapter which will be used by Payload. Read more [here](/docs/database/overview). Required. |
|
||||
| `serverURL` | A string used to define the absolute URL of your app including the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port |
|
||||
| `collections` | An array of all Collections that Payload will manage. To read more about how to define your collection configs, [click here](/docs/configuration/collections). |
|
||||
| `globals` | An array of all Globals that Payload will manage. For more on Globals and their configs, [click here](/docs/configuration/globals). |
|
||||
| `cors` | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. |
|
||||
| `localization` | Opt-in and control how Payload handles the translation of your content into multiple locales. [More](/docs/configuration/localization) |
|
||||
| `graphQL` | Manage GraphQL-specific functionality here. Define your own queries and mutations, manage query complexity limits, and [more](/docs/graphql/overview#graphql-options). |
|
||||
| `cookiePrefix` | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| `csrf` | A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. [More](/docs/authentication/overview#csrf-protection) |
|
||||
| `defaultDepth` | If a user does not specify `depth` while requesting a resource, this depth will be used. [More](/docs/getting-started/concepts#depth) |
|
||||
| `maxDepth` | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. |
|
||||
| `indexSortableFields` | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
| `upload` | Base Payload upload configuration. [More](/docs/upload/overview#payload-wide-upload-options). |
|
||||
| `routes` | Control the routing structure that Payload binds itself to. Specify `admin`, `api`, `graphQL`, and `graphQLPlayground`. |
|
||||
| `email` | Base email settings to allow Payload to generate email such as Forgot Password requests and other requirements. [More](/docs/email/overview#configuration) |
|
||||
| `express` | Express-specific middleware options such as compression and JSON parsing. [More](/docs/configuration/express) |
|
||||
| `debug` | Enable to expose more detailed error information. |
|
||||
| `telemetry` | Disable Payload telemetry by passing `false`. [More](/docs/configuration/overview#telemetry) |
|
||||
| `rateLimit` | Control IP-based rate limiting for all Payload resources. Used to prevent DDoS attacks and [more](/docs/production/preventing-abuse#rate-limiting-requests). |
|
||||
| `hooks` | Tap into Payload-wide hooks. [More](/docs/hooks/overview) |
|
||||
| `plugins` | An array of Payload plugins. [More](/docs/plugins/overview) |
|
||||
| `endpoints` | An array of custom API endpoints added to the Payload router. [More](/docs/rest-api/overview#custom-endpoints) |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
#### Simple example
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres' // beta
|
||||
|
||||
import { viteBundler } from '@payloadcms/bundler-vite'
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical' // beta
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
|
||||
export default buildConfig({
|
||||
bundler: webpackBundler() // or viteBundler(),
|
||||
admin: {
|
||||
bundler: webpackBundler(), // or viteBundler()
|
||||
},
|
||||
db: mongooseAdapter({}) // or postgresAdapter({}),
|
||||
editor: lexicalEditor({}) // or slateEditor({})
|
||||
collections: [
|
||||
|
||||
@@ -88,13 +88,14 @@ This package provides the following functions:
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`subscribe`** | Subscribes to the Admin panel's `window.postMessage` events and calls the provided callback function. |
|
||||
| **`unsubscribe`** | Unsubscribes from the Admin panel's `window.postMessage` events. |
|
||||
| **`ready`** | Sends a `window.postMessage` event to the Admin panel to indicate that the front-end is ready to receive messages. |
|
||||
|
||||
The `subscribe` function takes the following args:
|
||||
|
||||
| Path | Description |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`callback`** \* | A callback function that is called with `data` every time a change is made to the document. |
|
||||
| **`serverURL`** \* | The URL of your Payload server. git s |
|
||||
| **`serverURL`** \* | The URL of your Payload server. |
|
||||
| **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. |
|
||||
| **`depth`** | The depth of the relationships to fetch. Defaults to `0`. |
|
||||
|
||||
@@ -103,18 +104,23 @@ With these functions, you can build your own hook using your front-end framework
|
||||
```tsx
|
||||
import { subscribe, unsubscribe } from '@payloadcms/live-preview';
|
||||
|
||||
// Build your own hook to subscribe to the live preview events
|
||||
// This function will handle everything for you like
|
||||
// 1. subscribing to `window.postMessage` events
|
||||
// 2. merging initial page data with incoming form state
|
||||
// 3. populating relationships and uploads
|
||||
// To build your own hook, subscribe to Live Preview events using the`subscribe` function
|
||||
// It handles everything from:
|
||||
// 1. Listening to `window.postMessage` events
|
||||
// 2. Merging initial data with active form state
|
||||
// 3. Populating relationships and uploads
|
||||
// 4. Calling the `onChange` callback with the result
|
||||
// Your hook should also:
|
||||
// 1. Tell the Admin panel when it is ready to receive messages
|
||||
// 2. Handle the results of the `onChange` callback to update the UI
|
||||
// 3. Unsubscribe from the `window.postMessage` events when it unmounts
|
||||
```
|
||||
|
||||
Here is an example of what the same `useLivePreview` React hook from above looks like under the hood:
|
||||
|
||||
```tsx
|
||||
import { subscribe, unsubscribe } from '@payloadcms/live-preview'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { subscribe, unsubscribe, ready } from '@payloadcms/live-preview'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||
|
||||
export const useLivePreview = <T extends any>(props: {
|
||||
depth?: number
|
||||
@@ -127,13 +133,18 @@ export const useLivePreview = <T extends any>(props: {
|
||||
const { depth = 0, initialData, serverURL } = props
|
||||
const [data, setData] = useState<T>(initialData)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const hasSentReadyMessage = useRef<boolean>(false)
|
||||
|
||||
const onChange = useCallback((mergedData) => {
|
||||
// When a change is made, the `onChange` callback will be called with the merged data
|
||||
// Set this merged data into state so that React will re-render the UI
|
||||
setData(mergedData)
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for `window.postMessage` events from the Admin panel
|
||||
// When a change is made, the `onChange` callback will be called with the merged data
|
||||
const subscription = subscribe({
|
||||
callback: onChange,
|
||||
depth,
|
||||
@@ -141,6 +152,17 @@ export const useLivePreview = <T extends any>(props: {
|
||||
serverURL,
|
||||
})
|
||||
|
||||
// Once subscribed, send a `ready` message back up to the Admin panel
|
||||
// This will indicate that the front-end is ready to receive messages
|
||||
if (!hasSentReadyMessage.current) {
|
||||
hasSentReadyMessage.current = true
|
||||
|
||||
ready({
|
||||
serverURL
|
||||
})
|
||||
}
|
||||
|
||||
// When the component unmounts, unsubscribe from the `window.postMessage` events
|
||||
return () => {
|
||||
unsubscribe(subscription)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ keywords: live preview, preview, live, iframe, iframe preview, visual editing, d
|
||||
|
||||
**With Live Preview you can render your front-end application directly within the Admin panel. As you type, your changes take effect in real-time. No need to save a draft or publish your changes.**
|
||||
|
||||
Live Preview works by rendering an iframe on the page that loads your front-end application. The Admin panel communicates with your app through `window.postMessage` events. These events are emitted every time a change is made to the document. Your app then listens for these events and re-renders itself with the data it receives.
|
||||
Live Preview works by rendering an iframe on the page that loads your front-end application. The Admin panel communicates with your app through [`window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) events. These events are emitted every time a change is made to the document. Your app then listens for these events and re-renders itself with the data it receives.
|
||||
|
||||
{/* IMAGE OF LIVE PREVIEW HERE */}
|
||||
|
||||
@@ -84,8 +84,8 @@ Here is an example of using a function that returns a dynamic URL:
|
||||
documentInfo,
|
||||
locale
|
||||
}) => `${data.tenant.url}${ // Multi-tenant top-level domain
|
||||
documentInfo.slug === 'posts' ? `/posts/${data.slug}` : `/${data.slug}
|
||||
`}?locale=${locale}`, // Localization query param
|
||||
documentInfo.slug === 'posts' ? `/posts/${data.slug}` : `${data.slug !== 'home' : `/${data.slug}` : ''}`
|
||||
}${locale ? `?locale=${locale?.code}` : ''}`, // Localization query param
|
||||
collections: ['pages'],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ const result = await payload.find({
|
||||
depth: 2,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
pagination: false, // If you want to disable pagination count, etc.
|
||||
where: {}, // pass a `where` query here
|
||||
sort: '-title',
|
||||
locale: 'en',
|
||||
|
||||
@@ -59,3 +59,7 @@ All Payload APIs support the pagination controls below. With them, you can creat
|
||||
| ------- | --------------------------------------- |
|
||||
| `limit` | Limits the number of documents returned |
|
||||
| `page` | Get a specific page number |
|
||||
|
||||
### Disabling pagination within Local API
|
||||
|
||||
For `find` operations within the Local API, you can disable pagination to retrieve all documents from a collection by passing `pagination: false` to the `find` local operation. This is not supported in REST or GraphQL, however, because it could potentially lead to malicious activity.
|
||||
@@ -17,9 +17,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,9 +20,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"express": "^4.17.1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,9 +15,9 @@
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"@faceless-ui/modal": "^2.0.1",
|
||||
"@payloadcms/plugin-form-builder": "^1.0.12",
|
||||
"@payloadcms/plugin-seo": "^1.0.8",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
|
||||
@@ -10,7 +10,8 @@ export const Pages: CollectionConfig = {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||
livePreview: {
|
||||
url: ({ data }) => `${process.env.PAYLOAD_PUBLIC_SITE_URL}/${data.slug}`,
|
||||
url: ({ data }) =>
|
||||
`${process.env.PAYLOAD_PUBLIC_SITE_URL}${data.slug !== 'home' ? `/${data.slug}` : ''}`,
|
||||
},
|
||||
},
|
||||
access: {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,9 @@
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "^1.0.0-beta.5",
|
||||
"@payloadcms/db-mongodb": "^1.0.0-beta.8",
|
||||
"@payloadcms/richtext-slate": "^1.0.0-beta.4",
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"@payloadcms/plugin-redirects": "^1.0.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,8 @@
|
||||
"lint-staged": "lint-staged",
|
||||
"pretest": "pnpm build",
|
||||
"reinstall": "pnpm clean:unix && pnpm install",
|
||||
"list:packages": "./scripts/list_published_packages.sh beta",
|
||||
"script:release:beta": "./scripts/release_beta.sh",
|
||||
"script:list-packages": "tsx ./scripts/list-packages.ts",
|
||||
"script:release": "tsx ./scripts/release.ts",
|
||||
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
|
||||
"test:components": "cross-env jest --config=jest.components.config.js",
|
||||
"test:e2e": "npx playwright install --with-deps && ts-node -T ./test/runE2E.ts",
|
||||
@@ -74,6 +74,7 @@
|
||||
"qs": "6.11.2",
|
||||
"rimraf": "3.0.2",
|
||||
"shelljs": "0.8.5",
|
||||
"simple-git": "^3.20.0",
|
||||
"slash": "3.0.0",
|
||||
"slate": "0.91.4",
|
||||
"ts-node": "10.9.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/bundler-webpack",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"description": "The officially supported Webpack bundler adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
@@ -25,6 +25,7 @@
|
||||
"css-loader": "5.2.7",
|
||||
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||
"file-loader": "6.2.0",
|
||||
"find-node-modules": "^2.1.3",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"md5": "2.3.0",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
@@ -48,6 +49,7 @@
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/extract-text-webpack-plugin": "^3.0.7",
|
||||
"@types/find-node-modules": "^2.1.0",
|
||||
"@types/html-webpack-plugin": "^3.2.6",
|
||||
"@types/mini-css-extract-plugin": "^1.4.3",
|
||||
"@types/optimize-css-assets-webpack-plugin": "^5.0.5",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { Configuration } from 'webpack'
|
||||
|
||||
import findNodeModules from 'find-node-modules'
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin'
|
||||
import path from 'path'
|
||||
import webpack from 'webpack'
|
||||
@@ -8,7 +9,8 @@ import webpack from 'webpack'
|
||||
const mockModulePath = path.resolve(__dirname, '../mocks/emptyModule.js')
|
||||
const mockDotENVPath = path.resolve(__dirname, '../mocks/dotENV.js')
|
||||
|
||||
const nodeModulesPath = path.resolve(__dirname, '../../../../')
|
||||
const nodeModulesPaths = findNodeModules({ cwd: process.cwd() })
|
||||
const nodeModulesPath = path.resolve(nodeModulesPaths[0])
|
||||
const adminFolderPath = path.resolve(nodeModulesPath, 'payload/dist/admin')
|
||||
|
||||
export const getBaseConfig = (payloadConfig: SanitizedConfig): Configuration => ({
|
||||
|
||||
@@ -23,7 +23,9 @@ export const getDevConfig = (payloadConfig: SanitizedConfig): Configuration => {
|
||||
entry: {
|
||||
...baseConfig.entry,
|
||||
main: [
|
||||
`webpack-hot-middleware/client?path=${payloadConfig.routes.admin}/__webpack_hmr`,
|
||||
`${require.resolve('webpack-hot-middleware/client')}?path=${
|
||||
payloadConfig.routes.admin
|
||||
}/__webpack_hmr`,
|
||||
...(baseConfig.entry.main as string[]),
|
||||
],
|
||||
},
|
||||
|
||||
44
packages/create-payload-app/.eslintrc.js
Normal file
44
packages/create-payload-app/.eslintrc.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/** @type {import('prettier').Config} */
|
||||
module.exports = {
|
||||
extends: ['@payloadcms'],
|
||||
ignorePatterns: ['README.md', '**/*.spec.ts'],
|
||||
overrides: [
|
||||
{
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['package.json', 'tsconfig.json'],
|
||||
rules: {
|
||||
'perfectionist/sort-array-includes': 'off',
|
||||
'perfectionist/sort-astro-attributes': 'off',
|
||||
'perfectionist/sort-classes': 'off',
|
||||
'perfectionist/sort-enums': 'off',
|
||||
'perfectionist/sort-exports': 'off',
|
||||
'perfectionist/sort-imports': 'off',
|
||||
'perfectionist/sort-interfaces': 'off',
|
||||
'perfectionist/sort-jsx-props': 'off',
|
||||
'perfectionist/sort-keys': 'off',
|
||||
'perfectionist/sort-maps': 'off',
|
||||
'perfectionist/sort-named-exports': 'off',
|
||||
'perfectionist/sort-named-imports': 'off',
|
||||
'perfectionist/sort-object-types': 'off',
|
||||
'perfectionist/sort-objects': 'off',
|
||||
'perfectionist/sort-svelte-attributes': 'off',
|
||||
'perfectionist/sort-union-types': 'off',
|
||||
'perfectionist/sort-vue-attributes': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
root: true,
|
||||
}
|
||||
34
packages/create-payload-app/README.md
Normal file
34
packages/create-payload-app/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Create Payload App
|
||||
|
||||
CLI for easily starting new Payload project
|
||||
|
||||
## Usage
|
||||
|
||||
```text
|
||||
|
||||
USAGE
|
||||
|
||||
$ npx create-payload-app
|
||||
$ npx create-payload-app my-project
|
||||
$ npx create-payload-app -n my-project -t blog
|
||||
|
||||
OPTIONS
|
||||
|
||||
-n my-payload-app Set project name
|
||||
-t template_name Choose specific template
|
||||
|
||||
Available templates:
|
||||
|
||||
blank Blank Template
|
||||
website Website Template
|
||||
ecommerce E-commerce Template
|
||||
plugin Template for creating a Payload plugin
|
||||
payload-demo Payload demo site at https://demo.payloadcms.com
|
||||
payload-website Payload website CMS at https://payloadcms.com
|
||||
|
||||
--use-npm Use npm to install dependencies
|
||||
--use-yarn Use yarn to install dependencies
|
||||
--use-pnpm Use pnpm to install dependencies
|
||||
--no-deps Do not install any dependencies
|
||||
-h Show help
|
||||
```
|
||||
2
packages/create-payload-app/bin/cli.js
Executable file
2
packages/create-payload-app/bin/cli.js
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
require('../dist/index.js')
|
||||
9
packages/create-payload-app/jest.config.js
Normal file
9
packages/create-payload-app/jest.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/src/**/?(*.)+(spec|test|it-test).[tj]s?(x)'],
|
||||
testTimeout: 10000,
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)?$': 'ts-jest',
|
||||
},
|
||||
verbose: true,
|
||||
}
|
||||
47
packages/create-payload-app/package.json
Normal file
47
packages/create-payload-app/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "0.5.2",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"create-payload-app": "bin/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && pnpm copyfiles",
|
||||
"copyfiles": "copyfiles -u 1 \"src/templates/**\" \"src/lib/common-files/**\" dist",
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
||||
"lint-staged": "lint-staged --quiet",
|
||||
"test": "jest",
|
||||
"prepublishOnly": "pnpm test && pnpm clean && pnpm build"
|
||||
},
|
||||
"files": [
|
||||
"package.json",
|
||||
"dist",
|
||||
"bin"
|
||||
],
|
||||
"dependencies": {
|
||||
"@sindresorhus/slugify": "^1.1.0",
|
||||
"arg": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"degit": "^2.8.4",
|
||||
"execa": "^5.0.0",
|
||||
"figures": "^3.2.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"handlebars": "^4.7.7",
|
||||
"ora": "^5.1.0",
|
||||
"prompts": "^2.4.2",
|
||||
"terminal-link": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/command-exists": "^1.2.0",
|
||||
"@types/degit": "^2.8.3",
|
||||
"@types/fs-extra": "^9.0.12",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/node": "^16.6.2",
|
||||
"@types/prompts": "^2.4.1",
|
||||
"ts-jest": "^29.1.0"
|
||||
}
|
||||
}
|
||||
8
packages/create-payload-app/src/index.ts
Normal file
8
packages/create-payload-app/src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Main } from './main'
|
||||
import { error } from './utils/log'
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await new Main().init()
|
||||
}
|
||||
|
||||
main().catch((e) => error(`An error has occurred: ${e instanceof Error ? e.message : e}`))
|
||||
117
packages/create-payload-app/src/lib/configure-payload-config.ts
Normal file
117
packages/create-payload-app/src/lib/configure-payload-config.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import fse from 'fs-extra'
|
||||
import path from 'path'
|
||||
|
||||
import type { DbDetails } from '../types'
|
||||
|
||||
import { warning } from '../utils/log'
|
||||
import { bundlerPackages, dbPackages, editorPackages } from './packages'
|
||||
|
||||
/** Update payload config with necessary imports and adapters */
|
||||
export async function configurePayloadConfig(args: {
|
||||
dbDetails: DbDetails | undefined
|
||||
projectDir: string
|
||||
}): Promise<void> {
|
||||
if (!args.dbDetails) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update package.json
|
||||
const packageJsonPath = path.resolve(args.projectDir, 'package.json')
|
||||
try {
|
||||
const packageObj = await fse.readJson(packageJsonPath)
|
||||
|
||||
packageObj.dependencies['payload'] = '^2.0.0'
|
||||
|
||||
const dbPackage = dbPackages[args.dbDetails.type]
|
||||
const bundlerPackage = bundlerPackages['webpack']
|
||||
const editorPackage = editorPackages['slate']
|
||||
|
||||
// Delete all other db adapters
|
||||
Object.values(dbPackages).forEach((p) => {
|
||||
if (p.packageName !== dbPackage.packageName) {
|
||||
delete packageObj.dependencies[p.packageName]
|
||||
}
|
||||
})
|
||||
|
||||
packageObj.dependencies[dbPackage.packageName] = dbPackage.version
|
||||
packageObj.dependencies[bundlerPackage.packageName] = bundlerPackage.version
|
||||
packageObj.dependencies[editorPackage.packageName] = editorPackage.version
|
||||
|
||||
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
|
||||
} catch (err: unknown) {
|
||||
warning('Unable to update name in package.json')
|
||||
}
|
||||
|
||||
try {
|
||||
const possiblePaths = [
|
||||
path.resolve(args.projectDir, 'src/payload.config.ts'),
|
||||
path.resolve(args.projectDir, 'src/payload/payload.config.ts'),
|
||||
]
|
||||
|
||||
let payloadConfigPath: string | undefined
|
||||
|
||||
possiblePaths.forEach((p) => {
|
||||
if (fse.pathExistsSync(p) && !payloadConfigPath) {
|
||||
payloadConfigPath = p
|
||||
}
|
||||
})
|
||||
|
||||
if (!payloadConfigPath) {
|
||||
warning('Unable to update payload.config.ts with plugins')
|
||||
return
|
||||
}
|
||||
|
||||
const configContent = fse.readFileSync(payloadConfigPath, 'utf-8')
|
||||
const configLines = configContent.split('\n')
|
||||
|
||||
const dbReplacement = dbPackages[args.dbDetails.type]
|
||||
const bundlerReplacement = bundlerPackages['webpack']
|
||||
const editorReplacement = editorPackages['slate']
|
||||
|
||||
let dbConfigStartLineIndex: number | undefined
|
||||
let dbConfigEndLineIndex: number | undefined
|
||||
|
||||
configLines.forEach((l, i) => {
|
||||
if (l.includes('// database-adapter-import')) {
|
||||
configLines[i] = dbReplacement.importReplacement
|
||||
}
|
||||
if (l.includes('// bundler-import')) {
|
||||
configLines[i] = bundlerReplacement.importReplacement
|
||||
}
|
||||
|
||||
if (l.includes('// bundler-config')) {
|
||||
configLines[i] = bundlerReplacement.configReplacement
|
||||
}
|
||||
|
||||
if (l.includes('// editor-import')) {
|
||||
configLines[i] = editorReplacement.importReplacement
|
||||
}
|
||||
|
||||
if (l.includes('// editor-config')) {
|
||||
configLines[i] = editorReplacement.configReplacement
|
||||
}
|
||||
|
||||
if (l.includes('// database-adapter-config-start')) {
|
||||
dbConfigStartLineIndex = i
|
||||
}
|
||||
if (l.includes('// database-adapter-config-end')) {
|
||||
dbConfigEndLineIndex = i
|
||||
}
|
||||
})
|
||||
|
||||
if (!dbConfigStartLineIndex || !dbConfigEndLineIndex) {
|
||||
warning('Unable to update payload.config.ts with database adapter import')
|
||||
} else {
|
||||
// Replaces lines between `// database-adapter-config-start` and `// database-adapter-config-end`
|
||||
configLines.splice(
|
||||
dbConfigStartLineIndex,
|
||||
dbConfigEndLineIndex - dbConfigStartLineIndex + 1,
|
||||
...dbReplacement.configReplacement,
|
||||
)
|
||||
}
|
||||
|
||||
fse.writeFileSync(payloadConfigPath, configLines.join('\n'))
|
||||
} catch (err: unknown) {
|
||||
warning('Unable to update payload.config.ts with plugins')
|
||||
}
|
||||
}
|
||||
151
packages/create-payload-app/src/lib/create-project.spec.ts
Normal file
151
packages/create-payload-app/src/lib/create-project.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import fse from 'fs-extra'
|
||||
import path from 'path'
|
||||
import type { BundlerType, CliArgs, DbType, ProjectTemplate } from '../types'
|
||||
import { createProject } from './create-project'
|
||||
import { bundlerPackages, dbPackages, editorPackages } from './packages'
|
||||
import exp from 'constants'
|
||||
|
||||
const projectDir = path.resolve(__dirname, './tmp')
|
||||
describe('createProject', () => {
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
if (fse.existsSync(projectDir)) {
|
||||
fse.rmdirSync(projectDir, { recursive: true })
|
||||
}
|
||||
})
|
||||
afterEach(() => {
|
||||
if (fse.existsSync(projectDir)) {
|
||||
fse.rmSync(projectDir, { recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe('#createProject', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const args = {
|
||||
_: ['project-name'],
|
||||
'--db': 'mongodb',
|
||||
'--no-deps': true,
|
||||
} as CliArgs
|
||||
const packageManager = 'yarn'
|
||||
|
||||
it('creates starter project', async () => {
|
||||
const projectName = 'starter-project'
|
||||
const template: ProjectTemplate = {
|
||||
name: 'blank',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/payload/templates/blank',
|
||||
description: 'Blank Template',
|
||||
}
|
||||
await createProject({
|
||||
cliArgs: args,
|
||||
projectName,
|
||||
projectDir,
|
||||
template,
|
||||
packageManager,
|
||||
})
|
||||
|
||||
const packageJsonPath = path.resolve(projectDir, 'package.json')
|
||||
const packageJson = fse.readJsonSync(packageJsonPath)
|
||||
|
||||
// Check package name and description
|
||||
expect(packageJson.name).toEqual(projectName)
|
||||
})
|
||||
|
||||
it('creates plugin template', async () => {
|
||||
const projectName = 'plugin'
|
||||
const template: ProjectTemplate = {
|
||||
name: 'plugin',
|
||||
type: 'plugin',
|
||||
url: 'https://github.com/payloadcms/payload-plugin-template',
|
||||
description: 'Template for creating a Payload plugin',
|
||||
}
|
||||
await createProject({
|
||||
cliArgs: args,
|
||||
projectName,
|
||||
projectDir,
|
||||
template,
|
||||
packageManager,
|
||||
})
|
||||
|
||||
const packageJsonPath = path.resolve(projectDir, 'package.json')
|
||||
const packageJson = fse.readJsonSync(packageJsonPath)
|
||||
|
||||
// Check package name and description
|
||||
expect(packageJson.name).toEqual(projectName)
|
||||
})
|
||||
|
||||
describe('db adapters and bundlers', () => {
|
||||
it.each([
|
||||
['mongodb', 'webpack'],
|
||||
['postgres', 'webpack'],
|
||||
])('update config and deps: %s, %s', async (db, bundler) => {
|
||||
const projectName = 'starter-project'
|
||||
const template: ProjectTemplate = {
|
||||
name: 'blank',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/payload/templates/blank',
|
||||
description: 'Blank Template',
|
||||
}
|
||||
await createProject({
|
||||
cliArgs: args,
|
||||
projectName,
|
||||
projectDir,
|
||||
template,
|
||||
packageManager,
|
||||
dbDetails: {
|
||||
dbUri: `${db}://localhost:27017/create-project-test`,
|
||||
type: db as DbType,
|
||||
},
|
||||
})
|
||||
|
||||
const dbReplacement = dbPackages[db as DbType]
|
||||
const bundlerReplacement = bundlerPackages[bundler as BundlerType]
|
||||
const editorReplacement = editorPackages['slate']
|
||||
|
||||
const packageJsonPath = path.resolve(projectDir, 'package.json')
|
||||
const packageJson = fse.readJsonSync(packageJsonPath)
|
||||
|
||||
// Check deps
|
||||
expect(packageJson.dependencies['payload']).toEqual('^2.0.0')
|
||||
expect(packageJson.dependencies[dbReplacement.packageName]).toEqual(dbReplacement.version)
|
||||
|
||||
// Should only have one db adapter
|
||||
expect(
|
||||
Object.keys(packageJson.dependencies).filter((n) => n.startsWith('@payloadcms/db-')),
|
||||
).toHaveLength(1)
|
||||
|
||||
expect(packageJson.dependencies[bundlerReplacement.packageName]).toEqual(
|
||||
bundlerReplacement.version,
|
||||
)
|
||||
expect(packageJson.dependencies[editorReplacement.packageName]).toEqual(
|
||||
editorReplacement.version,
|
||||
)
|
||||
|
||||
const payloadConfigPath = path.resolve(projectDir, 'src/payload.config.ts')
|
||||
const content = fse.readFileSync(payloadConfigPath, 'utf-8')
|
||||
|
||||
// Check payload.config.ts
|
||||
expect(content).not.toContain('// database-adapter-import')
|
||||
expect(content).toContain(dbReplacement.importReplacement)
|
||||
|
||||
expect(content).not.toContain('// database-adapter-config-start')
|
||||
expect(content).not.toContain('// database-adapter-config-end')
|
||||
expect(content).toContain(dbReplacement.configReplacement.join('\n'))
|
||||
|
||||
expect(content).not.toContain('// bundler-config-import')
|
||||
expect(content).toContain(bundlerReplacement.importReplacement)
|
||||
|
||||
expect(content).not.toContain('// bundler-config')
|
||||
expect(content).toContain(bundlerReplacement.configReplacement)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Templates', () => {
|
||||
it.todo('Verify that all templates are valid')
|
||||
// Loop through all templates.ts that should have replacement comments, and verify that they are present
|
||||
})
|
||||
})
|
||||
102
packages/create-payload-app/src/lib/create-project.ts
Normal file
102
packages/create-payload-app/src/lib/create-project.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import chalk from 'chalk'
|
||||
import degit from 'degit'
|
||||
import execa from 'execa'
|
||||
import fse from 'fs-extra'
|
||||
import ora from 'ora'
|
||||
import path from 'path'
|
||||
|
||||
import type { CliArgs, DbDetails, PackageManager, ProjectTemplate } from '../types'
|
||||
|
||||
import { error, success, warning } from '../utils/log'
|
||||
import { configurePayloadConfig } from './configure-payload-config'
|
||||
|
||||
async function createOrFindProjectDir(projectDir: string): Promise<void> {
|
||||
const pathExists = await fse.pathExists(projectDir)
|
||||
if (!pathExists) {
|
||||
await fse.mkdir(projectDir)
|
||||
}
|
||||
}
|
||||
|
||||
async function installDeps(args: {
|
||||
cliArgs: CliArgs
|
||||
packageManager: PackageManager
|
||||
projectDir: string
|
||||
}): Promise<boolean> {
|
||||
const { cliArgs, packageManager, projectDir } = args
|
||||
if (cliArgs['--no-deps']) {
|
||||
return true
|
||||
}
|
||||
let installCmd = 'npm install --legacy-peer-deps'
|
||||
|
||||
if (packageManager === 'yarn') {
|
||||
installCmd = 'yarn'
|
||||
} else if (packageManager === 'pnpm') {
|
||||
installCmd = 'pnpm install'
|
||||
}
|
||||
|
||||
try {
|
||||
await execa.command(installCmd, {
|
||||
cwd: path.resolve(projectDir),
|
||||
})
|
||||
return true
|
||||
} catch (err: unknown) {
|
||||
console.log({ err })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function createProject(args: {
|
||||
cliArgs: CliArgs
|
||||
dbDetails?: DbDetails
|
||||
packageManager: PackageManager
|
||||
projectDir: string
|
||||
projectName: string
|
||||
template: ProjectTemplate
|
||||
}): Promise<void> {
|
||||
const { cliArgs, dbDetails, packageManager, projectDir, projectName, template } = args
|
||||
|
||||
await createOrFindProjectDir(projectDir)
|
||||
|
||||
console.log(`\n Creating project in ${chalk.green(path.resolve(projectDir))}\n`)
|
||||
|
||||
if ('url' in template) {
|
||||
const emitter = degit(template.url)
|
||||
await emitter.clone(projectDir)
|
||||
}
|
||||
|
||||
const spinner = ora('Checking latest Payload version...').start()
|
||||
|
||||
await updatePackageJSON({ projectDir, projectName })
|
||||
await configurePayloadConfig({ dbDetails, projectDir })
|
||||
|
||||
// Remove yarn.lock file. This is only desired in Payload Cloud.
|
||||
const lockPath = path.resolve(projectDir, 'yarn.lock')
|
||||
if (fse.existsSync(lockPath)) {
|
||||
await fse.remove(lockPath)
|
||||
}
|
||||
|
||||
spinner.text = 'Installing dependencies...'
|
||||
const result = await installDeps({ cliArgs, packageManager, projectDir })
|
||||
spinner.stop()
|
||||
spinner.clear()
|
||||
if (result) {
|
||||
success('Dependencies installed')
|
||||
} else {
|
||||
error('Error installing dependencies')
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePackageJSON(args: {
|
||||
projectDir: string
|
||||
projectName: string
|
||||
}): Promise<void> {
|
||||
const { projectDir, projectName } = args
|
||||
const packageJsonPath = path.resolve(projectDir, 'package.json')
|
||||
try {
|
||||
const packageObj = await fse.readJson(packageJsonPath)
|
||||
packageObj.name = projectName
|
||||
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
|
||||
} catch (err: unknown) {
|
||||
warning('Unable to update name in package.json')
|
||||
}
|
||||
}
|
||||
5
packages/create-payload-app/src/lib/generate-secret.ts
Normal file
5
packages/create-payload-app/src/lib/generate-secret.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
export function generateSecret(): string {
|
||||
return randomBytes(32).toString('hex').slice(0, 24)
|
||||
}
|
||||
83
packages/create-payload-app/src/lib/packages.ts
Normal file
83
packages/create-payload-app/src/lib/packages.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { BundlerType, DbType, EditorType } from '../types'
|
||||
|
||||
type DbAdapterReplacement = {
|
||||
configReplacement: string[]
|
||||
importReplacement: string
|
||||
packageName: string
|
||||
version: string
|
||||
}
|
||||
|
||||
type BundlerReplacement = {
|
||||
configReplacement: string
|
||||
importReplacement: string
|
||||
packageName: string
|
||||
version: string
|
||||
}
|
||||
|
||||
type EditorReplacement = {
|
||||
configReplacement: string
|
||||
importReplacement: string
|
||||
packageName: string
|
||||
version: string
|
||||
}
|
||||
|
||||
const mongodbReplacement: DbAdapterReplacement = {
|
||||
importReplacement: "import { mongooseAdapter } from '@payloadcms/db-mongodb'",
|
||||
packageName: '@payloadcms/db-mongodb',
|
||||
// Replacement between `// database-adapter-config-start` and `// database-adapter-config-end`
|
||||
configReplacement: [' db: mongooseAdapter({', ' url: process.env.DATABASE_URI,', ' }),'],
|
||||
version: '^1.0.0',
|
||||
}
|
||||
|
||||
const postgresReplacement: DbAdapterReplacement = {
|
||||
configReplacement: [
|
||||
' db: postgresAdapter({',
|
||||
' pool: {',
|
||||
' connectionString: process.env.DATABASE_URI,',
|
||||
' },',
|
||||
' }),',
|
||||
],
|
||||
importReplacement: "import { postgresAdapter } from '@payloadcms/db-postgres'",
|
||||
packageName: '@payloadcms/db-postgres',
|
||||
version: '^0.x', // up to, not including 1.0.0
|
||||
}
|
||||
|
||||
export const dbPackages: Record<DbType, DbAdapterReplacement> = {
|
||||
mongodb: mongodbReplacement,
|
||||
postgres: postgresReplacement,
|
||||
}
|
||||
|
||||
const webpackReplacement: BundlerReplacement = {
|
||||
importReplacement: "import { webpackBundler } from '@payloadcms/bundler-webpack'",
|
||||
packageName: '@payloadcms/bundler-webpack',
|
||||
// Replacement of line containing `// bundler-config`
|
||||
configReplacement: ' bundler: webpackBundler(),',
|
||||
version: '^1.0.0',
|
||||
}
|
||||
|
||||
const viteReplacement: BundlerReplacement = {
|
||||
configReplacement: ' bundler: viteBundler(),',
|
||||
importReplacement: "import { viteBundler } from '@payloadcms/bundler-vite'",
|
||||
packageName: '@payloadcms/bundler-vite',
|
||||
version: '^0.x', // up to, not including 1.0.0
|
||||
}
|
||||
|
||||
export const bundlerPackages: Record<BundlerType, BundlerReplacement> = {
|
||||
vite: viteReplacement,
|
||||
webpack: webpackReplacement,
|
||||
}
|
||||
|
||||
export const editorPackages: Record<EditorType, EditorReplacement> = {
|
||||
lexical: {
|
||||
configReplacement: ' editor: lexicalEditor({}),',
|
||||
importReplacement: "import { lexicalEditor } from '@payloadcms/richtext-lexical'",
|
||||
packageName: '@payloadcms/richtext-lexical',
|
||||
version: '^0.x', // up to, not including 1.0.0
|
||||
},
|
||||
slate: {
|
||||
configReplacement: ' editor: slateEditor({}),',
|
||||
importReplacement: "import { slateEditor } from '@payloadcms/richtext-slate'",
|
||||
packageName: '@payloadcms/richtext-slate',
|
||||
version: '^1.0.0',
|
||||
},
|
||||
}
|
||||
24
packages/create-payload-app/src/lib/parse-project-name.ts
Normal file
24
packages/create-payload-app/src/lib/parse-project-name.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { CliArgs } from '../types'
|
||||
|
||||
export async function parseProjectName(args: CliArgs): Promise<string> {
|
||||
if (args['--name']) return args['--name']
|
||||
if (args._[0]) return args._[0]
|
||||
|
||||
const response = await prompts(
|
||||
{
|
||||
name: 'value',
|
||||
message: 'Project name?',
|
||||
type: 'text',
|
||||
validate: (value: string) => !!value.length,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return response.value
|
||||
}
|
||||
41
packages/create-payload-app/src/lib/parse-template.ts
Normal file
41
packages/create-payload-app/src/lib/parse-template.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { CliArgs, ProjectTemplate } from '../types'
|
||||
|
||||
export async function parseTemplate(
|
||||
args: CliArgs,
|
||||
validTemplates: ProjectTemplate[],
|
||||
): Promise<ProjectTemplate> {
|
||||
if (args['--template']) {
|
||||
const templateName = args['--template']
|
||||
const template = validTemplates.find((t) => t.name === templateName)
|
||||
if (!template) throw new Error('Invalid template given')
|
||||
return template
|
||||
}
|
||||
|
||||
const response = await prompts(
|
||||
{
|
||||
name: 'value',
|
||||
choices: validTemplates.map((p) => {
|
||||
return {
|
||||
description: p.description,
|
||||
title: p.name,
|
||||
value: p.name,
|
||||
}
|
||||
}),
|
||||
message: 'Choose project template',
|
||||
type: 'select',
|
||||
validate: (value: string) => !!value.length,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const template = validTemplates.find((t) => t.name === response.value)
|
||||
if (!template) throw new Error('Template is undefined')
|
||||
|
||||
return template
|
||||
}
|
||||
86
packages/create-payload-app/src/lib/select-db.ts
Normal file
86
packages/create-payload-app/src/lib/select-db.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import slugify from '@sindresorhus/slugify'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { CliArgs, DbDetails, DbType } from '../types'
|
||||
|
||||
type DbChoice = {
|
||||
dbConnectionPrefix: `${string}/`
|
||||
title: string
|
||||
value: DbType
|
||||
}
|
||||
|
||||
const dbChoiceRecord: Record<DbType, DbChoice> = {
|
||||
mongodb: {
|
||||
dbConnectionPrefix: 'mongodb://127.0.0.1/',
|
||||
title: 'MongoDB',
|
||||
value: 'mongodb',
|
||||
},
|
||||
postgres: {
|
||||
dbConnectionPrefix: 'postgres://127.0.0.1:5432/',
|
||||
title: 'PostgreSQL (beta)',
|
||||
value: 'postgres',
|
||||
},
|
||||
}
|
||||
|
||||
export async function selectDb(args: CliArgs, projectName: string): Promise<DbDetails> {
|
||||
let dbType: DbType | undefined = undefined
|
||||
if (args['--db']) {
|
||||
if (!Object.values(dbChoiceRecord).some((dbChoice) => dbChoice.value === args['--db'])) {
|
||||
throw new Error(
|
||||
`Invalid database type given. Valid types are: ${Object.values(dbChoiceRecord)
|
||||
.map((dbChoice) => dbChoice.value)
|
||||
.join(', ')}`,
|
||||
)
|
||||
}
|
||||
dbType = args['--db'] as DbType
|
||||
} else {
|
||||
const dbTypeRes = await prompts(
|
||||
{
|
||||
name: 'value',
|
||||
choices: Object.values(dbChoiceRecord).map((dbChoice) => {
|
||||
return {
|
||||
title: dbChoice.title,
|
||||
value: dbChoice.value,
|
||||
}
|
||||
}),
|
||||
message: 'Select a database',
|
||||
type: 'select',
|
||||
validate: (value: string) => !!value.length,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
dbType = dbTypeRes.value
|
||||
}
|
||||
|
||||
const dbChoice = dbChoiceRecord[dbType]
|
||||
|
||||
const dbUriRes = await prompts(
|
||||
{
|
||||
name: 'value',
|
||||
initial: `${dbChoice.dbConnectionPrefix}${
|
||||
projectName === '.' ? `payload-${getRandomDigitSuffix()}` : slugify(projectName)
|
||||
}`,
|
||||
message: `Enter ${dbChoice.title.split(' ')[0]} connection string`, // strip beta from title
|
||||
type: 'text',
|
||||
validate: (value: string) => !!value.length,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
dbUri: dbUriRes.value,
|
||||
type: dbChoice.value,
|
||||
}
|
||||
}
|
||||
|
||||
function getRandomDigitSuffix(): string {
|
||||
return (Math.random() * Math.pow(10, 6)).toFixed(0)
|
||||
}
|
||||
54
packages/create-payload-app/src/lib/templates.ts
Normal file
54
packages/create-payload-app/src/lib/templates.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { ProjectTemplate } from '../types'
|
||||
|
||||
import { error, info } from '../utils/log'
|
||||
|
||||
export function validateTemplate(templateName: string): boolean {
|
||||
const validTemplates = getValidTemplates()
|
||||
if (!validTemplates.map((t) => t.name).includes(templateName)) {
|
||||
error(`'${templateName}' is not a valid template.`)
|
||||
info(`Valid templates: ${validTemplates.map((t) => t.name).join(', ')}`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function getValidTemplates(): ProjectTemplate[] {
|
||||
return [
|
||||
{
|
||||
name: 'blank',
|
||||
description: 'Blank Template',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/payload/templates/blank',
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
description: 'Website Template',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/payload/templates/website',
|
||||
},
|
||||
{
|
||||
name: 'ecommerce',
|
||||
description: 'E-commerce Template',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/payload/templates/ecommerce',
|
||||
},
|
||||
{
|
||||
name: 'plugin',
|
||||
description: 'Template for creating a Payload plugin',
|
||||
type: 'plugin',
|
||||
url: 'https://github.com/payloadcms/payload-plugin-template',
|
||||
},
|
||||
{
|
||||
name: 'payload-demo',
|
||||
description: 'Payload demo site at https://demo.payloadcms.com',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/public-demo',
|
||||
},
|
||||
{
|
||||
name: 'payload-website',
|
||||
description: 'Payload website CMS at https://payloadcms.com',
|
||||
type: 'starter',
|
||||
url: 'https://github.com/payloadcms/website-cms',
|
||||
},
|
||||
]
|
||||
}
|
||||
55
packages/create-payload-app/src/lib/write-env-file.ts
Normal file
55
packages/create-payload-app/src/lib/write-env-file.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
|
||||
import type { ProjectTemplate } from '../types'
|
||||
|
||||
import { error, success } from '../utils/log'
|
||||
|
||||
/** Parse and swap .env.example values and write .env */
|
||||
export async function writeEnvFile(args: {
|
||||
databaseUri: string
|
||||
payloadSecret: string
|
||||
projectDir: string
|
||||
template: ProjectTemplate
|
||||
}): Promise<void> {
|
||||
const { databaseUri, payloadSecret, projectDir, template } = args
|
||||
try {
|
||||
if (template.type === 'starter' && fs.existsSync(path.join(projectDir, '.env.example'))) {
|
||||
// Parse .env file into key/value pairs
|
||||
const envFile = await fs.readFile(path.join(projectDir, '.env.example'), 'utf8')
|
||||
const envWithValues: string[] = envFile
|
||||
.split('\n')
|
||||
.filter((e) => e)
|
||||
.map((line) => {
|
||||
if (line.startsWith('#') || !line.includes('=')) return line
|
||||
|
||||
const split = line.split('=')
|
||||
const key = split[0]
|
||||
let value = split[1]
|
||||
|
||||
if (key === 'MONGODB_URI' || key === 'MONGO_URL' || key === 'DATABASE_URI') {
|
||||
value = databaseUri
|
||||
}
|
||||
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
|
||||
value = payloadSecret
|
||||
}
|
||||
|
||||
return `${key}=${value}`
|
||||
})
|
||||
|
||||
// Write new .env file
|
||||
await fs.writeFile(path.join(projectDir, '.env'), envWithValues.join('\n'))
|
||||
} else {
|
||||
const content = `MONGODB_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}`
|
||||
await fs.outputFile(`${projectDir}/.env`, content)
|
||||
}
|
||||
|
||||
success('.env file created')
|
||||
} catch (err: unknown) {
|
||||
error('Unable to write .env file')
|
||||
if (err instanceof Error) {
|
||||
error(err.message)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
133
packages/create-payload-app/src/main.ts
Normal file
133
packages/create-payload-app/src/main.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import slugify from '@sindresorhus/slugify'
|
||||
import arg from 'arg'
|
||||
import commandExists from 'command-exists'
|
||||
|
||||
import type { CliArgs, PackageManager } from './types'
|
||||
|
||||
import { createProject } from './lib/create-project'
|
||||
import { generateSecret } from './lib/generate-secret'
|
||||
import { parseProjectName } from './lib/parse-project-name'
|
||||
import { parseTemplate } from './lib/parse-template'
|
||||
import { selectDb } from './lib/select-db'
|
||||
import { getValidTemplates, validateTemplate } from './lib/templates'
|
||||
import { writeEnvFile } from './lib/write-env-file'
|
||||
import { success } from './utils/log'
|
||||
import { helpMessage, successMessage, welcomeMessage } from './utils/messages'
|
||||
|
||||
export class Main {
|
||||
args: CliArgs
|
||||
|
||||
constructor() {
|
||||
// @ts-expect-error bad typings
|
||||
this.args = arg(
|
||||
{
|
||||
'--db': String,
|
||||
'--help': Boolean,
|
||||
'--name': String,
|
||||
'--secret': String,
|
||||
'--template': String,
|
||||
|
||||
// Package manager
|
||||
'--no-deps': Boolean,
|
||||
'--use-npm': Boolean,
|
||||
'--use-pnpm': Boolean,
|
||||
'--use-yarn': Boolean,
|
||||
|
||||
// Flags
|
||||
'--beta': Boolean,
|
||||
'--dry-run': Boolean,
|
||||
|
||||
// Aliases
|
||||
'-d': '--db',
|
||||
'-h': '--help',
|
||||
'-n': '--name',
|
||||
'-t': '--template',
|
||||
},
|
||||
{ permissive: true },
|
||||
)
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
try {
|
||||
if (this.args['--help']) {
|
||||
console.log(helpMessage())
|
||||
process.exit(0)
|
||||
}
|
||||
const templateArg = this.args['--template']
|
||||
if (templateArg) {
|
||||
const valid = validateTemplate(templateArg)
|
||||
if (!valid) {
|
||||
console.log(helpMessage())
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(welcomeMessage)
|
||||
const projectName = await parseProjectName(this.args)
|
||||
const validTemplates = getValidTemplates()
|
||||
const template = await parseTemplate(this.args, validTemplates)
|
||||
|
||||
const projectDir = projectName === '.' ? process.cwd() : `./${slugify(projectName)}`
|
||||
const packageManager = await getPackageManager(this.args)
|
||||
|
||||
if (template.type !== 'plugin') {
|
||||
const dbDetails = await selectDb(this.args, projectName)
|
||||
const payloadSecret = generateSecret()
|
||||
if (!this.args['--dry-run']) {
|
||||
await createProject({
|
||||
cliArgs: this.args,
|
||||
dbDetails,
|
||||
packageManager,
|
||||
projectDir,
|
||||
projectName,
|
||||
template,
|
||||
})
|
||||
await writeEnvFile({
|
||||
databaseUri: dbDetails.dbUri,
|
||||
payloadSecret,
|
||||
projectDir,
|
||||
template,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (!this.args['--dry-run']) {
|
||||
await createProject({
|
||||
cliArgs: this.args,
|
||||
packageManager,
|
||||
projectDir,
|
||||
projectName,
|
||||
template,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
success('Payload project successfully created')
|
||||
console.log(successMessage(projectDir, packageManager))
|
||||
} catch (error: unknown) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getPackageManager(args: CliArgs): Promise<PackageManager> {
|
||||
let packageManager: PackageManager = 'npm'
|
||||
|
||||
if (args['--use-npm']) {
|
||||
packageManager = 'npm'
|
||||
} else if (args['--use-yarn']) {
|
||||
packageManager = 'yarn'
|
||||
} else if (args['--use-pnpm']) {
|
||||
packageManager = 'pnpm'
|
||||
} else {
|
||||
try {
|
||||
if (await commandExists('yarn')) {
|
||||
packageManager = 'yarn'
|
||||
} else if (await commandExists('pnpm')) {
|
||||
packageManager = 'pnpm'
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
packageManager = 'npm'
|
||||
}
|
||||
}
|
||||
return packageManager
|
||||
}
|
||||
58
packages/create-payload-app/src/types.ts
Normal file
58
packages/create-payload-app/src/types.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type arg from 'arg'
|
||||
|
||||
export interface Args extends arg.Spec {
|
||||
'--beta': BooleanConstructor
|
||||
'--db': StringConstructor
|
||||
'--dry-run': BooleanConstructor
|
||||
'--help': BooleanConstructor
|
||||
'--name': StringConstructor
|
||||
'--no-deps': BooleanConstructor
|
||||
'--secret': StringConstructor
|
||||
'--template': StringConstructor
|
||||
'--use-npm': BooleanConstructor
|
||||
'--use-pnpm': BooleanConstructor
|
||||
'--use-yarn': BooleanConstructor
|
||||
'-h': string
|
||||
'-n': string
|
||||
'-t': string
|
||||
}
|
||||
|
||||
export type CliArgs = arg.Result<Args>
|
||||
|
||||
export type ProjectTemplate = GitTemplate | PluginTemplate
|
||||
|
||||
/**
|
||||
* Template that is cloned verbatim from a git repo
|
||||
* Performs .env manipulation based upon input
|
||||
*/
|
||||
export interface GitTemplate extends Template {
|
||||
type: 'starter'
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Type specifically for the plugin template
|
||||
* No .env manipulation is done
|
||||
*/
|
||||
export interface PluginTemplate extends Template {
|
||||
type: 'plugin'
|
||||
url: string
|
||||
}
|
||||
|
||||
interface Template {
|
||||
description?: string
|
||||
name: string
|
||||
type: ProjectTemplate['type']
|
||||
}
|
||||
|
||||
export type PackageManager = 'npm' | 'pnpm' | 'yarn'
|
||||
|
||||
export type DbType = 'mongodb' | 'postgres'
|
||||
|
||||
export type DbDetails = {
|
||||
dbUri: string
|
||||
type: DbType
|
||||
}
|
||||
|
||||
export type BundlerType = 'vite' | 'webpack'
|
||||
export type EditorType = 'lexical' | 'slate'
|
||||
18
packages/create-payload-app/src/utils/log.ts
Normal file
18
packages/create-payload-app/src/utils/log.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import chalk from 'chalk'
|
||||
import figures from 'figures'
|
||||
|
||||
export const success = (message: string): void => {
|
||||
console.log(`${chalk.green(figures.tick)} ${chalk.bold(message)}`)
|
||||
}
|
||||
|
||||
export const warning = (message: string): void => {
|
||||
console.log(chalk.yellow('? ') + chalk.bold(message))
|
||||
}
|
||||
|
||||
export const info = (message: string): void => {
|
||||
console.log(`${chalk.yellow(figures.info)} ${chalk.bold(message)}`)
|
||||
}
|
||||
|
||||
export const error = (message: string): void => {
|
||||
console.log(`${chalk.red(figures.cross)} ${chalk.bold(message)}`)
|
||||
}
|
||||
76
packages/create-payload-app/src/utils/messages.ts
Normal file
76
packages/create-payload-app/src/utils/messages.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import chalk from 'chalk'
|
||||
import figures from 'figures'
|
||||
import path from 'path'
|
||||
import terminalLink from 'terminal-link'
|
||||
|
||||
import type { ProjectTemplate } from '../types'
|
||||
|
||||
import { getValidTemplates } from '../lib/templates'
|
||||
|
||||
const header = (message: string): string => `${chalk.yellow(figures.star)} ${chalk.bold(message)}`
|
||||
|
||||
export const welcomeMessage = chalk`
|
||||
{green Welcome to Payload. Let's create a project! }
|
||||
`
|
||||
|
||||
const spacer = ' '.repeat(8)
|
||||
|
||||
export function helpMessage(): string {
|
||||
const validTemplates = getValidTemplates()
|
||||
return chalk`
|
||||
{bold USAGE}
|
||||
|
||||
{dim $} {bold npx create-payload-app}
|
||||
{dim $} {bold npx create-payload-app} my-project
|
||||
{dim $} {bold npx create-payload-app} -n my-project -t blog
|
||||
|
||||
{bold OPTIONS}
|
||||
|
||||
-n {underline my-payload-app} Set project name
|
||||
-t {underline template_name} Choose specific template
|
||||
|
||||
{dim Available templates: ${formatTemplates(validTemplates)}}
|
||||
|
||||
--use-npm Use npm to install dependencies
|
||||
--use-yarn Use yarn to install dependencies
|
||||
--use-pnpm Use pnpm to install dependencies
|
||||
--no-deps Do not install any dependencies
|
||||
-h Show help
|
||||
`
|
||||
}
|
||||
|
||||
function formatTemplates(templates: ProjectTemplate[]) {
|
||||
return `\n\n${spacer}${templates
|
||||
.map((t) => `${t.name}${' '.repeat(28 - t.name.length)}${t.description}`)
|
||||
.join(`\n${spacer}`)}`
|
||||
}
|
||||
|
||||
export function successMessage(projectDir: string, packageManager: string): string {
|
||||
return `
|
||||
${header('Launch Application:')}
|
||||
|
||||
- cd ${projectDir}
|
||||
- ${
|
||||
packageManager === 'yarn' ? 'yarn' : 'npm run'
|
||||
} dev or follow directions in ${createTerminalLink(
|
||||
'README.md',
|
||||
`file://${path.resolve(projectDir, 'README.md')}`,
|
||||
)}
|
||||
|
||||
${header('Documentation:')}
|
||||
|
||||
- ${createTerminalLink(
|
||||
'Getting Started',
|
||||
'https://payloadcms.com/docs/getting-started/what-is-payload',
|
||||
)}
|
||||
- ${createTerminalLink('Configuration', 'https://payloadcms.com/docs/configuration/overview')}
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
// Create terminalLink with fallback for unsupported terminals
|
||||
function createTerminalLink(text: string, url: string) {
|
||||
return terminalLink(text, url, {
|
||||
fallback: (text, url) => `${text}: ${url}`,
|
||||
})
|
||||
}
|
||||
24
packages/create-payload-app/tsconfig.json
Normal file
24
packages/create-payload-app/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||
"noEmit": false /* Do not emit outputs. */,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||
"rootDir": "./src" /* Specify the root folder within your source files. */
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"build",
|
||||
"tests",
|
||||
"test",
|
||||
"node_modules",
|
||||
".eslintrc.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||
"references": [{ "path": "../payload" }]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.7",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
@@ -22,7 +22,7 @@
|
||||
"dependencies": {
|
||||
"@libsql/client": "^0.3.1",
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-kit": "0.19.13",
|
||||
"drizzle-kit": "0.19.13-e99bac1",
|
||||
"drizzle-orm": "0.28.5",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
@@ -34,9 +34,6 @@
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"better-sqlite3": "^8.5.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"main": "./dist/index.js",
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Connect } from 'payload/database'
|
||||
|
||||
import { pushSchema } from 'drizzle-kit/utils'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { numeric, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core'
|
||||
@@ -40,6 +39,8 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
|
||||
)
|
||||
return
|
||||
|
||||
const { pushSchema } = require('drizzle-kit/utils')
|
||||
|
||||
// This will prompt if clarifications are needed for Drizzle to push new schema
|
||||
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
|
||||
this.schema,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { DrizzleSnapshotJSON } from 'drizzle-kit/utils'
|
||||
import type { CreateMigration } from 'payload/database'
|
||||
|
||||
import { generateDrizzleJson, generateMigration } from 'drizzle-kit/utils'
|
||||
import fs from 'fs'
|
||||
import prompts from 'prompts'
|
||||
|
||||
@@ -61,6 +60,8 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
|
||||
|
||||
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
|
||||
const formattedDate = yyymmdd.replace(/\D/g, '')
|
||||
const formattedTime = hhmmss.split('.')[0].replace(/\D/g, '')
|
||||
|
||||
@@ -15,7 +15,6 @@ type Args = Omit<FindArgs, 'collection'> & {
|
||||
adapter: PostgresAdapter
|
||||
fields: Field[]
|
||||
tableName: string
|
||||
version?: boolean
|
||||
}
|
||||
|
||||
export const findMany = async function find({
|
||||
@@ -27,9 +26,8 @@ export const findMany = async function find({
|
||||
pagination,
|
||||
req = {} as PayloadRequest,
|
||||
skip,
|
||||
sort: sortArg,
|
||||
sort,
|
||||
tableName,
|
||||
version,
|
||||
where: whereArg,
|
||||
}: Args) {
|
||||
const db = adapter.sessions[req.transactionID]?.db || adapter.drizzle
|
||||
@@ -42,11 +40,6 @@ export const findMany = async function find({
|
||||
let hasNextPage: boolean
|
||||
let pagingCounter: number
|
||||
let selectDistinctResult
|
||||
let sort = sortArg
|
||||
if (version && sort) {
|
||||
const direction = sort[0] === '-' ? '-' : ''
|
||||
sort = `${direction}version.${direction.length === 1 ? sort.substring(1) : sort}`
|
||||
}
|
||||
|
||||
const { joinAliases, joins, orderBy, selectFields, where } = await buildQuery({
|
||||
adapter,
|
||||
|
||||
@@ -39,7 +39,6 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
skip,
|
||||
sort,
|
||||
tableName,
|
||||
version: true,
|
||||
where,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
import type { Payload } from 'payload'
|
||||
import type { Migration } from 'payload/database'
|
||||
|
||||
import { generateDrizzleJson } from 'drizzle-kit/utils'
|
||||
import { readMigrationFiles } from 'payload/database'
|
||||
import { DatabaseError } from 'pg'
|
||||
import prompts from 'prompts'
|
||||
|
||||
import type { PostgresAdapter } from './types'
|
||||
@@ -78,6 +76,8 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
|
||||
}
|
||||
|
||||
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
|
||||
const { generateDrizzleJson } = require('drizzle-kit/utils')
|
||||
|
||||
const start = Date.now()
|
||||
|
||||
payload.logger.info({ msg: `Migrating: ${migration.name}` })
|
||||
|
||||
@@ -34,12 +34,14 @@ type Args = {
|
||||
aliasTable?: GenericTable
|
||||
collectionPath: string
|
||||
columnPrefix?: string
|
||||
constraintPath?: string
|
||||
constraints?: Constraint[]
|
||||
fields: (Field | TabAsField)[]
|
||||
joinAliases: BuildQueryJoinAliases
|
||||
joins: BuildQueryJoins
|
||||
locale?: string
|
||||
pathSegments: string[]
|
||||
rootTableName?: string
|
||||
selectFields: Record<string, GenericColumn>
|
||||
tableName: string
|
||||
}
|
||||
@@ -53,17 +55,22 @@ export const getTableColumnFromPath = ({
|
||||
aliasTable,
|
||||
collectionPath,
|
||||
columnPrefix = '',
|
||||
constraintPath: incomingConstraintPath,
|
||||
constraints = [],
|
||||
fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale: incomingLocale,
|
||||
pathSegments: incomingSegments,
|
||||
rootTableName: incomingRootTableName,
|
||||
selectFields,
|
||||
tableName,
|
||||
}: Args): TableColumn => {
|
||||
const fieldPath = incomingSegments[0]
|
||||
let locale = incomingLocale
|
||||
const rootTableName = incomingRootTableName || tableName
|
||||
let constraintPath = incomingConstraintPath || ''
|
||||
|
||||
const field = flattenTopLevelFields(fields as Field[]).find(
|
||||
(fieldToFind) => fieldAffectsData(fieldToFind) && fieldToFind.name === fieldPath,
|
||||
) as Field | TabAsField
|
||||
@@ -105,6 +112,7 @@ export const getTableColumnFromPath = ({
|
||||
aliasTable,
|
||||
collectionPath,
|
||||
columnPrefix,
|
||||
constraintPath,
|
||||
constraints,
|
||||
fields: field.tabs.map((tab) => ({
|
||||
...tab,
|
||||
@@ -114,6 +122,7 @@ export const getTableColumnFromPath = ({
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -125,12 +134,14 @@ export const getTableColumnFromPath = ({
|
||||
aliasTable,
|
||||
collectionPath,
|
||||
columnPrefix: `${columnPrefix}${field.name}_`,
|
||||
constraintPath,
|
||||
constraints,
|
||||
fields: field.fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -140,12 +151,14 @@ export const getTableColumnFromPath = ({
|
||||
aliasTable,
|
||||
collectionPath,
|
||||
columnPrefix,
|
||||
constraintPath,
|
||||
constraints,
|
||||
fields: field.fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -172,12 +185,14 @@ export const getTableColumnFromPath = ({
|
||||
aliasTable,
|
||||
collectionPath,
|
||||
columnPrefix: `${columnPrefix}${field.name}_`,
|
||||
constraintPath,
|
||||
constraints,
|
||||
fields: field.fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -185,6 +200,7 @@ export const getTableColumnFromPath = ({
|
||||
|
||||
case 'array': {
|
||||
newTableName = `${tableName}_${toSnakeCase(field.name)}`
|
||||
constraintPath = `${constraintPath}${field.name}.%.`
|
||||
if (locale && field.localized && adapter.payload.config.localization) {
|
||||
joins[newTableName] = and(
|
||||
eq(adapter.tables[tableName].id, adapter.tables[newTableName]._parentID),
|
||||
@@ -206,12 +222,14 @@ export const getTableColumnFromPath = ({
|
||||
return getTableColumnFromPath({
|
||||
adapter,
|
||||
collectionPath,
|
||||
constraintPath,
|
||||
constraints,
|
||||
fields: field.fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -229,12 +247,14 @@ export const getTableColumnFromPath = ({
|
||||
result = getTableColumnFromPath({
|
||||
adapter,
|
||||
collectionPath,
|
||||
constraintPath: '',
|
||||
constraints: blockConstraints,
|
||||
fields: block.fields,
|
||||
joinAliases,
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName,
|
||||
selectFields: blockSelectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
@@ -283,9 +303,8 @@ export const getTableColumnFromPath = ({
|
||||
case 'relationship':
|
||||
case 'upload': {
|
||||
let relationshipFields
|
||||
const relationTableName = `${tableName}_rels`
|
||||
const relationTableName = `${rootTableName}_rels`
|
||||
const newCollectionPath = pathSegments.slice(1).join('.')
|
||||
|
||||
const aliasRelationshipTableName = uuid()
|
||||
const aliasRelationshipTable = alias(
|
||||
adapter.tables[relationTableName],
|
||||
@@ -295,7 +314,7 @@ export const getTableColumnFromPath = ({
|
||||
// Join in the relationships table
|
||||
joinAliases.push({
|
||||
condition: eq(
|
||||
(aliasTable || adapter.tables[tableName]).id,
|
||||
(aliasTable || adapter.tables[rootTableName]).id,
|
||||
aliasRelationshipTable.parent,
|
||||
),
|
||||
table: aliasRelationshipTable,
|
||||
@@ -306,7 +325,7 @@ export const getTableColumnFromPath = ({
|
||||
constraints.push({
|
||||
columnName: 'path',
|
||||
table: aliasRelationshipTable,
|
||||
value: field.name,
|
||||
value: `${constraintPath}${field.name}`,
|
||||
})
|
||||
|
||||
let newAliasTable
|
||||
@@ -368,6 +387,7 @@ export const getTableColumnFromPath = ({
|
||||
joins,
|
||||
locale,
|
||||
pathSegments: pathSegments.slice(1),
|
||||
rootTableName: newTableName,
|
||||
selectFields,
|
||||
tableName: newTableName,
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
import type { Field, Operator, Where } from 'payload/types'
|
||||
|
||||
import { and, ilike, isNotNull, isNull, ne, or, sql } from 'drizzle-orm'
|
||||
import { and, ilike, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm'
|
||||
import { QueryError } from 'payload/errors'
|
||||
import { validOperators } from 'payload/types'
|
||||
|
||||
@@ -100,7 +100,11 @@ export async function parseParams({
|
||||
const val = where[relationOrPath][operator]
|
||||
|
||||
queryConstraints.forEach(({ columnName: col, table: constraintTable, value }) => {
|
||||
constraints.push(operatorMap.equals(constraintTable[col], value))
|
||||
if (typeof value === 'string' && value.indexOf('%') > -1) {
|
||||
constraints.push(operatorMap.like(constraintTable[col], value))
|
||||
} else {
|
||||
constraints.push(operatorMap.equals(constraintTable[col], value))
|
||||
}
|
||||
})
|
||||
|
||||
if (['json', 'richText'].includes(field.type) && Array.isArray(pathSegments)) {
|
||||
@@ -147,6 +151,7 @@ export async function parseParams({
|
||||
const { operator: queryOperator, value: queryValue } = sanitizeQueryValue({
|
||||
field,
|
||||
operator,
|
||||
relationOrPath,
|
||||
val,
|
||||
})
|
||||
|
||||
@@ -158,6 +163,17 @@ export async function parseParams({
|
||||
ne<any>(rawColumn || table[columnName], queryValue),
|
||||
),
|
||||
)
|
||||
} else if (
|
||||
(field.type === 'relationship' || field.type === 'upload') &&
|
||||
Array.isArray(queryValue) &&
|
||||
operator === 'not_in'
|
||||
) {
|
||||
constraints.push(
|
||||
sql`${notInArray(table[columnName], queryValue)} OR
|
||||
${table[columnName]}
|
||||
IS
|
||||
NULL`,
|
||||
)
|
||||
} else {
|
||||
constraints.push(
|
||||
operatorMap[queryOperator](rawColumn || table[columnName], queryValue),
|
||||
|
||||
@@ -5,12 +5,14 @@ import { createArrayFromCommaDelineated } from 'payload/utilities'
|
||||
type SanitizeQueryValueArgs = {
|
||||
field: Field | TabAsField
|
||||
operator: string
|
||||
relationOrPath: string
|
||||
val: any
|
||||
}
|
||||
|
||||
export const sanitizeQueryValue = ({
|
||||
field,
|
||||
operator: operatorArg,
|
||||
relationOrPath,
|
||||
val,
|
||||
}: SanitizeQueryValueArgs): { operator: string; value: unknown } => {
|
||||
let operator = operatorArg
|
||||
@@ -18,6 +20,22 @@ export const sanitizeQueryValue = ({
|
||||
|
||||
if (!fieldAffectsData(field)) return { operator, value: formattedValue }
|
||||
|
||||
if (
|
||||
(field.type === 'relationship' || field.type === 'upload') &&
|
||||
!relationOrPath.endsWith('relationTo') &&
|
||||
Array.isArray(formattedValue)
|
||||
) {
|
||||
const allPossibleIDTypes: (number | string)[] = []
|
||||
formattedValue.forEach((val) => {
|
||||
if (typeof val === 'string') {
|
||||
allPossibleIDTypes.push(val, parseInt(val))
|
||||
} else {
|
||||
allPossibleIDTypes.push(val, String(val))
|
||||
}
|
||||
})
|
||||
formattedValue = allPossibleIDTypes
|
||||
}
|
||||
|
||||
// Cast incoming values as proper searchable types
|
||||
if (field.type === 'checkbox' && typeof val === 'string') {
|
||||
if (val.toLowerCase() === 'true') formattedValue = true
|
||||
|
||||
@@ -32,7 +32,6 @@ export const queryDrafts: QueryDrafts = async function queryDrafts({
|
||||
req,
|
||||
sort,
|
||||
tableName,
|
||||
version: true,
|
||||
where: combinedWhere,
|
||||
})
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
|
||||
// drizzle-kit@utils
|
||||
|
||||
import { generateDrizzleJson, generateMigration, pushSchema } from 'drizzle-kit/utils'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { Pool } from 'pg'
|
||||
|
||||
async function generateUsage() {
|
||||
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
|
||||
|
||||
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
|
||||
const schema = await import('./data/users')
|
||||
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
|
||||
@@ -25,6 +26,8 @@ async function generateUsage() {
|
||||
}
|
||||
|
||||
async function pushUsage() {
|
||||
const { pushSchema } = require('drizzle-kit/utils')
|
||||
|
||||
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
|
||||
const schemaAfter = await import('./data/users-after')
|
||||
|
||||
|
||||
@@ -252,6 +252,8 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const arrayTableName = `${newTableName}_${toSnakeCase(field.name)}`
|
||||
const baseColumns: Record<string, PgColumnBuilder> = {
|
||||
_order: integer('_order').notNull(),
|
||||
@@ -277,7 +279,7 @@ export const traverseFields = ({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fields: field.fields,
|
||||
rootRelationsToBuild,
|
||||
@@ -314,6 +316,8 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
field.blocks.forEach((block) => {
|
||||
const blockTableName = `${rootTableName}_blocks_${toSnakeCase(block.slug)}`
|
||||
if (!adapter.tables[blockTableName]) {
|
||||
@@ -343,7 +347,7 @@ export const traverseFields = ({
|
||||
adapter,
|
||||
baseColumns,
|
||||
baseExtraConfig,
|
||||
disableNotNull,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fields: block.fields,
|
||||
rootRelationsToBuild,
|
||||
@@ -428,6 +432,8 @@ export const traverseFields = ({
|
||||
break
|
||||
}
|
||||
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const {
|
||||
hasLocalizedField: groupHasLocalizedField,
|
||||
hasLocalizedManyNumberField: groupHasLocalizedManyNumberField,
|
||||
@@ -438,7 +444,7 @@ export const traverseFields = ({
|
||||
buildRelationships,
|
||||
columnPrefix: `${columnName}_`,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.fields,
|
||||
@@ -463,6 +469,8 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
case 'tabs': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
const {
|
||||
hasLocalizedField: tabHasLocalizedField,
|
||||
hasLocalizedManyNumberField: tabHasLocalizedManyNumberField,
|
||||
@@ -473,7 +481,7 @@ export const traverseFields = ({
|
||||
buildRelationships,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
@@ -500,6 +508,7 @@ export const traverseFields = ({
|
||||
|
||||
case 'row':
|
||||
case 'collapsible': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
const {
|
||||
hasLocalizedField: rowHasLocalizedField,
|
||||
hasLocalizedManyNumberField: rowHasLocalizedManyNumberField,
|
||||
@@ -510,7 +519,7 @@ export const traverseFields = ({
|
||||
buildRelationships,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableUnique,
|
||||
fieldPrefix,
|
||||
fields: field.fields,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "The official live preview React SDK for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { subscribe, unsubscribe } from '@payloadcms/live-preview'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ready, subscribe, unsubscribe } from '@payloadcms/live-preview'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
// To prevent the flicker of missing data on initial load,
|
||||
// you can pass in the initial page data from the server
|
||||
@@ -17,6 +17,7 @@ export const useLivePreview = <T extends any>(props: {
|
||||
const { depth = 0, initialData, serverURL } = props
|
||||
const [data, setData] = useState<T>(initialData)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const hasSentReadyMessage = useRef<boolean>(false)
|
||||
|
||||
const onChange = useCallback((mergedData) => {
|
||||
setData(mergedData)
|
||||
@@ -31,6 +32,14 @@ export const useLivePreview = <T extends any>(props: {
|
||||
serverURL,
|
||||
})
|
||||
|
||||
if (!hasSentReadyMessage.current) {
|
||||
hasSentReadyMessage.current = true
|
||||
|
||||
ready({
|
||||
serverURL,
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsubscribe(subscription)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -7,11 +7,10 @@ export const handleMessage = async <T>(args: {
|
||||
serverURL: string
|
||||
}): Promise<T> => {
|
||||
const { depth, event, initialData, serverURL } = args
|
||||
|
||||
if (event.origin === serverURL && event.data) {
|
||||
const eventData = JSON.parse(event?.data)
|
||||
|
||||
if (eventData.type === 'livePreview') {
|
||||
if (eventData.type === 'payload-live-preview') {
|
||||
const mergedData = await mergeData<T>({
|
||||
depth,
|
||||
fieldSchema: eventData.fieldSchemaJSON,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { handleMessage } from './handleMessage'
|
||||
export { mergeData } from './mergeData'
|
||||
export { ready } from './ready'
|
||||
export { subscribe } from './subscribe'
|
||||
export { unsubscribe } from './unsubscribe'
|
||||
|
||||
15
packages/live-preview/src/ready.ts
Normal file
15
packages/live-preview/src/ready.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const ready = (args: { serverURL: string }): void => {
|
||||
const { serverURL } = args
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// This subscription may have been from either an iframe `src` or `window.open()`
|
||||
// i.e. `window?.opener` || `window?.parent`
|
||||
window?.opener?.postMessage(
|
||||
JSON.stringify({
|
||||
popupReady: true,
|
||||
type: 'payload-live-preview',
|
||||
}),
|
||||
serverURL,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,14 @@ export const subscribe = <T>(args: {
|
||||
}): ((event: MessageEvent) => void) => {
|
||||
const { callback, depth, initialData, serverURL } = args
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const handleMessageCallback = async (event: MessageEvent) => {
|
||||
const mergedData = await handleMessage({ depth, event, initialData, serverURL })
|
||||
callback(mergedData)
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessageCallback)
|
||||
window.parent.postMessage('ready', serverURL)
|
||||
|
||||
return handleMessageCallback
|
||||
const onMessage = async (event: MessageEvent) => {
|
||||
const mergedData = await handleMessage({ depth, event, initialData, serverURL })
|
||||
callback(mergedData)
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('message', onMessage)
|
||||
}
|
||||
|
||||
return onMessage
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.6",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
@@ -208,8 +208,7 @@
|
||||
"webpack": "^5.78.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"pnpm": ">=8"
|
||||
"node": ">=14"
|
||||
},
|
||||
"files": [
|
||||
"bin.js",
|
||||
|
||||
@@ -55,24 +55,6 @@ export const DocumentControls: React.FC<{
|
||||
|
||||
const { i18n, t } = useTranslation('general')
|
||||
|
||||
let showPreviewButton = false
|
||||
|
||||
if (collection) {
|
||||
showPreviewButton =
|
||||
isEditing &&
|
||||
collection?.admin?.preview &&
|
||||
collection?.versions?.drafts &&
|
||||
!collection?.versions?.drafts?.autosave
|
||||
}
|
||||
|
||||
if (global) {
|
||||
showPreviewButton =
|
||||
isEditing &&
|
||||
global?.admin?.preview &&
|
||||
global?.versions?.drafts &&
|
||||
!global?.versions?.drafts?.autosave
|
||||
}
|
||||
|
||||
const showDotMenu = Boolean(collection && id && !disableActions)
|
||||
|
||||
return (
|
||||
@@ -165,9 +147,12 @@ export const DocumentControls: React.FC<{
|
||||
</div>
|
||||
<div className={`${baseClass}__controls-wrapper`}>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
{showPreviewButton && (
|
||||
{(collection?.admin?.preview || global?.admin?.preview) && (
|
||||
<PreviewButton
|
||||
CustomComponent={collection?.admin?.components?.edit?.PreviewButton}
|
||||
CustomComponent={
|
||||
collection?.admin?.components?.edit?.PreviewButton ||
|
||||
global?.admin?.components?.elements?.PreviewButton
|
||||
}
|
||||
generatePreviewURL={collection?.admin?.preview || global?.admin?.preview}
|
||||
/>
|
||||
)}
|
||||
@@ -178,13 +163,26 @@ export const DocumentControls: React.FC<{
|
||||
{((collection?.versions?.drafts && !collection?.versions?.drafts?.autosave) ||
|
||||
(global?.versions?.drafts && !global?.versions?.drafts?.autosave)) && (
|
||||
<SaveDraft
|
||||
CustomComponent={collection?.admin?.components?.edit?.SaveDraftButton}
|
||||
CustomComponent={
|
||||
collection?.admin?.components?.edit?.SaveDraftButton ||
|
||||
global?.admin?.components?.elements?.SaveDraftButton
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Publish CustomComponent={collection?.admin?.components?.edit?.PublishButton} />
|
||||
<Publish
|
||||
CustomComponent={
|
||||
collection?.admin?.components?.edit?.PublishButton ||
|
||||
global?.admin?.components?.elements?.PublishButton
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Save CustomComponent={collection?.admin?.components?.edit?.SaveButton} />
|
||||
<Save
|
||||
CustomComponent={
|
||||
collection?.admin?.components?.edit?.SaveButton ||
|
||||
global?.admin?.components?.elements?.SaveButton
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -51,6 +51,7 @@ const Duplicate: React.FC<Props> = ({ id, collection, slug }) => {
|
||||
},
|
||||
params: {
|
||||
depth: 0,
|
||||
draft: true,
|
||||
locale,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -54,13 +54,12 @@ const StepNav: React.FC<{
|
||||
)}
|
||||
{stepNav.map((item, i) => {
|
||||
const StepLabel = <span key={i}>{getTranslation(item.label, i18n)}</span>
|
||||
|
||||
const Step =
|
||||
stepNav.length === i + 1 ? (
|
||||
StepLabel
|
||||
) : (
|
||||
<Fragment key={i}>
|
||||
<Link to={item.url}>{StepLabel}</Link>
|
||||
{item.url ? <Link to={item.url}>{StepLabel}</Link> : StepLabel}
|
||||
<span>/</span>
|
||||
</Fragment>
|
||||
)
|
||||
|
||||
@@ -61,6 +61,7 @@ export const addFieldStatePromise = async ({
|
||||
user,
|
||||
value: data?.[field.name],
|
||||
})
|
||||
|
||||
if (data?.[field.name]) {
|
||||
data[field.name] = valueWithDefault
|
||||
}
|
||||
@@ -145,8 +146,8 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.value = null
|
||||
fieldState.initialValue = null
|
||||
} else {
|
||||
fieldState.value = arrayValue
|
||||
fieldState.initialValue = arrayValue
|
||||
fieldState.value = arrayValue.length
|
||||
fieldState.initialValue = arrayValue.length
|
||||
|
||||
if (arrayValue.length > 0) {
|
||||
fieldState.disableFormData = true
|
||||
@@ -236,8 +237,8 @@ export const addFieldStatePromise = async ({
|
||||
fieldState.value = null
|
||||
fieldState.initialValue = null
|
||||
} else {
|
||||
fieldState.value = blocksValue
|
||||
fieldState.initialValue = blocksValue
|
||||
fieldState.value = blocksValue.length
|
||||
fieldState.initialValue = blocksValue.length
|
||||
|
||||
if (blocksValue.length > 0) {
|
||||
fieldState.disableFormData = true
|
||||
|
||||
@@ -8,6 +8,9 @@ import getSiblingData from './getSiblingData'
|
||||
import reduceFieldsToValues from './reduceFieldsToValues'
|
||||
import { flattenRows, separateRows } from './rows'
|
||||
|
||||
/**
|
||||
* Reducer which modifies the form field state (all the current data of the fields in the form). When called using dispatch, it will return a new state object.
|
||||
*/
|
||||
export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
switch (action.type) {
|
||||
case 'REPLACE_STATE': {
|
||||
@@ -123,7 +126,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
...state[path],
|
||||
disableFormData: rows.length > 0,
|
||||
rows: rowsMetadata,
|
||||
value: rows,
|
||||
value: rows.length,
|
||||
},
|
||||
...flattenRows(path, rows),
|
||||
}
|
||||
@@ -161,10 +164,6 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
const { remainingFields, rows: siblingRows } = separateRows(path, state)
|
||||
siblingRows.splice(rowIndex, 0, subFieldState)
|
||||
|
||||
// add new row to array _value_
|
||||
const currentValue = (Array.isArray(state[path]?.value) ? state[path]?.value : []) as Fields[]
|
||||
const newValue = currentValue.splice(rowIndex, 0, reduceFieldsToValues(subFieldState, true))
|
||||
|
||||
const newState: Fields = {
|
||||
...remainingFields,
|
||||
...flattenRows(path, siblingRows),
|
||||
@@ -172,7 +171,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
...state[path],
|
||||
disableFormData: true,
|
||||
rows: rowsMetadata,
|
||||
value: newValue,
|
||||
value: siblingRows.length,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -203,10 +202,6 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
// replace form _field state_
|
||||
siblingRows[rowIndex] = subFieldState
|
||||
|
||||
// replace array _value_
|
||||
const newValue = Array.isArray(state[path]?.value) ? state[path]?.value : []
|
||||
newValue[rowIndex] = reduceFieldsToValues(subFieldState, true)
|
||||
|
||||
const newState: Fields = {
|
||||
...remainingFields,
|
||||
...flattenRows(path, siblingRows),
|
||||
@@ -214,7 +209,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
...state[path],
|
||||
disableFormData: true,
|
||||
rows: rowsMetadata,
|
||||
value: newValue,
|
||||
value: siblingRows.length,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -245,7 +240,7 @@ export function fieldReducer(state: Fields, action: FieldAction): Fields {
|
||||
...state[path],
|
||||
disableFormData: true,
|
||||
rows: rowsMetadata,
|
||||
value: rows,
|
||||
value: rows.length,
|
||||
},
|
||||
...flattenRows(path, rows),
|
||||
}
|
||||
|
||||
@@ -50,12 +50,15 @@ import reduceFieldsToValues from './reduceFieldsToValues'
|
||||
const baseClass = 'form'
|
||||
|
||||
const Form: React.FC<Props> = (props) => {
|
||||
const { id, collection, getDocPreferences, global } = useDocumentInfo()
|
||||
|
||||
const {
|
||||
action,
|
||||
children,
|
||||
className,
|
||||
disableSuccessStatus,
|
||||
disabled,
|
||||
fields: fieldsFromProps = collection?.fields || global?.fields,
|
||||
handleResponse,
|
||||
initialData, // values only, paths are required as key - form should build initial state as convenience
|
||||
initialState, // fully formed initial field state
|
||||
@@ -71,7 +74,6 @@ const Form: React.FC<Props> = (props) => {
|
||||
const { code: locale } = useLocale()
|
||||
const { i18n, t } = useTranslation('general')
|
||||
const { refreshCookie, user } = useAuth()
|
||||
const { id, collection, getDocPreferences, global } = useDocumentInfo()
|
||||
const operation = useOperation()
|
||||
|
||||
const config = useConfig()
|
||||
@@ -90,6 +92,10 @@ const Form: React.FC<Props> = (props) => {
|
||||
if (initialState) initialFieldState = initialState
|
||||
|
||||
const fieldsReducer = useReducer(fieldReducer, {}, () => initialFieldState)
|
||||
/**
|
||||
* `fields` is the current, up-to-date state/data of all fields in the form. It can be modified by using dispatchFields,
|
||||
* which calls the fieldReducer, which then updates the state.
|
||||
*/
|
||||
const [fields, dispatchFields] = fieldsReducer
|
||||
|
||||
contextRef.current.fields = fields
|
||||
@@ -167,7 +173,13 @@ const Form: React.FC<Props> = (props) => {
|
||||
let validationResult: boolean | string = true
|
||||
|
||||
if (typeof field.validate === 'function') {
|
||||
validationResult = await field.validate(field.value, {
|
||||
let valueToValidate = field.value
|
||||
|
||||
if (field?.rows && Array.isArray(field.rows)) {
|
||||
valueToValidate = contextRef.current.getDataByPath(path)
|
||||
}
|
||||
|
||||
validationResult = await field.validate(valueToValidate, {
|
||||
id,
|
||||
config,
|
||||
data,
|
||||
@@ -434,7 +446,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
const getRowSchemaByPath = React.useCallback(
|
||||
({ blockType, path }: { blockType?: string; path: string }) => {
|
||||
const rowConfig = traverseRowConfigs({
|
||||
fieldConfig: collection?.fields || global?.fields,
|
||||
fieldConfig: fieldsFromProps,
|
||||
path,
|
||||
})
|
||||
const rowFieldConfigs = buildFieldSchemaMap(rowConfig)
|
||||
@@ -442,10 +454,11 @@ const Form: React.FC<Props> = (props) => {
|
||||
const fieldKey = pathSegments.at(-1)
|
||||
return rowFieldConfigs.get(blockType ? `${fieldKey}.${blockType}` : fieldKey)
|
||||
},
|
||||
[traverseRowConfigs, collection?.fields, global?.fields],
|
||||
[traverseRowConfigs, fieldsFromProps],
|
||||
)
|
||||
|
||||
// Array/Block row manipulation
|
||||
// Array/Block row manipulation. This is called when, for example, you add a new block to a blocks field.
|
||||
// The block data is saved in the rows property of the state, which is modified updated here.
|
||||
const addFieldRow: Context['addFieldRow'] = useCallback(
|
||||
async ({ data, path, rowIndex }) => {
|
||||
const preferences = await getDocPreferences()
|
||||
|
||||
@@ -2,7 +2,12 @@ import type React from 'react'
|
||||
import type { Dispatch } from 'react'
|
||||
|
||||
import type { User } from '../../../../auth/types'
|
||||
import type { Condition, Field as FieldConfig, Validate } from '../../../../fields/config/types'
|
||||
import type {
|
||||
Condition,
|
||||
Field,
|
||||
Field as FieldConfig,
|
||||
Validate,
|
||||
} from '../../../../fields/config/types'
|
||||
|
||||
export type Row = {
|
||||
blockType?: string
|
||||
@@ -41,6 +46,12 @@ export type Props = {
|
||||
className?: string
|
||||
disableSuccessStatus?: boolean
|
||||
disabled?: boolean
|
||||
/**
|
||||
* By default, the form will get the field schema (not data) from the current document. If you pass this in, you can override that behavior.
|
||||
* This is very useful for sub-forms, where the form's field schema is not necessarily the field schema of the current document (e.g. for the Blocks
|
||||
* feature of the Lexical Rich Text field)
|
||||
*/
|
||||
fields?: Field[]
|
||||
handleResponse?: (res: Response) => void
|
||||
initialData?: Data
|
||||
initialState?: Fields
|
||||
|
||||
@@ -17,10 +17,21 @@ const intersectionObserverOptions = {
|
||||
rootMargin: '1000px',
|
||||
}
|
||||
|
||||
// If you send `fields` through, it will render those fields explicitly
|
||||
// Otherwise, it will reduce your fields using the other provided props
|
||||
// This is so that we can conditionally render fields before reducing them, if desired
|
||||
// See the sidebar in '../collections/Edit/Default/index.tsx' for an example
|
||||
/**
|
||||
* If you send `fields` through, it will render those fields explicitly
|
||||
* Otherwise, it will reduce your fields using the other provided props
|
||||
* This is so that we can conditionally render fields before reducing them, if desired
|
||||
* See the sidebar in '../collections/Edit/Default/index.tsx' for an example
|
||||
*
|
||||
* The state/data for the fields it renders is not managed by this component. Instead, every component it renders has
|
||||
* their own handling of their own value, usually through the useField hook. This hook will get the field's value
|
||||
* from the Form the field is in, using the field's path.
|
||||
*
|
||||
* Thus, if you would like to set the value of a field you render here, you must do so in the Form that contains the field, or in the
|
||||
* Field component itself.
|
||||
*
|
||||
* All this component does is render the field's Field Components, and pass them the props they need to function.
|
||||
**/
|
||||
const RenderFields: React.FC<Props> = (props) => {
|
||||
const { className, fieldTypes, forceRender, margins } = props
|
||||
|
||||
|
||||
@@ -6,21 +6,23 @@ import type { ReducedField } from './filterFields'
|
||||
export type Props = {
|
||||
className?: string
|
||||
fieldTypes: FieldTypes
|
||||
margins?: 'small' | false
|
||||
forceRender?: boolean
|
||||
margins?: 'small' | false
|
||||
permissions?:
|
||||
| {
|
||||
[field: string]: FieldPermissions
|
||||
}
|
||||
| FieldPermissions
|
||||
readOnly?: boolean
|
||||
} & (
|
||||
| {
|
||||
// Fields to be filtered by the component
|
||||
fieldSchema: FieldWithPath[]
|
||||
filter?: (field: Field) => boolean
|
||||
indexPath?: string
|
||||
permissions?:
|
||||
| {
|
||||
[field: string]: FieldPermissions
|
||||
}
|
||||
| FieldPermissions
|
||||
readOnly?: boolean
|
||||
}
|
||||
| {
|
||||
// Pre-filtered fields to be simply rendered
|
||||
fields: ReducedField[]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -91,7 +91,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
showError,
|
||||
valid,
|
||||
value,
|
||||
} = useField<[]>({
|
||||
} = useField<number>({
|
||||
condition,
|
||||
hasRows: true,
|
||||
path,
|
||||
@@ -123,8 +123,8 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
const removeRow = useCallback(
|
||||
async (rowIndex: number) => {
|
||||
await removeFieldRow({ path, rowIndex })
|
||||
(rowIndex: number) => {
|
||||
removeFieldRow({ path, rowIndex })
|
||||
setModified(true)
|
||||
},
|
||||
[removeFieldRow, path, setModified],
|
||||
@@ -278,7 +278,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
icon="plus"
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
onClick={() => addRow(value?.length || 0)}
|
||||
onClick={() => addRow(value || 0)}
|
||||
>
|
||||
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
|
||||
</Button>
|
||||
|
||||
@@ -90,7 +90,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
showError,
|
||||
valid,
|
||||
value,
|
||||
} = useField<[]>({
|
||||
} = useField<number>({
|
||||
condition,
|
||||
hasRows: true,
|
||||
path,
|
||||
@@ -128,8 +128,8 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
const removeRow = useCallback(
|
||||
async (rowIndex: number) => {
|
||||
await removeFieldRow({ path, rowIndex })
|
||||
(rowIndex: number) => {
|
||||
removeFieldRow({ path, rowIndex })
|
||||
setModified(true)
|
||||
},
|
||||
[path, removeFieldRow, setModified],
|
||||
@@ -297,7 +297,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
</DrawerToggler>
|
||||
<BlocksDrawer
|
||||
addRow={addRow}
|
||||
addRowIndex={value?.length || 0}
|
||||
addRowIndex={value || 0}
|
||||
blocks={blocks}
|
||||
drawerSlug={drawerSlug}
|
||||
labels={labels}
|
||||
|
||||
@@ -2,24 +2,24 @@ import type { PayloadRequest } from '../../../../../express/types'
|
||||
import type { RichTextField, Validate } from '../../../../../fields/config/types'
|
||||
import type { CellComponentProps } from '../../../views/collections/List/Cell/types'
|
||||
|
||||
export type RichTextFieldProps<AdapterProps = object> = Omit<
|
||||
RichTextField<AdapterProps>,
|
||||
export type RichTextFieldProps<Value extends object, AdapterProps> = Omit<
|
||||
RichTextField<Value, AdapterProps>,
|
||||
'type'
|
||||
> & {
|
||||
path?: string
|
||||
}
|
||||
|
||||
export type RichTextAdapter<AdapterProps = object> = {
|
||||
CellComponent: React.FC<CellComponentProps<RichTextField<AdapterProps>>>
|
||||
FieldComponent: React.FC<RichTextFieldProps<AdapterProps>>
|
||||
export type RichTextAdapter<Value extends object = object, AdapterProps = any> = {
|
||||
CellComponent: React.FC<CellComponentProps<RichTextField<Value, AdapterProps>>>
|
||||
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps>>
|
||||
afterReadPromise?: (data: {
|
||||
currentDepth?: number
|
||||
depth: number
|
||||
field: RichTextField<AdapterProps>
|
||||
field: RichTextField<Value, AdapterProps>
|
||||
overrideAccess?: boolean
|
||||
req: PayloadRequest
|
||||
showHiddenFields: boolean
|
||||
siblingDoc: Record<string, unknown>
|
||||
}) => Promise<void> | null
|
||||
validate: Validate<unknown, unknown, RichTextField<AdapterProps>>
|
||||
validate: Validate<Value, Value, unknown, RichTextField<Value, AdapterProps>>
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
const dispatchField = useFormFields(([_, dispatch]) => dispatch)
|
||||
const config = useConfig()
|
||||
|
||||
const { getData, getSiblingData, setModified } = useForm()
|
||||
const { getData, getDataByPath, getSiblingData, setModified } = useForm()
|
||||
|
||||
const value = field?.value as T
|
||||
const initialValue = field?.initialValue as T
|
||||
@@ -116,8 +116,14 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
user,
|
||||
}
|
||||
|
||||
let valueToValidate = value
|
||||
|
||||
if (field?.rows && Array.isArray(field.rows)) {
|
||||
valueToValidate = getDataByPath(path)
|
||||
}
|
||||
|
||||
const validationResult =
|
||||
typeof validate === 'function' ? await validate(value, validateOptions) : true
|
||||
typeof validate === 'function' ? await validate(valueToValidate, validateOptions) : true
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
action.errorMessage = validationResult
|
||||
@@ -132,7 +138,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
}
|
||||
}
|
||||
|
||||
validateField()
|
||||
void validateField()
|
||||
},
|
||||
150,
|
||||
[
|
||||
@@ -142,6 +148,7 @@ const useField = <T,>(options: Options): FieldType<T> => {
|
||||
dispatchField,
|
||||
getData,
|
||||
getSiblingData,
|
||||
getDataByPath,
|
||||
id,
|
||||
operation,
|
||||
path,
|
||||
|
||||
@@ -220,7 +220,13 @@ export const API: React.FC<EditViewProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Gutter className={classes} right={false}>
|
||||
<SetStepNav collection={collection} global={global} id={id} isEditing={isEditing} />
|
||||
<SetStepNav
|
||||
collection={collection}
|
||||
global={global}
|
||||
id={id}
|
||||
isEditing={isEditing}
|
||||
view="API"
|
||||
/>
|
||||
<div className={`${baseClass}__configuration`}>
|
||||
<div className={`${baseClass}__api-url`}>
|
||||
<span className={`${baseClass}__label`}>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DocumentHeader } from '../../elements/DocumentHeader'
|
||||
import { FormLoadingOverlayToggle } from '../../elements/Loading'
|
||||
import Form from '../../forms/Form'
|
||||
import { OperationContext } from '../../utilities/OperationProvider'
|
||||
import { SetStepNav } from '../collections/Edit/SetStepNav'
|
||||
import { GlobalRoutes } from './Routes'
|
||||
import { CustomGlobalComponent } from './Routes/CustomComponent'
|
||||
import './index.scss'
|
||||
@@ -40,6 +41,7 @@ const DefaultGlobalView: React.FC<
|
||||
return (
|
||||
<main className={baseClass}>
|
||||
<OperationContext.Provider value="update">
|
||||
<SetStepNav global={global} />
|
||||
<Form
|
||||
action={action}
|
||||
className={`${baseClass}__form`}
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
}
|
||||
|
||||
&__fields {
|
||||
& > .tabs-field {
|
||||
& > .tabs-field,
|
||||
& > .group-field {
|
||||
margin-right: calc(var(--base) * -2);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +52,7 @@
|
||||
position: sticky;
|
||||
top: var(--doc-controls-height);
|
||||
width: 33.33%;
|
||||
height: 100%;
|
||||
height: calc(100vh - var(--doc-controls-height));
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
@@ -110,7 +111,8 @@
|
||||
}
|
||||
|
||||
&__fields {
|
||||
& > .tabs-field {
|
||||
& > .tabs-field,
|
||||
& > .group-field {
|
||||
margin-right: calc(var(--gutter-h) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +90,8 @@ export const DefaultGlobalEdit: React.FC<GlobalEditViewProps> = (props) => {
|
||||
<div className={`${baseClass}__sidebar-sticky-wrap`}>
|
||||
<div className={`${baseClass}__sidebar-fields`}>
|
||||
<RenderFields
|
||||
fieldSchema={fields}
|
||||
fieldTypes={fieldTypes}
|
||||
filter={(field) => field.admin.position === 'sidebar'}
|
||||
fields={sidebarFields}
|
||||
permissions={permissions.fields}
|
||||
readOnly={!hasSavePermission}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { SizeReducerAction } from './sizeReducer'
|
||||
export interface LivePreviewContextType {
|
||||
breakpoint: LivePreviewConfig['breakpoints'][number]['name']
|
||||
breakpoints: LivePreviewConfig['breakpoints']
|
||||
deviceFrameRef: React.RefObject<HTMLDivElement>
|
||||
iframeHasLoaded: boolean
|
||||
iframeRef: React.RefObject<HTMLIFrameElement>
|
||||
measuredDeviceSize: {
|
||||
@@ -18,6 +17,7 @@ export interface LivePreviewContextType {
|
||||
setBreakpoint: (breakpoint: LivePreviewConfig['breakpoints'][number]['name']) => void
|
||||
setHeight: (height: number) => void
|
||||
setIframeHasLoaded: (loaded: boolean) => void
|
||||
setMeasuredDeviceSize: (size: { height: number; width: number }) => void
|
||||
setSize: Dispatch<SizeReducerAction>
|
||||
setToolbarPosition: (position: { x: number; y: number }) => void
|
||||
setWidth: (width: number) => void
|
||||
@@ -36,7 +36,6 @@ export interface LivePreviewContextType {
|
||||
export const LivePreviewContext = createContext<LivePreviewContextType>({
|
||||
breakpoint: undefined,
|
||||
breakpoints: undefined,
|
||||
deviceFrameRef: undefined,
|
||||
iframeHasLoaded: false,
|
||||
iframeRef: undefined,
|
||||
measuredDeviceSize: {
|
||||
@@ -46,6 +45,7 @@ export const LivePreviewContext = createContext<LivePreviewContextType>({
|
||||
setBreakpoint: () => {},
|
||||
setHeight: () => {},
|
||||
setIframeHasLoaded: () => {},
|
||||
setMeasuredDeviceSize: () => {},
|
||||
setSize: () => {},
|
||||
setToolbarPosition: () => {},
|
||||
setWidth: () => {},
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { LivePreviewConfig } from '../../../../../exports/config'
|
||||
import type { EditViewProps } from '../../types'
|
||||
import type { usePopupWindow } from '../usePopupWindow'
|
||||
|
||||
import { useResize } from '../../../../utilities/useResize'
|
||||
import { customCollisionDetection } from './collisionDetection'
|
||||
import { LivePreviewContext } from './context'
|
||||
import { sizeReducer } from './sizeReducer'
|
||||
@@ -26,8 +25,6 @@ export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
|
||||
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
||||
|
||||
const deviceFrameRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const [iframeHasLoaded, setIframeHasLoaded] = React.useState(false)
|
||||
|
||||
const [zoom, setZoom] = React.useState(1)
|
||||
@@ -36,6 +33,11 @@ export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
|
||||
|
||||
const [size, setSize] = React.useReducer(sizeReducer, { height: 0, width: 0 })
|
||||
|
||||
const [measuredDeviceSize, setMeasuredDeviceSize] = React.useState({
|
||||
height: 0,
|
||||
width: 0,
|
||||
})
|
||||
|
||||
const [breakpoint, setBreakpoint] =
|
||||
React.useState<LivePreviewConfig['breakpoints'][0]['name']>('responsive')
|
||||
|
||||
@@ -92,22 +94,18 @@ export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
|
||||
}
|
||||
}, [breakpoint, breakpoints])
|
||||
|
||||
// keep an accurate measurement of the actual device size as it is truly rendered
|
||||
// this is helpful when `sizes` are non-number units like percentages, etc.
|
||||
const { size: measuredDeviceSize } = useResize(deviceFrameRef)
|
||||
|
||||
return (
|
||||
<LivePreviewContext.Provider
|
||||
value={{
|
||||
breakpoint,
|
||||
breakpoints,
|
||||
deviceFrameRef,
|
||||
iframeHasLoaded,
|
||||
iframeRef,
|
||||
measuredDeviceSize,
|
||||
setBreakpoint,
|
||||
setHeight,
|
||||
setIframeHasLoaded,
|
||||
setMeasuredDeviceSize,
|
||||
setSize,
|
||||
setToolbarPosition: setPosition,
|
||||
setWidth,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import { useResize } from '../../../../utilities/useResize'
|
||||
import { useLivePreviewContext } from '../Context/context'
|
||||
|
||||
export const DeviceContainer: React.FC<{
|
||||
@@ -7,7 +8,22 @@ export const DeviceContainer: React.FC<{
|
||||
}> = (props) => {
|
||||
const { children } = props
|
||||
|
||||
const { breakpoint, deviceFrameRef, size, zoom } = useLivePreviewContext()
|
||||
const deviceFrameRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const { breakpoint, setMeasuredDeviceSize, size, zoom } = useLivePreviewContext()
|
||||
|
||||
// Keep an accurate measurement of the actual device size as it is truly rendered
|
||||
// This is helpful when `sizes` are non-number units like percentages, etc.
|
||||
const { size: measuredDeviceSize } = useResize(deviceFrameRef)
|
||||
|
||||
// Sync the measured device size with the context so that other components can use it
|
||||
// This happens from the bottom up so that as this component mounts and unmounts,
|
||||
// Its size is freshly populated again upon re-mounting, i.e. going from iframe->popup->iframe
|
||||
useEffect(() => {
|
||||
if (measuredDeviceSize) {
|
||||
setMeasuredDeviceSize(measuredDeviceSize)
|
||||
}
|
||||
}, [measuredDeviceSize, setMeasuredDeviceSize])
|
||||
|
||||
let x = '0'
|
||||
let margin = '0'
|
||||
|
||||
@@ -7,7 +7,7 @@ export const DeviceContainer: React.FC<{
|
||||
}> = (props) => {
|
||||
const { children } = props
|
||||
|
||||
const { breakpoint, breakpoints, deviceFrameRef, size, zoom } = useLivePreviewContext()
|
||||
const { breakpoint, breakpoints, size, zoom } = useLivePreviewContext()
|
||||
|
||||
const foundBreakpoint = breakpoint && breakpoints?.find((bp) => bp.name === breakpoint)
|
||||
|
||||
@@ -31,7 +31,6 @@ export const DeviceContainer: React.FC<{
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={deviceFrameRef}
|
||||
style={{
|
||||
height:
|
||||
foundBreakpoint && foundBreakpoint?.name !== 'responsive'
|
||||
|
||||
@@ -58,13 +58,16 @@ const Preview: React.FC<
|
||||
const values = reduceFieldsToValues(fields, true)
|
||||
|
||||
// TODO: only send `fieldSchemaToJSON` one time
|
||||
const message = JSON.stringify({ data: values, fieldSchemaJSON, type: 'livePreview' })
|
||||
const message = JSON.stringify({
|
||||
data: values,
|
||||
fieldSchemaJSON,
|
||||
type: 'payload-live-preview',
|
||||
})
|
||||
|
||||
// external window
|
||||
if (isPopupOpen) {
|
||||
setIframeHasLoaded(false)
|
||||
|
||||
if (popupHasLoaded && popupRef.current) {
|
||||
if (popupRef.current) {
|
||||
popupRef.current.postMessage(message, url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ export const LivePreviewView: React.FC<EditViewProps> = (props) => {
|
||||
: livePreviewConfig?.url
|
||||
|
||||
const popupState = usePopupWindow({
|
||||
eventType: 'livePreview',
|
||||
href: url,
|
||||
eventType: 'payload-live-preview',
|
||||
url,
|
||||
})
|
||||
|
||||
const { apiURL, data, permissions } = props
|
||||
@@ -94,7 +94,13 @@ export const LivePreviewView: React.FC<EditViewProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SetStepNav collection={collection} global={global} id={id} isEditing={isEditing} />
|
||||
<SetStepNav
|
||||
collection={collection}
|
||||
global={global}
|
||||
id={id}
|
||||
isEditing={isEditing}
|
||||
view={t('livePreview')}
|
||||
/>
|
||||
<DocumentControls
|
||||
apiURL={apiURL}
|
||||
collection={collection}
|
||||
|
||||
@@ -14,28 +14,29 @@ export interface PopupMessage {
|
||||
|
||||
export const usePopupWindow = (props: {
|
||||
eventType?: string
|
||||
href: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onMessage?: (searchParams: PopupMessage['searchParams']) => Promise<void>
|
||||
url: string
|
||||
}): {
|
||||
isPopupOpen: boolean
|
||||
openPopupWindow: (e: React.MouseEvent<HTMLAnchorElement>) => void
|
||||
popupHasLoaded: boolean
|
||||
popupRef?: React.MutableRefObject<Window | null>
|
||||
} => {
|
||||
const { eventType, href, onMessage } = props
|
||||
const { eventType, onMessage, url } = props
|
||||
const isReceivingMessage = useRef(false)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [popupHasLoaded, setPopupHasLoaded] = useState(false)
|
||||
const { serverURL } = useConfig()
|
||||
const popupRef = useRef<Window | null>(null)
|
||||
const hasAttachedMessageListener = useRef(false)
|
||||
|
||||
// Optionally broadcast messages back out to the parent component
|
||||
useEffect(() => {
|
||||
const receiveMessage = async (event: MessageEvent): Promise<void> => {
|
||||
if (
|
||||
event.origin !== window.location.origin ||
|
||||
event.origin !== href ||
|
||||
event.origin !== url ||
|
||||
event.origin !== serverURL
|
||||
) {
|
||||
// console.warn(`Message received by ${event.origin}; IGNORED.`) // eslint-disable-line no-console
|
||||
@@ -53,12 +54,14 @@ export const usePopupWindow = (props: {
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', receiveMessage, false)
|
||||
if (isOpen && popupRef.current) {
|
||||
window.addEventListener('message', receiveMessage, false)
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', receiveMessage)
|
||||
}
|
||||
}, [onMessage, eventType, href, serverURL])
|
||||
}, [onMessage, eventType, url, serverURL, isOpen])
|
||||
|
||||
// Customize the size, position, and style of the popup window
|
||||
const openPopupWindow = useCallback(
|
||||
@@ -93,23 +96,36 @@ export const usePopupWindow = (props: {
|
||||
return strCopy
|
||||
}, '')
|
||||
.slice(0, -1) // remove last ',' (comma)
|
||||
const newWindow = window.open(href, '_blank', popupOptions)
|
||||
|
||||
const newWindow = window.open(url, '_blank', popupOptions)
|
||||
|
||||
popupRef.current = newWindow
|
||||
|
||||
setIsOpen(true)
|
||||
},
|
||||
[href],
|
||||
[url],
|
||||
)
|
||||
|
||||
// the only cross-origin way of detecting when a popup window has loaded
|
||||
// we catch a message event that the site rendered within the popup window fires
|
||||
// there is no way in js to add an event listener to a popup window across domains
|
||||
useEffect(() => {
|
||||
if (hasAttachedMessageListener.current) return
|
||||
hasAttachedMessageListener.current = true
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.origin === href && event.data === 'ready') {
|
||||
const data = JSON.parse(event.data)
|
||||
|
||||
if (
|
||||
url.startsWith(event.origin) &&
|
||||
data.type === eventType &&
|
||||
data.popupReady &&
|
||||
!popupHasLoaded
|
||||
) {
|
||||
setPopupHasLoaded(true)
|
||||
}
|
||||
})
|
||||
}, [href])
|
||||
}, [url, eventType, popupHasLoaded])
|
||||
|
||||
// this is the most stable and widely supported way to check if a popup window is no longer open
|
||||
// we poll its ref every x ms and use the popup window's `closed` property
|
||||
|
||||
@@ -21,4 +21,11 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__inputWrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: base(1);
|
||||
margin-bottom: base(0.25);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ const Login: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const prefillForm = autoLogin && autoLogin.prefillOnly
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{user ? (
|
||||
@@ -75,22 +77,33 @@ const Login: React.FC = () => {
|
||||
action={`${serverURL}${api}/${userSlug}/login`}
|
||||
className={`${baseClass}__form`}
|
||||
disableSuccessStatus
|
||||
initialData={{
|
||||
email: autoLogin && autoLogin.prefillOnly ? autoLogin.email : undefined,
|
||||
password: autoLogin && autoLogin.prefillOnly ? autoLogin.password : undefined,
|
||||
}}
|
||||
initialData={
|
||||
prefillForm
|
||||
? {
|
||||
email: autoLogin.email,
|
||||
password: autoLogin.password,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
method="post"
|
||||
onSuccess={onSuccess}
|
||||
waitForAutocomplete
|
||||
>
|
||||
<FormLoadingOverlayToggle action="loading" name="login-form" />
|
||||
<Email
|
||||
admin={{ autoComplete: 'email' }}
|
||||
label={t('general:email')}
|
||||
name="email"
|
||||
required
|
||||
/>
|
||||
<Password autoComplete="off" label={t('general:password')} name="password" required />
|
||||
<div className={`${baseClass}__inputWrap`}>
|
||||
<Email
|
||||
admin={{ autoComplete: 'email' }}
|
||||
label={t('general:email')}
|
||||
name="email"
|
||||
required
|
||||
/>
|
||||
<Password
|
||||
autoComplete="off"
|
||||
label={t('general:password')}
|
||||
name="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Link to={`${admin}/forgot`}>{t('forgotPasswordQuestion')}</Link>
|
||||
<FormSubmit>{t('login')}</FormSubmit>
|
||||
</Form>
|
||||
|
||||
@@ -1,88 +1,30 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import type { StepNavItem } from '../../elements/StepNav/types'
|
||||
import type { Props } from './types'
|
||||
|
||||
import { getTranslation } from '../../../../utilities/getTranslation'
|
||||
import { Gutter } from '../../elements/Gutter'
|
||||
import { LoadingOverlayToggle } from '../../elements/Loading'
|
||||
import Paginator from '../../elements/Paginator'
|
||||
import PerPage from '../../elements/PerPage'
|
||||
import { useStepNav } from '../../elements/StepNav'
|
||||
import { Table } from '../../elements/Table'
|
||||
import { useConfig } from '../../utilities/Config'
|
||||
import Meta from '../../utilities/Meta'
|
||||
import { useSearchParams } from '../../utilities/SearchParams'
|
||||
import { SetStepNav } from '../collections/Edit/SetStepNav'
|
||||
import { buildVersionColumns } from './columns'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'versions'
|
||||
|
||||
export const DefaultVersionsView: React.FC<Props> = (props) => {
|
||||
const { id, collection, data, editURL, entityLabel, global, isLoadingVersions, versionsData } =
|
||||
props
|
||||
const { id, collection, data, entityLabel, global, isLoadingVersions, versionsData } = props
|
||||
|
||||
const {
|
||||
routes: { admin },
|
||||
} = useConfig()
|
||||
|
||||
const { setStepNav } = useStepNav()
|
||||
|
||||
const { i18n, t } = useTranslation('version')
|
||||
const { t } = useTranslation('version')
|
||||
|
||||
const { limit } = useSearchParams()
|
||||
|
||||
const useAsTitle = collection?.admin?.useAsTitle || 'id'
|
||||
|
||||
useEffect(() => {
|
||||
let nav: StepNavItem[] = []
|
||||
|
||||
if (collection) {
|
||||
let docLabel = ''
|
||||
|
||||
if (data) {
|
||||
if (useAsTitle) {
|
||||
if (data[useAsTitle]) {
|
||||
docLabel = data[useAsTitle]
|
||||
} else {
|
||||
docLabel = `[${t('general:untitled')}]`
|
||||
}
|
||||
} else {
|
||||
docLabel = data.id
|
||||
}
|
||||
}
|
||||
|
||||
nav = [
|
||||
{
|
||||
label: getTranslation(collection.labels.plural, i18n),
|
||||
url: `${admin}/collections/${collection.slug}`,
|
||||
},
|
||||
{
|
||||
label: docLabel,
|
||||
url: editURL,
|
||||
},
|
||||
{
|
||||
label: t('versions'),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (global) {
|
||||
nav = [
|
||||
{
|
||||
label: getTranslation(global.label, i18n),
|
||||
url: editURL,
|
||||
},
|
||||
{
|
||||
label: t('versions'),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
setStepNav(nav)
|
||||
}, [setStepNav, collection, global, useAsTitle, data, admin, id, editURL, t, i18n])
|
||||
|
||||
let metaDesc: string
|
||||
let metaTitle: string
|
||||
|
||||
@@ -100,6 +42,7 @@ export const DefaultVersionsView: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SetStepNav collection={collection} global={global} id={id} isEditing view={t('versions')} />
|
||||
<LoadingOverlayToggle name="versions" show={isLoadingVersions} />
|
||||
<main className={baseClass}>
|
||||
<Meta description={metaDesc} title={metaTitle} />
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
}
|
||||
|
||||
&__fields {
|
||||
& > .tabs-field {
|
||||
& > .tabs-field,
|
||||
& > .group-field {
|
||||
margin-right: calc(var(--base) * -2);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +56,7 @@
|
||||
position: sticky;
|
||||
top: var(--doc-controls-height);
|
||||
width: 33.33%;
|
||||
height: 100%;
|
||||
height: calc(100vh - var(--doc-controls-height));
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
@@ -106,7 +107,8 @@
|
||||
}
|
||||
|
||||
&__fields {
|
||||
& > .tabs-field {
|
||||
& > .tabs-field,
|
||||
& > .group-field {
|
||||
margin-right: calc(var(--gutter-h) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,12 @@ export const DefaultCollectionEdit: React.FC<CollectionEditViewProps> = (props)
|
||||
<div className={`${baseClass}__sidebar`}>
|
||||
<div className={`${baseClass}__sidebar-sticky-wrap`}>
|
||||
<div className={`${baseClass}__sidebar-fields`}>
|
||||
<RenderFields fieldTypes={fieldTypes} fields={sidebarFields} />
|
||||
<RenderFields
|
||||
fieldTypes={fieldTypes}
|
||||
fields={sidebarFields}
|
||||
permissions={permissions.fields}
|
||||
readOnly={!hasSavePermission}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,15 +9,18 @@ import { getTranslation } from '../../../../../utilities/getTranslation'
|
||||
import useTitle from '../../../../hooks/useTitle'
|
||||
import { useStepNav } from '../../../elements/StepNav'
|
||||
import { useConfig } from '../../../utilities/Config'
|
||||
import { useEditDepth } from '../../../utilities/EditDepth'
|
||||
|
||||
export const SetStepNav: React.FC<
|
||||
| {
|
||||
collection: SanitizedCollectionConfig
|
||||
id: number | string
|
||||
isEditing: boolean
|
||||
view?: string
|
||||
}
|
||||
| {
|
||||
global: SanitizedGlobalConfig
|
||||
view?: string
|
||||
}
|
||||
> = (props) => {
|
||||
let collection: SanitizedCollectionConfig | undefined
|
||||
@@ -28,6 +31,7 @@ export const SetStepNav: React.FC<
|
||||
let slug: string
|
||||
let isEditing = false
|
||||
let id: number | string | undefined
|
||||
const view: string | undefined = props?.view || undefined
|
||||
|
||||
if ('collection' in props) {
|
||||
const {
|
||||
@@ -35,18 +39,23 @@ export const SetStepNav: React.FC<
|
||||
collection: collectionFromProps,
|
||||
isEditing: isEditingFromProps,
|
||||
} = props
|
||||
collection = collectionFromProps
|
||||
useAsTitle = collection.admin.useAsTitle
|
||||
pluralLabel = collection.labels.plural
|
||||
slug = collection.slug
|
||||
isEditing = isEditingFromProps
|
||||
id = idFromProps
|
||||
|
||||
if (collectionFromProps) {
|
||||
collection = collectionFromProps
|
||||
useAsTitle = collection.admin.useAsTitle
|
||||
pluralLabel = collection.labels.plural
|
||||
slug = collection.slug
|
||||
isEditing = isEditingFromProps
|
||||
id = idFromProps
|
||||
}
|
||||
}
|
||||
|
||||
if ('global' in props) {
|
||||
const { global: globalFromProps } = props
|
||||
global = globalFromProps
|
||||
slug = globalFromProps?.slug
|
||||
if (globalFromProps) {
|
||||
global = globalFromProps
|
||||
slug = globalFromProps?.slug
|
||||
}
|
||||
}
|
||||
|
||||
const title = useTitle({ collection, global })
|
||||
@@ -59,6 +68,8 @@ export const SetStepNav: React.FC<
|
||||
routes: { admin },
|
||||
} = useConfig()
|
||||
|
||||
const drawerDepth = useEditDepth()
|
||||
|
||||
useEffect(() => {
|
||||
const nav: StepNavItem[] = []
|
||||
|
||||
@@ -70,7 +81,8 @@ export const SetStepNav: React.FC<
|
||||
|
||||
if (isEditing) {
|
||||
nav.push({
|
||||
label: useAsTitle && useAsTitle !== 'id' ? title || `[${t('untitled')}]` : `${id}`,
|
||||
label: (useAsTitle && useAsTitle !== 'id' && title) || `${id}`,
|
||||
url: `${admin}/collections/${slug}/${id}`,
|
||||
})
|
||||
} else {
|
||||
nav.push({
|
||||
@@ -84,7 +96,13 @@ export const SetStepNav: React.FC<
|
||||
})
|
||||
}
|
||||
|
||||
setStepNav(nav)
|
||||
if (view) {
|
||||
nav.push({
|
||||
label: view,
|
||||
})
|
||||
}
|
||||
|
||||
if (drawerDepth <= 1) setStepNav(nav)
|
||||
}, [
|
||||
setStepNav,
|
||||
isEditing,
|
||||
@@ -98,6 +116,8 @@ export const SetStepNav: React.FC<
|
||||
title,
|
||||
global,
|
||||
collection,
|
||||
view,
|
||||
drawerDepth,
|
||||
])
|
||||
|
||||
return null
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user