Compare commits
333 Commits
bundler-we
...
live-previ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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>
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
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.4",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -150,7 +150,10 @@ export const findMany = async function find({
|
||||
const countResult = await chainMethods({
|
||||
methods: selectCountMethods,
|
||||
query: db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.select({
|
||||
count: sql<number>`count
|
||||
(*)`,
|
||||
})
|
||||
.from(table)
|
||||
.where(where),
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -147,6 +147,7 @@ export async function parseParams({
|
||||
const { operator: queryOperator, value: queryValue } = sanitizeQueryValue({
|
||||
field,
|
||||
operator,
|
||||
relationOrPath,
|
||||
val,
|
||||
})
|
||||
|
||||
@@ -158,6 +159,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
|
||||
|
||||
@@ -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.5",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -167,7 +167,10 @@ export const DocumentControls: React.FC<{
|
||||
<div className={`${baseClass}__controls`}>
|
||||
{showPreviewButton && (
|
||||
<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 +181,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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>>
|
||||
}
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ import { initTransaction } from '../../utilities/initTransaction'
|
||||
import { killTransaction } from '../../utilities/killTransaction'
|
||||
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields'
|
||||
import { appendVersionToQueryKey } from '../../versions/drafts/appendVersionToQueryKey'
|
||||
import { getQueryDraftsSort } from '../../versions/drafts/getQueryDraftsSort'
|
||||
import { buildAfterOperation } from './utils'
|
||||
|
||||
export type Arguments = {
|
||||
@@ -127,7 +128,7 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
|
||||
page: sanitizedPage,
|
||||
pagination: usePagination,
|
||||
req,
|
||||
sort,
|
||||
sort: getQueryDraftsSort(sort),
|
||||
where: fullWhere,
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -517,7 +517,7 @@ export type Config = {
|
||||
*/
|
||||
defaultMaxTextLength?: number
|
||||
/** Default richtext editor to use for richText fields */
|
||||
editor: RichTextAdapter
|
||||
editor: RichTextAdapter<any, any>
|
||||
/**
|
||||
* Email configuration options. This value is overridden by `email` in Payload.init if passed.
|
||||
*
|
||||
|
||||
@@ -398,11 +398,13 @@ export type RelationshipValue =
|
||||
| ValueWithRelation[]
|
||||
| (number | string)
|
||||
|
||||
export type RichTextField<AdapterProps = object> = FieldBase & {
|
||||
type IsAny<T> = 0 extends 1 & T ? true : false
|
||||
|
||||
export type RichTextField<Value extends object = any, AdapterProps = any> = FieldBase & {
|
||||
admin?: Admin
|
||||
editor?: RichTextAdapter<AdapterProps>
|
||||
editor?: RichTextAdapter<Value, AdapterProps>
|
||||
type: 'richText'
|
||||
} & AdapterProps
|
||||
} & (IsAny<AdapterProps> extends true ? {} : AdapterProps)
|
||||
|
||||
export type ArrayField = FieldBase & {
|
||||
admin?: Admin & {
|
||||
|
||||
@@ -211,7 +211,7 @@ export const date: Validate<unknown, unknown, DateField> = (value, { required, t
|
||||
return true
|
||||
}
|
||||
|
||||
export const richText: Validate<unknown, unknown, RichTextField, RichTextField> = async (
|
||||
export const richText: Validate<object, unknown, RichTextField, RichTextField> = async (
|
||||
value,
|
||||
options,
|
||||
) => {
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
"addFilter": "Добави филтър",
|
||||
"adminTheme": "Цветова тема",
|
||||
"and": "И",
|
||||
"applyChanges": "Приложете промените",
|
||||
"applyChanges": "Приложи промените",
|
||||
"ascending": "Възходящ",
|
||||
"automatic": "Автоматична",
|
||||
"backToDashboard": "Обратно към таблото",
|
||||
@@ -176,7 +176,7 @@
|
||||
"deletedSuccessfully": "Изтрито успешно.",
|
||||
"deleting": "Изтриване...",
|
||||
"descending": "Низходящо",
|
||||
"deselectAllRows": "Деселектирайте всички редове",
|
||||
"deselectAllRows": "Деселектирай всички редове",
|
||||
"duplicate": "Дупликирай",
|
||||
"duplicateWithoutSaving": "Дупликирай без да запазваш промените",
|
||||
"edit": "Редактирай",
|
||||
@@ -231,7 +231,7 @@
|
||||
"saving": "Запазване...",
|
||||
"searchBy": "Търси по {{label}}",
|
||||
"selectAll": "Избери всички {{count}} {{label}}",
|
||||
"selectAllRows": "Изберете всички редове",
|
||||
"selectAllRows": "Избери всички редове",
|
||||
"selectValue": "Избери стойност",
|
||||
"selectedCount": "{{count}} {{label}} избрани",
|
||||
"showAllLabel": "Покажи всички {{label}}",
|
||||
@@ -273,23 +273,23 @@
|
||||
"near": "близко"
|
||||
},
|
||||
"upload": {
|
||||
"crop": "Реколта",
|
||||
"cropToolDescription": "Плъзнете ъглите на избраната област, нарисувайте нова област или коригирайте стойностите по-долу.",
|
||||
"crop": "Изрязване",
|
||||
"cropToolDescription": "Плъзни ъглите на избраната област, избери нова област или коригирай стойностите по-долу.",
|
||||
"dragAndDrop": "Дръпни и пусни файл",
|
||||
"dragAndDropHere": "или дръпни и пусни файла тук",
|
||||
"editImage": "Редактирай изображение",
|
||||
"fileName": "Име на файла",
|
||||
"fileSize": "Големина на файла",
|
||||
"focalPoint": "Фокусна точка",
|
||||
"focalPointDescription": "Преместете фокусната точка директно върху визуализацията или регулирайте стойностите по-долу.",
|
||||
"focalPointDescription": "Премести фокусната точка директно върху визуализацията или регулирай стойностите по-долу.",
|
||||
"height": "Височина",
|
||||
"lessInfo": "По-малко информация",
|
||||
"moreInfo": "Повече информация",
|
||||
"previewSizes": "Преглед на размери",
|
||||
"selectCollectionToBrowse": "Избери колекция, която да разгледаш",
|
||||
"selectFile": "Избери файл",
|
||||
"setCropArea": "Задайте област за изрязване",
|
||||
"setFocalPoint": "Задайте фокусна точка",
|
||||
"setCropArea": "Задай област за изрязване",
|
||||
"setFocalPoint": "Задай фокусна точка",
|
||||
"sizes": "Големини",
|
||||
"sizesFor": "Размери за {{label}}",
|
||||
"width": "Ширина"
|
||||
@@ -368,4 +368,4 @@
|
||||
"viewingVersions": "Гледане на версии за {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Гледане на версии за глобалния документ {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
packages/payload/src/versions/drafts/getQueryDraftsSort.ts
Normal file
17
packages/payload/src/versions/drafts/getQueryDraftsSort.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Takes the incoming sort argument and prefixes it with `versions.` and preserves any `-` prefixes for descending order
|
||||
* @param sort
|
||||
*/
|
||||
export const getQueryDraftsSort = (sort: string): string => {
|
||||
if (!sort) return sort
|
||||
|
||||
let direction = ''
|
||||
let orderBy = sort
|
||||
|
||||
if (sort[0] === '-') {
|
||||
direction = '-'
|
||||
orderBy = sort.substring(1)
|
||||
}
|
||||
|
||||
return `${direction}version.${orderBy}`
|
||||
}
|
||||
37
packages/plugin-cloud/.eslintrc.cjs
Normal file
37
packages/plugin-cloud/.eslintrc.cjs
Normal file
@@ -0,0 +1,37 @@
|
||||
/** @type {import('prettier').Config} */
|
||||
module.exports = {
|
||||
extends: ['@payloadcms'],
|
||||
overrides: [
|
||||
{
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
|
||||
},
|
||||
{
|
||||
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,
|
||||
}
|
||||
248
packages/plugin-cloud/.gitignore
vendored
Normal file
248
packages/plugin-cloud/.gitignore
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
dev/tmp
|
||||
dev/yarn.lock
|
||||
|
||||
# Created by https://www.gitignore.io/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
|
||||
|
||||
### macOS ###
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Yarn Berry
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
|
||||
### SublimeText ###
|
||||
# cache files for sublime text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using SublimeText
|
||||
# *.sublime-project
|
||||
|
||||
# sftp configuration file
|
||||
sftp-config.json
|
||||
|
||||
# Package control specific files
|
||||
Package Control.last-run
|
||||
Package Control.ca-list
|
||||
Package Control.ca-bundle
|
||||
Package Control.system-ca-bundle
|
||||
Package Control.cache/
|
||||
Package Control.ca-certs/
|
||||
Package Control.merged-ca-bundle
|
||||
Package Control.user-ca-bundle
|
||||
oscrypto-ca-bundle.crt
|
||||
bh_unicode_properties.cache
|
||||
|
||||
# Sublime-github package stores a github token in this file
|
||||
# https://packagecontrol.io/packages/sublime-github
|
||||
GitHub.sublime-settings
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history
|
||||
|
||||
### WebStorm ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
.idea/*
|
||||
# User-specific stuff:
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.xml
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# CMake
|
||||
cmake-build-debug/
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Ruby plugin and RubyMine
|
||||
/.rakeTasks
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
### WebStorm Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.gitignore.io/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
|
||||
|
||||
# Ignore all uploads
|
||||
demo/upload
|
||||
demo/media
|
||||
demo/files
|
||||
|
||||
# Ignore build folder
|
||||
build
|
||||
|
||||
# Ignore built components
|
||||
components/index.js
|
||||
components/styles.css
|
||||
|
||||
# Ignore generated
|
||||
demo/generated-types.ts
|
||||
demo/generated-schema.graphql
|
||||
|
||||
# Ignore dist, no need for git
|
||||
dist
|
||||
|
||||
# Ignore emulator volumes
|
||||
src/adapters/s3/emulator/.localstack/
|
||||
10
packages/plugin-cloud/.prettierignore
Normal file
10
packages/plugin-cloud/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
22
packages/plugin-cloud/LICENSE.md
Normal file
22
packages/plugin-cloud/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
99
packages/plugin-cloud/README.md
Normal file
99
packages/plugin-cloud/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Payload Cloud Plugin
|
||||
|
||||
This is the official Payload Cloud plugin that connects your Payload instance to the resources that Payload Cloud provides.
|
||||
|
||||
## File storage
|
||||
|
||||
Payload Cloud gives you S3 file storage backed by Cloudflare as a CDN, and this plugin extends Payload so that all of your media will be stored in S3 rather than locally.
|
||||
|
||||
## Email delivery
|
||||
|
||||
Payload Cloud provides an email delivery service out-of-the-box for all Payload Cloud customers. Powered by [Resend](https://resend.com).
|
||||
|
||||
## Upload caching
|
||||
|
||||
Payload Cloud provides a caching for all upload collections by default through Cloudflare's CDN.
|
||||
|
||||
## How to use
|
||||
|
||||
Add the plugin to your Payload config
|
||||
|
||||
`yarn add @payloadcms/plugin-cloud`
|
||||
|
||||
```ts
|
||||
import { payloadCloud } from '@payloadcms/plugin-cloud'
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
export default buildConfig({
|
||||
plugins: [payloadCloud()],
|
||||
// rest of config
|
||||
})
|
||||
```
|
||||
|
||||
NOTE: If your Payload config already has an email with transport, this will take precedence over Payload Cloud's email service.
|
||||
|
||||
### Optional configuration
|
||||
|
||||
If you wish to opt-out of any Payload cloud features, the plugin also accepts options to do so.
|
||||
|
||||
```ts
|
||||
payloadCloud({
|
||||
storage: false, // Disable file storage
|
||||
email: false, // Disable email delivery
|
||||
uploadCaching: false, // Disable upload caching
|
||||
})
|
||||
```
|
||||
|
||||
#### Upload Caching Configuration
|
||||
|
||||
If you wish to configure upload caching on a per-collection basis, you can do so by passing in a keyed object of collection names. By default, all collections will be cached for 24 hours (86400 seconds). The cache is invalidated when an item is updated or deleted.
|
||||
|
||||
```ts
|
||||
payloadCloud({
|
||||
uploadCaching: {
|
||||
maxAge: 604800, // Override default maxAge for all collections
|
||||
collection1Slug: {
|
||||
maxAge: 10, // Collection-specific maxAge, takes precedence over others
|
||||
},
|
||||
collection2Slug: {
|
||||
enabled: false, // Disable caching for this collection
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Accessing File Storage from Local Environment
|
||||
|
||||
This plugin works off of a specific set of environment variables in order to access your file resources. Here is this list with some prefilled.
|
||||
|
||||
```txt
|
||||
PORT=3000
|
||||
MONGODB_URI=
|
||||
PAYLOAD_CLOUD=true
|
||||
PAYLOAD_CLOUD_ENVIRONMENT=prod
|
||||
PAYLOAD_CLOUD_BUCKET=
|
||||
PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID=
|
||||
PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID=
|
||||
PAYLOAD_CLOUD_COGNITO_USER_POOL_ID=
|
||||
PAYLOAD_CLOUD_PROJECT_ID=
|
||||
PAYLOAD_CLOUD_COGNITO_PASSWORD=
|
||||
PAYLOAD_CLOUD_BUCKET_REGION=
|
||||
|
||||
PAYLOAD_SECRET=
|
||||
```
|
||||
|
||||
- `MONGODB_URI` is on the the Database tab.
|
||||
- `PAYLOAD_CLOUD_PROJECT_ID` is from Settings -> Billing.
|
||||
- `PAYLOAD_SECRET` is from Settings -> Environment Variables
|
||||
|
||||
The remaining values can be seen on your project's File Storage tab. You'll have to match up the values appropriately. We plan on adding the ability to easily copy these values in the near future.
|
||||
|
||||
## Future enhancements
|
||||
|
||||
### API CDN
|
||||
|
||||
In the future, this plugin will also ship with a way to dynamically cache API requests as well as purge them whenever a resource is updated.
|
||||
|
||||
## When it executes
|
||||
|
||||
This plugin will only execute if the required environment variables set by Payload Cloud are in place. If they are not, the plugin will not execute and your Payload instance will behave as normal.
|
||||
14
packages/plugin-cloud/dev/.env.example
Normal file
14
packages/plugin-cloud/dev/.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
MONGODB_URI=mongodb://localhost/payload-plugin-cloud
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
PAYLOAD_SECRET=45ligj345ligj4wl5igj4lw5igj45ligj45wlijl
|
||||
PAYLOAD_CONFIG_PATH=src/payload.config.ts
|
||||
|
||||
PAYLOAD_CLOUD=true
|
||||
PAYLOAD_CLOUD_BUCKET=
|
||||
PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID=
|
||||
PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID=
|
||||
PAYLOAD_CLOUD_COGNITO_USER_POOL_ID=
|
||||
PAYLOAD_CLOUD_ENVIRONMENT=
|
||||
PAYLOAD_CLOUD_PROJECT_ID=
|
||||
PAYLOAD_CLOUD_COGNITO_PASSWORD=
|
||||
PAYLOAD_CLOUD_BUCKET_REGION=
|
||||
5
packages/plugin-cloud/dev/nodemon.json
Normal file
5
packages/plugin-cloud/dev/nodemon.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"exec": "ts-node src/server.ts",
|
||||
"ext": "ts",
|
||||
"watch": ["src/**/*.ts", "../src/**/*.ts"]
|
||||
}
|
||||
28
packages/plugin-cloud/dev/package.json
Normal file
28
packages/plugin-cloud/dev/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "payload-plugin-cloud-demo",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "yarn build:payload && yarn build:server",
|
||||
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.142.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "^1.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.9",
|
||||
"cross-env": "^7.0.3",
|
||||
"nodemon": "^2.0.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.1.3"
|
||||
}
|
||||
}
|
||||
56
packages/plugin-cloud/dev/src/collections/Media.ts
Normal file
56
packages/plugin-cloud/dev/src/collections/Media.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { CollectionConfig, Field } from 'payload/types'
|
||||
|
||||
const urlField: Field = {
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
hooks: {
|
||||
afterRead: [
|
||||
({ value }) => {
|
||||
console.log('hello from hook')
|
||||
return value
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
upload: {
|
||||
imageSizes: [
|
||||
{
|
||||
height: 400,
|
||||
width: 400,
|
||||
crop: 'center',
|
||||
name: 'square',
|
||||
},
|
||||
{
|
||||
width: 900,
|
||||
height: 450,
|
||||
crop: 'center',
|
||||
name: 'sixteenByNineMedium',
|
||||
},
|
||||
],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'alt',
|
||||
label: 'Alt Text',
|
||||
type: 'text',
|
||||
},
|
||||
|
||||
// The following fields should be able to be merged in to default upload fields
|
||||
urlField,
|
||||
{
|
||||
name: 'sizes',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'square',
|
||||
type: 'group',
|
||||
fields: [urlField],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
23
packages/plugin-cloud/dev/src/collections/Users.ts
Normal file
23
packages/plugin-cloud/dev/src/collections/Users.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'avatar',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'background',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default Users
|
||||
49
packages/plugin-cloud/dev/src/payload.config.ts
Normal file
49
packages/plugin-cloud/dev/src/payload.config.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { buildConfig } from 'payload/config'
|
||||
import path from 'path'
|
||||
import Users from './collections/Users'
|
||||
import { payloadCloud } from '../../src'
|
||||
import { Media } from './collections/Media'
|
||||
|
||||
export default buildConfig({
|
||||
serverURL: 'http://localhost:3000',
|
||||
collections: [Media, Users],
|
||||
admin: {
|
||||
// NOTE - these webpack extensions are only required
|
||||
// for development of this plugin.
|
||||
// No need to use these aliases within your own projects.
|
||||
webpack: (config) => {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...(config.resolve || {}),
|
||||
alias: {
|
||||
...(config.resolve.alias || {}),
|
||||
[path.resolve(__dirname, '../../src')]: path.resolve(__dirname, '../../src/admin.js'),
|
||||
react: path.resolve(__dirname, '../node_modules/react'),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
},
|
||||
// @ts-expect-error local reference
|
||||
plugins: [payloadCloud()],
|
||||
onInit: async (payload) => {
|
||||
const users = await payload.find({
|
||||
collection: 'users',
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
if (!users.docs.length) {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
26
packages/plugin-cloud/dev/src/server.ts
Normal file
26
packages/plugin-cloud/dev/src/server.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import dotenv from 'dotenv'
|
||||
import express from 'express'
|
||||
import payload from 'payload'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const app = express()
|
||||
|
||||
// Redirect root to Admin panel
|
||||
app.get('/', (_, res) => {
|
||||
res.redirect('/admin')
|
||||
})
|
||||
|
||||
// Initialize Payload
|
||||
payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
mongoURL: process.env.MONGODB_URI,
|
||||
express: app,
|
||||
onInit: () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
|
||||
},
|
||||
})
|
||||
|
||||
// Add your own express routes here
|
||||
|
||||
app.listen(3000)
|
||||
16
packages/plugin-cloud/dev/tsconfig.json
Normal file
16
packages/plugin-cloud/dev/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "../",
|
||||
"jsx": "react",
|
||||
"sourceMap": true
|
||||
},
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
}
|
||||
15
packages/plugin-cloud/jest.config.js
Normal file
15
packages/plugin-cloud/jest.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/src/**/*.spec.ts'],
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)?$': 'ts-jest',
|
||||
},
|
||||
verbose: true,
|
||||
// globalSetup: './test/jest.setup.ts',
|
||||
moduleNameMapper: {
|
||||
'\\.(css|scss)$': '<rootDir>/src/webpack/mocks/emptyModule.js',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
|
||||
'<rootDir>/src/webpack/mocks/fileMock.js',
|
||||
},
|
||||
testTimeout: 60000,
|
||||
}
|
||||
41
packages/plugin-cloud/package.json
Normal file
41
packages/plugin-cloud/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"version": "2.2.5",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
"build:watch": "tsc -w",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||
"clean": "rimraf dist && rimraf dev/yarn.lock",
|
||||
"prepublishOnly": "yarn clean && yarn build && yarn test"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"nodemailer": "^6.9.0",
|
||||
"payload": "^1.8.2 || ^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cognito-identity": "^3.289.0",
|
||||
"@aws-sdk/client-s3": "^3.142.0",
|
||||
"@aws-sdk/credential-providers": "^3.289.0",
|
||||
"@aws-sdk/lib-storage": "^3.267.0",
|
||||
"amazon-cognito-identity-js": "^6.1.2",
|
||||
"resend": "^0.17.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/jest": "^29.5.1",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"nodemailer": "^6.9.1",
|
||||
"payload": "^1.11.6",
|
||||
"ts-jest": "^29.1.0",
|
||||
"webpack": "^5.78.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
1
packages/plugin-cloud/src/admin.js
Normal file
1
packages/plugin-cloud/src/admin.js
Normal file
@@ -0,0 +1 @@
|
||||
export const payloadCloud = () => (config) => config
|
||||
65
packages/plugin-cloud/src/email.spec.ts
Normal file
65
packages/plugin-cloud/src/email.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { Config } from 'payload/config'
|
||||
|
||||
import { defaults } from 'payload/dist/config/defaults'
|
||||
|
||||
import { payloadCloudEmail } from './email'
|
||||
|
||||
describe('email', () => {
|
||||
let defaultConfig: Config
|
||||
beforeAll(() => {
|
||||
jest.mock('resend')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
defaultConfig = { ...defaults }
|
||||
})
|
||||
|
||||
describe('not in Payload Cloud', () => {
|
||||
it('should return undefined', () => {
|
||||
const email = payloadCloudEmail({
|
||||
apiKey: 'test',
|
||||
config: defaultConfig,
|
||||
defaultDomain: 'test',
|
||||
})
|
||||
|
||||
expect(email).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('in Payload Cloud', () => {
|
||||
beforeEach(() => {
|
||||
process.env.PAYLOAD_CLOUD = 'true'
|
||||
})
|
||||
|
||||
it('should respect PAYLOAD_CLOUD env var', () => {
|
||||
const email = payloadCloudEmail({
|
||||
apiKey: 'test',
|
||||
config: defaultConfig,
|
||||
defaultDomain: 'test',
|
||||
})
|
||||
expect(email?.fromName).toBeDefined()
|
||||
expect(email?.fromAddress).toBeDefined()
|
||||
expect(email?.transport?.transporter.name).toEqual('payload-cloud')
|
||||
})
|
||||
|
||||
it('should allow setting fromName and fromAddress', () => {
|
||||
const fromName = 'custom from name'
|
||||
const fromAddress = 'custom@fromaddress.com'
|
||||
const configWithFrom: Config = {
|
||||
...defaultConfig,
|
||||
email: {
|
||||
fromAddress,
|
||||
fromName,
|
||||
},
|
||||
}
|
||||
const email = payloadCloudEmail({
|
||||
apiKey: 'test',
|
||||
config: configWithFrom,
|
||||
defaultDomain: 'test',
|
||||
})
|
||||
|
||||
expect(email?.fromName).toEqual(fromName)
|
||||
expect(email?.fromAddress).toEqual(fromAddress)
|
||||
})
|
||||
})
|
||||
})
|
||||
168
packages/plugin-cloud/src/email.ts
Normal file
168
packages/plugin-cloud/src/email.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { EmailTransport } from 'payload/config'
|
||||
|
||||
import nodemailer from 'nodemailer'
|
||||
import { Resend } from 'resend'
|
||||
|
||||
import type { PayloadCloudEmailOptions } from './types'
|
||||
|
||||
type TransportArgs = Parameters<typeof nodemailer.createTransport>[0]
|
||||
|
||||
export const payloadCloudEmail = (args: PayloadCloudEmailOptions): EmailTransport | undefined => {
|
||||
if (process.env.PAYLOAD_CLOUD !== 'true' || !args) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!args.apiKey) throw new Error('apiKey must be provided to use Payload Cloud Email ')
|
||||
if (!args.defaultDomain)
|
||||
throw new Error('defaultDomain must be provided to use Payload Cloud Email')
|
||||
|
||||
const { apiKey, config, defaultDomain } = args
|
||||
|
||||
const customDomainEnvs = Object.keys(process.env).filter(
|
||||
(e) => e.startsWith('PAYLOAD_CLOUD_EMAIL_DOMAIN_') && !e.endsWith('API_KEY'),
|
||||
)
|
||||
|
||||
// Match up the envs with api keys: { key: PAYLOAD_CLOUD_EMAIL_DOMAIN_${i}, value: domain }
|
||||
const customDomainsResendMap =
|
||||
customDomainEnvs?.reduce(
|
||||
(acc, envKey) => {
|
||||
const apiKey = process.env[`${envKey}_API_KEY`]
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
`PAYLOAD_CLOUD_EMAIL_DOMAIN_${envKey} is missing a corresponding PAYLOAD_CLOUD_EMAIL_DOMAIN_${envKey}_API_KEY`,
|
||||
)
|
||||
}
|
||||
|
||||
acc[process.env[envKey] as string] = new Resend(apiKey)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, Resend>,
|
||||
) || {}
|
||||
|
||||
const customDomains = Object.keys(customDomainsResendMap)
|
||||
|
||||
if (customDomains.length) {
|
||||
console.log(
|
||||
`Configuring Payload Cloud Email for ${[defaultDomain, ...(customDomains || [])].join(', ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
const resendDomainMap: Record<string, Resend> = {
|
||||
[defaultDomain]: new Resend(apiKey),
|
||||
...customDomainsResendMap,
|
||||
}
|
||||
|
||||
const fromName = config.email?.fromName || 'Payload CMS'
|
||||
const fromAddress =
|
||||
config.email?.fromAddress || `cms@${customDomains.length ? customDomains[0] : defaultDomain}`
|
||||
|
||||
const existingTransport = config.email && 'transport' in config.email && config.email?.transport
|
||||
|
||||
if (existingTransport) {
|
||||
return {
|
||||
fromAddress: fromAddress,
|
||||
fromName: fromName,
|
||||
transport: existingTransport,
|
||||
}
|
||||
}
|
||||
|
||||
const transportConfig: TransportArgs = {
|
||||
name: 'payload-cloud',
|
||||
send: async (mail, callback) => {
|
||||
const { from, html, subject, text, to } = mail.data
|
||||
|
||||
if (!to) return callback(new Error('No "to" address provided'), null)
|
||||
|
||||
if (!from) return callback(new Error('No "from" address provided'), null)
|
||||
|
||||
const cleanTo: string[] = []
|
||||
const toArr = Array.isArray(to) ? to : [to]
|
||||
|
||||
toArr.forEach((toItem) => {
|
||||
if (typeof toItem === 'string') {
|
||||
cleanTo.push(toItem)
|
||||
} else {
|
||||
cleanTo.push(toItem.address)
|
||||
}
|
||||
})
|
||||
|
||||
let fromToUse: string
|
||||
|
||||
if (typeof from === 'string') {
|
||||
fromToUse = from
|
||||
} else if (typeof from === 'object' && 'name' in from && 'address' in from) {
|
||||
fromToUse = `${from.name} <${from.address}>`
|
||||
} else {
|
||||
fromToUse = `${fromName} <${fromAddress}>`
|
||||
}
|
||||
|
||||
// Parse domain. Can be in 2 possible formats: "name@domain.com" or "Friendly Name <name@domain.com>"
|
||||
const domainMatch = fromToUse.match(/(?<=@)[^(\s|>)]+/g)
|
||||
|
||||
if (!domainMatch) {
|
||||
return callback(new Error(`Could not parse domain from "from" address: ${fromToUse}`), null)
|
||||
}
|
||||
|
||||
const fromDomain = domainMatch[0]
|
||||
const resend = resendDomainMap[fromDomain]
|
||||
|
||||
if (!resend) {
|
||||
callback(
|
||||
new Error(
|
||||
`No Resend instance found for domain: ${fromDomain}. Available domains: ${Object.keys(
|
||||
resendDomainMap,
|
||||
).join(', ')}`,
|
||||
),
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const sendResponse = await resend.sendEmail({
|
||||
from: fromToUse,
|
||||
html: (html || text) as string,
|
||||
subject: subject || '<No subject>',
|
||||
to: cleanTo,
|
||||
})
|
||||
|
||||
if ('error' in sendResponse) {
|
||||
return callback(new Error('Error sending email', { cause: sendResponse.error }), null)
|
||||
}
|
||||
return callback(null, sendResponse)
|
||||
} catch (err: unknown) {
|
||||
if (isResendError(err)) {
|
||||
return callback(
|
||||
new Error(`Error sending email: ${err.statusCode} ${err.name}: ${err.message}`),
|
||||
null,
|
||||
)
|
||||
} else if (err instanceof Error) {
|
||||
return callback(
|
||||
new Error(`Unexpected error sending email: ${err.message}: ${err.stack}`),
|
||||
null,
|
||||
)
|
||||
} else {
|
||||
return callback(new Error(`Unexpected error sending email: ${err}`), null)
|
||||
}
|
||||
}
|
||||
},
|
||||
version: '0.0.1',
|
||||
}
|
||||
|
||||
return {
|
||||
fromAddress: fromAddress,
|
||||
fromName: fromName,
|
||||
transport: nodemailer.createTransport(transportConfig),
|
||||
}
|
||||
}
|
||||
|
||||
type ResendError = {
|
||||
message: string
|
||||
name: string
|
||||
statusCode: number
|
||||
}
|
||||
|
||||
function isResendError(err: unknown): err is ResendError {
|
||||
return Boolean(
|
||||
err && typeof err === 'object' && 'message' in err && 'statusCode' in err && 'name' in err,
|
||||
)
|
||||
}
|
||||
42
packages/plugin-cloud/src/hooks/afterDelete.ts
Normal file
42
packages/plugin-cloud/src/hooks/afterDelete.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { TypeWithID } from 'payload/dist/globals/config/types'
|
||||
import type { FileData } from 'payload/dist/uploads/types'
|
||||
import type { CollectionAfterDeleteHook, CollectionConfig } from 'payload/types'
|
||||
|
||||
import type { TypeWithPrefix } from '../types'
|
||||
|
||||
import { createKey } from '../utilities/createKey'
|
||||
import { getStorageClient } from '../utilities/getStorageClient'
|
||||
|
||||
interface Args {
|
||||
collection: CollectionConfig
|
||||
}
|
||||
|
||||
export const getAfterDeleteHook = ({
|
||||
collection,
|
||||
}: Args): CollectionAfterDeleteHook<FileData & TypeWithID & TypeWithPrefix> => {
|
||||
return async ({ doc, req }) => {
|
||||
try {
|
||||
const { identityID, storageClient } = await getStorageClient()
|
||||
|
||||
const filesToDelete: string[] = [
|
||||
doc.filename,
|
||||
...Object.values(doc?.sizes || []).map((resizedFileData) => resizedFileData?.filename),
|
||||
]
|
||||
|
||||
const promises = filesToDelete.map(async (filename) => {
|
||||
await storageClient.deleteObject({
|
||||
Bucket: process.env.PAYLOAD_CLOUD_BUCKET,
|
||||
Key: createKey({ collection: collection.slug, filename, identityID }),
|
||||
})
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error(
|
||||
`There was an error while deleting files corresponding to the ${collection.labels?.singular} with ID ${doc.id}:`,
|
||||
)
|
||||
req.payload.logger.error(err)
|
||||
}
|
||||
return doc
|
||||
}
|
||||
}
|
||||
91
packages/plugin-cloud/src/hooks/beforeChange.ts
Normal file
91
packages/plugin-cloud/src/hooks/beforeChange.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { TypeWithID } from 'payload/dist/collections/config/types'
|
||||
import type { FileData } from 'payload/dist/uploads/types'
|
||||
import type { CollectionBeforeChangeHook, CollectionConfig } from 'payload/types'
|
||||
import type stream from 'stream'
|
||||
|
||||
import { Upload } from '@aws-sdk/lib-storage'
|
||||
import fs from 'fs'
|
||||
|
||||
import { createKey } from '../utilities/createKey'
|
||||
import { getIncomingFiles } from '../utilities/getIncomingFiles'
|
||||
import { getStorageClient } from '../utilities/getStorageClient'
|
||||
|
||||
interface Args {
|
||||
collection: CollectionConfig
|
||||
}
|
||||
|
||||
const MB = 1024 * 1024
|
||||
|
||||
export const getBeforeChangeHook =
|
||||
({ collection }: Args): CollectionBeforeChangeHook<FileData & TypeWithID> =>
|
||||
async ({ data, req }) => {
|
||||
try {
|
||||
const files = getIncomingFiles({ data, req })
|
||||
|
||||
req.payload.logger.debug({
|
||||
msg: `Preparing to upload ${files.length} files`,
|
||||
})
|
||||
|
||||
const { identityID, storageClient } = await getStorageClient()
|
||||
|
||||
const promises = files.map(async (file) => {
|
||||
const fileKey = file.filename
|
||||
|
||||
req.payload.logger.debug({
|
||||
fileKey,
|
||||
msg: `File buffer length: ${file.buffer.length / MB}MB`,
|
||||
tempFilePath: file.tempFilePath ?? 'undefined',
|
||||
})
|
||||
|
||||
const fileBufferOrStream: Buffer | stream.Readable = file.tempFilePath
|
||||
? fs.createReadStream(file.tempFilePath)
|
||||
: file.buffer
|
||||
|
||||
if (file.buffer.length > 0) {
|
||||
req.payload.logger.debug({
|
||||
fileKey,
|
||||
msg: `Uploading ${fileKey} from buffer. Size: ${file.buffer.length / MB}MB`,
|
||||
})
|
||||
|
||||
await storageClient.putObject({
|
||||
Body: fileBufferOrStream,
|
||||
Bucket: process.env.PAYLOAD_CLOUD_BUCKET,
|
||||
ContentType: file.mimeType,
|
||||
Key: createKey({ collection: collection.slug, filename: fileKey, identityID }),
|
||||
})
|
||||
}
|
||||
|
||||
// This will buffer at max 4 * 5MB = 20MB. Default queueSize is 4 and default partSize is 5MB.
|
||||
const parallelUploadS3 = new Upload({
|
||||
client: storageClient,
|
||||
params: {
|
||||
Body: fileBufferOrStream,
|
||||
Bucket: process.env.PAYLOAD_CLOUD_BUCKET,
|
||||
ContentType: file.mimeType,
|
||||
Key: createKey({ collection: collection.slug, filename: fileKey, identityID }),
|
||||
},
|
||||
})
|
||||
|
||||
parallelUploadS3.on('httpUploadProgress', (progress) => {
|
||||
if (progress.total) {
|
||||
req.payload.logger.debug({
|
||||
fileKey,
|
||||
msg: `Uploaded part ${progress.part} - ${(progress.loaded || 0) / MB}MB out of ${
|
||||
(progress.total || 0) / MB
|
||||
}MB`,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await parallelUploadS3.done()
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error(
|
||||
`There was an error while uploading files corresponding to the collection ${collection.slug} with filename ${data.filename}:`,
|
||||
)
|
||||
req.payload.logger.error(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
92
packages/plugin-cloud/src/hooks/uploadCache.ts
Normal file
92
packages/plugin-cloud/src/hooks/uploadCache.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type {
|
||||
CollectionAfterChangeHook,
|
||||
CollectionAfterDeleteHook,
|
||||
PayloadRequest,
|
||||
} from 'payload/types'
|
||||
|
||||
interface Args {
|
||||
endpoint: string
|
||||
}
|
||||
|
||||
export const getCacheUploadsAfterChangeHook =
|
||||
({ endpoint }: Args): CollectionAfterChangeHook =>
|
||||
async ({ doc, operation, req }) => {
|
||||
if (!req || !process.env.PAYLOAD_CLOUD_CACHE_KEY) return doc
|
||||
|
||||
const { res } = req
|
||||
if (res) {
|
||||
if (operation === 'update') {
|
||||
// Unawaited promise
|
||||
purge({ doc, endpoint, operation, req })
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
export const getCacheUploadsAfterDeleteHook =
|
||||
({ endpoint }: Args): CollectionAfterDeleteHook =>
|
||||
async ({ doc, req }) => {
|
||||
if (!req || !process.env.PAYLOAD_CLOUD_CACHE_KEY) return doc
|
||||
|
||||
const { res } = req
|
||||
if (res) {
|
||||
// Unawaited promise
|
||||
purge({ doc, endpoint, operation: 'delete', req })
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
type PurgeRequest = {
|
||||
doc: any
|
||||
endpoint: string
|
||||
operation: string
|
||||
req: PayloadRequest
|
||||
}
|
||||
|
||||
async function purge({ doc, endpoint, operation, req }: PurgeRequest) {
|
||||
const filePath = doc.url
|
||||
|
||||
if (!filePath) {
|
||||
req.payload.logger.error({
|
||||
msg: 'No url found on doc',
|
||||
project: {
|
||||
id: process.env.PAYLOAD_CLOUD_PROJECT_ID,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const body = {
|
||||
cacheKey: process.env.PAYLOAD_CLOUD_CACHE_KEY,
|
||||
filepath: doc.url,
|
||||
projectID: process.env.PAYLOAD_CLOUD_PROJECT_ID,
|
||||
}
|
||||
req.payload.logger.debug({
|
||||
filepath: doc.url,
|
||||
msg: 'Attempting to purge cache',
|
||||
operation,
|
||||
project: {
|
||||
id: process.env.PAYLOAD_CLOUD_PROJECT_ID,
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const purgeRes = await fetch(`${endpoint}/api/purge-cache`, {
|
||||
body: JSON.stringify({
|
||||
...body,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
req.payload.logger.debug({
|
||||
msg: 'Purge cache result',
|
||||
operation,
|
||||
statusCode: purgeRes.status,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error({ body, err, msg: '/purge-cache call failed' })
|
||||
}
|
||||
}
|
||||
3
packages/plugin-cloud/src/index.ts
Normal file
3
packages/plugin-cloud/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { payloadCloud } from './plugin'
|
||||
export { createKey } from './utilities/createKey'
|
||||
export { getStorageClient } from './utilities/getStorageClient'
|
||||
1
packages/plugin-cloud/src/mocks/email.js
Normal file
1
packages/plugin-cloud/src/mocks/email.js
Normal file
@@ -0,0 +1 @@
|
||||
exports.payloadCloudEmail = () => undefined
|
||||
1
packages/plugin-cloud/src/mocks/fileStub.js
Normal file
1
packages/plugin-cloud/src/mocks/fileStub.js
Normal file
@@ -0,0 +1 @@
|
||||
export default 'file-stub'
|
||||
9
packages/plugin-cloud/src/mocks/s3.js
Normal file
9
packages/plugin-cloud/src/mocks/s3.js
Normal file
@@ -0,0 +1,9 @@
|
||||
exports.S3 = () => null
|
||||
exports.Upload = () => null
|
||||
|
||||
exports.HeadObjectCommand = () => null
|
||||
exports.PutObjectCommand = () => null
|
||||
exports.UploadPartCommand = () => null
|
||||
exports.CreateMultipartUploadCommand = () => null
|
||||
exports.CompleteMultipartUploadCommand = () => null
|
||||
exports.PutObjectTaggingCommand = () => null
|
||||
1
packages/plugin-cloud/src/mocks/storageClient.js
Normal file
1
packages/plugin-cloud/src/mocks/storageClient.js
Normal file
@@ -0,0 +1 @@
|
||||
exports.getStorageClient = () => null
|
||||
152
packages/plugin-cloud/src/plugin.spec.ts
Normal file
152
packages/plugin-cloud/src/plugin.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { Config } from 'payload/config'
|
||||
|
||||
import nodemailer from 'nodemailer'
|
||||
import { defaults } from 'payload/dist/config/defaults'
|
||||
|
||||
import { payloadCloud } from './plugin'
|
||||
|
||||
describe('plugin', () => {
|
||||
beforeAll(() => {
|
||||
jest.mock('resend')
|
||||
})
|
||||
|
||||
describe('not in Payload Cloud', () => {
|
||||
it('should return unmodified config, with webpack aliases', () => {
|
||||
const plugin = payloadCloud()
|
||||
const config = plugin(createConfig())
|
||||
|
||||
assertNoCloudStorage(config)
|
||||
assertNoCloudEmail(config)
|
||||
})
|
||||
})
|
||||
|
||||
describe('in Payload Cloud', () => {
|
||||
beforeEach(() => {
|
||||
process.env.PAYLOAD_CLOUD = 'true'
|
||||
process.env.PAYLOAD_CLOUD_EMAIL_API_KEY = 'test-key'
|
||||
process.env.PAYLOAD_CLOUD_DEFAULT_DOMAIN = 'test-domain.com'
|
||||
})
|
||||
|
||||
describe('storage', () => {
|
||||
it('should default to using payload cloud storage', () => {
|
||||
const plugin = payloadCloud()
|
||||
const config = plugin(createConfig())
|
||||
|
||||
assertCloudStorage(config)
|
||||
})
|
||||
|
||||
it('should allow opt-out', () => {
|
||||
const plugin = payloadCloud({ storage: false })
|
||||
const config = plugin(createConfig())
|
||||
|
||||
assertNoCloudStorage(config)
|
||||
})
|
||||
})
|
||||
|
||||
describe('email', () => {
|
||||
it('should default to using payload cloud email', () => {
|
||||
const plugin = payloadCloud()
|
||||
const config = plugin(createConfig())
|
||||
|
||||
assertCloudEmail(config)
|
||||
})
|
||||
|
||||
it('should allow opt-out', () => {
|
||||
const plugin = payloadCloud({ email: false })
|
||||
const config = plugin(createConfig())
|
||||
|
||||
assertNoCloudEmail(config)
|
||||
})
|
||||
|
||||
it('should allow PAYLOAD_CLOUD_EMAIL_* env vars to be unset', () => {
|
||||
delete process.env.PAYLOAD_CLOUD_EMAIL_API_KEY
|
||||
delete process.env.PAYLOAD_CLOUD_DEFAULT_DOMAIN
|
||||
|
||||
const plugin = payloadCloud()
|
||||
const config = plugin(createConfig())
|
||||
|
||||
assertNoCloudEmail(config)
|
||||
})
|
||||
|
||||
it('should not modify existing email transport', () => {
|
||||
const existingTransport = nodemailer.createTransport({
|
||||
name: 'existing-transport',
|
||||
send: async (mail) => {
|
||||
console.log('mock send', mail)
|
||||
},
|
||||
version: '0.0.1',
|
||||
})
|
||||
|
||||
const configWithTransport = createConfig({
|
||||
email: {
|
||||
fromAddress: 'test@test.com',
|
||||
fromName: 'Test',
|
||||
transport: existingTransport,
|
||||
},
|
||||
})
|
||||
|
||||
const plugin = payloadCloud()
|
||||
const config = plugin(configWithTransport)
|
||||
|
||||
expect(
|
||||
config.email && 'transport' in config.email && config.email.transport?.transporter.name,
|
||||
).toEqual('existing-transport')
|
||||
|
||||
assertNoCloudEmail(config)
|
||||
})
|
||||
|
||||
it('should allow setting fromName and fromAddress', () => {
|
||||
const configWithPartialEmail = createConfig({
|
||||
email: {
|
||||
fromAddress: 'test@test.com',
|
||||
fromName: 'Test',
|
||||
},
|
||||
})
|
||||
|
||||
const plugin = payloadCloud()
|
||||
const config = plugin(configWithPartialEmail)
|
||||
|
||||
expect(config.email?.fromName).toEqual(configWithPartialEmail.email?.fromName)
|
||||
expect(config.email?.fromAddress).toEqual(configWithPartialEmail.email?.fromAddress)
|
||||
|
||||
assertCloudEmail(config)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function assertCloudStorage(config: Config) {
|
||||
expect(config.admin).toHaveProperty('webpack')
|
||||
expect(config.upload?.useTempFiles).toEqual(true)
|
||||
}
|
||||
|
||||
function assertNoCloudStorage(config: Config) {
|
||||
expect(config.admin).toHaveProperty('webpack')
|
||||
expect(config.upload?.useTempFiles).toBeFalsy()
|
||||
}
|
||||
|
||||
function assertCloudEmail(config: Config) {
|
||||
expect(config.admin).toHaveProperty('webpack')
|
||||
if (config.email && 'transport' in config.email) {
|
||||
expect(config.email?.transport?.transporter.name).toEqual('payload-cloud')
|
||||
}
|
||||
}
|
||||
|
||||
/** Asserts that plugin did not run (other than webpack aliases) */
|
||||
function assertNoCloudEmail(config: Config) {
|
||||
expect(config.admin).toHaveProperty('webpack')
|
||||
|
||||
// No transport set
|
||||
if (!config.email) return
|
||||
|
||||
if ('transport' in config.email) {
|
||||
expect(config.email?.transport?.transporter.name).not.toEqual('payload-cloud')
|
||||
}
|
||||
}
|
||||
|
||||
function createConfig(overrides?: Partial<Config>): Config {
|
||||
return {
|
||||
...defaults,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
98
packages/plugin-cloud/src/plugin.ts
Normal file
98
packages/plugin-cloud/src/plugin.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { Config } from 'payload/config'
|
||||
|
||||
import type { PluginOptions } from './types'
|
||||
|
||||
import { payloadCloudEmail } from './email'
|
||||
import { getAfterDeleteHook } from './hooks/afterDelete'
|
||||
import { getBeforeChangeHook } from './hooks/beforeChange'
|
||||
import { getCacheUploadsAfterChangeHook, getCacheUploadsAfterDeleteHook } from './hooks/uploadCache'
|
||||
import { getStaticHandler } from './staticHandler'
|
||||
import { extendWebpackConfig } from './webpack'
|
||||
|
||||
export const payloadCloud =
|
||||
(pluginOptions?: PluginOptions) =>
|
||||
(incomingConfig: Config): Config => {
|
||||
let config = { ...incomingConfig }
|
||||
const webpack = extendWebpackConfig(incomingConfig)
|
||||
|
||||
config.admin = {
|
||||
...(config.admin || {}),
|
||||
webpack,
|
||||
}
|
||||
|
||||
if (process.env.PAYLOAD_CLOUD !== 'true') {
|
||||
return config // only modified webpack
|
||||
}
|
||||
|
||||
const cachingEnabled =
|
||||
pluginOptions?.uploadCaching !== false && !!process.env.PAYLOAD_CLOUD_CACHE_KEY
|
||||
|
||||
const apiEndpoint = pluginOptions?.endpoint || 'https://cloud-api.payloadcms.com'
|
||||
|
||||
// Configure cloud storage
|
||||
if (pluginOptions?.storage !== false) {
|
||||
config = {
|
||||
...config,
|
||||
collections: (config.collections || []).map((collection) => {
|
||||
if (collection.upload) {
|
||||
return {
|
||||
...collection,
|
||||
hooks: {
|
||||
...(collection.hooks || {}),
|
||||
afterChange: [
|
||||
...(collection.hooks?.afterChange || []),
|
||||
...(cachingEnabled
|
||||
? [getCacheUploadsAfterChangeHook({ endpoint: apiEndpoint })]
|
||||
: []),
|
||||
],
|
||||
afterDelete: [
|
||||
...(collection.hooks?.afterDelete || []),
|
||||
getAfterDeleteHook({ collection }),
|
||||
...(cachingEnabled
|
||||
? [getCacheUploadsAfterDeleteHook({ endpoint: apiEndpoint })]
|
||||
: []),
|
||||
],
|
||||
beforeChange: [
|
||||
...(collection.hooks?.beforeChange || []),
|
||||
getBeforeChangeHook({ collection }),
|
||||
],
|
||||
},
|
||||
upload: {
|
||||
...(typeof collection.upload === 'object' ? collection.upload : {}),
|
||||
disableLocalStorage: true,
|
||||
handlers: [
|
||||
...(typeof collection.upload === 'object' &&
|
||||
Array.isArray(collection.upload.handlers)
|
||||
? collection.upload.handlers
|
||||
: []),
|
||||
getStaticHandler({
|
||||
cachingOptions: pluginOptions?.uploadCaching,
|
||||
collection,
|
||||
}),
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return collection
|
||||
}),
|
||||
upload: {
|
||||
...(config.upload || {}),
|
||||
useTempFiles: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Configure cloud email
|
||||
const apiKey = process.env.PAYLOAD_CLOUD_EMAIL_API_KEY
|
||||
const defaultDomain = process.env.PAYLOAD_CLOUD_DEFAULT_DOMAIN
|
||||
if (pluginOptions?.email !== false && apiKey && defaultDomain) {
|
||||
config.email = payloadCloudEmail({
|
||||
apiKey,
|
||||
config,
|
||||
defaultDomain,
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
63
packages/plugin-cloud/src/staticHandler.ts
Normal file
63
packages/plugin-cloud/src/staticHandler.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { CollectionConfig } from 'payload/types'
|
||||
import type { Readable } from 'stream'
|
||||
|
||||
import type { CollectionCachingConfig, PluginOptions, StaticHandler } from './types'
|
||||
|
||||
import { createKey } from './utilities/createKey'
|
||||
import { getStorageClient } from './utilities/getStorageClient'
|
||||
|
||||
interface Args {
|
||||
cachingOptions?: PluginOptions['uploadCaching']
|
||||
collection: CollectionConfig
|
||||
}
|
||||
|
||||
export const getStaticHandler = ({ cachingOptions, collection }: Args): StaticHandler => {
|
||||
let maxAge = 86400 // 24 hours default
|
||||
let collCacheConfig: CollectionCachingConfig | undefined
|
||||
if (cachingOptions !== false) {
|
||||
// Set custom maxAge for all collections
|
||||
maxAge = cachingOptions?.maxAge || maxAge
|
||||
collCacheConfig = cachingOptions?.collections?.[collection.slug] || {}
|
||||
}
|
||||
|
||||
// Set maxAge using collection-specific override
|
||||
maxAge = collCacheConfig?.maxAge || maxAge
|
||||
|
||||
const cachingEnabled =
|
||||
cachingOptions !== false &&
|
||||
!!process.env.PAYLOAD_CLOUD_CACHE_KEY &&
|
||||
collCacheConfig?.enabled !== false
|
||||
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
const { identityID, storageClient } = await getStorageClient()
|
||||
|
||||
const Key = createKey({
|
||||
collection: collection.slug,
|
||||
filename: req.params.filename,
|
||||
identityID,
|
||||
})
|
||||
|
||||
const object = await storageClient.getObject({
|
||||
Bucket: process.env.PAYLOAD_CLOUD_BUCKET,
|
||||
Key,
|
||||
})
|
||||
|
||||
res.set({
|
||||
'Content-Length': object.ContentLength,
|
||||
'Content-Type': object.ContentType,
|
||||
...(cachingEnabled && { 'Cache-Control': `public, max-age=${maxAge}` }),
|
||||
ETag: object.ETag,
|
||||
})
|
||||
|
||||
if (object?.Body) {
|
||||
return (object.Body as Readable).pipe(res)
|
||||
}
|
||||
|
||||
return next()
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error({ err, msg: 'Error getting file from cloud storage' })
|
||||
return next()
|
||||
}
|
||||
}
|
||||
}
|
||||
113
packages/plugin-cloud/src/types.ts
Normal file
113
packages/plugin-cloud/src/types.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { NextFunction, Response } from 'express'
|
||||
import type { Config } from 'payload/config'
|
||||
import type { TypeWithID } from 'payload/dist/collections/config/types'
|
||||
import type { FileData } from 'payload/dist/uploads/types'
|
||||
import type { CollectionConfig, PayloadRequest } from 'payload/types'
|
||||
|
||||
export interface File {
|
||||
buffer: Buffer
|
||||
filename: string
|
||||
filesize: number
|
||||
mimeType: string
|
||||
tempFilePath?: string
|
||||
}
|
||||
|
||||
export type HandleUpload = (args: {
|
||||
collection: CollectionConfig
|
||||
data: any
|
||||
file: File
|
||||
req: PayloadRequest
|
||||
}) => Promise<void> | void
|
||||
|
||||
export interface TypeWithPrefix {
|
||||
prefix?: string
|
||||
}
|
||||
|
||||
export type HandleDelete = (args: {
|
||||
collection: CollectionConfig
|
||||
doc: TypeWithID & FileData & TypeWithPrefix
|
||||
filename: string
|
||||
req: PayloadRequest
|
||||
}) => Promise<void> | void
|
||||
|
||||
export type GenerateURL = (args: {
|
||||
collection: CollectionConfig
|
||||
filename: string
|
||||
prefix?: string
|
||||
}) => Promise<string> | string
|
||||
|
||||
export type StaticHandler = (
|
||||
req: PayloadRequest,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => Promise<unknown> | unknown
|
||||
|
||||
export interface PayloadCloudEmailOptions {
|
||||
apiKey: string
|
||||
config: Config
|
||||
defaultDomain: string
|
||||
}
|
||||
|
||||
export interface PluginOptions {
|
||||
/** Payload Cloud Email
|
||||
* @default true
|
||||
*/
|
||||
email?: false
|
||||
|
||||
/**
|
||||
* Payload Cloud API endpoint
|
||||
*
|
||||
* @internal Endpoint override for developement
|
||||
*/
|
||||
endpoint?: string
|
||||
|
||||
/** Payload Cloud Storage
|
||||
* @default true
|
||||
*/
|
||||
storage?: false
|
||||
|
||||
/**
|
||||
* Upload caching. Defaults to 24 hours for all collections.
|
||||
*
|
||||
* Optionally configure caching per collection
|
||||
*
|
||||
* ```ts
|
||||
* {
|
||||
* collSlug1: {
|
||||
* maxAge: 3600 // Custom value in seconds
|
||||
* },
|
||||
* collSlug2: {
|
||||
* enabled: false // Disable caching for this collection
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
|
||||
uploadCaching?:
|
||||
| {
|
||||
/**
|
||||
* Caching configuration per-collection
|
||||
*/
|
||||
collections?: Record<string, CollectionCachingConfig>
|
||||
/** Caching in seconds override for all collections
|
||||
* @default 86400 (24 hours)
|
||||
*/
|
||||
maxAge?: number
|
||||
}
|
||||
| false
|
||||
}
|
||||
|
||||
export type CollectionCachingConfig = {
|
||||
/**
|
||||
* Enable/disable caching for this collection
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
enabled?: boolean
|
||||
/** Caching in seconds override for this collection
|
||||
* @default 86400 (24 hours)
|
||||
*/
|
||||
maxAge?: number
|
||||
}
|
||||
45
packages/plugin-cloud/src/utilities/authAsCognitoUser.ts
Normal file
45
packages/plugin-cloud/src/utilities/authAsCognitoUser.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { CognitoUserSession } from 'amazon-cognito-identity-js'
|
||||
|
||||
import { AuthenticationDetails, CognitoUser, CognitoUserPool } from 'amazon-cognito-identity-js'
|
||||
|
||||
let sessionAndToken: CognitoUserSession | null = null
|
||||
|
||||
export const authAsCognitoUser = async (
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<CognitoUserSession> => {
|
||||
// TODO: Check that isValid evaluates expiration
|
||||
if (sessionAndToken?.isValid()) {
|
||||
return sessionAndToken
|
||||
}
|
||||
|
||||
const userPool = new CognitoUserPool({
|
||||
ClientId: process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID as string,
|
||||
UserPoolId: process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_ID as string,
|
||||
})
|
||||
|
||||
const authenticationDetails = new AuthenticationDetails({
|
||||
Password: password,
|
||||
Username: username,
|
||||
})
|
||||
|
||||
const cognitoUser = new CognitoUser({
|
||||
Pool: userPool,
|
||||
Username: username,
|
||||
})
|
||||
|
||||
const result: CognitoUserSession = await new Promise((resolve, reject) => {
|
||||
cognitoUser.authenticateUser(authenticationDetails, {
|
||||
onFailure: (err) => {
|
||||
reject(err)
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
resolve(res)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
sessionAndToken = result
|
||||
|
||||
return sessionAndToken
|
||||
}
|
||||
8
packages/plugin-cloud/src/utilities/createKey.ts
Normal file
8
packages/plugin-cloud/src/utilities/createKey.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
interface Args {
|
||||
collection: string
|
||||
filename: string
|
||||
identityID: string
|
||||
}
|
||||
|
||||
export const createKey = ({ collection, filename, identityID }: Args): string =>
|
||||
`${identityID}/${process.env.PAYLOAD_CLOUD_ENVIRONMENT}/${collection}/${filename}`
|
||||
45
packages/plugin-cloud/src/utilities/getIncomingFiles.ts
Normal file
45
packages/plugin-cloud/src/utilities/getIncomingFiles.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { FileData } from 'payload/dist/uploads/types'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
|
||||
import type { File } from '../types'
|
||||
|
||||
export function getIncomingFiles({
|
||||
data,
|
||||
req,
|
||||
}: {
|
||||
data: Partial<FileData>
|
||||
req: PayloadRequest
|
||||
}): File[] {
|
||||
const file = req.files?.file
|
||||
|
||||
let files: File[] = []
|
||||
|
||||
if (file && data.filename && data.mimeType) {
|
||||
const mainFile: File = {
|
||||
buffer: file.data,
|
||||
filename: data.filename,
|
||||
filesize: file.size,
|
||||
mimeType: data.mimeType,
|
||||
tempFilePath: file.tempFilePath,
|
||||
}
|
||||
|
||||
files = [mainFile]
|
||||
|
||||
if (data?.sizes) {
|
||||
Object.entries(data.sizes).forEach(([key, resizedFileData]) => {
|
||||
if (req.payloadUploadSizes?.[key] && data.mimeType) {
|
||||
files = files.concat([
|
||||
{
|
||||
buffer: req.payloadUploadSizes[key],
|
||||
filename: `${resizedFileData.filename}`,
|
||||
filesize: req.payloadUploadSizes[key].length,
|
||||
mimeType: data.mimeType,
|
||||
},
|
||||
])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
58
packages/plugin-cloud/src/utilities/getStorageClient.ts
Normal file
58
packages/plugin-cloud/src/utilities/getStorageClient.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { CognitoUserSession } from 'amazon-cognito-identity-js'
|
||||
|
||||
import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'
|
||||
import * as AWS from '@aws-sdk/client-s3'
|
||||
import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers'
|
||||
|
||||
import { authAsCognitoUser } from './authAsCognitoUser'
|
||||
|
||||
export type GetStorageClient = () => Promise<{
|
||||
identityID: string
|
||||
storageClient: AWS.S3
|
||||
}>
|
||||
|
||||
let storageClient: AWS.S3 | null = null
|
||||
let session: CognitoUserSession | null = null
|
||||
let identityID: string
|
||||
|
||||
export const getStorageClient: GetStorageClient = async () => {
|
||||
if (storageClient && session?.isValid()) {
|
||||
return {
|
||||
identityID,
|
||||
storageClient,
|
||||
}
|
||||
}
|
||||
|
||||
session = await authAsCognitoUser(
|
||||
process.env.PAYLOAD_CLOUD_PROJECT_ID as string,
|
||||
process.env.PAYLOAD_CLOUD_COGNITO_PASSWORD as string,
|
||||
)
|
||||
|
||||
const cognitoIdentity = new CognitoIdentityClient({
|
||||
credentials: fromCognitoIdentityPool({
|
||||
clientConfig: {
|
||||
region: 'us-east-1',
|
||||
},
|
||||
identityPoolId: process.env.PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID as string,
|
||||
logins: {
|
||||
[`cognito-idp.us-east-1.amazonaws.com/${process.env.PAYLOAD_CLOUD_COGNITO_USER_POOL_ID}`]:
|
||||
session.getIdToken().getJwtToken(),
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const credentials = await cognitoIdentity.config.credentials()
|
||||
|
||||
// @ts-expect-error
|
||||
identityID = credentials.identityId
|
||||
|
||||
storageClient = new AWS.S3({
|
||||
credentials,
|
||||
region: process.env.PAYLOAD_CLOUD_BUCKET_REGION,
|
||||
})
|
||||
|
||||
return {
|
||||
identityID,
|
||||
storageClient,
|
||||
}
|
||||
}
|
||||
24
packages/plugin-cloud/src/webpack.ts
Normal file
24
packages/plugin-cloud/src/webpack.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Config } from 'payload/config'
|
||||
import type { Configuration as WebpackConfig } from 'webpack'
|
||||
|
||||
import path from 'path'
|
||||
|
||||
export const extendWebpackConfig =
|
||||
(config: Config): ((webpackConfig: WebpackConfig) => WebpackConfig) =>
|
||||
(webpackConfig) => {
|
||||
const existingWebpackConfig =
|
||||
typeof config.admin?.webpack === 'function'
|
||||
? config.admin.webpack(webpackConfig)
|
||||
: webpackConfig
|
||||
|
||||
return {
|
||||
...existingWebpackConfig,
|
||||
resolve: {
|
||||
...(existingWebpackConfig.resolve || {}),
|
||||
alias: {
|
||||
...(existingWebpackConfig.resolve?.alias || {}),
|
||||
'@payloadcms/plugin-cloud': path.resolve(__dirname, './admin.js'),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
24
packages/plugin-cloud/tsconfig.json
Normal file
24
packages/plugin-cloud/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" }]
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import type { AdapterProps } from '../types'
|
||||
import { getEnabledNodes } from '../field/lexical/nodes'
|
||||
|
||||
export const RichTextCell: React.FC<
|
||||
CellComponentProps<RichTextField<AdapterProps>, SerializedEditorState> & AdapterProps
|
||||
CellComponentProps<RichTextField<SerializedEditorState, AdapterProps>, SerializedEditorState> &
|
||||
AdapterProps
|
||||
> = ({ data, editorConfig }) => {
|
||||
const [preview, setPreview] = React.useState('Loading...')
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ function fallbackRender({ error }): JSX.Element {
|
||||
// Call resetErrorBoundary() to reset the error boundary and retry the render.
|
||||
|
||||
return (
|
||||
<div role="alert">
|
||||
<div className="errorBoundary" role="alert">
|
||||
<p>Something went wrong:</p>
|
||||
<pre style={{ color: 'red' }}>{error.message}</pre>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LexicalCommand } from 'lexical'
|
||||
import type { Fields } from 'payload/types'
|
||||
import type { Data, Fields } from 'payload/types'
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
createCommand,
|
||||
} from 'lexical'
|
||||
import { formatDrawerSlug } from 'payload/components/elements'
|
||||
import { reduceFieldsToValues } from 'payload/components/forms'
|
||||
import {
|
||||
buildStateFromSchema,
|
||||
useAuth,
|
||||
@@ -317,17 +316,14 @@ export function LinkEditor({
|
||||
<LinkDrawer
|
||||
drawerSlug={drawerSlug}
|
||||
fieldSchema={fieldSchema}
|
||||
handleModalSubmit={(fields: Fields) => {
|
||||
handleModalSubmit={(fields: Fields, data: Data) => {
|
||||
closeModal(drawerSlug)
|
||||
|
||||
const data = reduceFieldsToValues(fields, true)
|
||||
|
||||
if (data?.fields?.doc?.value) {
|
||||
data.fields.doc.value = {
|
||||
id: data.fields.doc.value,
|
||||
}
|
||||
}
|
||||
|
||||
const newLinkPayload: LinkPayload = data as LinkPayload
|
||||
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, newLinkPayload)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user