Compare commits

..

64 Commits

Author SHA1 Message Date
Elliot DeNolf
baad7d3360 chore(release): live-preview/0.1.5 [skip ci] 2023-10-19 16:42:33 -04:00
Jacob Fletcher
7fcb972dfa fix(live-preview): blocks field (#3753) 2023-10-19 16:40:16 -04:00
Elliot DeNolf
01245b07f8 chore(release): richtext-lexical/0.1.14 [skip ci] 2023-10-19 16:14:06 -04:00
Elliot DeNolf
d2f45343da chore(release): db-postgres/0.1.10 [skip ci] 2023-10-19 16:13:55 -04:00
Elliot DeNolf
5ba95df674 chore(release): db-mongodb/1.0.4 [skip ci] 2023-10-19 16:13:46 -04:00
Elliot DeNolf
40f98e4a0d chore(release): bundler-webpack/1.0.4 [skip ci] 2023-10-19 16:13:39 -04:00
Elliot DeNolf
584ead9fe2 chore(release): bundler-vite/0.1.3 [skip ci] 2023-10-19 16:13:29 -04:00
Elliot DeNolf
b6bf354f6a chore(release): payload/2.0.11 [skip ci] 2023-10-19 16:10:46 -04:00
Elliot DeNolf
9918c2499a chore(deps): bump sass (#3768)
* chore(deps): bump sass and sass-loader

* chore: handle sass slash div deprecation
2023-10-19 15:52:39 -04:00
Jarrod Flesch
8c48c8beb5 fix(webpack-bundler): corrects payload alias (#3769) 2023-10-19 15:21:39 -04:00
Jacob Fletcher
2697753715 chore(live-preview): significantly improves test coverage (#3763) 2023-10-19 14:56:16 -04:00
Jarrod Flesch
4b13686f61 fix: corrects versions collection casing (#3739) 2023-10-19 13:08:24 -04:00
Jessica Chowdhury
ab7999d3c1 fix: updates req after file resize (#3754) 2023-10-19 12:56:24 -04:00
Jessica Chowdhury
a592188c1d fix: correctly renders focal point when crop is set to false (#3759) 2023-10-19 12:51:13 -04:00
Elliot DeNolf
5ff0846b6c feat: add ability to opt out of type gen declare statement (#3765)
* feat: add ability to opt out of type gen declare statement

* chore: docs wording
2023-10-19 12:44:28 -04:00
xHomu
13cabf129e fix: account for many slug types in generate types (#3698)
* Fix generate:types bug #3697

generateEntityDeclarations function creates mismatched type names. We'll simply use the existing Config type instead.

* code cleanup
2023-10-19 11:36:26 -04:00
Elliot DeNolf
c173e55b89 fix(bundler-webpack): better node_modules resolution (#3744)
* fix(bundler-webpack): better node_modules resolution

* chore: see if retries are affecting new webpack changes

* chore: reinstate retries

This reverts commit 96989295ba.

* chore: default to process.cwd() if cannot find node_modules path
2023-10-19 11:28:31 -04:00
Take Weiland
bcdd2d626f fix: handle graphQL: false on globals when building policy type (#3729) 2023-10-19 09:13:51 -04:00
Elliot DeNolf
67682248c8 chore: more master -> main readme renames 2023-10-19 09:08:40 -04:00
Jacob Fletcher
7c52d6ee28 Merge pull request #3745 from payloadcms/fix/misc-admin
Fix/misc admin
2023-10-19 09:06:20 -04:00
Elliot DeNolf
bc65b53ce5 chore(release): eslint-config-payload/1.0.0 2023-10-18 21:37:35 -04:00
Elliot DeNolf
c8cc6ea1cc chore(script): more prompts during publish 2023-10-18 21:29:26 -04:00
Jacob Fletcher
4e05e6fd85 fix(a11y): tab indices 2023-10-18 17:55:55 -04:00
Jacob Fletcher
6988a68eaf fix: renders id as fallback title in DeleteDocument 2023-10-18 17:55:55 -04:00
Jacob Fletcher
a272692726 chore: refines drawer and blur styles 2023-10-18 17:55:39 -04:00
Dan Ribbens
229e4459cb fix(db-postgres): block and array inserts error (#3714)
Co-authored-by: James <james@trbl.design>
2023-10-18 16:53:26 -04:00
Take Weiland
056585ed31 fix: properly handles hideAPIURL (#3721) 2023-10-18 16:36:57 -04:00
Elliot DeNolf
b545433ee6 chore(templates): add payload helper npm script 2023-10-18 16:11:58 -04:00
Elliot DeNolf
4c938b5f9e chore(plugin-nested-docs): lint fix (#3740) 2023-10-18 14:40:48 -04:00
Elliot DeNolf
f1d8fa9999 chore: add 2.0 announcement banner 2023-10-18 14:38:58 -04:00
Jarrod Flesch
1670a603f6 chore: adjust where sharp types are imported from (#3645) 2023-10-18 11:44:49 -04:00
Elliot DeNolf
22f1fa8fc9 chore: update issue template and repro guide 2023-10-18 11:43:11 -04:00
Jacob Fletcher
370e8d1938 chore: replaces bg blur in document controls (#3736) 2023-10-18 11:40:25 -04:00
Elliot DeNolf
3a3eab761e fix: filesRequiredOnCreate typing, tests, linting (#3737) 2023-10-18 11:27:55 -04:00
Jacob Fletcher
238f7e1b94 chore(examples/live-preview): pins @payloadcms/live-preview-react to latest 2023-10-18 10:18:01 -04:00
Jacob Fletcher
58e2083882 Merge pull request #3719 from payloadcms/fix/live-preview/uploads
Fix/live preview/uploads
2023-10-17 17:05:30 -04:00
Jacob Fletcher
20cde242fb fix(live-preview): properly handles uploads and hasOne monomorphic relationships 2023-10-17 17:00:59 -04:00
Elliot DeNolf
f50a392d59 chore(script): update release script [skip ci] 2023-10-17 17:00:14 -04:00
Elliot DeNolf
fa1740d906 chore: update changelog 2023-10-17 16:51:24 -04:00
Elliot DeNolf
e847061c74 chore(release): payload/2.0.10 [skip ci] 2023-10-17 16:45:10 -04:00
Jacob Fletcher
ebd5e6ae8f chore: types fieldSchemaToJSON 2023-10-17 16:39:36 -04:00
TomDo1234
48de89794b feat: filesRequired is optional for uploads (#3668)
* filesRequired is optional for uploads

* More accurate name

* Updated docs

* ensure that by default you throw on missing file
2023-10-17 15:21:16 -04:00
Elliot DeNolf
ef4b5d8bfd chore: add payload as peer dep to all adapters (#3716) 2023-10-17 15:20:09 -04:00
Elliot DeNolf
5711d42eca chore: update pnpm-lock.yaml 2023-10-17 15:03:58 -04:00
Elliot DeNolf
a446a788a9 chore(release): payload/2.0.9 2023-10-17 15:00:55 -04:00
Elliot DeNolf
df57196d19 chore(live-preview-react): adjust live-preview semver dep 2023-10-17 15:00:55 -04:00
Alessio Gravili
86c563e4e5 Merge remote-tracking branch 'origin/main' 2023-10-17 20:57:17 +02:00
Alessio Gravili
734b8c08ed chore(richtext-lexical): add additional safety checks for incorrect data passed into the editor 2023-10-17 20:56:22 +02:00
geminigeek
68c5a57515 [fix] Register first user verify update missing transaction id / req (#3665)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2023-10-17 14:50:56 -04:00
Elliot DeNolf
d9f0b7bd30 chore(release): live-preview-react/0.1.4 2023-10-17 13:23:10 -04:00
Elliot DeNolf
5ecfe3da28 chore(release): richtext-lexical/0.1.13 2023-10-17 13:18:30 -04:00
Elliot DeNolf
3e3163e875 chore(release): live-preview/0.1.4 2023-10-17 13:17:33 -04:00
Elliot DeNolf
cfe1698dfd chore: update changelog 2023-10-17 13:15:36 -04:00
Elliot DeNolf
5722634660 chore(release): v2.0.8 2023-10-17 13:06:25 -04:00
Jacob Fletcher
cdbfc9132a chore: optimizes live preview (#3713) 2023-10-17 12:42:31 -04:00
Jacob Fletcher
dd0ac066ce feat(live-preview): caches field schema (#3711) 2023-10-17 12:11:55 -04:00
PatrikKozak
8b8ceabbdd fix(templates): user access control (#3712) 2023-10-17 12:07:37 -04:00
Alessio Gravili
8da18d3496 Merge pull request #3707 from payloadcms/fix/3683
fix(richtext-lexical): Blocks Field: Sub-forms being re-rendered unnecessarily
2023-10-17 17:33:05 +02:00
Elliot DeNolf
6cd4df3dc4 chore(script): properly version and publish non-latest packages 2023-10-17 11:30:09 -04:00
Elliot DeNolf
e74dc8633b ci: add retries to e2e tests (#3708)
* ci: add retries to e2e tests

* ci: add timeout_minutes for retry
2023-10-17 11:26:34 -04:00
Alessio Gravili
2f945919a3 chore: disable props for useIntersect hook 2023-10-17 17:24:50 +02:00
Alessio Gravili
52dc9177d7 chore: enable forceRender for richtext-lexical's Blocks feature form, and all their sub-forms 2023-10-17 16:44:34 +02:00
Alessio Gravili
cec757d098 chore: make sure intersectionObserver does not cause re-renders if forceRender is enabled 2023-10-17 16:34:44 +02:00
Jacob Fletcher
80e57150a0 chore: refines live preview shadow and bg (#3706) 2023-10-17 10:11:21 -04:00
161 changed files with 3863 additions and 1793 deletions

View File

@@ -10,7 +10,12 @@ body:
id: reproduction-link
attributes:
label: Link to reproduction
description: Please add a link to a reproduction. See the fork [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
description: Want us to look into your issue faster? Follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
validations:
required: false
- type: textarea
attributes:
label: Describe the Bug
validations:
required: true
- type: textarea
@@ -19,11 +24,6 @@ body:
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
validations:
required: true
- type: textarea
attributes:
label: Describe the Bug
validations:
required: true
- type: input
id: version
attributes:

View File

@@ -1,10 +1,11 @@
# Reproduction Guide
1. [fork](https://github.com/payloadcms/payload/fork) this repo
2. run `yarn` to install dependencies
3. open up the `test/_community` directory
4. add any necessary `collections/globals/fields` in this directory to recreate the issue you are experiencing
5. run `yarn dev _community` to start the admin panel
1. [Fork](https://github.com/payloadcms/payload/fork) this repo
2. Optionally, create a new branch for your reproduction
3. Run `pnpm install` to install dependencies
4. Open up the `test/_community` directory
5. Add any necessary `collections/globals/fields` in this directory to recreate the issue you are experiencing
6. Run `pnpm dev _community` to start the admin panel
**NOTE:** The goal is to isolate the problem by reducing the number of `collections/globals/fields` you add to the `test/_community` folder. This folder is _not_ meant for you to copy your project into, but rather recreate the issue you are experiencing with minimal config.
@@ -21,7 +22,7 @@
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
- `int.spec.ts` [Optional] - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
- `e2e.spec.ts` [Optional] - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests.
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `yarn dev:generate-types _community`.
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types _community`.
The directory split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config. You should modify the files in `test/_community` to get started.
@@ -44,7 +45,7 @@ There are a couple ways run integration tests:
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
```bash
yarn test:int _community
pnpm test:int _community
```
### Running E2E tests (Admin Panel UI tests)

View File

@@ -132,7 +132,12 @@ jobs:
key: ${{ github.sha }}-${{ github.run_number }}
- name: E2E Tests
run: pnpm test:e2e --part ${{ matrix.part }} --bail
uses: nick-fields/retry@v2
with:
retry_on: error
max_attempts: 2
timeout_minutes: 15
command: pnpm test:e2e --part ${{ matrix.part }} --bail
- uses: actions/upload-artifact@v3
if: always()

View File

@@ -1,3 +1,36 @@
## [2.0.10](https://github.com/payloadcms/payload/compare/v2.0.9...v2.0.10) (2023-10-17)
### Features
* filesRequired is optional for uploads ([#3668](https://github.com/payloadcms/payload/pull/3668)) ([48de897](https://github.com/payloadcms/payload/commit/48de89794b2c5d94183090b0830fd355d8d6c6f3))
### Bug Fixes
* Register first user verify update missing transaction id / req ([#3665](https://github.com/payloadcms/payload/pull/3665)) ([68c5a5751](https://github.com/payloadcms/payload/commit/68c5a57515ffbba37c9194a75d0f672bdb10d96b))
## [2.0.8](https://github.com/payloadcms/payload/compare/v2.0.7...v2.0.8) (2023-10-17)
### Features
* allows filterOptions to return null ([c4cac99](https://github.com/payloadcms/payload/commit/c4cac998752730e7084598c92c77789da8c82e0d))
* **live-preview:** caches field schema ([#3711](https://github.com/payloadcms/payload/issues/3711)) ([dd0ac06](https://github.com/payloadcms/payload/commit/dd0ac066ce2ed88b85025309303610a95b6089e1))
### Bug Fixes
* autosave time shown minutes only ([#3492](https://github.com/payloadcms/payload/issues/3492)) ([e311e8f](https://github.com/payloadcms/payload/commit/e311e8fff9cd4264d7a71903f63c4fa825a3564d))
* blocks within groups in postgres ([45a62ba](https://github.com/payloadcms/payload/commit/45a62ba949aca33b25e0808773a5c2f1cf4adf82))
* bug with seeding ecommerce ([993568a](https://github.com/payloadcms/payload/commit/993568a1959ea10f960e35e4ed7a8e06af672a72))
* corrects add block index ([#3681](https://github.com/payloadcms/payload/issues/3681)) ([3c50443](https://github.com/payloadcms/payload/commit/3c5044368d5b30c76a2ff20c25b9234ef89dc205))
* misc upload crop/focal point updates ([#3580](https://github.com/payloadcms/payload/issues/3580)) ([d616772](https://github.com/payloadcms/payload/commit/d6167727401a01282345e63636560e029ae8e0f3))
* renders mobile document controls ([#3695](https://github.com/payloadcms/payload/issues/3695)) ([1625ff2](https://github.com/payloadcms/payload/commit/1625ff244e6e81e6edc0357037c3abc1a3bf8ba7))
* some local operations missing req.transactionID ([#3651](https://github.com/payloadcms/payload/issues/3651)) ([150799e](https://github.com/payloadcms/payload/commit/150799e10e580281d1af49388eb142ee9639a002))
* **richtext-*:** extra fields not being iterated correctly ([#3693](https://github.com/payloadcms/payload/issues/3693)) ([b8a5866](https://github.com/payloadcms/payload/commit/b8a58666e70f604af1e1cf349bcb4f9add0147e7))
* **richtext-*:** link drawer form receiving incorrect field schema ([#3696](https://github.com/payloadcms/payload/issues/3696)) ([cb39354](https://github.com/payloadcms/payload/commit/cb39354a9de3d20960110e453f62c4aa166d8448))
* **richtext-lexical:** [#3682](https://github.com/payloadcms/payload/issues/3682) isolated editor container causing z-index issues ([24918fe](https://github.com/payloadcms/payload/commit/24918fe1d2ca251e211632765d370c214cef2a38))
* **templates:** user access control ([#3712](https://github.com/payloadcms/payload/issues/3712)) ([8b8ceab](https://github.com/payloadcms/payload/commit/8b8ceabbdd6354761e7d744cacb1192cac3a2427))
## [2.0.6](https://github.com/payloadcms/payload/compare/v2.0.5...v2.0.6) (2023-10-14)
### Bug Fixes

View File

@@ -11,19 +11,23 @@ keywords: documentation, getting started, guide, Content Management System, cms,
title="Payload Introduction - Closing the Gap Between Headless CMS and Application Frameworks"
/>
Payload is a headless CMS and application framework. It's meant to provide a massive boost to your
development process, but importantly, stay out of your way as your apps get more complex.
<Banner type="success">
Payload is a headless CMS and application framework. Its meant to provide a massive boost to your
development process, but importantly, stay out of your way as your apps get more complex.
<strong>Payload 2.0 has been released!</strong>
<br />
Includes Postgres support, Live Preview, Lexical Editor, and more. <a href="/blog/payload-2-0">Read the announcement</a>.
</Banner>
Out of the box, Payload gives you a lot of the things that you often need when developing a new website, web app, or native app:
- A MongoDB database to store your data
- A database to store your data (Postgres and MongoDB supported)
- A way to store, retrieve, and manipulate data of any shape via full REST and GraphQL APIs
- Authentication—complete with commonly required functionality like registration, email verification, login, & password reset
- Deep access control to your data, based on document or field-level functions
- File storage and access control
- A beautiful admin UI thats generated specifically to suit your data
- A beautiful admin UI that's generated specifically to suit your data
## What does "headless" mean?

View File

@@ -18,6 +18,32 @@ payload generate:types
You can run this command whenever you need to regenerate your types, and then you can use these types in your Payload code directly.
### Disable declare statement
By default, `generate:types` will add a `declare` statement to your types file, which automatically enables type inference within Payload.
If you are using your `payload-types.ts` file in other repos, though, it might be better to disable this `declare` statement, so that you don't get any TS errors in projects that use your Payload types, but do not have Payload installed.
```ts
// payload.config.ts
{
// ...
typescript: {
declare: false, // defaults to true if not set
},
}
```
If you do disable the `declare` pattern, you'll need to manually add a `declare` statement to your code in order for Payload types to be recognized. Here's an example showing how to declare your types in your `payload.config.ts` file:
```ts
import { Config } from './payload-types'
declare module 'payload' {
export interface GeneratedTypes extends Config {}
}
```
### Custom output file path
You can specify where you want your types to be generated by adding a property to your Payload config:

View File

@@ -40,20 +40,21 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
#### Collection Upload Options
| Option | Description |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. |
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) format) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| Option | Description |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. |
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) format) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
_An asterisk denotes that a property above is required._

View File

@@ -1,6 +1,6 @@
# Form Builder 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 [Form Builder Example](https://github.com/payloadcms/payload/tree/master/examples/form-builder/payload).
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 [Form Builder Example](https://github.com/payloadcms/payload/tree/main/examples/form-builder/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), we will add an example for that soon.
@@ -8,7 +8,7 @@ This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nex
### Payload
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/master/examples/form-builder/payload). If you have not done so already, clone it down and follow the setup instructions there.
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/form-builder/payload). If you have not done so already, clone it down and follow the setup instructions there.
### Next.js App
@@ -18,7 +18,7 @@ First you'll need a running Payload app. There is one made explicitly for this e
4. `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Form Builder Example](https://github.com/payloadcms/payload/tree/master/examples/form-builder/payload) for full details.
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Form Builder Example](https://github.com/payloadcms/payload/tree/main/examples/form-builder/payload) for full details.
## Learn More

View File

@@ -1,14 +1,14 @@
# Payload Live 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 [Live Preview Example](https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload).
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 [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-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/live-preview/next-pages).
> 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/main/examples/live-preview/next-pages).
## Getting Started
### Payload
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for authentication.
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for authentication.
### Next.js
@@ -18,7 +18,7 @@ First you'll need a running Payload app. There is one made explicitly for this e
4. `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Live Preview Example](https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload) for full details.
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload) for full details.
## Learn More
@@ -32,6 +32,6 @@ You can check out [the Payload GitHub repository](https://github.com/payloadcms/
## Deployment
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/master/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import).
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/main/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import).
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@payloadcms/live-preview-react": "^1.0.0-beta.3",
"@payloadcms/live-preview-react": "latest",
"escape-html": "^1.0.3",
"next": "^13.4.8",
"payload-admin-bar": "^1.0.6",

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
# Payload Live 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 [Live Preview Example](https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload).
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 [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-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/live-preview/next-app).
> 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/main/examples/live-preview/next-app).
## Getting Started
### Payload
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for authentication.
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for authentication.
### Next.js
@@ -18,7 +18,7 @@ First you'll need a running Payload app. There is one made explicitly for this e
4. `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Live Preview Example](https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload) for full details.
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload) for full details.
## Learn More
@@ -32,6 +32,6 @@ You can check out [the Payload GitHub repository](https://github.com/payloadcms/
## Deployment
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/master/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import).
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. You could also combine this app into a [single Express server](https://github.com/payloadcms/payload/tree/main/examples/custom-server) and deploy in to [Payload Cloud](https://payloadcms.com/new/import).
Check out our [Payload deployment documentation](https://payloadcms.com/docs/production/deployment) or the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@payloadcms/live-preview-react": "^1.0.0-beta.3",
"@payloadcms/live-preview-react": "latest",
"@types/escape-html": "^1.0.2",
"escape-html": "^1.0.3",
"next": "^13.4.8",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# Payload Live Preview Example
The [Payload Live Preview Example](https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload) demonstrates how to implement [Live Preview](https://payloadcms.com/docs/live-preview) in [Payload](https://github.com/payloadcms/payload). With Live Preview you can render your front-end application directly within the Admin panel. As you type, your changes take effect in real-time. No need to save a draft or publish your changes.
The [Payload Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload) demonstrates how to implement [Live Preview](https://payloadcms.com/docs/live-preview) in [Payload](https://github.com/payloadcms/payload). With Live Preview you can render your front-end application directly within the Admin panel. As you type, your changes take effect in real-time. No need to save a draft or publish your changes.
There are various fully working front-ends made explicitly for this example, including:
@@ -40,7 +40,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
}
```
For additional help with authentication, see the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs or the official [Auth Example](https://github.com/payloadcms/payload/tree/master/examples/auth).
For additional help with authentication, see the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs or the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth).
- #### Pages

View File

@@ -1,6 +1,6 @@
# Redirects 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 [Redirects Example](https://github.com/payloadcms/payload/tree/master/examples/redireects/payload).
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 [Redirects Example](https://github.com/payloadcms/payload/tree/main/examples/redireects/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), we will soon add a new example for you to use soon.
@@ -8,7 +8,7 @@ This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nex
### Payload
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/master/examples/redirects/payload). If you have not done so already, clone it down and follow the setup instructions there.
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/redirects/payload). If you have not done so already, clone it down and follow the setup instructions there.
### Next.js App

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/bundler-vite",
"version": "0.1.2",
"version": "0.1.3",
"description": "The officially supported Vite bundler adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -36,6 +36,7 @@
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.0",
"react-dom": "18.2.0"
},
"publishConfig": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/bundler-webpack",
"version": "1.0.3",
"version": "1.0.4",
"description": "The officially supported Webpack bundler adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -58,6 +58,9 @@
"@types/webpack-hot-middleware": "2.25.6",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.0"
},
"publishConfig": {
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",

View File

@@ -2,6 +2,7 @@ import type { SanitizedConfig } from 'payload/config'
import type { Configuration } from 'webpack'
import findNodeModules from 'find-node-modules'
import fs from 'fs'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import path from 'path'
import webpack from 'webpack'
@@ -9,78 +10,94 @@ import webpack from 'webpack'
const mockModulePath = path.resolve(__dirname, '../mocks/emptyModule.js')
const mockDotENVPath = path.resolve(__dirname, '../mocks/dotENV.js')
const nodeModulesPaths = findNodeModules({ cwd: process.cwd() })
const nodeModulesPath = path.resolve(nodeModulesPaths[0])
const adminFolderPath = path.resolve(nodeModulesPath, 'payload/dist/admin')
const nodeModulesPaths = findNodeModules({ cwd: process.cwd(), relative: false })
export const getBaseConfig = (payloadConfig: SanitizedConfig): Configuration => ({
entry: {
main: [adminFolderPath],
},
module: {
rules: [
{
exclude: /\/node_modules\/(?!.+\.tsx?$).*$/,
test: /\.(t|j)sx?$/,
use: [
{
loader: require.resolve('swc-loader'),
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
export const getBaseConfig = (payloadConfig: SanitizedConfig): Configuration => {
let nodeModulesPath = nodeModulesPaths.find((p) => {
const guess = path.resolve(p, 'payload/dist/admin')
if (fs.existsSync(guess)) {
return true
}
return false
})
if (!nodeModulesPath) {
nodeModulesPath = process.cwd()
}
const adminFolderPath = path.resolve(nodeModulesPath, 'payload/dist/admin')
const config: Configuration = {
entry: {
main: [adminFolderPath],
},
module: {
rules: [
{
exclude: /\/node_modules\/(?!.+\.tsx?$).*$/,
test: /\.(t|j)sx?$/,
use: [
{
loader: require.resolve('swc-loader'),
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
},
},
},
},
],
},
{
oneOf: [
{
test: /\.(?:ico|gif|png|jpg|jpeg|woff(2)?|eot|ttf|otf|svg)$/i,
type: 'asset/resource',
},
],
},
],
},
plugins: [
new webpack.ProvidePlugin({ process: require.resolve('process/browser') }),
new webpack.DefinePlugin(
Object.entries(process.env).reduce((values, [key, val]) => {
if (key.indexOf('PAYLOAD_PUBLIC_') === 0) {
return {
...values,
[`process.env.${key}`]: `'${val}'`,
],
},
{
oneOf: [
{
test: /\.(?:ico|gif|png|jpg|jpeg|woff(2)?|eot|ttf|otf|svg)$/i,
type: 'asset/resource',
},
],
},
],
},
plugins: [
new webpack.ProvidePlugin({ process: require.resolve('process/browser') }),
new webpack.DefinePlugin(
Object.entries(process.env).reduce((values, [key, val]) => {
if (key.indexOf('PAYLOAD_PUBLIC_') === 0) {
return {
...values,
[`process.env.${key}`]: `'${val}'`,
}
}
}
return values
}, {}),
),
new HtmlWebpackPlugin({
filename: path.normalize('./index.html'),
template: payloadConfig.admin.indexHTML,
}),
new webpack.HotModuleReplacementPlugin(),
],
resolve: {
alias: {
'@payloadcms/bundler-webpack': mockModulePath,
dotenv: mockDotENVPath,
path: require.resolve('path-browserify'),
payload$: mockModulePath,
'payload-config': payloadConfig.paths.rawConfig,
'payload-user-css': payloadConfig.admin.css,
return values
}, {}),
),
new HtmlWebpackPlugin({
filename: path.normalize('./index.html'),
template: payloadConfig.admin.indexHTML,
}),
new webpack.HotModuleReplacementPlugin(),
],
resolve: {
alias: {
'@payloadcms/bundler-webpack': mockModulePath,
dotenv: mockDotENVPath,
path: require.resolve('path-browserify'),
payload$: mockModulePath,
'payload-config': payloadConfig.paths.rawConfig,
'payload-user-css': payloadConfig.admin.css,
},
extensions: ['.ts', '.tsx', '.js', '.json'],
fallback: {
crypto: false,
http: false,
https: false,
},
modules: ['node_modules', nodeModulesPath, path.resolve(__dirname, '../../node_modules')],
},
extensions: ['.ts', '.tsx', '.js', '.json'],
fallback: {
crypto: false,
http: false,
https: false,
},
modules: ['node_modules', nodeModulesPath, path.resolve(__dirname, '../../node_modules')],
},
})
}
return config
}

View File

@@ -1 +1,3 @@
export const webpackBundler = () => {}
export default () => {}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.0.3",
"version": "1.0.4",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -35,6 +35,9 @@
"mongodb-memory-server": "8.13.0",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.0"
},
"publishConfig": {
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",

View File

@@ -52,7 +52,7 @@ export const init: Init = async function init(this: MongooseAdapter) {
const model = mongoose.model(
versionModelName,
versionSchema,
versionModelName,
this.autoPluralization === true ? undefined : versionModelName,
) as CollectionModel
// this.payload.versions[collection.slug] = model;
this.versions[collection.slug] = model

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.1.9",
"version": "0.1.10",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -34,6 +34,9 @@
"@types/to-snake-case": "1.0.0",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.0"
},
"publishConfig": {
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",

View File

@@ -128,7 +128,7 @@ export const traverseFields = ({
with: {},
}
if (adapter.tables[`${topLevelTableName}_${toSnakeCase(block.slug)}_locales`])
if (adapter.tables[`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}_locales`])
withBlock.with._locales = _locales
topLevelArgs.with[blockKey] = withBlock

View File

@@ -0,0 +1,13 @@
import type { Field } from 'payload/types'
export const idToUUID = (fields: Field[]): Field[] =>
fields.map((field) => {
if ('name' in field && field.name === 'id') {
return {
...field,
name: '_uuid',
}
}
return field
})

View File

@@ -26,6 +26,7 @@ import type { GenericColumns, PostgresAdapter } from '../types'
import { hasLocalesTable } from '../utilities/hasLocalesTable'
import { buildTable } from './build'
import { createIndex } from './createIndex'
import { idToUUID } from './idToUUID'
import { parentIDColumnMap } from './parentIDColumnMap'
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical'
@@ -281,7 +282,7 @@ export const traverseFields = ({
baseExtraConfig,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: field.fields,
fields: disableUnique ? idToUUID(field.fields) : field.fields,
rootRelationsToBuild,
rootRelationships: relationships,
rootTableIDColType,
@@ -349,7 +350,7 @@ export const traverseFields = ({
baseExtraConfig,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: block.fields,
fields: disableUnique ? idToUUID(block.fields) : block.fields,
rootRelationsToBuild,
rootRelationships: relationships,
rootTableIDColType,

View File

@@ -22,7 +22,7 @@ export const validateExistingBlockIsIdentical = ({
const fieldNames = flattenTopLevelFields(block.fields).flatMap((field) => field.name)
Object.keys(table).forEach((fieldName) => {
if (!['_locale', '_order', '_parentID', '_path'].includes(fieldName)) {
if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) {
if (fieldNames.indexOf(fieldName) === -1) {
throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${fieldName}, while the other block does not.`,

View File

@@ -31,6 +31,7 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
}
const blocks = createBlocksMap(data)
const deletions = []
const result = traverseFields<T>({
blocks,
@@ -38,6 +39,7 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
dataRef: {
id: data.id,
},
deletions,
fieldPrefix: '',
fields,
numbers,
@@ -46,5 +48,7 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
table: data,
})
deletions.forEach((deletion) => deletion())
return result
}

View File

@@ -22,6 +22,10 @@ type TraverseFieldsArgs = {
* The data reference to be mutated within this recursive function
*/
dataRef: Record<string, unknown>
/**
* Data that needs to be removed from the result after all fields have populated
*/
deletions: (() => void)[]
/**
* Column prefix can be built up by group and named tab fields
*/
@@ -54,6 +58,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
blocks,
config,
dataRef,
deletions,
fieldPrefix,
fields,
numbers,
@@ -69,6 +74,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
blocks,
config,
dataRef,
deletions,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
numbers,
@@ -87,6 +93,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
blocks,
config,
dataRef,
deletions,
fieldPrefix,
fields: field.fields,
numbers,
@@ -99,7 +106,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (fieldAffectsData(field)) {
const fieldName = `${fieldPrefix || ''}${field.name}`
const fieldData = table[fieldName]
if (field.type === 'array') {
if (Array.isArray(fieldData)) {
if (field.localized) {
@@ -109,11 +115,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
const locale = row._locale
const data = {}
delete row._locale
if (row._uuid) {
row.id = row._uuid
delete row._uuid
}
const rowResult = traverseFields<T>({
blocks,
config,
dataRef: data,
deletions,
fieldPrefix: '',
fields: field.fields,
numbers,
@@ -129,10 +140,15 @@ export const traverseFields = <T extends Record<string, unknown>>({
}, {})
} else {
result[field.name] = fieldData.map((row, i) => {
if (row._uuid) {
row.id = row._uuid
delete row._uuid
}
return traverseFields<T>({
blocks,
config,
dataRef: row,
deletions,
fieldPrefix: '',
fields: field.fields,
numbers,
@@ -155,6 +171,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
result[field.name] = {}
blocks[blockFieldPath].forEach((row) => {
if (row._uuid) {
row.id = row._uuid
delete row._uuid
}
if (typeof row._locale === 'string') {
if (!result[field.name][row._locale]) result[field.name][row._locale] = []
result[field.name][row._locale].push(row)
@@ -171,6 +191,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
blocks,
config,
dataRef: row,
deletions,
fieldPrefix: '',
fields: block.fields,
numbers,
@@ -189,6 +210,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
} else {
result[field.name] = blocks[blockFieldPath].map((row, i) => {
delete row._order
if (row._uuid) {
row.id = row._uuid
delete row._uuid
}
const block = field.blocks.find(({ slug }) => slug === row.blockType)
if (block) {
@@ -196,6 +221,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
blocks,
config,
dataRef: row,
deletions,
fieldPrefix: '',
fields: block.fields,
numbers,
@@ -345,6 +371,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
blocks,
config,
dataRef: groupLocaleData as Record<string, unknown>,
deletions,
fieldPrefix: groupFieldPrefix,
fields: field.fields,
numbers,
@@ -360,6 +387,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
blocks,
config,
dataRef: groupData as Record<string, unknown>,
deletions,
fieldPrefix: groupFieldPrefix,
fields: field.fields,
numbers,
@@ -425,5 +453,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
return result
}, dataRef)
if (Array.isArray(table._locales)) {
deletions.push(() => delete table._locales)
}
return formatted as T
}

View File

@@ -1,12 +1,14 @@
/* eslint-disable no-param-reassign */
import type { ArrayField } from 'payload/types'
import type { PostgresAdapter } from '../../types'
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types'
import { isArrayOfRows } from '../../utilities/isArrayOfRows'
import { traverseFields } from './traverseFields'
type Args = {
adapter: PostgresAdapter
arrayTableName: string
baseTableName: string
blocks: {
@@ -25,6 +27,7 @@ type Args = {
}
export const transformArray = ({
adapter,
arrayTableName,
baseTableName,
blocks,
@@ -38,6 +41,7 @@ export const transformArray = ({
selects,
}: Args) => {
const newRows: ArrayRowToInsert[] = []
const hasUUID = adapter.tables[arrayTableName]._uuid
if (isArrayOfRows(data)) {
data.forEach((arrayRow, i) => {
@@ -49,6 +53,16 @@ export const transformArray = ({
},
}
// If we have declared a _uuid field on arrays,
// that means the ID has to be unique,
// and our ids within arrays are not unique.
// So move the ID to a uuid field for storage
// and allow the database to generate a serial id automatically
if (hasUUID) {
newRow.row._uuid = arrayRow.id
delete arrayRow.id
}
if (locale) {
newRow.locales[locale] = {
_locale: locale,
@@ -60,6 +74,7 @@ export const transformArray = ({
}
traverseFields({
adapter,
arrays: newRow.arrays,
baseTableName,
blocks,

View File

@@ -3,11 +3,13 @@ import type { BlockField } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../../types'
import type { BlockRowToInsert, RelationshipToDelete } from './types'
import { traverseFields } from './traverseFields'
type Args = {
adapter: PostgresAdapter
baseTableName: string
blocks: {
[blockType: string]: BlockRowToInsert[]
@@ -24,6 +26,7 @@ type Args = {
}
}
export const transformBlocks = ({
adapter,
baseTableName,
blocks,
data,
@@ -56,7 +59,20 @@ export const transformBlocks = ({
const blockTableName = `${baseTableName}_blocks_${blockType}`
const hasUUID = adapter.tables[blockTableName]._uuid
// If we have declared a _uuid field on arrays,
// that means the ID has to be unique,
// and our ids within arrays are not unique.
// So move the ID to a uuid field for storage
// and allow the database to generate a serial id automatically
if (hasUUID) {
newRow.row._uuid = blockRow.id
delete blockRow.id
}
traverseFields({
adapter,
arrays: newRow.arrays,
baseTableName,
blocks,

View File

@@ -1,18 +1,26 @@
/* eslint-disable no-param-reassign */
import type { Field } from 'payload/types'
import type { PostgresAdapter } from '../../types'
import type { RowToInsert } from './types'
import { traverseFields } from './traverseFields'
type Args = {
adapter: PostgresAdapter
data: Record<string, unknown>
fields: Field[]
path?: string
tableName: string
}
export const transformForWrite = ({ data, fields, path = '', tableName }: Args): RowToInsert => {
export const transformForWrite = ({
adapter,
data,
fields,
path = '',
tableName,
}: Args): RowToInsert => {
// Split out the incoming data into rows to insert / delete
const rowToInsert: RowToInsert = {
arrays: {},
@@ -28,6 +36,7 @@ export const transformForWrite = ({ data, fields, path = '', tableName }: Args):
// This function is responsible for building up the
// above rowToInsert
traverseFields({
adapter,
arrays: rowToInsert.arrays,
baseTableName: tableName,
blocks: rowToInsert.blocks,

View File

@@ -4,6 +4,7 @@ import type { Field } from 'payload/types'
import { fieldAffectsData } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from '../../types'
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types'
import { isArrayOfRows } from '../../utilities/isArrayOfRows'
@@ -14,6 +15,7 @@ import { transformRelationship } from './relationships'
import { transformSelects } from './selects'
type Args = {
adapter: PostgresAdapter
arrays: {
[tableName: string]: ArrayRowToInsert[]
}
@@ -56,6 +58,7 @@ type Args = {
}
export const traverseFields = ({
adapter,
arrays,
baseTableName,
blocks,
@@ -95,6 +98,7 @@ export const traverseFields = ({
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
const newRows = transformArray({
adapter,
arrayTableName,
baseTableName,
blocks,
@@ -114,6 +118,7 @@ export const traverseFields = ({
}
} else {
const newRows = transformArray({
adapter,
arrayTableName,
baseTableName,
blocks,
@@ -138,6 +143,7 @@ export const traverseFields = ({
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
transformBlocks({
adapter,
baseTableName,
blocks,
data: localeData,
@@ -154,6 +160,7 @@ export const traverseFields = ({
}
} else if (isArrayOfRows(fieldData)) {
transformBlocks({
adapter,
baseTableName,
blocks,
data: fieldData,
@@ -174,6 +181,7 @@ export const traverseFields = ({
if (field.localized) {
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
traverseFields({
adapter,
arrays,
baseTableName,
blocks,
@@ -195,6 +203,7 @@ export const traverseFields = ({
})
} else {
traverseFields({
adapter,
arrays,
baseTableName,
blocks,
@@ -225,6 +234,7 @@ export const traverseFields = ({
if (tab.localized) {
Object.entries(data[tab.name]).forEach(([localeKey, localeData]) => {
traverseFields({
adapter,
arrays,
baseTableName,
blocks,
@@ -246,6 +256,7 @@ export const traverseFields = ({
})
} else {
traverseFields({
adapter,
arrays,
baseTableName,
blocks,
@@ -267,6 +278,7 @@ export const traverseFields = ({
}
} else {
traverseFields({
adapter,
arrays,
baseTableName,
blocks,
@@ -290,6 +302,7 @@ export const traverseFields = ({
if (field.type === 'row' || field.type === 'collapsible') {
traverseFields({
adapter,
arrays,
baseTableName,
blocks,

View File

@@ -28,6 +28,7 @@ export const upsertRow = async <T extends TypeWithID>({
// Split out the incoming data into the corresponding:
// base row, locales, relationships, blocks, and arrays
const rowToInsert = transformForWrite({
adapter,
data,
fields,
path,
@@ -107,6 +108,9 @@ export const upsertRow = async <T extends TypeWithID>({
rowToInsert.blocks[blockName].forEach((blockRow) => {
blockRow.row._parentID = insertedRow.id
if (!blocksToInsert[blockName]) blocksToInsert[blockName] = []
if (blockRow.row.uuid) {
delete blockRow.row.uuid
}
blocksToInsert[blockName].push(blockRow)
})
})

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/eslint-config",
"version": "0.0.1",
"version": "1.0.0",
"description": "Payload styles for ESLint and Prettier",
"license": "MIT",
"author": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "0.1.3",
"version": "0.1.4",
"description": "The official live preview React SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -17,7 +17,7 @@
"prepublishOnly": "pnpm clean && pnpm build"
},
"dependencies": {
"@payloadcms/live-preview": "workspace:*"
"@payloadcms/live-preview": "workspace:^0.x"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
@@ -25,6 +25,7 @@
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "0.1.3",
"version": "0.1.5",
"description": "The official live preview JavaScript SDK for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -20,6 +20,9 @@
"@payloadcms/eslint-config": "workspace:*",
"payload": "workspace:*"
},
"peerDependencies": {
"payload": "^2.0.0"
},
"exports": {
".": {
"default": "./src/index.ts",

View File

@@ -1,5 +1,11 @@
import { mergeData } from '.'
// For performance reasons, `fieldSchemaJSON` will only be sent once on the initial message
// We need to cache this value so that it can be used across subsequent messages
// To do this, save `fieldSchemaJSON` when it arrives as a global variable
// Send this cached value to `mergeData`, instead of `eventData.fieldSchemaJSON` directly
let payloadLivePreviewFieldSchema = undefined // TODO: type this from `fieldSchemaToJSON` return type
export const handleMessage = async <T>(args: {
depth: number
event: MessageEvent
@@ -11,9 +17,13 @@ export const handleMessage = async <T>(args: {
const eventData = JSON.parse(event?.data)
if (eventData.type === 'payload-live-preview') {
if (!payloadLivePreviewFieldSchema && eventData.fieldSchemaJSON) {
payloadLivePreviewFieldSchema = eventData.fieldSchemaJSON
}
const mergedData = await mergeData<T>({
depth,
fieldSchema: eventData.fieldSchemaJSON,
fieldSchema: payloadLivePreviewFieldSchema,
incomingData: eventData.data,
initialData,
serverURL,

View File

@@ -1,10 +1,12 @@
import type { fieldSchemaToJSON } from 'payload/utilities'
import { traverseFields } from './traverseFields'
export type MergeLiveDataArgs<T> = {
apiRoute?: string
depth: number
fieldSchema: Record<string, unknown>[]
incomingData: T
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: Partial<T>
initialData: T
serverURL: string
}

View File

@@ -2,11 +2,14 @@ export const ready = (args: { serverURL: string }): void => {
const { serverURL } = args
if (typeof window !== 'undefined') {
// This subscription may have been from either an iframe `src` or `window.open()`
// i.e. `window?.opener` || `window?.parent`
window?.opener?.postMessage(
// This subscription may have been from either an iframe or a popup
// We need to report 'ready' to the parent window, whichever it may be
// i.e. `window?.opener` for popups, `window?.parent` for iframes
const windowToPostTo: Window = window?.opener || window?.parent
windowToPostTo?.postMessage(
JSON.stringify({
popupReady: true,
ready: true,
type: 'payload-live-preview',
}),
serverURL,

View File

@@ -9,7 +9,7 @@ export const subscribe = <T>(args: {
const { callback, depth, initialData, serverURL } = args
const onMessage = async (event: MessageEvent) => {
const mergedData = await handleMessage({ depth, event, initialData, serverURL })
const mergedData = await handleMessage<T>({ depth, event, initialData, serverURL })
callback(mergedData)
}

View File

@@ -1,9 +1,11 @@
import type { fieldSchemaToJSON } from 'payload/utilities'
import { promise } from './promise'
type Args<T> = {
apiRoute?: string
depth: number
fieldSchema: Record<string, unknown>[]
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: T
populationPromises: Promise<void>[]
result: T
@@ -19,12 +21,11 @@ export const traverseFields = <T>({
result,
serverURL,
}: Args<T>): void => {
fieldSchema.forEach((field) => {
if ('name' in field && typeof field.name === 'string') {
// TODO: type this
const fieldName = field.name
fieldSchema.forEach((fieldJSON) => {
if ('name' in fieldJSON && typeof fieldJSON.name === 'string') {
const fieldName = fieldJSON.name
switch (field.type) {
switch (fieldJSON.type) {
case 'array':
if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((row, i) => {
@@ -38,7 +39,7 @@ export const traverseFields = <T>({
traverseFields({
apiRoute,
depth,
fieldSchema: field.fields as Record<string, unknown>[], // TODO: type this
fieldSchema: fieldJSON.fields,
incomingData: row,
populationPromises,
result: newRow,
@@ -52,37 +53,39 @@ export const traverseFields = <T>({
case 'blocks':
if (Array.isArray(incomingData[fieldName])) {
result[fieldName] = incomingData[fieldName].map((row, i) => {
const matchedBlock = field.blocks[row.blockType]
result[fieldName] = incomingData[fieldName].map((incomingBlock, i) => {
const incomingBlockJSON = fieldJSON.blocks[incomingBlock.blockType]
const hasExistingRow =
// Compare the index and id to determine if this block already exists in the result
// If so, we want to use the existing block as the base, otherwise take the incoming block
// Either way, we will traverse the fields of the block to populate relationships
const isExistingBlock =
Array.isArray(result[fieldName]) &&
typeof result[fieldName][i] === 'object' &&
result[fieldName][i] !== null &&
result[fieldName][i].blockType === row.blockType
result[fieldName][i].id === incomingBlock.id
const newRow = hasExistingRow
? { ...result[fieldName][i] }
: {
blockType: matchedBlock.slug,
}
const block = isExistingBlock ? result[fieldName][i] : incomingBlock
traverseFields({
apiRoute,
depth,
fieldSchema: matchedBlock.fields as Record<string, unknown>[], // TODO: type this
incomingData: row,
fieldSchema: incomingBlockJSON.fields,
incomingData: incomingBlock,
populationPromises,
result: newRow,
result: block,
serverURL,
})
return newRow
return block
})
} else {
result[fieldName] = []
}
break
case 'tab':
case 'tabs':
case 'group':
if (!result[fieldName]) {
result[fieldName] = {}
@@ -91,7 +94,7 @@ export const traverseFields = <T>({
traverseFields({
apiRoute,
depth,
fieldSchema: field.fields as Record<string, unknown>[], // TODO: type this
fieldSchema: fieldJSON.fields,
incomingData: incomingData[fieldName] || {},
populationPromises,
result: result[fieldName],
@@ -102,7 +105,7 @@ export const traverseFields = <T>({
case 'upload':
case 'relationship':
if (field.hasMany && Array.isArray(incomingData[fieldName])) {
if (fieldJSON.hasMany && Array.isArray(incomingData[fieldName])) {
const existingValue = Array.isArray(result[fieldName]) ? [...result[fieldName]] : []
result[fieldName] = Array.isArray(result[fieldName])
? [...result[fieldName]].slice(0, incomingData[fieldName].length)
@@ -110,7 +113,7 @@ export const traverseFields = <T>({
incomingData[fieldName].forEach((relation, i) => {
// Handle `hasMany` polymorphic
if (Array.isArray(field.relationTo)) {
if (Array.isArray(fieldJSON.relationTo)) {
const existingID = existingValue[i]?.value?.id
if (
@@ -134,7 +137,7 @@ export const traverseFields = <T>({
)
}
} else {
// Handle `hasMany` singular
// Handle `hasMany` monomorphic
const existingID = existingValue[i]?.id
if (existingID !== relation) {
@@ -143,7 +146,7 @@ export const traverseFields = <T>({
id: relation,
accessor: i,
apiRoute,
collection: String(field.relationTo),
collection: String(fieldJSON.relationTo),
depth,
ref: result[fieldName],
serverURL,
@@ -154,7 +157,7 @@ export const traverseFields = <T>({
})
} else {
// Handle `hasOne` polymorphic
if (Array.isArray(field.relationTo)) {
if (Array.isArray(fieldJSON.relationTo)) {
const hasNewValue =
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
const hasOldValue =
@@ -190,31 +193,37 @@ export const traverseFields = <T>({
result[fieldName] = null
}
} else {
const hasNewValue =
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
const hasOldValue =
typeof result[fieldName] === 'object' && result[fieldName] !== null
// Handle `hasOne` monomorphic
const newID: string =
(typeof incomingData[fieldName] === 'string' && incomingData[fieldName]) ||
(typeof incomingData[fieldName] === 'object' &&
incomingData[fieldName] !== null &&
incomingData[fieldName].id) ||
''
const newValue = hasNewValue ? incomingData[fieldName].value : ''
const oldID: string =
(typeof result[fieldName] === 'string' && result[fieldName]) ||
(typeof result[fieldName] === 'object' &&
result[fieldName] !== null &&
result[fieldName].id) ||
''
const oldValue = hasOldValue ? result[fieldName].value : ''
if (newValue !== oldValue) {
if (newValue) {
if (newID !== oldID) {
if (newID) {
populationPromises.push(
promise({
id: newValue,
id: newID,
accessor: fieldName,
apiRoute,
collection: String(field.relationTo),
collection: String(fieldJSON.relationTo),
depth,
ref: result as Record<string, unknown>,
serverURL,
}),
)
} else {
result[fieldName] = null
}
} else {
result[fieldName] = null
}
}
}

View File

@@ -12,7 +12,7 @@ module.exports = {
skipChecks: true,
},
hooks: {
'before:init': ['pnpm install', 'pnpm clean', 'pnpm test'],
'before:init': ['pnpm install', 'pnpm clean', 'pnpm build'], // Assume tests have already been run
},
plugins: {
'@release-it/conventional-changelog': {

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.0.7",
"version": "2.0.11",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",
@@ -127,7 +127,7 @@
"react-select": "5.7.4",
"react-toastify": "8.2.0",
"sanitize-filename": "1.6.3",
"sass": "1.64.0",
"sass": "1.69.4",
"scheduler": "0.23.0",
"scmp": "2.1.0",
"sharp": "0.31.3",

View File

@@ -31,13 +31,14 @@ const DeleteDocument: React.FC<Props> = (props) => {
routes: { admin, api },
serverURL,
} = useConfig()
const { setModified } = useForm()
const [deleting, setDeleting] = useState(false)
const { toggleModal } = useModal()
const history = useHistory()
const { i18n, t } = useTranslation('general')
const title = useTitle({ collection })
const titleToRender = titleFromProps || title
const titleToRender = titleFromProps || title || id
const modalSlug = `delete-${id}`

View File

@@ -1,7 +1,7 @@
@import '../../../scss/styles.scss';
.doc-controls {
@include blur-bg;
@include blur-bg-light;
position: sticky;
top: 0;
width: 100%;
@@ -9,7 +9,7 @@
display: flex;
align-items: center;
&::after {
&__divider {
content: '';
display: block;
position: absolute;

View File

@@ -225,6 +225,7 @@ export const DocumentControls: React.FC<{
)}
</div>
</div>
<div className={`${baseClass}__divider`} />
</Gutter>
)
}

View File

@@ -10,6 +10,19 @@
justify-content: center;
align-items: center;
white-space: nowrap;
// Use a pseudo element for the accessability so that it doesn't take up DOM space
// Also because the parent element has `overflow: hidden` which would clip an outline
&:focus-visible::after {
content: '';
border: var(--accessibility-outline);
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
}
}
&:focus:not(:focus-visible) {

View File

@@ -64,6 +64,7 @@ export const DocumentTab: React.FC<DocumentTabProps & DocumentTabConfig> = (prop
className={`${baseClass}__link`}
to={href}
{...(newTab && { rel: 'noopener noreferrer', target: '_blank' })}
tabIndex={isActive ? -1 : 0}
>
<span className={`${baseClass}__label`}>
{labelToRender}

View File

@@ -43,7 +43,7 @@ export const tabs: DocumentTabConfig[] = [
// API
{
condition: ({ collection, global }) =>
!collection?.admin?.hideAPIURL || !global?.admin?.hideAPIURL,
(collection && !collection?.admin?.hideAPIURL) || (global && !global?.admin?.hideAPIURL),
href: '/api',
label: 'API',
},

View File

@@ -1,6 +1,6 @@
@import '../../../scss/styles.scss';
$transTime: 200ms;
$transTime: 200;
.drawer {
display: flex;
@@ -9,7 +9,7 @@ $transTime: 200ms;
height: 100vh;
&__blur-bg {
@include blur-bg();
@include blur-bg;
position: absolute;
z-index: 1;
top: 0;
@@ -17,7 +17,7 @@ $transTime: 200ms;
bottom: 0;
left: 0;
opacity: 0;
transition: all $transTime linear;
transition: all #{$transTime}ms linear;
}
&__content {
@@ -27,7 +27,8 @@ $transTime: 200ms;
z-index: 2;
width: calc(100% - var(--gutter-h));
overflow: hidden;
transition: all $transTime linear;
transition: all #{$transTime}ms linear;
background-color: var(--theme-bg);
}
&__content-children {
@@ -40,14 +41,14 @@ $transTime: 200ms;
&--is-open {
.drawer {
&__content,
&__blur-bg,
&__close {
&__blur-bg {
opacity: 1;
}
&__close {
transition: opacity $transTime linear;
transition-delay: $transTime;
opacity: 0.1;
transition: opacity #{$transTime}ms linear;
transition-delay: #{calc($transTime / 2)}ms;
}
&__content {
@@ -68,7 +69,7 @@ $transTime: 200ms;
transition: none;
transition-delay: 0ms;
flex-grow: 1;
background: transparent;
background: var(--theme-elevation-800);
&:active,
&:focus {
@@ -120,7 +121,15 @@ $transTime: 200ms;
}
html[data-theme='dark'] {
.drawer__close {
background: rgba(0, 0, 0, 0.2);
.drawer {
&__close {
background: var(--color-base-1000);
}
&--is-open {
.drawer__close {
opacity: 0.25;
}
}
}
}

View File

@@ -1,5 +1,4 @@
import { Modal, useModal } from '@faceless-ui/modal'
import { useWindowInfo } from '@faceless-ui/window-info'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -52,9 +51,6 @@ export const Drawer: React.FC<Props> = ({
}) => {
const { t } = useTranslation('general')
const { closeModal, modalState } = useModal()
const {
breakpoints: { m: midBreak },
} = useWindowInfo()
const drawerDepth = useEditDepth()
const [isOpen, setIsOpen] = useState(false)
const [animateIn, setAnimateIn] = useState(false)
@@ -72,7 +68,12 @@ export const Drawer: React.FC<Props> = ({
return (
<Modal
className={[className, baseClass, animateIn && `${baseClass}--is-open`]
className={[
className,
baseClass,
animateIn && `${baseClass}--is-open`,
drawerDepth > 1 && `${baseClass}--nested`,
]
.filter(Boolean)
.join(' ')}
slug={slug}
@@ -80,7 +81,7 @@ export const Drawer: React.FC<Props> = ({
zIndex: zBase + drawerDepth,
}}
>
{drawerDepth === 1 && <div className={`${baseClass}__blur-bg`} />}
{(!drawerDepth || drawerDepth === 1) && <div className={`${baseClass}__blur-bg`} />}
<button
aria-label={t('close')}
className={`${baseClass}__close`}
@@ -89,7 +90,7 @@ export const Drawer: React.FC<Props> = ({
type="button"
/>
<div className={`${baseClass}__content`}>
<div className={`${baseClass}__blur-bg`} />
<div className={`${baseClass}__blur-bg-content`} />
<Gutter className={`${baseClass}__content-children`} left={gutter} right={gutter}>
<EditDepthContext.Provider value={drawerDepth + 1}>
{header && header}

View File

@@ -97,7 +97,9 @@ export const EditUpload: React.FC<{
const centerFocalPoint = () => {
const containerRect = focalWrapRef.current.getBoundingClientRect()
const boundsRect = cropRef.current.getBoundingClientRect()
const boundsRect = showCrop
? cropRef.current.getBoundingClientRect()
: imageRef.current.getBoundingClientRect()
const xCenter =
((boundsRect.left - containerRect.left + boundsRect.width / 2) / containerRect.width) * 100
const yCenter =
@@ -164,17 +166,19 @@ export const EditUpload: React.FC<{
) : (
<img alt={t('upload:setFocalPoint')} ref={imageRef} src={fileSrcToUse} />
)}
<DraggableElement
boundsRef={cropRef}
checkBounds={checkBounds}
className={`${baseClass}__focalPoint`}
containerRef={focalWrapRef}
initialPosition={pointPosition}
onDragEnd={onDragEnd}
setCheckBounds={setCheckBounds}
>
<Plus />
</DraggableElement>
{showFocalPoint && (
<DraggableElement
boundsRef={showCrop ? cropRef : imageRef}
checkBounds={showCrop ? checkBounds : false}
className={`${baseClass}__focalPoint`}
containerRef={focalWrapRef}
initialPosition={pointPosition}
onDragEnd={onDragEnd}
setCheckBounds={showCrop ? setCheckBounds : false}
>
<Plus />
</DraggableElement>
)}
</div>
</div>
{(showCrop || showFocalPoint) && (

View File

@@ -7,27 +7,36 @@
background-color: transparent;
outline: none;
position: relative;
@include blur-bg;
--hamburger-padding: 8px;
--hamburger-size: 9px;
--hamburger-line-gap: 3px;
padding: var(--hamburger-padding);
border: 1px solid var(--theme-elevation-150);
color: var(--theme-text);
border-radius: 3px;
&:hover {
border: 1px solid var(--theme-elevation-500);
background-color: var(--theme-elevation-100);
}
&__wrapper {
border: 1px solid var(--theme-elevation-150);
padding: var(--hamburger-padding);
border-radius: 3px;
position: relative;
z-index: 1;
height: 100%;
width: 100%;
&:focus {
outline: none;
&:hover {
border: 1px solid var(--theme-elevation-500);
background-color: var(--theme-elevation-100);
}
&:focus {
outline: none;
}
}
&__icon {
position: relative;
z-index: 1;
height: var(--hamburger-size);
width: var(--hamburger-size);
display: flex;

View File

@@ -14,32 +14,34 @@ export const Hamburger: React.FC<{
const { closeIcon = 'x', isActive = false } = props
return (
<div className={[baseClass].filter(Boolean).join(' ')}>
<div className={`${baseClass}__icon`}>
{!isActive && (
<div className={`${baseClass}__lines`} title={t('open')}>
<div className={`${baseClass}__line ${baseClass}__top`} />
<div className={`${baseClass}__line ${baseClass}__middle`} />
<div className={`${baseClass}__line ${baseClass}__bottom`} />
</div>
)}
{isActive && (
<div
aria-label={closeIcon === 'collapse' ? t('collapse') : t('close')}
className={`${baseClass}__close-icon`}
title={closeIcon === 'collapse' ? t('collapse') : t('close')}
>
{closeIcon === 'x' && (
<React.Fragment>
<div className={`${baseClass}__line ${baseClass}__x-left`} />
<div className={`${baseClass}__line ${baseClass}__x-right`} />
</React.Fragment>
)}
{closeIcon === 'collapse' && (
<Chevron className={`${baseClass}__collapse-chevron`} direction="left" />
)}
</div>
)}
<div className={baseClass}>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__icon`}>
{!isActive && (
<div className={`${baseClass}__lines`} title={t('open')}>
<div className={`${baseClass}__line ${baseClass}__top`} />
<div className={`${baseClass}__line ${baseClass}__middle`} />
<div className={`${baseClass}__line ${baseClass}__bottom`} />
</div>
)}
{isActive && (
<div
aria-label={closeIcon === 'collapse' ? t('collapse') : t('close')}
className={`${baseClass}__close-icon`}
title={closeIcon === 'collapse' ? t('collapse') : t('close')}
>
{closeIcon === 'x' && (
<React.Fragment>
<div className={`${baseClass}__line ${baseClass}__x-left`} />
<div className={`${baseClass}__line ${baseClass}__x-right`} />
</React.Fragment>
)}
{closeIcon === 'collapse' && (
<Chevron className={`${baseClass}__collapse-chevron`} direction="left" />
)}
</div>
)}
</div>
</div>
</div>
)

View File

@@ -24,7 +24,6 @@
}
&__bg {
@include blur-bg;
opacity: 0;
position: absolute;
left: 0;
@@ -58,9 +57,28 @@
}
&__account {
position: relative;
&:focus:not(:focus-visible) {
opacity: 1;
}
// Use a pseudo element for the accessability so that it doesn't take up DOM space
// Also because the parent element has `overflow: hidden` which would clip an outline
&:focus-visible {
outline: none;
&::after {
content: '';
border: var(--accessibility-outline);
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
}
}
}
&__controls-wrapper {

View File

@@ -14,7 +14,7 @@ import './index.scss'
const baseClass = 'app-header'
export const AppHeader: React.FC = (props) => {
export const AppHeader: React.FC = () => {
const { t } = useTranslation()
const {
@@ -29,7 +29,7 @@ export const AppHeader: React.FC = (props) => {
<div className={`${baseClass}__bg`} />
<div className={`${baseClass}__content`}>
<div className={`${baseClass}__wrapper`}>
<NavToggler className={`${baseClass}__mobile-nav-toggler`}>
<NavToggler className={`${baseClass}__mobile-nav-toggler`} tabIndex={-1}>
<Hamburger />
</NavToggler>
<div className={`${baseClass}__controls-wrapper`}>
@@ -46,6 +46,7 @@ export const AppHeader: React.FC = (props) => {
<Link
aria-label={t('authentication:account')}
className={`${baseClass}__account`}
tabIndex={0}
to={`${adminRoute}/account`}
>
<Account />

View File

@@ -8,20 +8,22 @@ import RenderCustomComponent from '../../utilities/RenderCustomComponent'
const baseClass = 'nav'
const DefaultLogout = () => {
const DefaultLogout: React.FC<{
tabIndex?: number
}> = ({ tabIndex }) => {
const { t } = useTranslation('authentication')
const config = useConfig()
const {
admin: {
components: { logout },
logoutRoute,
},
admin: { logoutRoute },
routes: { admin },
} = config
return (
<Link
aria-label={t('logOut')}
className={`${baseClass}__log-out`}
tabIndex={tabIndex}
to={`${admin}${logoutRoute}`}
>
<LogOut />
@@ -29,7 +31,9 @@ const DefaultLogout = () => {
)
}
const Logout: React.FC = () => {
const Logout: React.FC<{
tabIndex?: number
}> = ({ tabIndex = 0 }) => {
const {
admin: {
components: {
@@ -40,7 +44,15 @@ const Logout: React.FC = () => {
} = {},
} = useConfig()
return <RenderCustomComponent CustomComponent={CustomLogout} DefaultComponent={DefaultLogout} />
return (
<RenderCustomComponent
CustomComponent={CustomLogout}
DefaultComponent={DefaultLogout}
componentProps={{
tabIndex,
}}
/>
)
}
export default Logout

View File

@@ -12,8 +12,9 @@ export const NavToggler: React.FC<{
children?: React.ReactNode
className?: string
id?: string
tabIndex?: number
}> = (props) => {
const { id, children, className } = props
const { id, children, className, tabIndex = 0 } = props
const { t } = useTranslation('general')
@@ -43,6 +44,7 @@ export const NavToggler: React.FC<{
})
}
}}
tabIndex={tabIndex}
type="button"
>
{children}

View File

@@ -16,7 +16,6 @@
}
&__header {
@include blur-bg;
position: absolute;
top: 0;
width: 100%;

View File

@@ -102,6 +102,7 @@ const DefaultNav: React.FC = () => {
className={`${baseClass}__link`}
id={id}
key={i}
tabIndex={!navOpen ? -1 : undefined}
to={href}
>
<span className={`${baseClass}__link-icon`}>
@@ -117,7 +118,7 @@ const DefaultNav: React.FC = () => {
{Array.isArray(afterNavLinks) &&
afterNavLinks.map((Component, i) => <Component key={i} />)}
<div className={`${baseClass}__controls`}>
<Logout />
<Logout tabIndex={!navOpen ? -1 : undefined} />
</div>
</nav>
</div>
@@ -128,6 +129,7 @@ const DefaultNav: React.FC = () => {
onClick={() => {
setNavOpen(false)
}}
tabIndex={!navOpen ? -1 : undefined}
type="button"
>
<Hamburger isActive />

View File

@@ -1,8 +1,9 @@
import React, { useEffect, useState } from 'react'
import AnimateHeight from 'react-animate-height'
import Chevron from '../../icons/Chevron'
import { usePreferences } from '../../utilities/Preferences'
import { useNav } from '../Nav/context'
import './index.scss'
const baseClass = 'nav-group'
@@ -16,6 +17,7 @@ const NavGroup: React.FC<Props> = ({ children, label }) => {
const [collapsed, setCollapsed] = useState(true)
const [animate, setAnimate] = useState(false)
const { getPreference, setPreference } = usePreferences()
const { navOpen } = useNav()
const preferencesKey = `collapsed-${label}-groups`
@@ -44,13 +46,12 @@ const NavGroup: React.FC<Props> = ({ children, label }) => {
return (
<div
id={`nav-group-${label}`}
className={[`${baseClass}`, `${label}`, collapsed && `${baseClass}--collapsed`]
.filter(Boolean)
.join(' ')}
id={`nav-group-${label}`}
>
<button
type="button"
className={[
`${baseClass}__toggle`,
`${baseClass}__toggle--${collapsed ? 'collapsed' : 'open'}`,
@@ -58,6 +59,8 @@ const NavGroup: React.FC<Props> = ({ children, label }) => {
.filter(Boolean)
.join(' ')}
onClick={toggleCollapsed}
tabIndex={!navOpen ? -1 : 0}
type="button"
>
<div className={`${baseClass}__label`}>{label}</div>
<div className={`${baseClass}__indicator`}>
@@ -67,7 +70,7 @@ const NavGroup: React.FC<Props> = ({ children, label }) => {
/>
</div>
</button>
<AnimateHeight height={collapsed ? 0 : 'auto'} duration={animate ? 200 : 0}>
<AnimateHeight duration={animate ? 200 : 0} height={collapsed ? 0 : 'auto'}>
<div className={`${baseClass}__content`}>{children}</div>
</AnimateHeight>
</div>

View File

@@ -36,7 +36,7 @@ export const PopupTrigger: React.FC<Props> = (props) => {
}
return (
<button className={classes} onClick={() => setActive(!active)} type="button">
<button className={classes} onClick={() => setActive(!active)} tabIndex={0} type="button">
{button}
</button>
)

View File

@@ -13,6 +13,27 @@
background: linear-gradient(to right, transparent, var(--theme-bg));
}
// Use a pseudo element for the accessability so that it doesn't take up DOM space
// Also because the parent element has `overflow: hidden` which would clip an outline
&__home {
position: relative;
&:focus-visible {
outline: none;
&::after {
content: '';
border: var(--accessibility-outline);
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
}
}
}
* {
display: block;
}

View File

@@ -9,6 +9,8 @@ import { getTranslation } from '../../../../utilities/getTranslation'
import { useConfig } from '../../utilities/Config'
import './index.scss'
const baseClass = 'step-nav'
const Context = createContext({} as ContextType)
const StepNavProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
@@ -41,32 +43,34 @@ const StepNav: React.FC<{
} = config
return (
<nav className={['step-nav', className].filter(Boolean).join(' ')}>
<Fragment>
{stepNav.length > 0 ? (
<Fragment>
<Link to={admin}>
<nav className={[baseClass, className].filter(Boolean).join(' ')}>
<Link className={`${baseClass}__home`} tabIndex={0} to={admin}>
<IconGraphic />
</Link>
<span>/</span>
</Fragment>
) : (
<IconGraphic />
)}
{stepNav.map((item, i) => {
const StepLabel = <span key={i}>{getTranslation(item.label, i18n)}</span>
const Step =
stepNav.length === i + 1 ? (
StepLabel
) : (
<Fragment key={i}>
{item.url ? <Link to={item.url}>{StepLabel}</Link> : StepLabel}
<span>/</span>
</Fragment>
)
{stepNav.map((item, i) => {
const StepLabel = <span key={i}>{getTranslation(item.label, i18n)}</span>
const Step =
stepNav.length === i + 1 ? (
StepLabel
) : (
<Fragment key={i}>
{item.url ? <Link to={item.url}>{StepLabel}</Link> : StepLabel}
<span>/</span>
</Fragment>
)
return Step
})}
</nav>
return Step
})}
</nav>
) : (
<div className={[baseClass, className].filter(Boolean).join(' ')}>
<IconGraphic />
</div>
)}
</Fragment>
)
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { Props } from './types'
@@ -37,7 +37,7 @@ const RenderFields: React.FC<Props> = (props) => {
const { i18n, t } = useTranslation('general')
const [hasRendered, setHasRendered] = useState(Boolean(forceRender))
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions)
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions, forceRender)
const isIntersecting = Boolean(entry?.isIntersecting)
const isAboveViewport = entry?.boundingClientRect?.top < 0
@@ -105,6 +105,7 @@ const RenderFields: React.FC<Props> = (props) => {
readOnly,
},
fieldTypes,
forceRender,
indexPath:
'indexPath' in props ? `${props?.indexPath}.${fieldIndex}` : `${fieldIndex}`,
path: field.path || (isFieldAffectingData && 'name' in field ? field.name : ''),

View File

@@ -24,6 +24,7 @@ type ArrayRowProps = UseDraggableSortableReturn &
CustomRowLabel?: RowLabelType
addRow: (rowIndex: number) => void
duplicateRow: (rowIndex: number) => void
forceRender?: boolean
hasMaxRows?: boolean
moveRow: (fromIndex: number, toIndex: number) => void
readOnly?: boolean
@@ -40,6 +41,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
duplicateRow,
fieldTypes,
fields,
forceRender = false,
hasMaxRows,
indexPath,
labels,
@@ -126,10 +128,11 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
path: createNestedFieldPath(path, field),
}))}
fieldTypes={fieldTypes}
forceRender={forceRender}
indexPath={indexPath}
margins="small"
permissions={permissions?.fields}
readOnly={readOnly}
margins="small"
/>
</Collapsible>
</div>

View File

@@ -32,6 +32,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
admin: { className, components, condition, description, readOnly },
fieldTypes,
fields,
forceRender = false,
indexPath,
localized,
maxRows,
@@ -234,6 +235,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
duplicateRow={duplicateRow}
fieldTypes={fieldTypes}
fields={fields}
forceRender={forceRender}
hasMaxRows={hasMaxRows}
indexPath={indexPath}
labels={labels}

View File

@@ -4,6 +4,7 @@ import type { ArrayField } from '../../../../../fields/config/types'
export type Props = Omit<ArrayField, 'type'> & {
fieldTypes: FieldTypes
forceRender?: boolean
indexPath: string
label: false | string
path?: string

View File

@@ -24,6 +24,7 @@ type BlockFieldProps = UseDraggableSortableReturn &
addRow: (rowIndex: number, blockType: string) => void
blockToRender: Block
duplicateRow: (rowIndex: number) => void
forceRender?: boolean
hasMaxRows?: boolean
moveRow: (fromIndex: number, toIndex: number) => void
readOnly: boolean
@@ -40,6 +41,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
blocks,
duplicateRow,
fieldTypes,
forceRender,
hasMaxRows,
indexPath,
labels,
@@ -130,6 +132,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
path: createNestedFieldPath(path, field),
}))}
fieldTypes={fieldTypes}
forceRender={forceRender}
indexPath={indexPath}
margins="small"
permissions={permissions?.blocks?.[row.blockType]?.fields}

View File

@@ -37,6 +37,7 @@ const BlocksField: React.FC<Props> = (props) => {
admin: { className, condition, description, readOnly },
blocks,
fieldTypes,
forceRender = false,
indexPath,
label,
labels: labelsFromProps,
@@ -238,6 +239,7 @@ const BlocksField: React.FC<Props> = (props) => {
blocks={blocks}
duplicateRow={duplicateRow}
fieldTypes={fieldTypes}
forceRender={forceRender}
hasMaxRows={hasMaxRows}
indexPath={indexPath}
labels={labels}

View File

@@ -4,6 +4,7 @@ import type { BlockField } from '../../../../../fields/config/types'
export type Props = Omit<BlockField, 'type'> & {
fieldTypes: FieldTypes
forceRender?: boolean
indexPath: string
path?: string
permissions: FieldPermissions

View File

@@ -14,9 +14,9 @@ import { WatchChildErrors } from '../../WatchChildErrors'
import withCondition from '../../withCondition'
import { useRow } from '../Row/provider'
import { useTabs } from '../Tabs/provider'
import { fieldBaseClass } from '../shared'
import './index.scss'
import { GroupProvider, useGroup } from './provider'
import { fieldBaseClass } from '../shared'
const baseClass = 'group-field'
@@ -26,6 +26,7 @@ const Group: React.FC<Props> = (props) => {
admin: { className, description, hideGutter = false, readOnly, style, width },
fieldTypes,
fields,
forceRender = false,
indexPath,
label,
path: pathFromProps,
@@ -88,6 +89,7 @@ const Group: React.FC<Props> = (props) => {
path: createNestedFieldPath(path, subField),
}))}
fieldTypes={fieldTypes}
forceRender={forceRender}
indexPath={indexPath}
margins="small"
permissions={permissions?.fields}

View File

@@ -4,6 +4,7 @@ import type { GroupField } from '../../../../../fields/config/types'
export type Props = Omit<GroupField, 'type'> & {
fieldTypes: FieldTypes
forceRender?: boolean
indexPath: string
path?: string
permissions: FieldPermissions

View File

@@ -14,6 +14,7 @@ const Row: React.FC<Props> = (props) => {
admin: { className, readOnly },
fieldTypes,
fields,
forceRender = false,
indexPath,
path,
permissions,
@@ -28,6 +29,7 @@ const Row: React.FC<Props> = (props) => {
path: createNestedFieldPath(path, field),
}))}
fieldTypes={fieldTypes}
forceRender={forceRender}
indexPath={indexPath}
permissions={permissions}
readOnly={readOnly}

View File

@@ -4,6 +4,7 @@ import type { RowField } from '../../../../../fields/config/types'
export type Props = Omit<RowField, 'type'> & {
fieldTypes: FieldTypes
forceRender?: boolean
indexPath: string
path?: string
permissions: FieldPermissions

View File

@@ -18,9 +18,9 @@ import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
import RenderFields from '../../RenderFields'
import { WatchChildErrors } from '../../WatchChildErrors'
import withCondition from '../../withCondition'
import { fieldBaseClass } from '../shared'
import './index.scss'
import { TabsProvider } from './provider'
import { fieldBaseClass } from '../shared'
const baseClass = 'tabs-field'
@@ -72,6 +72,7 @@ const TabsField: React.FC<Props> = (props) => {
const {
admin: { className, readOnly },
fieldTypes,
forceRender = false,
indexPath,
path,
permissions,
@@ -188,7 +189,7 @@ const TabsField: React.FC<Props> = (props) => {
}
})}
fieldTypes={fieldTypes}
forceRender
forceRender={forceRender}
indexPath={indexPath}
key={String(activeTabConfig.label)}
margins="small"

View File

@@ -4,6 +4,7 @@ import type { TabsField } from '../../../../../fields/config/types'
export type Props = Omit<TabsField, 'type'> & {
fieldTypes: FieldTypes
forceRender?: boolean
indexPath: string
path?: string
permissions: FieldPermissions

View File

@@ -1,6 +1,7 @@
.graphic-account {
vector-effect: non-scaling-stroke;
overflow: visible;
position: relative;
&__bg {
fill: var(--theme-elevation-50);

View File

@@ -9,11 +9,11 @@ const css = `
width: 18px;
height: 18px;
}
.graphic-icon path {
fill: var(--theme-elevation-1000);
}
@media (max-width: 768px) {
.graphic-icon {
width: 16px;

View File

@@ -26,7 +26,7 @@ const Default: React.FC<Props> = ({ children, className }) => {
const { t } = useTranslation('general')
const { navOpen, setNavOpen } = useNav()
const { navOpen } = useNav()
return (
<Fragment>
@@ -35,6 +35,11 @@ const Default: React.FC<Props> = ({ children, className }) => {
keywords={`${t('dashboard')}, Payload`}
title={t('dashboard')}
/>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
<NavToggler className={`${baseClass}__nav-toggler`}>
<Hamburger closeIcon="collapse" isActive={navOpen} />
</NavToggler>
</div>
<div
className={[baseClass, className, navOpen && `${baseClass}--nav-open`]
.filter(Boolean)
@@ -46,11 +51,6 @@ const Default: React.FC<Props> = ({ children, className }) => {
{children}
</div>
</div>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
<NavToggler className={`${baseClass}__nav-toggler`}>
<Hamburger closeIcon="collapse" isActive={navOpen} />
</NavToggler>
</div>
</Fragment>
)
}

View File

@@ -41,9 +41,11 @@ export const GlobalRoutes: React.FC<GlobalEditViewProps> = (props) => {
<Unauthorized />
)}
</Route>
<Route exact key={`${global.slug}-api`} path={`${adminRoute}/globals/${global.slug}/api`}>
{permissions?.read ? <CustomGlobalComponent view="API" {...props} /> : <Unauthorized />}
</Route>
{global?.admin?.hideAPIURL !== true && (
<Route exact key={`${global.slug}-api`} path={`${adminRoute}/globals/${global.slug}/api`}>
{permissions?.read ? <CustomGlobalComponent view="API" {...props} /> : <Unauthorized />}
</Route>
)}
<Route
exact
key={`${global.slug}-view-version`}

View File

@@ -3,21 +3,31 @@ import type { Dispatch } from 'react'
import { createContext, useContext } from 'react'
import type { LivePreviewConfig } from '../../../../../exports/config'
import type { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
import type { usePopupWindow } from '../usePopupWindow'
import type { SizeReducerAction } from './sizeReducer'
export interface LivePreviewContextType {
appIsReady: boolean
breakpoint: LivePreviewConfig['breakpoints'][number]['name']
breakpoints: LivePreviewConfig['breakpoints']
fieldSchemaJSON?: ReturnType<typeof fieldSchemaToJSON>
iframeHasLoaded: boolean
iframeRef: React.RefObject<HTMLIFrameElement>
isPopupOpen: boolean
measuredDeviceSize: {
height: number
width: number
}
openPopupWindow: ReturnType<typeof usePopupWindow>['openPopupWindow']
popupRef?: React.MutableRefObject<Window | null>
previewWindowType: 'iframe' | 'popup'
setAppIsReady: (appIsReady: boolean) => void
setBreakpoint: (breakpoint: LivePreviewConfig['breakpoints'][number]['name']) => void
setHeight: (height: number) => void
setIframeHasLoaded: (loaded: boolean) => void
setMeasuredDeviceSize: (size: { height: number; width: number }) => void
setPreviewWindowType: (previewWindowType: 'iframe' | 'popup') => void
setSize: Dispatch<SizeReducerAction>
setToolbarPosition: (position: { x: number; y: number }) => void
setWidth: (width: number) => void
@@ -30,22 +40,31 @@ export interface LivePreviewContextType {
x: number
y: number
}
url: string | undefined
zoom: number
}
export const LivePreviewContext = createContext<LivePreviewContextType>({
appIsReady: false,
breakpoint: undefined,
breakpoints: undefined,
fieldSchemaJSON: undefined,
iframeHasLoaded: false,
iframeRef: undefined,
isPopupOpen: false,
measuredDeviceSize: {
height: 0,
width: 0,
},
openPopupWindow: () => {},
popupRef: undefined,
previewWindowType: 'iframe',
setAppIsReady: () => {},
setBreakpoint: () => {},
setHeight: () => {},
setIframeHasLoaded: () => {},
setMeasuredDeviceSize: () => {},
setPreviewWindowType: () => {},
setSize: () => {},
setToolbarPosition: () => {},
setWidth: () => {},
@@ -58,6 +77,7 @@ export const LivePreviewContext = createContext<LivePreviewContextType>({
x: 0,
y: 0,
},
url: undefined,
zoom: 1,
})

View File

@@ -1,39 +1,48 @@
import { DndContext } from '@dnd-kit/core'
import React, { useCallback, useEffect } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import type { LivePreviewConfig } from '../../../../../exports/config'
import type { Field } from '../../../../../fields/config/types'
import type { EditViewProps } from '../../types'
import type { usePopupWindow } from '../usePopupWindow'
import { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
import { customCollisionDetection } from './collisionDetection'
import { LivePreviewContext } from './context'
import { sizeReducer } from './sizeReducer'
export type ToolbarProviderProps = EditViewProps & {
export type LivePreviewProviderProps = EditViewProps & {
appIsReady?: boolean
breakpoints?: LivePreviewConfig['breakpoints']
children: React.ReactNode
deviceSize?: {
height: number
width: number
}
popupState: ReturnType<typeof usePopupWindow>
isPopupOpen?: boolean
openPopupWindow?: ReturnType<typeof usePopupWindow>['openPopupWindow']
popupRef?: React.MutableRefObject<Window>
url?: string
}
export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
const { breakpoints, children } = props
export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = (props) => {
const { breakpoints, children, isPopupOpen, openPopupWindow, popupRef, url } = props
const [previewWindowType, setPreviewWindowType] = useState<'iframe' | 'popup'>('iframe')
const [appIsReady, setAppIsReady] = useState(false)
const iframeRef = React.useRef<HTMLIFrameElement>(null)
const [iframeHasLoaded, setIframeHasLoaded] = React.useState(false)
const [iframeHasLoaded, setIframeHasLoaded] = useState(false)
const [zoom, setZoom] = React.useState(1)
const [zoom, setZoom] = useState(1)
const [position, setPosition] = React.useState({ x: 0, y: 0 })
const [position, setPosition] = useState({ x: 0, y: 0 })
const [size, setSize] = React.useReducer(sizeReducer, { height: 0, width: 0 })
const [measuredDeviceSize, setMeasuredDeviceSize] = React.useState({
const [measuredDeviceSize, setMeasuredDeviceSize] = useState({
height: 0,
width: 0,
})
@@ -41,6 +50,22 @@ export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
const [breakpoint, setBreakpoint] =
React.useState<LivePreviewConfig['breakpoints'][0]['name']>('responsive')
const [fieldSchemaJSON] = useState(() => {
let fields: Field[]
if ('collection' in props) {
const { collection } = props
fields = collection.fields
}
if ('global' in props) {
const { global } = props
fields = global.fields
}
return fieldSchemaToJSON(fields)
})
// The toolbar needs to freely drag and drop around the page
const handleDragEnd = (ev) => {
// only update position if the toolbar is completely within the preview area
@@ -94,24 +119,70 @@ export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
}
}, [breakpoint, breakpoints])
// Receive the `ready` message from the popup window
// This indicates that the app is ready to receive `window.postMessage` events
// This is also the only cross-origin way of detecting when a popup window has loaded
// Unlike iframe elements which have an `onLoad` handler, there is no way to access `window.open` on popups
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data)
if (url.startsWith(event.origin) && data.type === 'payload-live-preview' && data.ready) {
setAppIsReady(true)
}
}
window.addEventListener('message', handleMessage)
return () => {
window.removeEventListener('message', handleMessage)
}
}, [url])
const handleWindowChange = useCallback(
(type: 'iframe' | 'popup') => {
setAppIsReady(false)
setPreviewWindowType(type)
if (type === 'popup') openPopupWindow()
},
[openPopupWindow],
)
// when the user closes the popup window, switch back to the iframe
// the `usePopupWindow` reports the `isPopupOpen` state for us to use here
useEffect(() => {
if (!isPopupOpen) {
handleWindowChange('iframe')
}
}, [isPopupOpen, handleWindowChange])
return (
<LivePreviewContext.Provider
value={{
appIsReady,
breakpoint,
breakpoints,
fieldSchemaJSON,
iframeHasLoaded,
iframeRef,
isPopupOpen,
measuredDeviceSize,
openPopupWindow,
popupRef,
previewWindowType,
setAppIsReady,
setBreakpoint,
setHeight,
setIframeHasLoaded,
setMeasuredDeviceSize,
setPreviewWindowType: handleWindowChange,
setSize,
setToolbarPosition: setPosition,
setWidth,
setZoom,
size,
toolbarPosition: position,
url,
zoom,
}}
>

View File

@@ -1,6 +1,7 @@
@import '../../../../scss/styles.scss';
.live-preview-iframe {
background-color: white;
border: 0;
width: 100%;
height: 100%;

View File

@@ -1,6 +1,7 @@
@import '../../../../scss/styles.scss';
.live-preview-window {
background-color: var(--theme-bg);
width: 60%;
flex-shrink: 0;
flex-grow: 0;

View File

@@ -1,14 +1,9 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect } from 'react'
import type { LivePreviewConfig } from '../../../../../exports/config'
import type { Field } from '../../../../../fields/config/types'
import type { EditViewProps } from '../../types'
import type { usePopupWindow } from '../usePopupWindow'
import { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
import { useAllFormFields } from '../../../forms/Form/context'
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
import { LivePreviewProvider } from '../Context'
import { useLivePreviewContext } from '../Context/context'
import { DeviceContainer } from '../Device'
import { IFrame } from '../IFrame'
@@ -17,93 +12,81 @@ import './index.scss'
const baseClass = 'live-preview-window'
const Preview: React.FC<
EditViewProps & {
popupState: ReturnType<typeof usePopupWindow>
url?: string
}
> = (props) => {
export const LivePreview: React.FC<EditViewProps> = (props) => {
const {
popupState: { isPopupOpen, popupHasLoaded, popupRef },
appIsReady,
iframeHasLoaded,
iframeRef,
popupRef,
previewWindowType,
setIframeHasLoaded,
url,
} = props
} = useLivePreviewContext()
const { iframeHasLoaded, iframeRef, setIframeHasLoaded } = useLivePreviewContext()
const { breakpoint, fieldSchemaJSON } = useLivePreviewContext()
const { breakpoint } = useLivePreviewContext()
const prevWindowType =
React.useRef<ReturnType<typeof useLivePreviewContext>['previewWindowType']>()
const [fields] = useAllFormFields()
const [fieldSchemaJSON] = useState(() => {
let fields: Field[]
if ('collection' in props) {
const { collection } = props
fields = collection.fields
}
if ('global' in props) {
const { global } = props
fields = global.fields
}
return fieldSchemaToJSON(fields)
})
// The preview could either be an iframe embedded on the page
// Or it could be a separate popup window
// We need to transmit data to both accordingly
useEffect(() => {
if (fields && window && 'postMessage' in window) {
// For performance, do no reduce fields to values until after the iframe or popup has loaded
if (fields && window && 'postMessage' in window && appIsReady) {
const values = reduceFieldsToValues(fields, true)
// TODO: only send `fieldSchemaToJSON` one time
// To reduce on large `postMessage` payloads, only send `fieldSchemaToJSON` one time
// To do this, the underlying JS function maintains a cache of this value
// So we need to send it through each time the window type changes
// But only once per window type change, not on every render, because this is a potentially large obj
const shouldSendSchema =
!prevWindowType.current || prevWindowType.current !== previewWindowType
prevWindowType.current = previewWindowType
const message = JSON.stringify({
data: values,
fieldSchemaJSON,
fieldSchemaJSON: shouldSendSchema ? fieldSchemaJSON : undefined,
type: 'payload-live-preview',
})
// external window
if (isPopupOpen) {
setIframeHasLoaded(false)
if (popupRef.current) {
popupRef.current.postMessage(message, url)
}
// Post message to external popup window
if (previewWindowType === 'popup' && popupRef.current) {
popupRef.current.postMessage(message, url)
}
// embedded iframe
if (!isPopupOpen) {
if (iframeHasLoaded && iframeRef.current) {
iframeRef.current.contentWindow?.postMessage(message, url)
}
// Post message to embedded iframe
if (previewWindowType === 'iframe' && iframeRef.current) {
iframeRef.current.contentWindow?.postMessage(message, url)
}
}
}, [
fields,
url,
iframeHasLoaded,
isPopupOpen,
previewWindowType,
popupRef,
popupHasLoaded,
appIsReady,
iframeRef,
setIframeHasLoaded,
fieldSchemaJSON,
])
if (!isPopupOpen) {
if (previewWindowType === 'iframe') {
return (
<div
className={[
baseClass,
isPopupOpen && `${baseClass}--popup-open`,
breakpoint && breakpoint !== 'responsive' && `${baseClass}--has-breakpoint`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${baseClass}__wrapper`}>
<LivePreviewToolbar {...props} iframeRef={iframeRef} url={url} />
<LivePreviewToolbar {...props} />
<div className={`${baseClass}__main`}>
<DeviceContainer>
<IFrame ref={iframeRef} setIframeHasLoaded={setIframeHasLoaded} url={url} />
@@ -114,29 +97,3 @@ const Preview: React.FC<
)
}
}
export const LivePreview: React.FC<
EditViewProps & {
livePreviewConfig?: LivePreviewConfig
popupState: ReturnType<typeof usePopupWindow>
url?: string
}
> = (props) => {
const { livePreviewConfig, url } = props
const breakpoints: LivePreviewConfig['breakpoints'] = [
...(livePreviewConfig?.breakpoints || []),
{
name: 'responsive',
height: '100%',
label: 'Responsive',
width: '100%',
},
]
return (
<LivePreviewProvider {...props} breakpoints={breakpoints} url={url}>
<Preview {...props} />
</LivePreviewProvider>
)
}

View File

@@ -1,6 +1,6 @@
import React from 'react'
import type { LivePreviewToolbarProps } from '..'
import type { EditViewProps } from '../../../types'
import { X } from '../../../..'
import { ExternalLinkIcon } from '../../../../graphics/ExternalLink'
@@ -10,19 +10,16 @@ import './index.scss'
const baseClass = 'live-preview-toolbar-controls'
export const ToolbarControls: React.FC<LivePreviewToolbarProps> = (props) => {
const { breakpoint, breakpoints, setBreakpoint, setZoom, zoom } = useLivePreviewContext()
const {
popupState: { openPopupWindow },
url,
} = props
export const ToolbarControls: React.FC<EditViewProps> = () => {
const { breakpoint, breakpoints, setBreakpoint, setPreviewWindowType, setZoom, url, zoom } =
useLivePreviewContext()
return (
<div className={baseClass}>
{breakpoints?.length > 0 && (
<select
className={`${baseClass}__breakpoint`}
name="live-preview-breakpoint"
onChange={(e) => setBreakpoint(e.target.value)}
value={breakpoint}
>
@@ -57,7 +54,15 @@ export const ToolbarControls: React.FC<LivePreviewToolbarProps> = (props) => {
<option value={150}>150%</option>
<option value={200}>200%</option>
</select>
<a className={`${baseClass}__external`} href={url} onClick={openPopupWindow} type="button">
<a
className={`${baseClass}__external`}
href={url}
onClick={(e) => {
e.preventDefault()
setPreviewWindowType('popup')
}}
type="button"
>
<ExternalLinkIcon />
</a>
</div>

View File

@@ -58,6 +58,7 @@ export const PreviewFrameSizeInput: React.FC<{
<input
className={baseClass}
min={0}
name={axis === 'x' ? 'live-preview-width' : 'live-preview-height'}
onChange={handleChange}
step={1}
type="number"

View File

@@ -1,7 +1,7 @@
import { useDraggable } from '@dnd-kit/core'
import React from 'react'
import type { ToolbarProviderProps } from '../Context'
import type { EditViewProps } from '../../types'
import DragHandle from '../../../icons/Drag'
import { useLivePreviewContext } from '../Context/context'
@@ -10,11 +10,7 @@ import './index.scss'
const baseClass = 'live-preview-toolbar'
export type LivePreviewToolbarProps = Omit<ToolbarProviderProps, 'children'> & {
iframeRef: React.RefObject<HTMLIFrameElement>
}
const DraggableToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
const DraggableToolbar: React.FC<EditViewProps> = (props) => {
const { toolbarPosition } = useLivePreviewContext()
const { attributes, listeners, setNodeRef, transform } = useDraggable({
@@ -50,7 +46,7 @@ const DraggableToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
)
}
const StaticToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
const StaticToolbar: React.FC<EditViewProps> = (props) => {
return (
<div className={[baseClass, `${baseClass}--static`].join(' ')}>
<ToolbarControls {...props} />
@@ -59,7 +55,7 @@ const StaticToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
}
export const LivePreviewToolbar: React.FC<
LivePreviewToolbarProps & {
EditViewProps & {
draggable?: boolean
}
> = (props) => {

View File

@@ -3,6 +3,7 @@
.live-preview {
width: 100%;
display: flex;
--gradient: linear-gradient(to left, rgba(0, 0, 0, 0.04) 0%, transparent 100%);
[dir='rtl'] & {
flex-direction: row-reverse;
@@ -34,7 +35,7 @@
right: 0;
width: calc(var(--base) * 2);
height: 100%;
background: linear-gradient(to left, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0) 100%);
background: var(--gradient);
pointer-events: none;
z-index: -1;
}
@@ -58,6 +59,10 @@
&__main {
min-height: initial;
width: 100%;
&::after {
display: none;
}
}
&__form {
@@ -77,3 +82,9 @@
}
}
}
html[data-theme='dark'] {
.live-preview {
--gradient: linear-gradient(to left, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0) 100%);
}
}

View File

@@ -17,47 +17,17 @@ import { useDocumentInfo } from '../../utilities/DocumentInfo'
import { useLocale } from '../../utilities/Locale'
import Meta from '../../utilities/Meta'
import { SetStepNav } from '../collections/Edit/SetStepNav'
import { LivePreviewProvider } from './Context'
import { useLivePreviewContext } from './Context/context'
import { LivePreview } from './Preview'
import './index.scss'
import { usePopupWindow } from './usePopupWindow'
const baseClass = 'live-preview'
export const LivePreviewView: React.FC<EditViewProps> = (props) => {
const PreviewView: React.FC<EditViewProps> = (props) => {
const { i18n, t } = useTranslation('general')
const config = useConfig()
const documentInfo = useDocumentInfo()
const locale = useLocale()
let livePreviewConfig: LivePreviewConfig = config?.admin?.livePreview
if ('collection' in props) {
livePreviewConfig = {
...(livePreviewConfig || {}),
...(props?.collection.admin.livePreview || {}),
}
}
if ('global' in props) {
livePreviewConfig = {
...(livePreviewConfig || {}),
...(props?.global.admin.livePreview || {}),
}
}
const url =
typeof livePreviewConfig?.url === 'function'
? livePreviewConfig?.url({
data: props?.data,
documentInfo,
locale,
})
: livePreviewConfig?.url
const popupState = usePopupWindow({
eventType: 'payload-live-preview',
url,
})
const { previewWindowType } = useLivePreviewContext()
const { apiURL, data, permissions } = props
@@ -113,14 +83,14 @@ export const LivePreviewView: React.FC<EditViewProps> = (props) => {
permissions={permissions}
/>
<div
className={[baseClass, popupState?.isPopupOpen && `${baseClass}--detached`]
className={[baseClass, previewWindowType === 'popup' && `${baseClass}--detached`]
.filter(Boolean)
.join(' ')}
>
<div
className={[
`${baseClass}__main`,
popupState?.isPopupOpen && `${baseClass}__main--popup-open`,
previewWindowType === 'popup' && `${baseClass}__main--popup-open`,
]
.filter(Boolean)
.join(' ')}
@@ -148,13 +118,67 @@ export const LivePreviewView: React.FC<EditViewProps> = (props) => {
)}
</Gutter>
</div>
<LivePreview
{...props}
livePreviewConfig={livePreviewConfig}
popupState={popupState}
url={url}
/>
<LivePreview {...props} />
</div>
</Fragment>
)
}
export const LivePreviewView: React.FC<EditViewProps> = (props) => {
const config = useConfig()
const documentInfo = useDocumentInfo()
const locale = useLocale()
let livePreviewConfig: LivePreviewConfig = config?.admin?.livePreview
if ('collection' in props) {
livePreviewConfig = {
...(livePreviewConfig || {}),
...(props?.collection.admin.livePreview || {}),
}
}
if ('global' in props) {
livePreviewConfig = {
...(livePreviewConfig || {}),
...(props?.global.admin.livePreview || {}),
}
}
const url =
typeof livePreviewConfig?.url === 'function'
? livePreviewConfig?.url({
data: props?.data,
documentInfo,
locale,
})
: livePreviewConfig?.url
const breakpoints: LivePreviewConfig['breakpoints'] = [
...(livePreviewConfig?.breakpoints || []),
{
name: 'responsive',
height: '100%',
label: 'Responsive',
width: '100%',
},
]
const { isPopupOpen, openPopupWindow, popupRef } = usePopupWindow({
eventType: 'payload-live-preview',
url,
})
return (
<LivePreviewProvider
{...props}
breakpoints={breakpoints}
isPopupOpen={isPopupOpen}
openPopupWindow={openPopupWindow}
popupRef={popupRef}
url={url}
>
<PreviewView {...props} />
</LivePreviewProvider>
)
}

View File

@@ -19,17 +19,14 @@ export const usePopupWindow = (props: {
url: string
}): {
isPopupOpen: boolean
openPopupWindow: (e: React.MouseEvent<HTMLAnchorElement>) => void
popupHasLoaded: boolean
openPopupWindow: () => void
popupRef?: React.MutableRefObject<Window | null>
} => {
const { eventType, onMessage, url } = props
const isReceivingMessage = useRef(false)
const [isOpen, setIsOpen] = useState(false)
const [popupHasLoaded, setPopupHasLoaded] = useState(false)
const { serverURL } = useConfig()
const popupRef = useRef<Window | null>(null)
const hasAttachedMessageListener = useRef(false)
// Optionally broadcast messages back out to the parent component
useEffect(() => {
@@ -65,8 +62,10 @@ export const usePopupWindow = (props: {
// Customize the size, position, and style of the popup window
const openPopupWindow = useCallback(
(e) => {
e.preventDefault()
(e?: MouseEvent) => {
if (e) {
e.preventDefault()
}
const features = {
height: 700,
@@ -106,27 +105,6 @@ export const usePopupWindow = (props: {
[url],
)
// the only cross-origin way of detecting when a popup window has loaded
// we catch a message event that the site rendered within the popup window fires
// there is no way in js to add an event listener to a popup window across domains
useEffect(() => {
if (hasAttachedMessageListener.current) return
hasAttachedMessageListener.current = true
window.addEventListener('message', (event) => {
const data = JSON.parse(event.data)
if (
url.startsWith(event.origin) &&
data.type === eventType &&
data.popupReady &&
!popupHasLoaded
) {
setPopupHasLoaded(true)
}
})
}, [url, eventType, popupHasLoaded])
// this is the most stable and widely supported way to check if a popup window is no longer open
// we poll its ref every x ms and use the popup window's `closed` property
useEffect(() => {
@@ -137,7 +115,6 @@ export const usePopupWindow = (props: {
if (popupRef.current.closed) {
clearInterval(timer)
setIsOpen(false)
setPopupHasLoaded(false)
}
}, 1000)
} else {
@@ -154,7 +131,6 @@ export const usePopupWindow = (props: {
return {
isPopupOpen: isOpen,
openPopupWindow,
popupHasLoaded,
popupRef,
}
}

View File

@@ -41,13 +41,19 @@ export const CollectionRoutes: React.FC<CollectionEditViewProps> = (props) => {
<Unauthorized />
)}
</Route>
<Route
exact
key={`${collection.slug}-api`}
path={`${adminRoute}/collections/${collection.slug}/:id/api`}
>
{permissions?.read ? <CustomCollectionComponent view="API" {...props} /> : <Unauthorized />}
</Route>
{collection?.admin?.hideAPIURL !== true && (
<Route
exact
key={`${collection.slug}-api`}
path={`${adminRoute}/collections/${collection.slug}/:id/api`}
>
{permissions?.read ? (
<CustomCollectionComponent view="API" {...props} />
) : (
<Unauthorized />
)}
</Route>
)}
<Route
exact
key={`${collection.slug}-view-version`}

View File

@@ -3,7 +3,10 @@ import { useEffect, useRef, useState } from 'react'
type Intersect = [setNode: React.Dispatch<Element>, entry: IntersectionObserverEntry]
const useIntersect = ({ root = null, rootMargin = '0px', threshold = 0 } = {}): Intersect => {
const useIntersect = (
{ root = null, rootMargin = '0px', threshold = 0 } = {},
disable?: boolean,
): Intersect => {
const [entry, updateEntry] = useState<IntersectionObserverEntry>()
const [node, setNode] = useState(null)
@@ -16,13 +19,16 @@ const useIntersect = ({ root = null, rootMargin = '0px', threshold = 0 } = {}):
)
useEffect(() => {
if (disable) {
return
}
const { current: currentObserver } = observer
currentObserver.disconnect()
if (node) currentObserver.observe(node)
return () => currentObserver.disconnect()
}, [node])
}, [node, disable])
return [setNode, entry]
}

View File

@@ -129,7 +129,7 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--theme-success-500);
// STYLE MIXINS
//////////////////////////////
@mixin blur-bg($color: var(--theme-bg)) {
@mixin blur-bg($color: var(--theme-bg), $opacity: 0.75) {
&:before,
&:after {
content: ' ';
@@ -142,14 +142,18 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--theme-success-500);
&:before {
background: $color;
opacity: 0.85;
opacity: $opacity;
}
&:after {
backdrop-filter: blur(5px);
backdrop-filter: blur(8px);
}
}
@mixin blur-bg-light {
@include blur-bg(var(--theme-bg), 0.3);
}
@mixin formInput() {
@include inputShadow;
font-family: var(--font-body);

View File

@@ -69,6 +69,7 @@ async function registerFirstUser<TSlug extends keyof GeneratedTypes['collections
data: {
_verified: true,
},
req,
})
}
@@ -79,6 +80,7 @@ async function registerFirstUser<TSlug extends keyof GeneratedTypes['collections
const { token } = await payload.login({
...args,
collection: slug,
req,
})
const resultToReturn = {

View File

@@ -1,39 +1,12 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import fs from 'fs'
import { compile } from 'json-schema-to-typescript'
import { singular } from 'pluralize'
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from '../exports/types'
import payload from '..'
import loadConfig from '../config/load'
import { configToJSONSchema } from '../utilities/configToJSONSchema'
import { toWords } from '../utilities/formatLabels'
import Logger from '../utilities/logger'
const generateEntityDeclarations = (
entities: (SanitizedCollectionConfig | SanitizedGlobalConfig)[],
key: 'collections' | 'globals',
): string => {
if (entities.length) {
return entities.reduce((dec, entity, i) => {
const title = entity.typescript?.interface
? entity.typescript.interface
: singular(toWords(entity.slug, true))
return `${dec}
'${entity.slug}': ${title}${
i + 1 === entities.length
? `
}`
: ''
}`
}, ` ${key}: {`)
}
return ''
}
export async function generateTypes(): Promise<void> {
const logger = Logger()
const config = await loadConfig()
@@ -50,9 +23,7 @@ export async function generateTypes(): Promise<void> {
const jsonSchema = configToJSONSchema(payload.config, payload.db.defaultIDType)
const collectionDeclaration = generateEntityDeclarations(config.collections, 'collections')
const globalDeclaration = generateEntityDeclarations(config.globals, 'globals')
const declare = `declare module 'payload' {\n export interface GeneratedTypes {\n${collectionDeclaration}\n${globalDeclaration}\n }\n}`
const declare = `declare module 'payload' {\n export interface GeneratedTypes extends Config {}\n}`
compile(jsonSchema, 'Config', {
bannerComment:
@@ -61,7 +32,10 @@ export async function generateTypes(): Promise<void> {
singleQuote: true,
},
}).then((compiled) => {
fs.writeFileSync(outputFile, `${compiled}\n\n${declare}`)
if (config.typescript.declare !== false) {
compiled += `\n\n${declare}`
}
fs.writeFileSync(outputFile, compiled)
logger.info(`Types written to ${outputFile}`)
})
}

View File

@@ -162,6 +162,7 @@ const collectionSchema = joi.object().keys({
adminThumbnail: joi.alternatives().try(joi.string(), joi.func()),
crop: joi.bool(),
disableLocalStorage: joi.bool(),
filesRequiredOnCreate: joi.bool(),
focalPoint: joi.bool(),
formatOptions: joi.object().keys({
format: joi.string(),

View File

@@ -128,7 +128,8 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
data,
overwriteExistingFiles,
req,
throwOnMissingFile: !shouldSaveDraft,
throwOnMissingFile:
!shouldSaveDraft && collection.config.upload.filesRequiredOnCreate !== false,
})
data = newFileData

Some files were not shown because too many files have changed in this diff Show More