Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
465acf0123 | ||
|
|
a6524720f5 | ||
|
|
59fb70077d | ||
|
|
6b3f20d01b | ||
|
|
5f6a0db869 | ||
|
|
26fcfba8fd | ||
|
|
88ad2ee1e3 | ||
|
|
b76aea9b9a | ||
|
|
d770b40cc5 | ||
|
|
3e6718034f | ||
|
|
a7beb1bb8f | ||
|
|
6a79efc5f5 | ||
|
|
9697269816 | ||
|
|
af375123a8 | ||
|
|
00b66a70c9 | ||
|
|
b0135a7ee0 | ||
|
|
fe2940a447 | ||
|
|
c745de15b9 | ||
|
|
4dcf0bcad6 | ||
|
|
93f043bc79 | ||
|
|
63bceedca4 | ||
|
|
0bbb5790d5 | ||
|
|
05c8bdc16a | ||
|
|
77c14033d6 | ||
|
|
0280cdd355 | ||
|
|
891ab34a12 | ||
|
|
26939a3331 | ||
|
|
9fa4a8fcfb | ||
|
|
96608a509b | ||
|
|
8586c85fab | ||
|
|
1a76e9580f | ||
|
|
8127fe39e2 | ||
|
|
e42c4dec98 | ||
|
|
2c82ea80de | ||
|
|
428aebd73e | ||
|
|
712bd6e8b1 | ||
|
|
1ca55b41c6 | ||
|
|
81352f3d41 | ||
|
|
6fb60cdbe3 | ||
|
|
e18784dd5b | ||
|
|
ff417368ff | ||
|
|
e28c77069e | ||
|
|
37fe2df2de | ||
|
|
8ca67d5aaa | ||
|
|
096d33718d | ||
|
|
0bd335303d | ||
|
|
e8e1ba00fe | ||
|
|
a7d47c627d | ||
|
|
5e1bed3177 | ||
|
|
364014a1e9 | ||
|
|
9cd5e5aefa | ||
|
|
429a88a5a1 | ||
|
|
cf12b5fc70 | ||
|
|
5096c37874 | ||
|
|
ac592b6d0d | ||
|
|
007f7e7d18 | ||
|
|
4ba072b0c6 | ||
|
|
8a4db150f0 | ||
|
|
b28b9020ea | ||
|
|
baa73fae62 | ||
|
|
55c27b6d2f | ||
|
|
6d148d7309 | ||
|
|
8e10ecae4b | ||
|
|
ef27b9f641 | ||
|
|
2dcce0339c | ||
|
|
b649ad7bb5 | ||
|
|
3cee0be314 | ||
|
|
8fc953605a | ||
|
|
e28dfc0c93 | ||
|
|
33561a8ea2 | ||
|
|
e500b46576 | ||
|
|
e79a84d200 | ||
|
|
16e94d401b | ||
|
|
9fbabc8fd6 | ||
|
|
9bc072ccaf | ||
|
|
45905c312f | ||
|
|
2c7a15ceca | ||
|
|
28e128241e | ||
|
|
21336cd61a | ||
|
|
ff1c10c382 | ||
|
|
6e8aa5e8af | ||
|
|
32e7c56a0d | ||
|
|
e5c783df5d | ||
|
|
63698e5e88 | ||
|
|
016ad3afc9 | ||
|
|
33e1e15ca9 | ||
|
|
59918aac91 | ||
|
|
a0c3cbd68d | ||
|
|
3a15e077c6 | ||
|
|
90a9e14e9d | ||
|
|
6d3b8636f4 | ||
|
|
cb8e07f852 | ||
|
|
301be0a5d4 | ||
|
|
466589b483 | ||
|
|
6754f55ce0 | ||
|
|
84d2bacb56 | ||
|
|
739abdcd81 | ||
|
|
c7cf2d3d2c | ||
|
|
79561497f9 | ||
|
|
600306274e | ||
|
|
398378a867 | ||
|
|
4e755dfde2 | ||
|
|
3634e2cc4d | ||
|
|
294fb5e574 | ||
|
|
f5f2332755 | ||
|
|
0acd7b8706 | ||
|
|
d91b44cbb3 | ||
|
|
e03a8e6b03 | ||
|
|
846485388a | ||
|
|
8d83e05948 | ||
|
|
7963d04a27 | ||
|
|
20b6b29c79 | ||
|
|
fdfdfc83f3 | ||
|
|
c154eb7e2b | ||
|
|
33686c6db8 | ||
|
|
6d6acbcfc1 | ||
|
|
4e2f2561ff |
@@ -2,16 +2,17 @@
|
||||
"verbose": true,
|
||||
"git": {
|
||||
"commitMessage": "chore(release): v${version}",
|
||||
"requireCleanWorkingDir": true
|
||||
"requireCleanWorkingDir": false
|
||||
},
|
||||
"github": {
|
||||
"release": true
|
||||
},
|
||||
"npm": {
|
||||
"skipChecks": true
|
||||
"skipChecks": true,
|
||||
"tag": "payload-1"
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": ["yarn", "yarn clean", "yarn test"]
|
||||
"before:init": ["yarn", "yarn clean"]
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/conventional-changelog": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
"npm": {
|
||||
"skipChecks": true,
|
||||
"tag": "canary"
|
||||
"tag": "payload-1"
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": ["yarn", "yarn clean", "yarn test"]
|
||||
|
||||
9
.vscode/launch.json
vendored
9
.vscode/launch.json
vendored
@@ -18,5 +18,12 @@
|
||||
"type": "node-terminal",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"command": "yarn run dev versions",
|
||||
"name": "Debug Versions",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
95
CHANGELOG.md
95
CHANGELOG.md
@@ -1,5 +1,100 @@
|
||||
|
||||
|
||||
## [1.15.10](https://github.com/payloadcms/payload/compare/v1.15.9...v1.15.10) (2025-03-10)
|
||||
|
||||
## [1.15.8](https://github.com/payloadcms/payload/compare/v1.15.7...v1.15.8) (2023-10-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* secures the user response from the me auth route ([#3409](https://github.com/payloadcms/payload/issues/3409)) ([26939a3](https://github.com/payloadcms/payload/commit/26939a333135810f2eebd3ebaf05885d152f6f13))
|
||||
|
||||
## [1.15.7](https://github.com/payloadcms/payload/compare/v1.15.6...v1.15.7) (2023-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* hotkey's pressed keys are not unset when window is not focused ([#3400](https://github.com/payloadcms/payload/issues/3400)) ([8586c85](https://github.com/payloadcms/payload/commit/8586c85fab3009a09be9bf86c22952f85aa0ad82))
|
||||
|
||||
## [1.15.6](https://github.com/payloadcms/payload/compare/v1.15.5...v1.15.6) (2023-09-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **#3289:** removes HMR plugin from prod webpack configs ([#3319](https://github.com/payloadcms/payload/issues/3319)) ([8ca67d5](https://github.com/payloadcms/payload/commit/8ca67d5aaa99f0f45eac56766fe42e07ab4a41f1)), closes [#3289](https://github.com/payloadcms/payload/issues/3289)
|
||||
* fields with relationTo[] correctly load returned data from form submission ([#3317](https://github.com/payloadcms/payload/issues/3317)) ([096d337](https://github.com/payloadcms/payload/commit/096d33718d28cb5207027f6737982b29a0ced90d))
|
||||
* greater than equal admin filter not working ([#3306](https://github.com/payloadcms/payload/issues/3306)) ([0bd3353](https://github.com/payloadcms/payload/commit/0bd335303dff71977b46b373fbee859d11c33337))
|
||||
|
||||
## [1.15.5](https://github.com/payloadcms/payload/compare/v1.15.4...v1.15.5) (2023-09-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* corrects hasMany relationships within addFieldStatePromise ([#3300](https://github.com/payloadcms/payload/issues/3300)) ([a7d47c6](https://github.com/payloadcms/payload/commit/a7d47c627d064e92ca541f70caf0ff3d903b2d1d))
|
||||
|
||||
## [1.15.4](https://github.com/payloadcms/payload/compare/v1.15.3...v1.15.4) (2023-09-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **#3274:** sets full user data from fetchFullUser instead of partial jwt data ([#3279](https://github.com/payloadcms/payload/issues/3279)) ([cf12b5f](https://github.com/payloadcms/payload/commit/cf12b5fc703be6341b2b23efebd8aa85e8602567)), closes [#3274](https://github.com/payloadcms/payload/issues/3274)
|
||||
* aligns depth behaviour between local api and admin panel ([#3276](https://github.com/payloadcms/payload/issues/3276)) ([5096c37](https://github.com/payloadcms/payload/commit/5096c378743f4c5eb5f4f2f7e67e5e206cc9da40))
|
||||
* appends versions key to incoming where query ([#3287](https://github.com/payloadcms/payload/issues/3287)) ([9cd5e5a](https://github.com/payloadcms/payload/commit/9cd5e5aefaf0ee2af4f577da9578bb31bf4b0acb))
|
||||
* change scoping of `force` parameter to prevent false negation; ([#3278](https://github.com/payloadcms/payload/issues/3278)) ([429a88a](https://github.com/payloadcms/payload/commit/429a88a5a18e8905c63dfe00b78b3e71d56758fd))
|
||||
|
||||
## [1.15.3](https://github.com/payloadcms/payload/compare/v1.15.2...v1.15.3) (2023-09-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* draft globals always displaying unpublish button ([9bc072c](https://github.com/payloadcms/payload/commit/9bc072ccaf318c61b2c4e2a553604a24ff6a188e))
|
||||
* globals not saving updatedAt and createdAt and version dates correctly ([9fbabc8](https://github.com/payloadcms/payload/commit/9fbabc8fd6a3bea5628bea8d0acc915ddb33bb5c))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* improves query speed for version enabled collections ([16e94d4](https://github.com/payloadcms/payload/commit/16e94d401bd7cb82de53142c5f9a325abd31a81a))
|
||||
|
||||
## [1.15.2](https://github.com/payloadcms/payload/compare/v1.15.1...v1.15.2) (2023-08-25)
|
||||
|
||||
## [1.15.1](https://github.com/payloadcms/payload/compare/v1.15.0...v1.15.1) (2023-08-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* arrays in richtext uploads ([#3222](https://github.com/payloadcms/payload/issues/3222)) ([cb8e07f](https://github.com/payloadcms/payload/commit/cb8e07f85232a26c265872faf408644424312af6))
|
||||
* correct out of order dark-mode color variables ([#3197](https://github.com/payloadcms/payload/issues/3197)) ([3a15e07](https://github.com/payloadcms/payload/commit/3a15e077c6914aba3ef26e453fee23c89f3db829))
|
||||
* mutation type with tabs missing previous tabs ([#3196](https://github.com/payloadcms/payload/issues/3196)) ([6d3b863](https://github.com/payloadcms/payload/commit/6d3b8636f4e14a4e4155279353fa06e86fe2b25c))
|
||||
|
||||
# [1.15.0](https://github.com/payloadcms/payload/compare/v1.14.0...v1.15.0) (2023-08-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* query support for geo within and intersects + dynamic GraphQL operator types ([#3183](https://github.com/payloadcms/payload/issues/3183)) ([739abdc](https://github.com/payloadcms/payload/commit/739abdcd81176b3e812470eeea97b1be0d8c4a27))
|
||||
|
||||
# [1.14.0](https://github.com/payloadcms/payload/compare/v1.13.4...v1.14.0) (2023-08-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* DatePicker showing only selected day by default ([#3169](https://github.com/payloadcms/payload/issues/3169)) ([edcb393](https://github.com/payloadcms/payload/commit/edcb3933cfb4532180c822135ea6a8be928e0fdc))
|
||||
* only allow redirects to /admin sub-routes ([c0f05a1](https://github.com/payloadcms/payload/commit/c0f05a1c38fb9c958de920fabb698b5ecfb661f0))
|
||||
* passes in height to resizeOptions upload option to allow height resize ([#3171](https://github.com/payloadcms/payload/issues/3171)) ([7963d04](https://github.com/payloadcms/payload/commit/7963d04a27888eb5a12d0ab37f2082cd33638abd))
|
||||
* WhereBuilder component does not accept all valid Where queries ([#3087](https://github.com/payloadcms/payload/issues/3087)) ([fdfdfc8](https://github.com/payloadcms/payload/commit/fdfdfc83f36a958971f8e4e4f9f5e51560cb26e0))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add afterOperation hook ([#2697](https://github.com/payloadcms/payload/issues/2697)) ([33686c6](https://github.com/payloadcms/payload/commit/33686c6db8373a16d7f6b0192e0701bf15881aa4))
|
||||
* add support for hotkeys ([#1821](https://github.com/payloadcms/payload/issues/1821)) ([942cfec](https://github.com/payloadcms/payload/commit/942cfec286ff050e13417b037cca64b9d757d868))
|
||||
* Added Azerbaijani language file ([#3164](https://github.com/payloadcms/payload/issues/3164)) ([63e3063](https://github.com/payloadcms/payload/commit/63e3063b9ecc1afd62d7a287a798d41215008f2a))
|
||||
* allow async relationship filter options ([#2951](https://github.com/payloadcms/payload/issues/2951)) ([bad3638](https://github.com/payloadcms/payload/commit/bad363882c9d00d3c73547ca3329eba988e728ff))
|
||||
* Improve admin dashboard accessibility ([#3053](https://github.com/payloadcms/payload/issues/3053)) ([e03a8e6](https://github.com/payloadcms/payload/commit/e03a8e6b030e82a17e1cdae5b4032433cf9c75a4))
|
||||
* improve field ops ([#3172](https://github.com/payloadcms/payload/issues/3172)) ([d91b44c](https://github.com/payloadcms/payload/commit/d91b44cbb3fd526caca2a6f4bd30fd06ede3a5da))
|
||||
* make PAYLOAD_CONFIG_PATH optional ([#2839](https://github.com/payloadcms/payload/issues/2839)) ([5744de7](https://github.com/payloadcms/payload/commit/5744de7ec63e3f17df7e02a7cc827818a79dbbb8))
|
||||
* text alignment for richtext editor ([#2803](https://github.com/payloadcms/payload/issues/2803)) ([a0b13a5](https://github.com/payloadcms/payload/commit/a0b13a5b01fa0d7f4c4dffd1895bfe507e5c676d))
|
||||
|
||||
## [1.13.4](https://github.com/payloadcms/payload/compare/v1.13.3...v1.13.4) (2023-08-11)
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ Payload documentation can be found directly within its codebase and you can feel
|
||||
|
||||
If you're an incredibly awesome person and want to help us make Payload even better through new features or additions, we would be thrilled to work with you.
|
||||
|
||||
## Design Contributions
|
||||
|
||||
When it comes to design-related changes or additions, it's crucial for us to ensure a cohesive user experience and alignment with our broader design vision. Before embarking on any implementation that would affect the design or UI/UX, we ask that you **first share your design proposal** with us for review and approval.
|
||||
|
||||
Our design review ensures that proposed changes fit seamlessly with other components, both existing and planned. This step is meant to prevent unintentional design inconsistencies and to save you from investing time in implementing features that might need significant design alterations later.
|
||||
|
||||
### Before Starting
|
||||
|
||||
To help us work on new features, you can create a new feature request post in [GitHub Discussion](https://github.com/payloadcms/payload/discussions) or discuss it in our [Discord](https://discord.com/invite/payload). New functionality often has large implications across the entire Payload repo, so it is best to discuss the architecture and approach before starting work on a pull request.
|
||||
|
||||
25
README.md
25
README.md
@@ -52,12 +52,21 @@ npx create-payload-app
|
||||
Alternatively, it only takes about five minutes to [create an app from scratch](https://payloadcms.com/docs/getting-started/installation#from-scratch).
|
||||
|
||||
## 🖱️ One-click templates
|
||||
### 🛒 [E-Commerce](https://github.com/payloadcms/payload/tree/master/templates/ecommerce)
|
||||
Eliminate the need to combine Shopify and a CMS, and instead do it all with Payload + Stripe. Best of all, you can extend it as much as you need.
|
||||
|
||||
[All Official Templates](https://github.com/orgs/payloadcms/repositories?q=topic%3Apayload-template) · [Community Templates](https://github.com/topics/payload-template)
|
||||
Jumpstart your next project by starting with a pre-made template. These are production-ready, end-to-end solutions designed to get you to market as fast as possible.
|
||||
|
||||
**If you maintain your own template, consider adding the `payload-template` topic to your GitHub repository for others to find.**
|
||||
### [🛒 E-Commerce](https://github.com/payloadcms/payload/tree/master/templates/ecommerce)
|
||||
|
||||
Eliminate the need to combine Shopify and a CMS, and instead do it all with Payload + Stripe. Comes with a beautiful, fully functional front-end complete with shopping cart, checkout, orders, and much more.
|
||||
|
||||
### [🌐 Website](https://github.com/payloadcms/payload/tree/master/templates/website)
|
||||
|
||||
Build any kind of website, blog, or portfolio from small to enterprise. Comes with a beautiful, fully functional front-end complete with posts, projects, comments, and much more.
|
||||
|
||||
We're constantly adding more templates to our [Templates Directory](https://github.com/payloadcms/payload/tree/master/templates). If you maintain your own template, consider adding the `payload-template` topic to your GitHub repository for others to find.
|
||||
|
||||
- [Official Templates](https://github.com/payloadcms/payload/tree/master/templates)
|
||||
- [Community Templates](https://github.com/topics/payload-template)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
@@ -87,13 +96,15 @@ Check out the [Payload website](https://payloadcms.com/docs/getting-started/what
|
||||
|
||||
## 🙋 Contributing
|
||||
|
||||
If you want to add contributions to this repository, please follow the instructions in [contributing.md](./contributing.md).
|
||||
If you want to add contributions to this repository, please follow the instructions in [contributing.md](./CONTRIBUTING.md).
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
The examples directory is a great resource for learning how to setup Payload in a variety of different ways.
|
||||
The [Examples Directory](./examples) is a great resource for learning how to setup Payload in a variety of different ways, but you can also find great examples in our blog and throughout our social media.
|
||||
|
||||
[Examples Directory](./examples)
|
||||
- [Examples Directory](./examples)
|
||||
- [Payload Blog](https://payloadcms.com/blog)
|
||||
- [Payload YouTube](https://www.youtube.com/@payloadcms)
|
||||
|
||||
## 🔌 Plugins
|
||||
|
||||
|
||||
@@ -31,8 +31,10 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`label`** | Text used as the heading in the Admin panel or an object with keys for each language. Auto-generated from name if not defined. |
|
||||
| **`fields`** \* | Array of field types to correspond to each row of the Array. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`fields`** \* | Array of field types to correspond to each row of the Array. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation)
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
|
||||
@@ -33,7 +33,9 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme
|
||||
| **`name`** * | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`label`** | Text used as the heading in the Admin panel or an object with keys for each language. Auto-generated from name if not defined. |
|
||||
| **`blocks`** * | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation)
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-level hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-level access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
|
||||
@@ -7,9 +7,7 @@ keywords: rich text, fields, config, configuration, documentation, Content Manag
|
||||
---
|
||||
|
||||
<Banner>
|
||||
The Rich Text field is a powerful way to allow editors to write dynamic
|
||||
content. The content is saved as JSON in the database and can be converted
|
||||
into any format, including HTML, that you need.
|
||||
The Rich Text field is a powerful way to allow editors to write dynamic content. The content is saved as JSON in the database and can be converted into any format, including HTML, that you need.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
@@ -22,14 +20,7 @@ keywords: rich text, fields, config, configuration, documentation, Content Manag
|
||||
The Admin component is built on the powerful [`slatejs`](https://docs.slatejs.org/) editor and is meant to be as extensible and customizable as possible.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>
|
||||
Consistent with Payload's goal of making you learn as little of Payload as
|
||||
possible, customizing and using the Rich Text Editor does not involve
|
||||
learning how to develop for a <em>Payload</em> rich text editor.
|
||||
</strong>{" "}
|
||||
Instead, you can invest your time and effort into learning Slate, an
|
||||
open-source tool that will allow you to apply your learnings elsewhere as
|
||||
well.
|
||||
<strong>Consistent with Payload's goal of making you learn as little of Payload as possible, customizing and using the Rich Text Editor does not involve learning how to develop for a <em>Payload</em> rich text editor.</strong> Instead, you can invest your time and effort into learning Slate, an open-source tool that will allow you to apply your learnings elsewhere as well.
|
||||
</Banner>
|
||||
|
||||
### Config
|
||||
@@ -125,13 +116,7 @@ The built-in `relationship` element is a powerful way to reference other Documen
|
||||
Similar to the `relationship` element, the `upload` element is a user-friendly way to reference [Upload-enabled collections](/docs/upload/overview) with a UI specifically designed for media / image-based uploads.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
<br />
|
||||
Collections are automatically allowed to be selected within the Rich Text
|
||||
relationship and upload elements by default. If you want to disable a
|
||||
collection from being able to be referenced in Rich Text fields, set the
|
||||
collection admin options of <strong>enableRichTextLink</strong> and{" "}
|
||||
<strong>enableRichTextRelationship</strong> to false.
|
||||
<strong>Tip:</strong><br />Collections are automatically allowed to be selected within the Rich Text relationship and upload elements by default. If you want to disable a collection from being able to be referenced in Rich Text fields, set the collection admin options of <strong>enableRichTextLink</strong> and <strong>enableRichTextRelationship</strong> to false.
|
||||
</Banner>
|
||||
|
||||
Relationship and Upload elements are populated dynamically into your Rich Text field' content. Within the REST and Local APIs, any present RichText `relationship` or `upload` elements will respect the `depth` option that you pass, and will be populated accordingly. In GraphQL, each `richText` field accepts an argument of `depth` for you to utilize.
|
||||
@@ -307,10 +292,7 @@ const serialize = (children) =>
|
||||
```
|
||||
|
||||
<Banner>
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
The above example is for how to render to JSX, although for plain HTML the
|
||||
pattern is similar. Just remove the JSX and return HTML strings instead!
|
||||
<strong>Note:</strong><br />The above example is for how to render to JSX, although for plain HTML the pattern is similar. Just remove the JSX and return HTML strings instead!
|
||||
</Banner>
|
||||
|
||||
### Built-in SlateJS Plugins
|
||||
|
||||
@@ -10,8 +10,7 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co
|
||||
The Select field provides a dropdown-style interface for choosing options from
|
||||
a predefined list as an enumeration.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/select.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/select-dark.png'
|
||||
alt='Shows a Select field in the Payload admin panel'
|
||||
@@ -99,3 +98,85 @@ export const ExampleCollection: CollectionConfig = {
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Customization
|
||||
|
||||
The Select field UI component can be customized by providing a custom React component to the `components` object in the Base config.
|
||||
|
||||
```ts
|
||||
export const CustomSelectField: Field = {
|
||||
name: 'customSelectField',
|
||||
type: 'select', // or 'text' if you have dynamic options
|
||||
admin: {
|
||||
components: {
|
||||
Field: CustomSelectComponent({
|
||||
options: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can import the existing Select component directly from Payload, then extend and customize it as needed.
|
||||
|
||||
```ts
|
||||
import * as React from 'react';
|
||||
import { SelectInput, useField } from 'payload/components/forms';
|
||||
import { useAuth } from 'payload/components/utilities';
|
||||
|
||||
type customSelectProps = {
|
||||
path: string;
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const CustomSelectComponent: React.FC<CustomSelectProps> = ({ path, options }) => {
|
||||
const { value, setValue } = useField<string>({ path });
|
||||
const { user } = useAuth();
|
||||
|
||||
const adjustedOptions = options.filter((option) => {
|
||||
/*
|
||||
A common use case for a custom select
|
||||
is to show different options based on
|
||||
the current user's role.
|
||||
*/
|
||||
return option;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="field-label">
|
||||
Custom Select
|
||||
</label>
|
||||
<SelectInput
|
||||
path={path}
|
||||
name={path}
|
||||
options={adjustedOptions}
|
||||
value={value}
|
||||
onChange={() => setValue(e.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
If you are looking to create a dynamic select field, the following tutorial will walk you through the process of creating a custom select field that fetches its options from an external API.
|
||||
|
||||
<VideoDrawer
|
||||
id='Efn9OxSjA6Y'
|
||||
label='How to Create a Custom Select Field'
|
||||
drawerTitle='How to Create a Custom Select Field: A Step-by-Step Guide'
|
||||
/>
|
||||
|
||||
If you want to learn more about custom components check out the [Admin > Custom Component](/docs/admin/components#field-component) docs.
|
||||
|
||||
@@ -16,6 +16,7 @@ Collections feature the ability to define the following hooks:
|
||||
- [afterRead](#afterread)
|
||||
- [beforeDelete](#beforedelete)
|
||||
- [afterDelete](#afterdelete)
|
||||
- [afterOperation](#afteroperation)
|
||||
|
||||
Additionally, `auth`-enabled collections feature the following hooks:
|
||||
|
||||
@@ -31,6 +32,7 @@ Additionally, `auth`-enabled collections feature the following hooks:
|
||||
All collection Hook properties accept arrays of synchronous or asynchronous functions. Each Hook type receives specific arguments and has the ability to modify specific outputs.
|
||||
|
||||
`collections/exampleHooks.js`
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload/types';
|
||||
|
||||
@@ -48,6 +50,7 @@ export const ExampleHooks: CollectionConfig = {
|
||||
afterChange: [(args) => {...}],
|
||||
afterRead: [(args) => {...}],
|
||||
afterDelete: [(args) => {...}],
|
||||
afterOperation: [(args) => {...}],
|
||||
|
||||
// Auth-enabled hooks
|
||||
beforeLogin: [(args) => {...}],
|
||||
@@ -62,19 +65,19 @@ export const ExampleHooks: CollectionConfig = {
|
||||
|
||||
### beforeOperation
|
||||
|
||||
The `beforeOperation` Hook type can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins.
|
||||
The `beforeOperation` hook can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins.
|
||||
|
||||
Available Collection operations include `create`, `read`, `update`, `delete`, `login`, `refresh` and `forgotPassword`.
|
||||
Available Collection operations include `create`, `read`, `update`, `delete`, `login`, `refresh`, and `forgotPassword`.
|
||||
|
||||
```ts
|
||||
import { CollectionBeforeOperationHook } from 'payload/types';
|
||||
import { CollectionBeforeOperationHook } from "payload/types";
|
||||
|
||||
const beforeOperationHook: CollectionBeforeOperationHook = async ({
|
||||
args, // Original arguments passed into the operation
|
||||
args, // original arguments passed into the operation
|
||||
operation, // name of the operation
|
||||
}) => {
|
||||
return args; // Return operation arguments as necessary
|
||||
}
|
||||
return args; // return modified operation arguments as necessary
|
||||
};
|
||||
```
|
||||
|
||||
### beforeValidate
|
||||
@@ -88,7 +91,7 @@ Please do note that this does not run before the client-side validation. If you
|
||||
3. `validate` runs on the server
|
||||
|
||||
```ts
|
||||
import { CollectionBeforeOperationHook } from 'payload/types';
|
||||
import { CollectionBeforeOperationHook } from "payload/types";
|
||||
|
||||
const beforeValidateHook: CollectionBeforeValidateHook = async ({
|
||||
data, // incoming data to update or create with
|
||||
@@ -97,7 +100,7 @@ const beforeValidateHook: CollectionBeforeValidateHook = async ({
|
||||
originalDoc, // original document
|
||||
}) => {
|
||||
return data; // Return data to either create or update a document with
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### beforeChange
|
||||
@@ -105,7 +108,7 @@ const beforeValidateHook: CollectionBeforeValidateHook = async ({
|
||||
Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage, you can be confident that the data that will be saved to the document is valid in accordance to your field validations. You can optionally modify the shape of data to be saved.
|
||||
|
||||
```ts
|
||||
import { CollectionBeforeChangeHook } from 'payload/types';
|
||||
import { CollectionBeforeChangeHook } from "payload/types";
|
||||
|
||||
const beforeChangeHook: CollectionBeforeChangeHook = async ({
|
||||
data, // incoming data to update or create with
|
||||
@@ -114,7 +117,7 @@ const beforeChangeHook: CollectionBeforeChangeHook = async ({
|
||||
originalDoc, // original document
|
||||
}) => {
|
||||
return data; // Return data to either create or update a document with
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### afterChange
|
||||
@@ -122,7 +125,7 @@ const beforeChangeHook: CollectionBeforeChangeHook = async ({
|
||||
After a document is created or updated, the `afterChange` hook runs. This hook is helpful to recalculate statistics such as total sales within a global, syncing user profile changes to a CRM, and more.
|
||||
|
||||
```ts
|
||||
import { CollectionAfterChangeHook } from 'payload/types';
|
||||
import { CollectionAfterChangeHook } from "payload/types";
|
||||
|
||||
const afterChangeHook: CollectionAfterChangeHook = async ({
|
||||
doc, // full document data
|
||||
@@ -130,8 +133,8 @@ const afterChangeHook: CollectionAfterChangeHook = async ({
|
||||
previousDoc, // document data before updating the collection
|
||||
operation, // name of the operation ie. 'create', 'update'
|
||||
}) => {
|
||||
return doc;
|
||||
}
|
||||
return doc; // value to be used in subsequent afterChange hooks
|
||||
};
|
||||
```
|
||||
|
||||
### beforeRead
|
||||
@@ -139,7 +142,7 @@ const afterChangeHook: CollectionAfterChangeHook = async ({
|
||||
Runs before `find` and `findByID` operations are transformed for output by `afterRead`. This hook fires before hidden fields are removed and before localized fields are flattened into the requested locale. Using this Hook will provide you with all locales and all hidden fields via the `doc` argument.
|
||||
|
||||
```ts
|
||||
import { CollectionBeforeReadHook } from 'payload/types';
|
||||
import { CollectionBeforeReadHook } from "payload/types";
|
||||
|
||||
const beforeReadHook: CollectionBeforeReadHook = async ({
|
||||
doc, // full document data
|
||||
@@ -147,7 +150,7 @@ const beforeReadHook: CollectionBeforeReadHook = async ({
|
||||
query, // JSON formatted query
|
||||
}) => {
|
||||
return doc;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### afterRead
|
||||
@@ -155,7 +158,7 @@ const beforeReadHook: CollectionBeforeReadHook = async ({
|
||||
Runs as the last step before documents are returned. Flattens locales, hides protected fields, and removes fields that users do not have access to.
|
||||
|
||||
```ts
|
||||
import { CollectionAfterReadHook } from 'payload/types';
|
||||
import { CollectionAfterReadHook } from "payload/types";
|
||||
|
||||
const afterReadHook: CollectionAfterReadHook = async ({
|
||||
doc, // full document data
|
||||
@@ -164,7 +167,7 @@ const afterReadHook: CollectionAfterReadHook = async ({
|
||||
findMany, // boolean to denote if this hook is running against finding one, or finding many
|
||||
}) => {
|
||||
return doc;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### beforeDelete
|
||||
@@ -194,19 +197,37 @@ const afterDeleteHook: CollectionAfterDeleteHook = async ({
|
||||
}) => {...}
|
||||
```
|
||||
|
||||
### afterOperation
|
||||
|
||||
The `afterOperation` hook can be used to modify the result of operations or execute side-effects that run after an operation has completed.
|
||||
|
||||
Available Collection operations include `create`, `find`, `findByID`, `update`, `updateByID`, `delete`, `deleteByID`, `login`, `refresh`, and `forgotPassword`.
|
||||
|
||||
```ts
|
||||
import { CollectionAfterOperationHook } from "payload/types";
|
||||
|
||||
const afterOperationHook: CollectionAfterOperationHook = async ({
|
||||
args, // arguments passed into the operation
|
||||
operation, // name of the operation
|
||||
result, // the result of the operation, before modifications
|
||||
}) => {
|
||||
return result; // return modified result as necessary
|
||||
};
|
||||
```
|
||||
|
||||
### beforeLogin
|
||||
|
||||
For auth-enabled Collections, this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added to the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation.
|
||||
|
||||
```ts
|
||||
import { CollectionBeforeLoginHook } from 'payload/types';
|
||||
import { CollectionBeforeLoginHook } from "payload/types";
|
||||
|
||||
const beforeLoginHook: CollectionBeforeLoginHook = async ({
|
||||
req, // full express request
|
||||
user, // user being logged in
|
||||
}) => {
|
||||
return user;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### afterLogin
|
||||
@@ -267,7 +288,7 @@ const afterMeHook: CollectionAfterMeHook = async ({
|
||||
For auth-enabled Collections, this hook runs after successful `forgotPassword` operations. Returned values are discarded.
|
||||
|
||||
```ts
|
||||
import { CollectionAfterForgotPasswordHook } from 'payload/types';
|
||||
import { CollectionAfterForgotPasswordHook } from "payload/types";
|
||||
|
||||
const afterLoginHook: CollectionAfterForgotPasswordHook = async ({
|
||||
req, // full express request
|
||||
@@ -275,7 +296,7 @@ const afterLoginHook: CollectionAfterForgotPasswordHook = async ({
|
||||
token, // user token
|
||||
}) => {
|
||||
return user;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
@@ -298,5 +319,5 @@ import type {
|
||||
CollectionAfterRefreshHook,
|
||||
CollectionAfterMeHook,
|
||||
CollectionAfterForgotPasswordHook,
|
||||
} from 'payload/types';
|
||||
} from "payload/types";
|
||||
```
|
||||
|
||||
@@ -4082,9 +4082,9 @@ graphql-type-json@^0.3.2:
|
||||
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
|
||||
|
||||
graphql@^16.6.0:
|
||||
version "16.7.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642"
|
||||
integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==
|
||||
version "16.8.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -6256,9 +6256,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.2.15, postcss@^8.4.21, postcss@^8.4.24:
|
||||
version "8.4.27"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
|
||||
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
@@ -2,5 +2,5 @@ MONGODB_URI=mongodb://127.0.0.1/payload-example-custom-server
|
||||
PAYLOAD_SECRET=PAYLOAD_CUSTOM_SERVER_EXAMPLE_SECRET_KEY
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
PAYLOAD_SEED=true
|
||||
PAYLOAD_PUBLIC_SEED=true
|
||||
PAYLOAD_DROP_DATABASE=true
|
||||
|
||||
@@ -10,7 +10,7 @@ To spin up this example locally, follow these steps:
|
||||
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
|
||||
1. Next `yarn && yarn dev`
|
||||
1. Now `open http://localhost:3000/admin` to access the admin panel
|
||||
1. Login with email `dev@payloadcms.com` and password `test`
|
||||
1. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -94,7 +94,7 @@ To spin up this example locally, follow the [Quick Start](#quick-start).
|
||||
|
||||
### Seed
|
||||
|
||||
On boot, a seed script is included to scaffold a basic database for you to use as an example. This is done by setting the `PAYLOAD_DROP_DATABASE` and `PAYLOAD_SEED` environment variables which are included in the `.env.example` by default. You can remove these from your `.env` to prevent this behavior. You can also freshly seed your project at any time by running `yarn seed`. This seed creates an admin user with email `dev@payloadcms.com`, password `test`, and a `home` page.
|
||||
On boot, a seed script is included to scaffold a basic database for you to use as an example. This is done by setting the `PAYLOAD_DROP_DATABASE` and `PAYLOAD_PUBLIC_SEED` environment variables which are included in the `.env.example` by default. You can remove these from your `.env` to prevent this behavior. You can also freshly seed your project at any time by running `yarn seed`. This seed creates an admin user with email `demo@payloadcms.com`, password `demo`, and a `home` page.
|
||||
|
||||
> NOTICE: seeding the database is destructive because it drops your current database to populate a fresh one from the seed template. Only run this command if you are starting a new project or can afford to lose your current data.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"seed": "rm -rf media && cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts ts-node src/server.ts",
|
||||
"seed": "rm -rf media && cross-env PAYLOAD_PUBLIC_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts ts-node src/server.ts",
|
||||
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc --project tsconfig.server.json",
|
||||
"build:next": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NEXT_BUILD=true node dist/server.js",
|
||||
@@ -52,4 +52,4 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
examples/custom-server/src/app/api/test-get/route.ts
Normal file
5
examples/custom-server/src/app/api/test-get/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
5
examples/custom-server/src/app/api/test-post/route.ts
Normal file
5
examples/custom-server/src/app/api/test-post/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function POST(): Promise<NextResponse> {
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
import { getPayloadClient } from '../getPayload'
|
||||
import { Page } from './../payload-types'
|
||||
import { Gutter } from './_components/Gutter'
|
||||
import { RichText } from './_components/RichText'
|
||||
@@ -8,11 +9,17 @@ import { RichText } from './_components/RichText'
|
||||
import classes from './page.module.scss'
|
||||
|
||||
export default async function Home() {
|
||||
const home: Page = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/pages?where[slug][equals]=home`,
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then(res => res?.docs?.[0])
|
||||
const payload = await getPayloadClient()
|
||||
const { docs } = await payload.find({
|
||||
collection: 'pages',
|
||||
where: {
|
||||
slug: {
|
||||
equals: 'home',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const home = docs?.[0] as Page
|
||||
|
||||
if (!home) {
|
||||
return notFound()
|
||||
|
||||
17
examples/custom-server/src/components/BeforeLogin/index.tsx
Normal file
17
examples/custom-server/src/components/BeforeLogin/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react'
|
||||
|
||||
const BeforeLogin: React.FC = () => {
|
||||
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
|
||||
return (
|
||||
<p>
|
||||
{'Log in with the email '}
|
||||
<strong>demo@payloadcms.com</strong>
|
||||
{' and the password '}
|
||||
<strong>demo</strong>.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default BeforeLogin
|
||||
59
examples/custom-server/src/getPayload.ts
Normal file
59
examples/custom-server/src/getPayload.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
import type { Payload } from 'payload'
|
||||
import payload from 'payload'
|
||||
import type { InitOptions } from 'payload/config'
|
||||
|
||||
import { seed as seedData } from './seed'
|
||||
|
||||
dotenv.config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
})
|
||||
|
||||
let cached = (global as any).payload
|
||||
|
||||
if (!cached) {
|
||||
cached = (global as any).payload = { client: null, promise: null }
|
||||
}
|
||||
|
||||
interface Args {
|
||||
initOptions?: Partial<InitOptions>
|
||||
seed?: boolean
|
||||
}
|
||||
|
||||
export const getPayloadClient = async ({ initOptions, seed }: Args = {}): Promise<Payload> => {
|
||||
if (!process.env.MONGODB_URI) {
|
||||
throw new Error('MONGODB_URI environment variable is missing')
|
||||
}
|
||||
|
||||
if (!process.env.PAYLOAD_SECRET) {
|
||||
throw new Error('PAYLOAD_SECRET environment variable is missing')
|
||||
}
|
||||
|
||||
if (cached.client) {
|
||||
return cached.client
|
||||
}
|
||||
|
||||
if (!cached.promise) {
|
||||
cached.promise = payload.init({
|
||||
mongoURL: process.env.MONGODB_URI,
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
local: initOptions?.express ? false : true,
|
||||
...(initOptions || {}),
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
process.env.PAYLOAD_DROP_DATABASE = seed ? 'true' : 'false'
|
||||
cached.client = await cached.promise
|
||||
|
||||
if (seed) {
|
||||
await seedData(payload)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
cached.promise = null
|
||||
throw e
|
||||
}
|
||||
|
||||
return cached.client
|
||||
}
|
||||
@@ -8,10 +8,16 @@ dotenv.config({
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
import { Pages } from './collections/Pages'
|
||||
import BeforeLogin from './components/BeforeLogin'
|
||||
|
||||
export default buildConfig({
|
||||
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL || '',
|
||||
collections: [Pages],
|
||||
admin: {
|
||||
components: {
|
||||
beforeLogin: [BeforeLogin],
|
||||
},
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
},
|
||||
|
||||
@@ -5,8 +5,8 @@ export const seed = async (payload: Payload): Promise<void> => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
email: 'demo@payloadcms.com',
|
||||
password: 'demo',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -10,33 +10,23 @@ dotenv.config({
|
||||
})
|
||||
|
||||
import express from 'express'
|
||||
import payload from 'payload'
|
||||
|
||||
import { seed } from './seed'
|
||||
import { getPayloadClient } from './getPayload'
|
||||
|
||||
const app = express()
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
// Redirect root to the admin panel
|
||||
app.get('/', (_, res) => {
|
||||
res.redirect('/admin')
|
||||
})
|
||||
|
||||
const start = async (): Promise<void> => {
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
mongoURL: process.env.MONGODB_URI || '',
|
||||
express: app,
|
||||
onInit: () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
|
||||
const payload = await getPayloadClient({
|
||||
initOptions: {
|
||||
express: app,
|
||||
onInit: async newPayload => {
|
||||
newPayload.logger.info(`Payload Admin URL: ${newPayload.getAdminURL()}`)
|
||||
},
|
||||
},
|
||||
seed: process.env.PAYLOAD_PUBLIC_SEED === 'true',
|
||||
})
|
||||
|
||||
if (process.env.PAYLOAD_SEED === 'true') {
|
||||
payload.logger.info('---- SEEDING DATABASE ----')
|
||||
await seed(payload)
|
||||
}
|
||||
|
||||
app.listen(PORT, async () => {
|
||||
payload.logger.info(`App URL: ${process.env.PAYLOAD_PUBLIC_SERVER_URL}`)
|
||||
})
|
||||
|
||||
@@ -8,28 +8,23 @@ dotenv.config({
|
||||
})
|
||||
|
||||
import express from 'express'
|
||||
import payload from 'payload'
|
||||
|
||||
import { seed } from './seed'
|
||||
import { getPayloadClient } from './getPayload'
|
||||
|
||||
const app = express()
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
const start = async (): Promise<void> => {
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
mongoURL: process.env.MONGODB_URI || '',
|
||||
express: app,
|
||||
onInit: () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
|
||||
const payload = await getPayloadClient({
|
||||
initOptions: {
|
||||
express: app,
|
||||
onInit: async newPayload => {
|
||||
newPayload.logger.info(`Payload Admin URL: ${newPayload.getAdminURL()}`)
|
||||
},
|
||||
},
|
||||
seed: process.env.PAYLOAD_PUBLIC_SEED === 'true',
|
||||
})
|
||||
|
||||
if (process.env.PAYLOAD_SEED === 'true') {
|
||||
payload.logger.info('---- SEEDING DATABASE ----')
|
||||
await seed(payload)
|
||||
}
|
||||
|
||||
if (process.env.NEXT_BUILD) {
|
||||
app.listen(PORT, async () => {
|
||||
payload.logger.info(`Next.js is now building...`)
|
||||
@@ -47,7 +42,7 @@ const start = async (): Promise<void> => {
|
||||
|
||||
const nextHandler = nextApp.getRequestHandler()
|
||||
|
||||
app.get('*', (req, res) => nextHandler(req, res))
|
||||
app.use((req, res) => nextHandler(req, res))
|
||||
|
||||
nextApp.prepare().then(() => {
|
||||
payload.logger.info('Next.js started')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# Payload Draft Preview Example Front-End
|
||||
|
||||
This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app). It was made explicitly for Payload's [Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview).
|
||||
This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextjs.org/docs/app). It was made explicitly for Payload's [Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/payload).
|
||||
|
||||
> This example uses the App Router, the latest API of Next.js. If your app is using the legacy [Pages Router](https://nextjs.org/docs/pages), check out the official [Pages Router Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/next-pages).
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ export const fetchPage = async (
|
||||
draft && payloadToken ? '&draft=true' : ''
|
||||
}`,
|
||||
{
|
||||
method: 'GET',
|
||||
// this is the key we'll use to on-demand revalidate pages that use this data
|
||||
// we do this by calling `revalidateTag()` using the same key
|
||||
// see `app/api/revalidate.ts` for more info
|
||||
next: { tags: [`pages_${slug}`] },
|
||||
...(draft && payloadToken
|
||||
? {
|
||||
headers: {
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { revalidatePath, revalidateTag } from 'next/cache'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
// this endpoint will revalidate a page by tag or path
|
||||
// this is to achieve on-demand revalidation of pages that use this data
|
||||
// send either `collection` and `slug` or `revalidatePath` as query params
|
||||
export async function GET(request: NextRequest): Promise<unknown> {
|
||||
const path = request.nextUrl.searchParams.get('revalidatePath')
|
||||
const collection = request.nextUrl.searchParams.get('collection')
|
||||
const slug = request.nextUrl.searchParams.get('slug')
|
||||
const path = request.nextUrl.searchParams.get('path')
|
||||
const secret = request.nextUrl.searchParams.get('secret')
|
||||
|
||||
if (secret !== process.env.NEXT_PRIVATE_REVALIDATION_KEY) {
|
||||
return NextResponse.json({ revalidated: false, now: Date.now() })
|
||||
}
|
||||
|
||||
if (typeof collection === 'string' && typeof slug === 'string') {
|
||||
revalidateTag(`${collection}_${slug}`)
|
||||
return NextResponse.json({ revalidated: true, now: Date.now() })
|
||||
}
|
||||
|
||||
// there is a known limitation with `revalidatePath` where it will not revalidate exact paths of dynamic routes
|
||||
// instead, Next.js expects us to revalidate entire directories, i.e. `revalidatePath('/[slug]')` instead of `/example-page`
|
||||
// for this reason, it is preferred to use `revalidateTag` instead of `revalidatePath`
|
||||
// - https://github.com/vercel/next.js/issues/49387
|
||||
// - https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830
|
||||
if (typeof path === 'string') {
|
||||
// there is a known bug with `revalidatePath` where it will not revalidate exact paths of dynamic routes
|
||||
// instead, Next.js expects us to revalidate entire directories, i.e. `/[slug]` instead of `/example-page`
|
||||
// for now we'll make this change but with expectation that it will be fixed so we can use `revalidatePath('/example-page')`
|
||||
// - https://github.com/vercel/next.js/issues/49387
|
||||
// - https://github.com/vercel/next.js/issues/49778#issuecomment-1547028830
|
||||
// revalidatePath(path)
|
||||
revalidatePath('/[slug]')
|
||||
revalidatePath(path)
|
||||
return NextResponse.json({ revalidated: true, now: Date.now() })
|
||||
}
|
||||
|
||||
|
||||
@@ -2107,14 +2107,14 @@ scheduler@^0.23.0:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
semver@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
|
||||
semver@^7.3.7:
|
||||
version "7.5.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e"
|
||||
integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Payload Draft Preview Example Front-End
|
||||
|
||||
This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages). It was made explicitly for Payload's [Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview).
|
||||
This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nextjs.org/docs/pages). It was made explicitly for Payload's [Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/payload).
|
||||
|
||||
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/next-app).
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ const revalidate = async (req: NextApiRequest, res: NextApiResponse): Promise<vo
|
||||
return res.status(401).json({ message: 'Invalid token' })
|
||||
}
|
||||
|
||||
if (typeof req.query.revalidatePath === 'string') {
|
||||
if (typeof req.query.path === 'string') {
|
||||
try {
|
||||
await res.revalidate(req.query.revalidatePath)
|
||||
await res.revalidate(req.query.path)
|
||||
return res.json({ revalidated: true })
|
||||
} catch (err: unknown) {
|
||||
// If there was an error, Next.js will continue
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Payload Draft Preview Example
|
||||
|
||||
The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview) demonstrates how to implement draft preview in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). Draft preview allows you to see content on your front-end before it is published. There are various fully working front-ends made explicitly for this example, including:
|
||||
The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/master/examples/draft-preview/payload) demonstrates how to implement draft preview in [Payload](https://github.com/payloadcms/payload) using [Versions](https://payloadcms.com/docs/versions/overview) and [Drafts](https://payloadcms.com/docs/versions/drafts). Draft preview allows you to see content on your front-end before it is published. There are various fully working front-ends made explicitly for this example, including:
|
||||
|
||||
- [Next.js App Router](../next-app)
|
||||
- [Next.js Pages Router](../next-pages)
|
||||
|
||||
@@ -7,9 +7,11 @@ export const formatAppURL = ({ doc }): string => {
|
||||
return pathname
|
||||
}
|
||||
|
||||
// Revalidate the page in the background, so the user doesn't have to wait
|
||||
// Notice that the hook itself is not async and we are not awaiting `revalidate`
|
||||
// Only revalidate existing docs that are published
|
||||
// revalidate the page in the background, so the user doesn't have to wait
|
||||
// notice that the hook itself is not async and we are not awaiting `revalidate`
|
||||
// only revalidate existing docs that are published (not drafts)
|
||||
// send `revalidatePath`, `collection`, and `slug` to the frontend to use in its revalidate route
|
||||
// frameworks may have different ways of doing this, but the idea is the same
|
||||
export const revalidatePage: AfterChangeHook = ({ doc, req, operation }) => {
|
||||
if (operation === 'update' && doc._status === 'published') {
|
||||
const url = formatAppURL({ doc })
|
||||
@@ -17,7 +19,7 @@ export const revalidatePage: AfterChangeHook = ({ doc, req, operation }) => {
|
||||
const revalidate = async (): Promise<void> => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&revalidatePath=${url}`,
|
||||
`${process.env.PAYLOAD_PUBLIC_SITE_URL}/api/revalidate?secret=${process.env.REVALIDATION_KEY}&collection=pages&slug=${doc?.slug}&path=${url}`,
|
||||
)
|
||||
|
||||
if (res.ok) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const Pages: CollectionConfig = {
|
||||
formatAppURL({
|
||||
doc,
|
||||
}),
|
||||
)}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}`
|
||||
)}&collection=pages&slug=${doc.slug}&secret=${process.env.PAYLOAD_PUBLIC_DRAFT_SECRET}`
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const examplePageDraft: Partial<Page> = {
|
||||
title: 'Example Page (Draft)',
|
||||
richText: [
|
||||
{
|
||||
children: [
|
||||
|
||||
@@ -4082,9 +4082,9 @@ graphql-type-json@^0.3.2:
|
||||
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
|
||||
|
||||
graphql@^16.6.0:
|
||||
version "16.7.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642"
|
||||
integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==
|
||||
version "16.8.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -6256,9 +6256,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.2.15, postcss@^8.4.21, postcss@^8.4.24:
|
||||
version "8.4.27"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
|
||||
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
@@ -3933,9 +3933,9 @@ graphql-type-json@^0.3.2:
|
||||
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
|
||||
|
||||
graphql@^16.6.0:
|
||||
version "16.7.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642"
|
||||
integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==
|
||||
version "16.8.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -6215,9 +6215,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.2.15, postcss@^8.4.21, postcss@^8.4.24:
|
||||
version "8.4.27"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
|
||||
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
@@ -3510,9 +3510,9 @@ graphql-type-json@^0.3.2:
|
||||
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
|
||||
|
||||
graphql@^16.6.0:
|
||||
version "16.7.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642"
|
||||
integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==
|
||||
version "16.8.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -5547,9 +5547,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.2.15, postcss@^8.4.21, postcss@^8.4.24:
|
||||
version "8.4.27"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
|
||||
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
@@ -17,7 +17,7 @@ export const Country: React.FC<CountryField & {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.select}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<Controller
|
||||
@@ -33,6 +33,7 @@ export const Country: React.FC<CountryField & {
|
||||
onChange={(val) => onChange(val.value)}
|
||||
className={classes.reactSelect}
|
||||
classNamePrefix="rs"
|
||||
inputId={name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -15,13 +15,14 @@ export const Email: React.FC<EmailField & {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.wrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
className={classes.input}
|
||||
id={name}
|
||||
{...register(name, { required: requiredFromProps, pattern: /^\S+@\S+$/i })}
|
||||
/>
|
||||
{requiredFromProps && errors[name] && <Error />}
|
||||
|
||||
@@ -15,12 +15,13 @@ export const Number: React.FC<TextField & {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.wrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className={classes.input}
|
||||
id={name}
|
||||
{...register(name, { required: requiredFromProps })}
|
||||
/>
|
||||
{requiredFromProps && errors[name] && <Error />}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const Select: React.FC<SelectField & {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.select}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<Controller
|
||||
@@ -32,6 +32,7 @@ export const Select: React.FC<SelectField & {
|
||||
onChange={(val) => onChange(val.value)}
|
||||
className={classes.reactSelect}
|
||||
classNamePrefix="rs"
|
||||
inputId={name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const State: React.FC<StateField & {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.select}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<Controller
|
||||
@@ -34,6 +34,7 @@ export const State: React.FC<StateField & {
|
||||
onChange={(val) => onChange(val.value)}
|
||||
className={classes.reactSelect}
|
||||
classNamePrefix="rs"
|
||||
inputId={name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -16,12 +16,13 @@ export const Text: React.FC<TextField & {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.wrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={classes.input}
|
||||
id={name}
|
||||
{...register(name, { required: requiredFromProps })}
|
||||
/>
|
||||
{requiredFromProps && errors[name] && <Error />}
|
||||
|
||||
@@ -16,12 +16,13 @@ export const Textarea: React.FC<TextField & {
|
||||
return (
|
||||
<Width width={width}>
|
||||
<div className={classes.wrap}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
<label htmlFor={name} className={classes.label}>
|
||||
{label}
|
||||
</label>
|
||||
<textarea
|
||||
rows={rows}
|
||||
className={classes.textarea}
|
||||
id={name}
|
||||
{...register(name, { required: requiredFromProps })}
|
||||
/>
|
||||
{requiredFromProps && errors[name] && <Error />}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@faceless-ui/css-grid": "^1.2.0",
|
||||
"@faceless-ui/modal": "^2.0.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql": "^16.8.1",
|
||||
"next": "12.3.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
|
||||
@@ -1345,10 +1345,10 @@ graphql-tag@^2.12.6:
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
graphql@^16.6.0:
|
||||
version "16.6.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb"
|
||||
integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==
|
||||
graphql@^16.8.1:
|
||||
version "16.8.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -2467,9 +2467,9 @@ which@^2.0.1:
|
||||
isexe "^2.0.0"
|
||||
|
||||
word-wrap@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
|
||||
@@ -4082,9 +4082,9 @@ graphql-type-json@^0.3.2:
|
||||
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
|
||||
|
||||
graphql@^16.6.0:
|
||||
version "16.7.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642"
|
||||
integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==
|
||||
version "16.8.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -6256,9 +6256,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.2.15, postcss@^8.4.21, postcss@^8.4.24:
|
||||
version "8.4.27"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
|
||||
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
@@ -4087,9 +4087,9 @@ graphql-type-json@^0.3.2:
|
||||
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
|
||||
|
||||
graphql@^16.6.0:
|
||||
version "16.7.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642"
|
||||
integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==
|
||||
version "16.8.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -6261,9 +6261,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.2.15, postcss@^8.4.21, postcss@^8.4.24:
|
||||
version "8.4.27"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
|
||||
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
@@ -1596,9 +1596,9 @@ scheduler@^0.23.0:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
semver@^7.3.7:
|
||||
version "7.3.8"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
|
||||
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
|
||||
version "7.5.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
|
||||
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
@@ -1814,9 +1814,9 @@ which@^2.0.1:
|
||||
isexe "^2.0.0"
|
||||
|
||||
word-wrap@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
|
||||
5
examples/testing/.env.example
Normal file
5
examples/testing/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
PORT=3000
|
||||
MONGO_URL=mongodb://127.0.0.1/your-project-name
|
||||
PAYLOAD_SECRET_KEY=alwifhjoq284jgo5w34jgo43f3
|
||||
PAYLOAD_CONFIG_PATH=src/payload.config.ts
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
142
examples/testing/README.md
Normal file
142
examples/testing/README.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Payload Testing Example
|
||||
|
||||
This example demonstrates how to get started with testing Payload using [Jest](https://jestjs.io/). You can clone this down and use it as a starting point for your own Payload projects, or you can follow the steps below to add testing to your existing Payload project.
|
||||
|
||||
## Add testing to your existing Payload project
|
||||
|
||||
1. Initial setup:
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
yarn add --dev jest mongodb-memory-server @swc/jest @swc/core isomorphic-fetch @types/jest
|
||||
```
|
||||
|
||||
```bash
|
||||
# create a .env file
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. This example uses the following folder structure:
|
||||
```
|
||||
root
|
||||
└─ /src
|
||||
└─ payload.config.ts
|
||||
└─ /tests
|
||||
```
|
||||
|
||||
3. Add test credentials to your project. Create a file at `src/tests/credentials.ts` with the following contents:
|
||||
|
||||
```ts
|
||||
export default {
|
||||
email: 'test@test.com',
|
||||
password: 'test',
|
||||
};
|
||||
```
|
||||
|
||||
4. Add the global setup file to your project. This file will be run before any tests are run. It will start a MongoDB server and create a Payload instance for you to use in your tests. Create a file at `src/tests/globalSetup.ts` with the following contents:
|
||||
|
||||
```ts
|
||||
import { resolve } from 'path';
|
||||
import payload from 'payload';
|
||||
import express from 'express';
|
||||
import testCredentials from './credentials';
|
||||
|
||||
require('dotenv').config({
|
||||
path: resolve(__dirname, '../../.env'),
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
const globalSetup = async () => {
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET_KEY,
|
||||
mongoURL: process.env.MONGO_URL,
|
||||
express: app,
|
||||
});
|
||||
|
||||
app.listen(process.env.PORT, async () => {
|
||||
console.log(`Express is now listening for incoming connections on port ${process.env.PORT}.`);
|
||||
});
|
||||
|
||||
const response = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/users/first-register`, {
|
||||
body: JSON.stringify({
|
||||
email: testCredentials.email,
|
||||
password: testCredentials.password,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.user || !data.user.token) {
|
||||
throw new Error('Failed to register first user');
|
||||
}
|
||||
};
|
||||
|
||||
export default globalSetup;
|
||||
```
|
||||
|
||||
5. Add a `jest.config.ts` file to the root of your project:
|
||||
|
||||
```ts
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
globalSetup: '<rootDir>/src/tests/globalSetup.ts',
|
||||
roots: ['<rootDir>/src/'],
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': [
|
||||
'@swc/jest',
|
||||
{
|
||||
jsc: {
|
||||
target: 'es2021',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
6. Write your first test. Create a file at `src/tests/login.spec.ts` with the following contents:
|
||||
|
||||
```ts
|
||||
import { User } from '../payload-types';
|
||||
import testCredentials from './credentials';
|
||||
|
||||
describe('Users', () => {
|
||||
it('should allow a user to log in', async () => {
|
||||
const result: {
|
||||
token: string
|
||||
user: User
|
||||
} = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/users/login`, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: testCredentials.email,
|
||||
password: testCredentials.password,
|
||||
}),
|
||||
}).then((res) => res.json());
|
||||
|
||||
expect(result.token).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
7. Add a script to run tests via the command line. Add the following to your `package.json` scripts:
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit --detectOpenHandles"
|
||||
}
|
||||
```
|
||||
|
||||
8. Run your tests:
|
||||
|
||||
```bash
|
||||
yarn test
|
||||
```
|
||||
16
examples/testing/jest.config.ts
Normal file
16
examples/testing/jest.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
globalSetup: '<rootDir>/src/tests/globalSetup.ts',
|
||||
roots: ['<rootDir>/src/'],
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': [
|
||||
'@swc/jest',
|
||||
{
|
||||
jsc: {
|
||||
target: 'es2021',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
6
examples/testing/nodemon.json
Normal file
6
examples/testing/nodemon.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"watch": [
|
||||
"./src/**/*.ts"
|
||||
],
|
||||
"exec": "ts-node ./src/server.ts"
|
||||
}
|
||||
32
examples/testing/package.json
Normal file
32
examples/testing/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "jest-payload",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"get-tsconfig": "^4.7.0",
|
||||
"payload": "^1.15.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.3.84",
|
||||
"@swc/jest": "^0.2.29",
|
||||
"@types/jest": "^29.5.4",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"mongodb-memory-server": "^8.15.1",
|
||||
"nodemon": "^3.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"generate:types": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"dev": "nodemon",
|
||||
"build:payload": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "yarn build:server && yarn build:payload",
|
||||
"serve": "PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit --detectOpenHandles"
|
||||
}
|
||||
}
|
||||
35
examples/testing/src/payload-types.ts
Normal file
35
examples/testing/src/payload-types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
collections: {
|
||||
posts: Post;
|
||||
users: User;
|
||||
};
|
||||
globals: {};
|
||||
}
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string;
|
||||
author?: string | User;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string;
|
||||
resetPasswordExpiration?: string;
|
||||
salt?: string;
|
||||
hash?: string;
|
||||
loginAttempts?: number;
|
||||
lockUntil?: string;
|
||||
password?: string;
|
||||
}
|
||||
33
examples/testing/src/payload.config.ts
Normal file
33
examples/testing/src/payload.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import path from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
import { buildConfig } from 'payload/config';
|
||||
|
||||
dotenv.config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
});
|
||||
|
||||
export default buildConfig({
|
||||
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'posts',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'author',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
24
examples/testing/src/server.ts
Normal file
24
examples/testing/src/server.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import path from 'path';
|
||||
import express from 'express';
|
||||
import payload from 'payload';
|
||||
|
||||
// Use `dotenv` to import your `.env` file automatically
|
||||
require('dotenv').config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
async function start() {
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET_KEY,
|
||||
mongoURL: process.env.MONGO_URL,
|
||||
express: app,
|
||||
});
|
||||
|
||||
app.listen(process.env.PORT, async () => {
|
||||
console.log(`Express is now listening for incoming connections on port ${process.env.PORT}.`);
|
||||
});
|
||||
}
|
||||
|
||||
start();
|
||||
4
examples/testing/src/tests/credentials.ts
Normal file
4
examples/testing/src/tests/credentials.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
email: 'test@test.com',
|
||||
password: 'test',
|
||||
};
|
||||
41
examples/testing/src/tests/globalSetup.ts
Normal file
41
examples/testing/src/tests/globalSetup.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { resolve } from 'path';
|
||||
import payload from 'payload';
|
||||
import express from 'express';
|
||||
import testCredentials from './credentials';
|
||||
|
||||
require('dotenv').config({
|
||||
path: resolve(__dirname, '../../.env'),
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
const globalSetup = async () => {
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET_KEY,
|
||||
mongoURL: process.env.MONGO_URL,
|
||||
express: app,
|
||||
});
|
||||
|
||||
app.listen(process.env.PORT, async () => {
|
||||
console.log(`Express is now listening for incoming connections on port ${process.env.PORT}.`);
|
||||
});
|
||||
|
||||
const response = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/users/first-register`, {
|
||||
body: JSON.stringify({
|
||||
email: testCredentials.email,
|
||||
password: testCredentials.password,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'post',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.user || !data.user.token) {
|
||||
throw new Error('Failed to register first user');
|
||||
}
|
||||
};
|
||||
|
||||
export default globalSetup;
|
||||
22
examples/testing/src/tests/login.spec.ts
Normal file
22
examples/testing/src/tests/login.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { User } from '../payload-types';
|
||||
import testCredentials from './credentials';
|
||||
|
||||
describe('Users', () => {
|
||||
it('should allow a user to log in', async () => {
|
||||
const result: {
|
||||
token: string
|
||||
user: User
|
||||
} = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/users/login`, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: testCredentials.email,
|
||||
password: testCredentials.password,
|
||||
}),
|
||||
}).then((res) => res.json());
|
||||
|
||||
expect(result.token).toBeDefined();
|
||||
});
|
||||
});
|
||||
27
examples/testing/tsconfig.json
Normal file
27
examples/testing/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"outDir": "./dist",
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
}
|
||||
8667
examples/testing/yarn.lock
Normal file
8667
examples/testing/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3497,9 +3497,9 @@ graphql-type-json@^0.3.2:
|
||||
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
|
||||
|
||||
graphql@^16.6.0:
|
||||
version "16.7.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.7.1.tgz#11475b74a7bff2aefd4691df52a0eca0abd9b642"
|
||||
integrity sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==
|
||||
version "16.8.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
gzip-size@^6.0.0:
|
||||
version "6.0.0"
|
||||
@@ -5534,9 +5534,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.2.15, postcss@^8.4.21, postcss@^8.4.24:
|
||||
version "8.4.27"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
|
||||
integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
@@ -2,8 +2,8 @@ module.exports = {
|
||||
verbose: true,
|
||||
testEnvironment: 'node',
|
||||
testMatch: [
|
||||
'**/src/**/*.spec.ts',
|
||||
'**/test/**/*int.spec.ts',
|
||||
'<rootDir>/src/**/*.spec.ts',
|
||||
'<rootDir>/test/**/*int.spec.ts',
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': ['@swc/jest'],
|
||||
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "1.13.4",
|
||||
"version": "1.15.12",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -40,15 +40,18 @@
|
||||
"dev:generate-types": "ts-node -T ./test/generateTypes.ts",
|
||||
"dev:generate-graphql-schema": "ts-node -T ./test/generateGraphQLSchema.ts",
|
||||
"pretest": "yarn build",
|
||||
"prepack": "yarn clean && yarn build",
|
||||
"prepublishOnly": "yarn clean && yarn build",
|
||||
"test": "yarn test:int && yarn test:components && yarn test:e2e",
|
||||
"test:int": "cross-env DISABLE_LOGGING=true jest --forceExit --detectOpenHandles",
|
||||
"test:e2e": "ts-node -T ./test/runE2E.ts",
|
||||
"test:e2e:headed": "cross-env DISABLE_LOGGING=true playwright test --headed",
|
||||
"test:e2e:debug": "cross-env PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||
"test:components": "cross-env jest --config=jest.components.config.js",
|
||||
"translateNewKeys": "ts-node -T ./scripts/translateNewKeys.ts",
|
||||
"clean:cache": "rimraf node_modules/.cache",
|
||||
"clean": "rimraf dist",
|
||||
"release:patch": "release-it patch",
|
||||
"release:patch": "release-it patch --npm.tag=payload-1",
|
||||
"release:minor": "release-it minor",
|
||||
"release:major": "release-it major",
|
||||
"release:beta": "release-it pre --preReleaseId=beta --npm.tag=beta --config .release-it.pre.json",
|
||||
@@ -88,8 +91,8 @@
|
||||
"@faceless-ui/scroll-info": "^1.3.0",
|
||||
"@faceless-ui/window-info": "^2.1.1",
|
||||
"@monaco-editor/react": "^4.5.1",
|
||||
"@swc/core": "^1.3.76",
|
||||
"@swc/register": "^0.1.10",
|
||||
"@swc/core": "1.3.78",
|
||||
"@swc/register": "0.1.10",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"body-parser": "^1.20.1",
|
||||
"bson-objectid": "^2.0.4",
|
||||
@@ -158,7 +161,6 @@
|
||||
"probe-image-size": "^6.0.0",
|
||||
"process": "^0.11.10",
|
||||
"qs": "^6.11.0",
|
||||
"qs-middleware": "^1.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-animate-height": "^2.1.2",
|
||||
"react-datepicker": "^4.10.0",
|
||||
|
||||
128
scripts/translateNewKeys.ts
Normal file
128
scripts/translateNewKeys.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const TRANSLATIONS_DIR = './src/translations';
|
||||
const SOURCE_LANG_FILE = 'en.json';
|
||||
const OPENAI_ENDPOINT = 'https://api.openai.com/v1/chat/completions'; // Adjust if needed
|
||||
const OPENAI_API_KEY = 'sk-YOURKEYHERE'; // Remember to replace with your actual key
|
||||
|
||||
|
||||
async function main() {
|
||||
const sourceLangContent = JSON.parse(fs.readFileSync(path.join(TRANSLATIONS_DIR, SOURCE_LANG_FILE), 'utf8'));
|
||||
|
||||
const files = fs.readdirSync(TRANSLATIONS_DIR);
|
||||
|
||||
for (const file of files) {
|
||||
if (file === SOURCE_LANG_FILE) {
|
||||
continue;
|
||||
}
|
||||
// check if file ends with .json
|
||||
if (!file.endsWith('.json')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip the translation-schema.json file
|
||||
if (file === 'translation-schema.json') {
|
||||
continue;
|
||||
}
|
||||
console.log('Processing file:', file);
|
||||
|
||||
const targetLangContent = JSON.parse(fs.readFileSync(path.join(TRANSLATIONS_DIR, file), 'utf8'));
|
||||
const missingKeys = findMissingKeys(sourceLangContent, targetLangContent);
|
||||
|
||||
let hasChanged = false;
|
||||
|
||||
for (const missingKey of missingKeys) {
|
||||
const keys = missingKey.split('.');
|
||||
const sourceText = keys.reduce((acc, key) => acc[key], sourceLangContent);
|
||||
const targetLang = file.split('.')[0];
|
||||
|
||||
const translatedText = await translateText(sourceText, targetLang);
|
||||
let targetObj = targetLangContent;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i += 1) {
|
||||
if (!targetObj[keys[i]]) {
|
||||
targetObj[keys[i]] = {};
|
||||
}
|
||||
targetObj = targetObj[keys[i]];
|
||||
}
|
||||
|
||||
targetObj[keys[keys.length - 1]] = translatedText;
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
|
||||
if (hasChanged) {
|
||||
const sortedContent = sortKeys(targetLangContent);
|
||||
fs.writeFileSync(path.join(TRANSLATIONS_DIR, file), JSON.stringify(sortedContent, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().then(() => {
|
||||
console.log('Translation update completed.');
|
||||
}).catch((error) => {
|
||||
console.error('Error occurred:', error);
|
||||
});
|
||||
|
||||
async function translateText(text: string, targetLang: string): Promise<string> {
|
||||
const response = await fetch(OPENAI_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
max_tokens: 150,
|
||||
model: 'gpt-4',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `Only respond with the translation of the text you receive. The original language is English and the translation language is ${targetLang}. Only respond with the translation - do not say anything else. If you cannot translate the text, respond with "[SKIPPED]"`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(' Old text:', text, 'New text:', data.choices[0].message.content.trim());
|
||||
return data.choices[0].message.content.trim();
|
||||
}
|
||||
|
||||
function findMissingKeys(baseObj: any, targetObj: any, prefix = ''): string[] {
|
||||
let missingKeys = [];
|
||||
|
||||
for (const key in baseObj) {
|
||||
if (typeof baseObj[key] === 'object') {
|
||||
missingKeys = missingKeys.concat(findMissingKeys(baseObj[key], targetObj[key] || {}, `${prefix}${key}.`));
|
||||
} else if (!(key in targetObj)) {
|
||||
missingKeys.push(`${prefix}${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
return missingKeys;
|
||||
}
|
||||
|
||||
function sortKeys(obj: any): any {
|
||||
if (typeof obj !== 'object' || obj === null) return obj;
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(sortKeys);
|
||||
}
|
||||
|
||||
const sortedKeys = Object.keys(obj).sort();
|
||||
const sortedObj: { [key: string]: any } = {};
|
||||
|
||||
for (const key of sortedKeys) {
|
||||
sortedObj[key] = sortKeys(obj[key]);
|
||||
}
|
||||
|
||||
return sortedObj;
|
||||
}
|
||||
@@ -72,6 +72,7 @@ const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>((props,
|
||||
iconPosition = 'right',
|
||||
newTab,
|
||||
tooltip,
|
||||
'aria-label': ariaLabel,
|
||||
} = props;
|
||||
|
||||
const [showTooltip, setShowTooltip] = React.useState(false);
|
||||
@@ -101,6 +102,8 @@ const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>((props,
|
||||
type,
|
||||
className: classes,
|
||||
disabled,
|
||||
'aria-disabled': disabled,
|
||||
'aria-label': ariaLabel,
|
||||
onMouseEnter: tooltip ? () => setShowTooltip(true) : undefined,
|
||||
onMouseLeave: tooltip ? () => setShowTooltip(false) : undefined,
|
||||
onClick: !disabled ? handleClick : undefined,
|
||||
|
||||
@@ -19,4 +19,5 @@ export type Props = {
|
||||
iconPosition?: 'left' | 'right',
|
||||
newTab?: boolean
|
||||
tooltip?: string
|
||||
'aria-label'?: string
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
padding: base(1.25) $baseline;
|
||||
position: relative;
|
||||
|
||||
h5 {
|
||||
&__title {
|
||||
@extend %h5;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -7,7 +7,7 @@ import './index.scss';
|
||||
const baseClass = 'card';
|
||||
|
||||
const Card: React.FC<Props> = (props) => {
|
||||
const { id, title, actions, onClick } = props;
|
||||
const { id, title, titleAs, buttonAriaLabel, actions, onClick } = props;
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
@@ -15,14 +15,16 @@ const Card: React.FC<Props> = (props) => {
|
||||
onClick && `${baseClass}--has-onclick`,
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const Tag = titleAs ?? 'div';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
id={id}
|
||||
>
|
||||
<h5>
|
||||
<Tag className={`${baseClass}__title`}>
|
||||
{title}
|
||||
</h5>
|
||||
</Tag>
|
||||
{actions && (
|
||||
<div className={`${baseClass}__actions`}>
|
||||
{actions}
|
||||
@@ -30,6 +32,7 @@ const Card: React.FC<Props> = (props) => {
|
||||
)}
|
||||
{onClick && (
|
||||
<Button
|
||||
aria-label={buttonAriaLabel}
|
||||
className={`${baseClass}__click`}
|
||||
buttonStyle="none"
|
||||
onClick={onClick}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ElementType } from 'react';
|
||||
|
||||
export type Props = {
|
||||
id?: string,
|
||||
title: string,
|
||||
titleAs?: ElementType,
|
||||
buttonAriaLabel?: string,
|
||||
actions?: React.ReactNode,
|
||||
onClick?: () => void,
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import type { Props } from './types';
|
||||
import { useTheme } from '../../utilities/Theme';
|
||||
import { ShimmerEffect } from '../ShimmerEffect';
|
||||
|
||||
import './index.scss';
|
||||
import { ShimmerEffect } from '../ShimmerEffect';
|
||||
|
||||
const baseClass = 'code-editor';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useId, useState } from 'react';
|
||||
import React, { useId } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Pill from '../Pill';
|
||||
import Plus from '../../icons/Plus';
|
||||
@@ -61,6 +61,7 @@ const ColumnSelector: React.FC<Props> = (props) => {
|
||||
alignIcon="left"
|
||||
key={`${collection.slug}-${col.name || i}${editDepth ? `-${editDepth}-` : ''}${uuid}`}
|
||||
icon={active ? <X /> : <Plus />}
|
||||
aria-checked={active}
|
||||
className={[
|
||||
`${baseClass}__column`,
|
||||
active && `${baseClass}__column--active`,
|
||||
|
||||
@@ -90,7 +90,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
|
||||
|
||||
const isEditing = Boolean(id);
|
||||
const apiURL = id ? `${serverURL}${api}/${collectionSlug}/${id}?locale=${locale}` : null;
|
||||
const action = `${serverURL}${api}/${collectionSlug}${id ? `/${id}` : ''}?locale=${locale}&depth=0&fallback-locale=null`;
|
||||
const action = `${serverURL}${api}/${collectionSlug}${id ? `/${id}` : ''}?locale=${locale}&fallback-locale=null`;
|
||||
const hasSavePermission = (isEditing && docPermissions?.update?.permission) || (!isEditing && (docPermissions as CollectionPermission)?.create?.permission);
|
||||
const isLoading = !internalState || !docPermissions || isLoadingDocument;
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Props, TogglerProps } from './types';
|
||||
import { EditDepthContext, useEditDepth } from '../../utilities/EditDepth';
|
||||
import { Gutter } from '../Gutter';
|
||||
import './index.scss';
|
||||
import X from '../../icons/X';
|
||||
|
||||
const baseClass = 'drawer';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'drawer';
|
||||
const zBase = 100;
|
||||
|
||||
export const formatDrawerSlug = ({
|
||||
|
||||
@@ -145,6 +145,7 @@ const EditMany: React.FC<Props> = (props) => {
|
||||
<Form
|
||||
className={`${baseClass}__form`}
|
||||
onSuccess={onSuccess}
|
||||
configFieldsSchema={selected}
|
||||
>
|
||||
<div className={`${baseClass}__main`}>
|
||||
<div className={`${baseClass}__header`}>
|
||||
|
||||
@@ -38,6 +38,11 @@ const getUseAsTitle = (collection: SanitizedCollectionConfig) => {
|
||||
return topLevelFields.find((field) => fieldAffectsData(field) && field.name === useAsTitle);
|
||||
};
|
||||
|
||||
/**
|
||||
* The ListControls component is used to render the controls (search, filter, where)
|
||||
* for a collection's list view. You can find those directly above the table which lists
|
||||
* the collection's documents.
|
||||
*/
|
||||
const ListControls: React.FC<Props> = (props) => {
|
||||
const {
|
||||
collection,
|
||||
@@ -105,6 +110,8 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
pillStyle="light"
|
||||
className={`${baseClass}__toggle-columns ${visibleDrawer === 'columns' ? `${baseClass}__buttons-active` : ''}`}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'columns' ? 'columns' : undefined)}
|
||||
aria-expanded={visibleDrawer === 'columns'}
|
||||
aria-controls={`${baseClass}-columns`}
|
||||
icon={<Chevron />}
|
||||
>
|
||||
{t('columns')}
|
||||
@@ -114,6 +121,8 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
pillStyle="light"
|
||||
className={`${baseClass}__toggle-where ${visibleDrawer === 'where' ? `${baseClass}__buttons-active` : ''}`}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'where' ? 'where' : undefined)}
|
||||
aria-expanded={visibleDrawer === 'where'}
|
||||
aria-controls={`${baseClass}-where`}
|
||||
icon={<Chevron />}
|
||||
>
|
||||
{t('filters')}
|
||||
@@ -123,6 +132,8 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
className={`${baseClass}__toggle-sort`}
|
||||
buttonStyle={visibleDrawer === 'sort' ? undefined : 'secondary'}
|
||||
onClick={() => setVisibleDrawer(visibleDrawer !== 'sort' ? 'sort' : undefined)}
|
||||
aria-expanded={visibleDrawer === 'sort'}
|
||||
aria-controls={`${baseClass}-sort`}
|
||||
icon="chevron"
|
||||
iconStyle="none"
|
||||
>
|
||||
@@ -136,6 +147,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__columns`}
|
||||
height={visibleDrawer === 'columns' ? 'auto' : 0}
|
||||
id={`${baseClass}-columns`}
|
||||
>
|
||||
<ColumnSelector collection={collection} />
|
||||
</AnimateHeight>
|
||||
@@ -143,6 +155,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__where`}
|
||||
height={visibleDrawer === 'where' ? 'auto' : 0}
|
||||
id={`${baseClass}-where`}
|
||||
>
|
||||
<WhereBuilder
|
||||
collection={collection}
|
||||
@@ -154,6 +167,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__sort`}
|
||||
height={visibleDrawer === 'sort' ? 'auto' : 0}
|
||||
id={`${baseClass}-sort`}
|
||||
>
|
||||
<SortComplex
|
||||
modifySearchQuery={modifySearchQuery}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import LogOut from '../../icons/LogOut';
|
||||
@@ -7,16 +8,21 @@ import LogOut from '../../icons/LogOut';
|
||||
const baseClass = 'nav';
|
||||
|
||||
const DefaultLogout = () => {
|
||||
const { t } = useTranslation('authentication');
|
||||
const config = useConfig();
|
||||
const {
|
||||
routes: { admin },
|
||||
admin: {
|
||||
logoutRoute,
|
||||
components: { logout }
|
||||
}
|
||||
components: { logout },
|
||||
},
|
||||
} = config;
|
||||
return (
|
||||
<Link to={`${admin}${logoutRoute}`} className={`${baseClass}__log-out`}>
|
||||
<Link
|
||||
to={`${admin}${logoutRoute}`}
|
||||
className={`${baseClass}__log-out`}
|
||||
aria-label={t('logOut')}
|
||||
>
|
||||
<LogOut />
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ const DefaultNav = () => {
|
||||
const [menuActive, setMenuActive] = useState(false);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const history = useHistory();
|
||||
const { i18n } = useTranslation('general');
|
||||
const { t, i18n } = useTranslation('general');
|
||||
const {
|
||||
collections,
|
||||
globals,
|
||||
@@ -81,6 +81,7 @@ const DefaultNav = () => {
|
||||
<Link
|
||||
to={admin}
|
||||
className={`${baseClass}__brand`}
|
||||
aria-label={t('dashboard')}
|
||||
>
|
||||
<Icon />
|
||||
</Link>
|
||||
@@ -141,6 +142,7 @@ const DefaultNav = () => {
|
||||
<Link
|
||||
to={`${admin}/account`}
|
||||
className={`${baseClass}__account`}
|
||||
aria-label={t('authentication:account')}
|
||||
>
|
||||
<Account />
|
||||
</Link>
|
||||
|
||||
@@ -45,6 +45,10 @@ const StaticPill: React.FC<Props> = (props) => {
|
||||
children,
|
||||
elementProps,
|
||||
rounded,
|
||||
'aria-label': ariaLabel,
|
||||
'aria-expanded': ariaExpanded,
|
||||
'aria-controls': ariaControls,
|
||||
'aria-checked': ariaChecked,
|
||||
} = props;
|
||||
|
||||
const classes = [
|
||||
@@ -67,6 +71,10 @@ const StaticPill: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<Element
|
||||
{...elementProps}
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={ariaExpanded}
|
||||
aria-controls={ariaControls}
|
||||
aria-checked={ariaChecked}
|
||||
className={classes}
|
||||
type={Element === 'button' ? 'button' : undefined}
|
||||
to={to || undefined}
|
||||
|
||||
@@ -11,6 +11,10 @@ export type Props = {
|
||||
draggable?: boolean,
|
||||
rounded?: boolean
|
||||
id?: string
|
||||
'aria-label'?: string,
|
||||
'aria-expanded'?: boolean,
|
||||
'aria-controls'?: string,
|
||||
'aria-checked'?: boolean,
|
||||
elementProps?: HTMLAttributes<HTMLElement> & {
|
||||
ref: React.RefCallback<HTMLElement>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { components as SelectComponents, MultiValueProps } from 'react-select';
|
||||
import type { Option } from '../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'multi-value-label';
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MultiValueRemoveProps } from 'react-select';
|
||||
import X from '../../../icons/X';
|
||||
import Tooltip from '../../Tooltip';
|
||||
import { Option as OptionType } from '../types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'multi-value-remove';
|
||||
|
||||
@@ -45,6 +45,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
|
||||
components,
|
||||
isCreatable,
|
||||
selectProps,
|
||||
noOptionsMessage,
|
||||
} = props;
|
||||
|
||||
const classes = [
|
||||
@@ -72,6 +73,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
|
||||
filterOption={filterOption}
|
||||
onMenuOpen={onMenuOpen}
|
||||
menuPlacement="auto"
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
components={{
|
||||
ValueContainer,
|
||||
SingleValue,
|
||||
@@ -134,6 +136,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
|
||||
inputValue={inputValue}
|
||||
onInputChange={(newValue) => setInputValue(newValue)}
|
||||
onKeyDown={handleKeyDown}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
components={{
|
||||
ValueContainer,
|
||||
SingleValue,
|
||||
|
||||
@@ -43,6 +43,7 @@ export type OptionGroup = {
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
inputId?: string
|
||||
className?: string
|
||||
value?: Option | Option[],
|
||||
onChange?: (value: any) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
@@ -76,4 +77,5 @@ export type Props = {
|
||||
*/
|
||||
selectProps?: CustomSelectProps
|
||||
backspaceRemovesValue?: boolean
|
||||
noOptionsMessage?: (obj: { inputValue: string }) => string
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const SortColumn: React.FC<Props> = (props) => {
|
||||
} = props;
|
||||
const params = useSearchParams();
|
||||
const history = useHistory();
|
||||
const { i18n } = useTranslation();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
|
||||
const { sort } = params;
|
||||
|
||||
@@ -50,6 +50,7 @@ const SortColumn: React.FC<Props> = (props) => {
|
||||
buttonStyle="none"
|
||||
className={ascClasses.join(' ')}
|
||||
onClick={() => setSort(asc)}
|
||||
aria-label={t('sortByLabelDirection', { label: getTranslation(label, i18n), direction: t('ascending') })}
|
||||
>
|
||||
<Chevron />
|
||||
</Button>
|
||||
@@ -58,6 +59,7 @@ const SortColumn: React.FC<Props> = (props) => {
|
||||
buttonStyle="none"
|
||||
className={descClasses.join(' ')}
|
||||
onClick={() => setSort(desc)}
|
||||
aria-label={t('sortByLabelDirection', { label: getTranslation(label, i18n), direction: t('descending') })}
|
||||
>
|
||||
<Chevron />
|
||||
</Button>
|
||||
|
||||
@@ -66,11 +66,11 @@ const Status: React.FC = () => {
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
url = `${serverURL}${api}/${collection.slug}/${id}?depth=0&locale=${locale}&fallback-locale=null`;
|
||||
url = `${serverURL}${api}/${collection.slug}/${id}?locale=${locale}&fallback-locale=null`;
|
||||
method = 'patch';
|
||||
}
|
||||
if (global) {
|
||||
url = `${serverURL}${api}/globals/${global.slug}?depth=0&locale=${locale}&fallback-locale=null`;
|
||||
url = `${serverURL}${api}/globals/${global.slug}?locale=${locale}&fallback-locale=null`;
|
||||
method = 'post';
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props, isComponent } from './types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const ViewDescription: React.FC<Props> = (props) => {
|
||||
|
||||
@@ -41,7 +41,7 @@ const numeric = [
|
||||
},
|
||||
{
|
||||
label: 'isGreaterThanOrEqualTo',
|
||||
value: 'greater_than_equals',
|
||||
value: 'greater_than_equal',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -57,6 +57,16 @@ const geo = [
|
||||
},
|
||||
];
|
||||
|
||||
const within = {
|
||||
label: 'within',
|
||||
value: 'within',
|
||||
};
|
||||
|
||||
const intersects = {
|
||||
label: 'intersects',
|
||||
value: 'intersects',
|
||||
};
|
||||
|
||||
const like = {
|
||||
label: 'isLike',
|
||||
value: 'like',
|
||||
@@ -86,7 +96,7 @@ const fieldTypeConditions = {
|
||||
},
|
||||
json: {
|
||||
component: 'Text',
|
||||
operators: [...base, like, contains],
|
||||
operators: [...base, like, contains, within, intersects],
|
||||
},
|
||||
richText: {
|
||||
component: 'Text',
|
||||
@@ -102,7 +112,7 @@ const fieldTypeConditions = {
|
||||
},
|
||||
point: {
|
||||
component: 'Point',
|
||||
operators: [...geo],
|
||||
operators: [...geo, within, intersects],
|
||||
},
|
||||
upload: {
|
||||
component: 'Text',
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useSearchParams } from '../../utilities/SearchParams';
|
||||
import validateWhereQuery from './validateWhereQuery';
|
||||
import { Where } from '../../../../types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
import { transformWhereQuery } from './transformWhereQuery';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -43,6 +44,10 @@ const reduceFields = (fields, i18n) => flattenTopLevelFields(fields).reduce((red
|
||||
return reduced;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* The WhereBuilder component is used to render the filter controls for a collection's list view.
|
||||
* It is part of the {@link ListControls} component which is used to render the controls (search, filter, where).
|
||||
*/
|
||||
const WhereBuilder: React.FC<Props> = (props) => {
|
||||
const {
|
||||
collection,
|
||||
@@ -59,16 +64,30 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
const params = useSearchParams();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
|
||||
// This handles initializing the where conditions from the search query (URL). That way, if you pass in
|
||||
// query params to the URL, the where conditions will be initialized from those and displayed in the UI.
|
||||
// Example: /admin/collections/posts?where[or][0][and][0][text][equals]=example%20post
|
||||
const [conditions, dispatchConditions] = useReducer(reducer, params.where, (whereFromSearch) => {
|
||||
if (modifySearchQuery && validateWhereQuery(whereFromSearch)) {
|
||||
return whereFromSearch.or;
|
||||
}
|
||||
if (modifySearchQuery && whereFromSearch) {
|
||||
if (validateWhereQuery(whereFromSearch)) {
|
||||
return whereFromSearch.or;
|
||||
}
|
||||
|
||||
// Transform the where query to be in the right format. This will transform something simple like [text][equals]=example%20post to the right format
|
||||
const transformedWhere = transformWhereQuery(whereFromSearch);
|
||||
|
||||
if (validateWhereQuery(transformedWhere)) {
|
||||
return transformedWhere.or;
|
||||
}
|
||||
|
||||
console.warn('Invalid where query in URL. Ignoring.');
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const [reducedFields] = useState(() => reduceFields(collection.fields, i18n));
|
||||
|
||||
// This handles updating the search query (URL) when the where conditions change
|
||||
useThrottledEffect(() => {
|
||||
const currentParams = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 10 }) as { where: Where };
|
||||
|
||||
@@ -83,8 +102,11 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
];
|
||||
}, []) : [];
|
||||
|
||||
const hasNewWhereConditions = conditions.length > 0;
|
||||
|
||||
|
||||
const newWhereQuery = {
|
||||
...typeof currentParams?.where === 'object' ? currentParams.where : {},
|
||||
...typeof currentParams?.where === 'object' && (validateWhereQuery(currentParams?.where) || !hasNewWhereConditions) ? currentParams.where : {},
|
||||
or: [
|
||||
...conditions,
|
||||
...paramsToKeep,
|
||||
@@ -94,7 +116,6 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
if (handleChange) handleChange(newWhereQuery as Where);
|
||||
|
||||
const hasExistingConditions = typeof currentParams?.where === 'object' && 'or' in currentParams.where;
|
||||
const hasNewWhereConditions = conditions.length > 0;
|
||||
|
||||
if (modifySearchQuery && ((hasExistingConditions && !hasNewWhereConditions) || hasNewWhereConditions)) {
|
||||
history.replace({
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Where } from '../../../../types';
|
||||
|
||||
/**
|
||||
* Something like [or][0][and][0][text][equals]=example%20post will work and pass through the validateWhereQuery check.
|
||||
* However, something like [text][equals]=example%20post will not work and will fail the validateWhereQuery check,
|
||||
* even though it is a valid Where query. This needs to be transformed here.
|
||||
*/
|
||||
export const transformWhereQuery = (whereQuery): Where => {
|
||||
if (!whereQuery) {
|
||||
return {};
|
||||
}
|
||||
// Check if 'whereQuery' has 'or' field but no 'and'. This is the case for "correct" queries
|
||||
if (whereQuery.or && !whereQuery.and) {
|
||||
return {
|
||||
or: whereQuery.or.map((query) => {
|
||||
// ...but if the or query does not have an and, we need to add it
|
||||
if(!query.and) {
|
||||
return {
|
||||
and: [query]
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Check if 'whereQuery' has 'and' field but no 'or'.
|
||||
if (whereQuery.and && !whereQuery.or) {
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
and: whereQuery.and,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Check if 'whereQuery' has neither 'or' nor 'and'.
|
||||
if (!whereQuery.or && !whereQuery.and) {
|
||||
return {
|
||||
or: [
|
||||
{
|
||||
and: [whereQuery], // top-level siblings are considered 'and'
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// If 'whereQuery' has 'or' and 'and', just return it as it is.
|
||||
return whereQuery;
|
||||
};
|
||||
@@ -1,8 +1,37 @@
|
||||
import { Where } from '../../../../types';
|
||||
import type { Operator, Where } from '../../../../types';
|
||||
import { validOperators } from '../../../../types/constants';
|
||||
|
||||
const validateWhereQuery = (whereQuery): whereQuery is Where => {
|
||||
if (whereQuery?.or?.length > 0 && whereQuery?.or?.[0]?.and && whereQuery?.or?.[0]?.and?.length > 0) {
|
||||
return true;
|
||||
// At this point we know that the whereQuery has 'or' and 'and' fields,
|
||||
// now let's check the structure and content of these fields.
|
||||
|
||||
const isValid = whereQuery.or.every((orQuery) => {
|
||||
if (orQuery.and && Array.isArray(orQuery.and)) {
|
||||
return orQuery.and.every((andQuery) => {
|
||||
if (typeof andQuery !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const andKeys = Object.keys(andQuery);
|
||||
// If there are no keys, it's not a valid WhereField.
|
||||
if (andKeys.length === 0) {
|
||||
return false;
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key of andKeys) {
|
||||
const operator = Object.keys(andQuery[key])[0];
|
||||
// Check if the key is a valid Operator.
|
||||
if (!operator || !validOperators.includes(operator as Operator)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -111,7 +111,7 @@ export const addFieldStatePromise = async ({
|
||||
|
||||
acc.rowMetadata.push({
|
||||
id: row.id,
|
||||
collapsed: collapsedRowIDs === undefined ? field.admin.initCollapsed : collapsedRowIDs.includes(row.id),
|
||||
collapsed: collapsedRowIDs === undefined ? Boolean(field?.admin?.initCollapsed) : collapsedRowIDs.includes(row.id),
|
||||
childErrorPaths: new Set(),
|
||||
});
|
||||
|
||||
@@ -191,7 +191,7 @@ export const addFieldStatePromise = async ({
|
||||
|
||||
acc.rowMetadata.push({
|
||||
id: row.id,
|
||||
collapsed: collapsedRowIDs === undefined ? field.admin.initCollapsed : collapsedRowIDs.includes(row.id),
|
||||
collapsed: collapsedRowIDs === undefined ? Boolean(field?.admin?.initCollapsed) : collapsedRowIDs.includes(row.id),
|
||||
blockType: row.blockType,
|
||||
childErrorPaths: new Set(),
|
||||
});
|
||||
@@ -245,6 +245,54 @@ export const addFieldStatePromise = async ({
|
||||
break;
|
||||
}
|
||||
|
||||
case 'relationship': {
|
||||
if (field.hasMany) {
|
||||
const relationshipValue = Array.isArray(valueWithDefault) ? valueWithDefault.map((relationship) => {
|
||||
if (Array.isArray(field.relationTo)) {
|
||||
return {
|
||||
relationTo: relationship.relationTo,
|
||||
value: typeof relationship.value === 'string' ? relationship.value : relationship.value?.id,
|
||||
};
|
||||
}
|
||||
if (typeof relationship === 'object' && relationship !== null) {
|
||||
return relationship.id;
|
||||
}
|
||||
return relationship;
|
||||
}) : undefined;
|
||||
|
||||
fieldState.value = relationshipValue;
|
||||
fieldState.initialValue = relationshipValue;
|
||||
} else if (Array.isArray(field.relationTo)) {
|
||||
if (valueWithDefault && typeof valueWithDefault === 'object' && 'relationTo' in valueWithDefault && 'value' in valueWithDefault) {
|
||||
const value = typeof valueWithDefault?.value === 'object' && 'id' in valueWithDefault.value ? valueWithDefault.value.id : valueWithDefault.value;
|
||||
const relationshipValue = {
|
||||
relationTo: valueWithDefault?.relationTo,
|
||||
value,
|
||||
};
|
||||
fieldState.value = relationshipValue;
|
||||
fieldState.initialValue = relationshipValue;
|
||||
}
|
||||
} else {
|
||||
const relationshipValue = valueWithDefault && typeof valueWithDefault === 'object' && 'id' in valueWithDefault ? valueWithDefault.id : valueWithDefault;
|
||||
fieldState.value = relationshipValue;
|
||||
fieldState.initialValue = relationshipValue;
|
||||
}
|
||||
|
||||
state[`${path}${field.name}`] = fieldState;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'upload': {
|
||||
const relationshipValue = valueWithDefault && typeof valueWithDefault === 'object' && 'id' in valueWithDefault ? valueWithDefault.id : valueWithDefault;
|
||||
fieldState.value = relationshipValue;
|
||||
fieldState.initialValue = relationshipValue;
|
||||
|
||||
state[`${path}${field.name}`] = fieldState;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
fieldState.value = valueWithDefault;
|
||||
fieldState.initialValue = valueWithDefault;
|
||||
|
||||
@@ -48,6 +48,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
initialState, // fully formed initial field state
|
||||
initialData, // values only, paths are required as key - form should build initial state as convenience
|
||||
waitForAutocomplete,
|
||||
configFieldsSchema,
|
||||
} = props;
|
||||
|
||||
const history = useHistory();
|
||||
@@ -409,12 +410,14 @@ const Form: React.FC<Props> = (props) => {
|
||||
path: string,
|
||||
blockType?: string
|
||||
}) => {
|
||||
const rowConfig = traverseRowConfigs({ path, fieldConfig: collection?.fields || global?.fields });
|
||||
if (!configFieldsSchema) return null;
|
||||
|
||||
const rowConfig = traverseRowConfigs({ path, fieldConfig: configFieldsSchema });
|
||||
const rowFieldConfigs = buildFieldSchemaMap(rowConfig);
|
||||
const pathSegments = splitPathByArrayFields(path);
|
||||
const fieldKey = pathSegments.at(-1);
|
||||
return rowFieldConfigs.get(blockType ? `${fieldKey}.${blockType}` : fieldKey);
|
||||
}, [traverseRowConfigs, collection?.fields, global?.fields]);
|
||||
}, [traverseRowConfigs, configFieldsSchema]);
|
||||
|
||||
// Array/Block row manipulation
|
||||
const addFieldRow: Context['addFieldRow'] = useCallback(async ({ path, rowIndex, data }) => {
|
||||
|
||||
@@ -49,6 +49,7 @@ export type Props = {
|
||||
validationOperation?: 'create' | 'update'
|
||||
children?: React.ReactNode
|
||||
action?: string
|
||||
configFieldsSchema?: FieldConfig[]
|
||||
}
|
||||
|
||||
export type SubmitOptions = {
|
||||
|
||||
@@ -1,62 +1,74 @@
|
||||
import React from 'react';
|
||||
import Check from '../../../icons/Check';
|
||||
import Label from '../../Label';
|
||||
import Line from '../../../icons/Line';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'custom-checkbox';
|
||||
|
||||
type CheckboxInputProps = {
|
||||
onToggle: React.MouseEventHandler<HTMLButtonElement>
|
||||
onToggle: React.FormEventHandler<HTMLInputElement>
|
||||
inputRef?: React.MutableRefObject<HTMLInputElement>
|
||||
readOnly?: boolean
|
||||
checked?: boolean
|
||||
partialChecked?: boolean
|
||||
name?: string
|
||||
id?: string
|
||||
label?: string
|
||||
'aria-label'?: string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
|
||||
const {
|
||||
onToggle,
|
||||
checked,
|
||||
partialChecked,
|
||||
inputRef,
|
||||
name,
|
||||
id,
|
||||
label,
|
||||
'aria-label': ariaLabel,
|
||||
readOnly,
|
||||
required,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
<div
|
||||
className={[
|
||||
baseClass,
|
||||
checked && `${baseClass}--checked`,
|
||||
(checked || partialChecked) && `${baseClass}--checked`,
|
||||
readOnly && `${baseClass}--read-only`,
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
checked={checked}
|
||||
readOnly
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span className={`${baseClass}__input`}>
|
||||
<Check />
|
||||
<div className={`${baseClass}__input`}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
id={id}
|
||||
type="checkbox"
|
||||
name={name}
|
||||
aria-label={ariaLabel}
|
||||
defaultChecked={Boolean(checked)}
|
||||
disabled={readOnly}
|
||||
onInput={onToggle}
|
||||
/>
|
||||
<span className={`${baseClass}__icon ${!partialChecked ? 'check' : 'partial'}`}>
|
||||
{!partialChecked && (
|
||||
<Check />
|
||||
)}
|
||||
{partialChecked && (
|
||||
<Line />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{label && (
|
||||
<Label
|
||||
htmlFor={id}
|
||||
label={label}
|
||||
required={required}
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
position: relative;
|
||||
margin-bottom: $baseline;
|
||||
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tooltip:not([aria-hidden="true"]) {
|
||||
right: auto;
|
||||
position: relative;
|
||||
@@ -22,32 +18,84 @@
|
||||
|
||||
|
||||
.custom-checkbox {
|
||||
display: inline-flex;
|
||||
|
||||
label {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
// hidden HTML checkbox
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
padding-left: base(.5);
|
||||
}
|
||||
|
||||
&__input {
|
||||
// visible checkbox
|
||||
@include formInput;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
width: $baseline;
|
||||
height: $baseline;
|
||||
margin-right: base(.5);
|
||||
|
||||
& input[type="checkbox"] {
|
||||
position: absolute;
|
||||
// Without the extra 4px, there is an uncheckable area due to the border of the parent element
|
||||
width: calc(100% + 4px);
|
||||
height: calc(100% + 4px);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: -2px;
|
||||
margin-top: -2px;
|
||||
opacity: 0;
|
||||
border-radius: 0;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
|
||||
svg {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:not(&--read-only) {
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus {
|
||||
.custom-checkbox__input, & input[type="checkbox"] {
|
||||
@include inputShadowActive;
|
||||
|
||||
outline: 0;
|
||||
box-shadow: 0 0 3px 3px var(--theme-success-400)!important;
|
||||
border: 1px solid var(--theme-elevation-150);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.custom-checkbox__input, & input[type="checkbox"] {
|
||||
border-color: var(--theme-elevation-250);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(&--read-only):not(&--checked) {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--checked {
|
||||
.custom-checkbox__icon {
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--read-only {
|
||||
.custom-checkbox__input {
|
||||
@@ -58,40 +106,6 @@
|
||||
color: var(--theme-elevation-400);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@extend %btn-reset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.custom-checkbox__input {
|
||||
box-shadow: 0 0 3px 3px var(--theme-success-400);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
opacity: .2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--checked {
|
||||
button {
|
||||
.custom-checkbox__input {
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme=light] {
|
||||
|
||||
@@ -88,6 +88,7 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
label={getTranslation(label || name, i18n)}
|
||||
name={path}
|
||||
checked={Boolean(value)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
<FieldDescription
|
||||
value={value}
|
||||
|
||||
@@ -10,9 +10,9 @@ import { Props } from './types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
import { Option } from '../../../elements/ReactSelect/types';
|
||||
import ReactSelect from '../../../elements/ReactSelect';
|
||||
import { isNumber } from '../../../../../utilities/isNumber';
|
||||
|
||||
import './index.scss';
|
||||
import { isNumber } from '../../../../../utilities/isNumber';
|
||||
|
||||
const NumberField: React.FC<Props> = (props) => {
|
||||
const {
|
||||
@@ -143,9 +143,17 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
isMulti
|
||||
isSortable
|
||||
isClearable
|
||||
noOptionsMessage={({ inputValue }) => {
|
||||
const isOverHasMany = Array.isArray(value) && value.length >= maxRows;
|
||||
if (isOverHasMany) {
|
||||
return t('validation:limitReached', { value: value.length + 1, max: maxRows });
|
||||
}
|
||||
return t('general:noOptions');
|
||||
}}
|
||||
filterOption={(option, rawInput) => {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
return isNumber(rawInput)
|
||||
const isOverHasMany = Array.isArray(value) && value.length >= maxRows;
|
||||
return isNumber(rawInput) && !isOverHasMany;
|
||||
}}
|
||||
numberOnly
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user