Compare commits
67 Commits
db-postgre
...
db-mongodb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ba95df674 | ||
|
|
40f98e4a0d | ||
|
|
584ead9fe2 | ||
|
|
b6bf354f6a | ||
|
|
9918c2499a | ||
|
|
8c48c8beb5 | ||
|
|
2697753715 | ||
|
|
4b13686f61 | ||
|
|
ab7999d3c1 | ||
|
|
a592188c1d | ||
|
|
5ff0846b6c | ||
|
|
13cabf129e | ||
|
|
c173e55b89 | ||
|
|
bcdd2d626f | ||
|
|
67682248c8 | ||
|
|
7c52d6ee28 | ||
|
|
bc65b53ce5 | ||
|
|
c8cc6ea1cc | ||
|
|
4e05e6fd85 | ||
|
|
6988a68eaf | ||
|
|
a272692726 | ||
|
|
229e4459cb | ||
|
|
056585ed31 | ||
|
|
b545433ee6 | ||
|
|
4c938b5f9e | ||
|
|
f1d8fa9999 | ||
|
|
1670a603f6 | ||
|
|
22f1fa8fc9 | ||
|
|
370e8d1938 | ||
|
|
3a3eab761e | ||
|
|
238f7e1b94 | ||
|
|
58e2083882 | ||
|
|
20cde242fb | ||
|
|
f50a392d59 | ||
|
|
fa1740d906 | ||
|
|
e847061c74 | ||
|
|
ebd5e6ae8f | ||
|
|
48de89794b | ||
|
|
ef4b5d8bfd | ||
|
|
5711d42eca | ||
|
|
a446a788a9 | ||
|
|
df57196d19 | ||
|
|
86c563e4e5 | ||
|
|
734b8c08ed | ||
|
|
68c5a57515 | ||
|
|
d9f0b7bd30 | ||
|
|
5ecfe3da28 | ||
|
|
3e3163e875 | ||
|
|
cfe1698dfd | ||
|
|
5722634660 | ||
|
|
cdbfc9132a | ||
|
|
dd0ac066ce | ||
|
|
8b8ceabbdd | ||
|
|
8da18d3496 | ||
|
|
6cd4df3dc4 | ||
|
|
e74dc8633b | ||
|
|
2f945919a3 | ||
|
|
52dc9177d7 | ||
|
|
cec757d098 | ||
|
|
80e57150a0 | ||
|
|
2969da7402 | ||
|
|
35da1db99f | ||
|
|
a578226d34 | ||
|
|
8c75f32620 | ||
|
|
c3ab8b9115 | ||
|
|
993568a195 | ||
|
|
c3cec64220 |
12
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
12
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
@@ -10,7 +10,12 @@ body:
|
|||||||
id: reproduction-link
|
id: reproduction-link
|
||||||
attributes:
|
attributes:
|
||||||
label: Link to reproduction
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- 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.
|
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:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Describe the Bug
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
15
.github/reproduction-guide.md
vendored
15
.github/reproduction-guide.md
vendored
@@ -1,10 +1,11 @@
|
|||||||
# Reproduction Guide
|
# Reproduction Guide
|
||||||
|
|
||||||
1. [fork](https://github.com/payloadcms/payload/fork) this repo
|
1. [Fork](https://github.com/payloadcms/payload/fork) this repo
|
||||||
2. run `yarn` to install dependencies
|
2. Optionally, create a new branch for your reproduction
|
||||||
3. open up the `test/_community` directory
|
3. Run `pnpm install` to install dependencies
|
||||||
4. add any necessary `collections/globals/fields` in this directory to recreate the issue you are experiencing
|
4. Open up the `test/_community` directory
|
||||||
5. run `yarn dev _community` to start the admin panel
|
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.
|
**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
|
- `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.
|
- `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.
|
- `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.
|
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:
|
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn test:int _community
|
pnpm test:int _community
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running E2E tests (Admin Panel UI tests)
|
### Running E2E tests (Admin Panel UI tests)
|
||||||
|
|||||||
7
.github/workflows/main.yml
vendored
7
.github/workflows/main.yml
vendored
@@ -132,7 +132,12 @@ jobs:
|
|||||||
key: ${{ github.sha }}-${{ github.run_number }}
|
key: ${{ github.sha }}-${{ github.run_number }}
|
||||||
|
|
||||||
- name: E2E Tests
|
- 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
|
- uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"verbose": true,
|
|
||||||
"git": {
|
|
||||||
"commitMessage": "chore(release): v${version}",
|
|
||||||
"requireCleanWorkingDir": true
|
|
||||||
},
|
|
||||||
"github": {
|
|
||||||
"release": true
|
|
||||||
},
|
|
||||||
"npm": {
|
|
||||||
"skipChecks": true
|
|
||||||
},
|
|
||||||
"hooks": {
|
|
||||||
"before:init": ["pnpm i", "pnpm clean", "pnpm test"]
|
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"@release-it/conventional-changelog": {
|
|
||||||
"preset": "angular",
|
|
||||||
"infile": "CHANGELOG.md"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
16
.release-it.pre.js
Normal file
16
.release-it.pre.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
verbose: true,
|
||||||
|
git: {
|
||||||
|
requireCleanWorkingDir: false,
|
||||||
|
commit: false,
|
||||||
|
push: false,
|
||||||
|
tag: false,
|
||||||
|
},
|
||||||
|
npm: {
|
||||||
|
skipChecks: true,
|
||||||
|
tag: 'beta',
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
'before:init': ['pnpm install', 'pnpm clean', 'pnpm build'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"verbose": true,
|
|
||||||
"git": {
|
|
||||||
"requireCleanWorkingDir": false,
|
|
||||||
"commit": false,
|
|
||||||
"push": false,
|
|
||||||
"tag": false
|
|
||||||
},
|
|
||||||
"github": {
|
|
||||||
"release": true
|
|
||||||
},
|
|
||||||
"npm": {
|
|
||||||
"skipChecks": true,
|
|
||||||
"tag": "canary"
|
|
||||||
},
|
|
||||||
"hooks": {
|
|
||||||
"before:init": ["pnpm i", "pnpm clean", "pnpm test"]
|
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"@release-it/conventional-changelog": {
|
|
||||||
"preset": "angular",
|
|
||||||
"infile": "CHANGELOG.md"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -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)
|
## [2.0.6](https://github.com/payloadcms/payload/compare/v2.0.5...v2.0.6) (2023-10-14)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -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"
|
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">
|
<Banner type="success">
|
||||||
Payload is a headless CMS and application framework. It’s meant to provide a massive boost to your
|
<strong>Payload 2.0 has been released!</strong>
|
||||||
development process, but importantly, stay out of your way as your apps get more complex.
|
<br />
|
||||||
|
Includes Postgres support, Live Preview, Lexical Editor, and more. <a href="/blog/payload-2-0">Read the announcement</a>.
|
||||||
</Banner>
|
</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:
|
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
|
- 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
|
- 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
|
- Deep access control to your data, based on document or field-level functions
|
||||||
- File storage and access control
|
- File storage and access control
|
||||||
- A beautiful admin UI that’s generated specifically to suit your data
|
- A beautiful admin UI that's generated specifically to suit your data
|
||||||
|
|
||||||
## What does "headless" mean?
|
## What does "headless" mean?
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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
|
### Custom output file path
|
||||||
|
|
||||||
You can specify where you want your types to be generated by adding a property to your Payload config:
|
You can specify where you want your types to be generated by adding a property to your Payload config:
|
||||||
|
|||||||
@@ -40,20 +40,21 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
|
|||||||
|
|
||||||
#### Collection Upload Options
|
#### Collection Upload Options
|
||||||
|
|
||||||
| Option | Description |
|
| 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. |
|
| **`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. |
|
| **`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) |
|
| **`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) |
|
| **`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) |
|
| **`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) |
|
| **`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) |
|
| **`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. |
|
| **`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) |
|
| **`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) |
|
| **`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) |
|
| **`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) |
|
| **`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._
|
_An asterisk denotes that a property above is required._
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Form Builder Example Front-End
|
# 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.
|
> 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
|
### 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
|
### 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
|
4. `yarn dev` or `npm run dev` to start the server
|
||||||
5. `open http://localhost:3001` to see the result
|
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
|
## Learn More
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# Payload Live Preview Example Front-End
|
# 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
|
## Getting Started
|
||||||
|
|
||||||
### Payload
|
### 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
|
### 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
|
4. `yarn dev` or `npm run dev` to start the server
|
||||||
5. `open http://localhost:3001` to see the result
|
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
|
## Learn More
|
||||||
|
|
||||||
@@ -32,6 +32,6 @@ You can check out [the Payload GitHub repository](https://github.com/payloadcms/
|
|||||||
|
|
||||||
## Deployment
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@payloadcms/live-preview-react": "^1.0.0-beta.3",
|
"@payloadcms/live-preview-react": "latest",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"next": "^13.4.8",
|
"next": "^13.4.8",
|
||||||
"payload-admin-bar": "^1.0.6",
|
"payload-admin-bar": "^1.0.6",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,14 @@
|
|||||||
# Payload Live Preview Example Front-End
|
# 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
|
## Getting Started
|
||||||
|
|
||||||
### Payload
|
### 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
|
### 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
|
4. `yarn dev` or `npm run dev` to start the server
|
||||||
5. `open http://localhost:3001` to see the result
|
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
|
## Learn More
|
||||||
|
|
||||||
@@ -32,6 +32,6 @@ You can check out [the Payload GitHub repository](https://github.com/payloadcms/
|
|||||||
|
|
||||||
## Deployment
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@payloadcms/live-preview-react": "^1.0.0-beta.3",
|
"@payloadcms/live-preview-react": "latest",
|
||||||
"@types/escape-html": "^1.0.2",
|
"@types/escape-html": "^1.0.2",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"next": "^13.4.8",
|
"next": "^13.4.8",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
# Payload Live Preview Example
|
# 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:
|
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
|
- #### Pages
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Redirects Example Front-End
|
# 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.
|
> 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
|
### 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
|
### Next.js App
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@payloadcms/bundler-vite",
|
"name": "@payloadcms/bundler-vite",
|
||||||
"version": "0.1.2",
|
"version": "0.1.3",
|
||||||
"description": "The officially supported Vite bundler adapter for Payload",
|
"description": "The officially supported Vite bundler adapter for Payload",
|
||||||
"repository": "https://github.com/payloadcms/payload",
|
"repository": "https://github.com/payloadcms/payload",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"payload": "workspace:*"
|
"payload": "workspace:*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"payload": "^2.0.0",
|
||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@payloadcms/bundler-webpack",
|
"name": "@payloadcms/bundler-webpack",
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"description": "The officially supported Webpack bundler adapter for Payload",
|
"description": "The officially supported Webpack bundler adapter for Payload",
|
||||||
"repository": "https://github.com/payloadcms/payload",
|
"repository": "https://github.com/payloadcms/payload",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -58,6 +58,9 @@
|
|||||||
"@types/webpack-hot-middleware": "2.25.6",
|
"@types/webpack-hot-middleware": "2.25.6",
|
||||||
"payload": "workspace:*"
|
"payload": "workspace:*"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "^2.0.0"
|
||||||
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"registry": "https://registry.npmjs.org/",
|
"registry": "https://registry.npmjs.org/",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { SanitizedConfig } from 'payload/config'
|
|||||||
import type { Configuration } from 'webpack'
|
import type { Configuration } from 'webpack'
|
||||||
|
|
||||||
import findNodeModules from 'find-node-modules'
|
import findNodeModules from 'find-node-modules'
|
||||||
|
import fs from 'fs'
|
||||||
import HtmlWebpackPlugin from 'html-webpack-plugin'
|
import HtmlWebpackPlugin from 'html-webpack-plugin'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import webpack from 'webpack'
|
import webpack from 'webpack'
|
||||||
@@ -9,78 +10,94 @@ import webpack from 'webpack'
|
|||||||
const mockModulePath = path.resolve(__dirname, '../mocks/emptyModule.js')
|
const mockModulePath = path.resolve(__dirname, '../mocks/emptyModule.js')
|
||||||
const mockDotENVPath = path.resolve(__dirname, '../mocks/dotENV.js')
|
const mockDotENVPath = path.resolve(__dirname, '../mocks/dotENV.js')
|
||||||
|
|
||||||
const nodeModulesPaths = findNodeModules({ cwd: process.cwd() })
|
const nodeModulesPaths = findNodeModules({ cwd: process.cwd(), relative: false })
|
||||||
const nodeModulesPath = path.resolve(nodeModulesPaths[0])
|
|
||||||
const adminFolderPath = path.resolve(nodeModulesPath, 'payload/dist/admin')
|
|
||||||
|
|
||||||
export const getBaseConfig = (payloadConfig: SanitizedConfig): Configuration => ({
|
export const getBaseConfig = (payloadConfig: SanitizedConfig): Configuration => {
|
||||||
entry: {
|
let nodeModulesPath = nodeModulesPaths.find((p) => {
|
||||||
main: [adminFolderPath],
|
const guess = path.resolve(p, 'payload/dist/admin')
|
||||||
},
|
if (fs.existsSync(guess)) {
|
||||||
module: {
|
return true
|
||||||
rules: [
|
}
|
||||||
{
|
return false
|
||||||
exclude: /\/node_modules\/(?!.+\.tsx?$).*$/,
|
})
|
||||||
test: /\.(t|j)sx?$/,
|
|
||||||
use: [
|
if (!nodeModulesPath) {
|
||||||
{
|
nodeModulesPath = process.cwd()
|
||||||
loader: require.resolve('swc-loader'),
|
}
|
||||||
options: {
|
|
||||||
jsc: {
|
const adminFolderPath = path.resolve(nodeModulesPath, 'payload/dist/admin')
|
||||||
parser: {
|
|
||||||
syntax: 'typescript',
|
const config: Configuration = {
|
||||||
tsx: true,
|
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: [
|
||||||
oneOf: [
|
{
|
||||||
{
|
test: /\.(?:ico|gif|png|jpg|jpeg|woff(2)?|eot|ttf|otf|svg)$/i,
|
||||||
test: /\.(?:ico|gif|png|jpg|jpeg|woff(2)?|eot|ttf|otf|svg)$/i,
|
type: 'asset/resource',
|
||||||
type: 'asset/resource',
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
plugins: [
|
||||||
plugins: [
|
new webpack.ProvidePlugin({ process: require.resolve('process/browser') }),
|
||||||
new webpack.ProvidePlugin({ process: require.resolve('process/browser') }),
|
new webpack.DefinePlugin(
|
||||||
new webpack.DefinePlugin(
|
Object.entries(process.env).reduce((values, [key, val]) => {
|
||||||
Object.entries(process.env).reduce((values, [key, val]) => {
|
if (key.indexOf('PAYLOAD_PUBLIC_') === 0) {
|
||||||
if (key.indexOf('PAYLOAD_PUBLIC_') === 0) {
|
return {
|
||||||
return {
|
...values,
|
||||||
...values,
|
[`process.env.${key}`]: `'${val}'`,
|
||||||
[`process.env.${key}`]: `'${val}'`,
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return values
|
return values
|
||||||
}, {}),
|
}, {}),
|
||||||
),
|
),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: path.normalize('./index.html'),
|
filename: path.normalize('./index.html'),
|
||||||
template: payloadConfig.admin.indexHTML,
|
template: payloadConfig.admin.indexHTML,
|
||||||
}),
|
}),
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@payloadcms/bundler-webpack': mockModulePath,
|
'@payloadcms/bundler-webpack': mockModulePath,
|
||||||
dotenv: mockDotENVPath,
|
dotenv: mockDotENVPath,
|
||||||
path: require.resolve('path-browserify'),
|
path: require.resolve('path-browserify'),
|
||||||
payload$: mockModulePath,
|
payload$: mockModulePath,
|
||||||
'payload-config': payloadConfig.paths.rawConfig,
|
'payload-config': payloadConfig.paths.rawConfig,
|
||||||
'payload-user-css': payloadConfig.admin.css,
|
'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,
|
return config
|
||||||
http: false,
|
}
|
||||||
https: false,
|
|
||||||
},
|
|
||||||
modules: ['node_modules', nodeModulesPath, path.resolve(__dirname, '../../node_modules')],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
export const webpackBundler = () => {}
|
export const webpackBundler = () => {}
|
||||||
|
|
||||||
|
export default () => {}
|
||||||
|
|||||||
15
packages/create-payload-app/.swcrc
Normal file
15
packages/create-payload-app/.swcrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/swcrc",
|
||||||
|
"sourceMaps": "inline",
|
||||||
|
"jsc": {
|
||||||
|
"target": "esnext",
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"tsx": true,
|
||||||
|
"dts": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "create-payload-app",
|
"name": "create-payload-app",
|
||||||
"version": "0.5.2",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"create-payload-app": "bin/cli.js"
|
"create-payload-app": "bin/cli.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && pnpm copyfiles",
|
"build": "pnpm build:swc",
|
||||||
"copyfiles": "copyfiles -u 1 \"src/templates/**\" \"src/lib/common-files/**\" dist",
|
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"lint": "eslint \"src/**/*.ts\"",
|
|
||||||
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
||||||
"lint-staged": "lint-staged --quiet",
|
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"prepublishOnly": "pnpm test && pnpm clean && pnpm build"
|
"prepublishOnly": "pnpm test && pnpm clean && pnpm build"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,5 @@
|
|||||||
"src/**/*.spec.ts",
|
"src/**/*.spec.ts",
|
||||||
"src/**/*.spec.tsx"
|
"src/**/*.spec.tsx"
|
||||||
],
|
],
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"]
|
||||||
"references": [{ "path": "../payload" }]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@payloadcms/db-mongodb",
|
"name": "@payloadcms/db-mongodb",
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"description": "The officially supported MongoDB database adapter for Payload",
|
"description": "The officially supported MongoDB database adapter for Payload",
|
||||||
"repository": "https://github.com/payloadcms/payload",
|
"repository": "https://github.com/payloadcms/payload",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -35,6 +35,9 @@
|
|||||||
"mongodb-memory-server": "8.13.0",
|
"mongodb-memory-server": "8.13.0",
|
||||||
"payload": "workspace:*"
|
"payload": "workspace:*"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "^2.0.0"
|
||||||
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"registry": "https://registry.npmjs.org/",
|
"registry": "https://registry.npmjs.org/",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const init: Init = async function init(this: MongooseAdapter) {
|
|||||||
const model = mongoose.model(
|
const model = mongoose.model(
|
||||||
versionModelName,
|
versionModelName,
|
||||||
versionSchema,
|
versionSchema,
|
||||||
versionModelName,
|
this.autoPluralization === true ? undefined : versionModelName,
|
||||||
) as CollectionModel
|
) as CollectionModel
|
||||||
// this.payload.versions[collection.slug] = model;
|
// this.payload.versions[collection.slug] = model;
|
||||||
this.versions[collection.slug] = model
|
this.versions[collection.slug] = model
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
"@types/to-snake-case": "1.0.0",
|
"@types/to-snake-case": "1.0.0",
|
||||||
"payload": "workspace:*"
|
"payload": "workspace:*"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "^2.0.0"
|
||||||
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"registry": "https://registry.npmjs.org/",
|
"registry": "https://registry.npmjs.org/",
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const traverseFields = ({
|
|||||||
with: {},
|
with: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adapter.tables[`${topLevelTableName}_${toSnakeCase(block.slug)}_locales`])
|
if (adapter.tables[`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}_locales`])
|
||||||
withBlock.with._locales = _locales
|
withBlock.with._locales = _locales
|
||||||
topLevelArgs.with[blockKey] = withBlock
|
topLevelArgs.with[blockKey] = withBlock
|
||||||
|
|
||||||
|
|||||||
13
packages/db-postgres/src/schema/idToUUID.ts
Normal file
13
packages/db-postgres/src/schema/idToUUID.ts
Normal 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
|
||||||
|
})
|
||||||
@@ -26,6 +26,7 @@ import type { GenericColumns, PostgresAdapter } from '../types'
|
|||||||
import { hasLocalesTable } from '../utilities/hasLocalesTable'
|
import { hasLocalesTable } from '../utilities/hasLocalesTable'
|
||||||
import { buildTable } from './build'
|
import { buildTable } from './build'
|
||||||
import { createIndex } from './createIndex'
|
import { createIndex } from './createIndex'
|
||||||
|
import { idToUUID } from './idToUUID'
|
||||||
import { parentIDColumnMap } from './parentIDColumnMap'
|
import { parentIDColumnMap } from './parentIDColumnMap'
|
||||||
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical'
|
import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdentical'
|
||||||
|
|
||||||
@@ -281,7 +282,7 @@ export const traverseFields = ({
|
|||||||
baseExtraConfig,
|
baseExtraConfig,
|
||||||
disableNotNull: disableNotNullFromHere,
|
disableNotNull: disableNotNullFromHere,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
fields: field.fields,
|
fields: disableUnique ? idToUUID(field.fields) : field.fields,
|
||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootRelationships: relationships,
|
rootRelationships: relationships,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
@@ -349,7 +350,7 @@ export const traverseFields = ({
|
|||||||
baseExtraConfig,
|
baseExtraConfig,
|
||||||
disableNotNull: disableNotNullFromHere,
|
disableNotNull: disableNotNullFromHere,
|
||||||
disableUnique,
|
disableUnique,
|
||||||
fields: block.fields,
|
fields: disableUnique ? idToUUID(block.fields) : block.fields,
|
||||||
rootRelationsToBuild,
|
rootRelationsToBuild,
|
||||||
rootRelationships: relationships,
|
rootRelationships: relationships,
|
||||||
rootTableIDColType,
|
rootTableIDColType,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const validateExistingBlockIsIdentical = ({
|
|||||||
const fieldNames = flattenTopLevelFields(block.fields).flatMap((field) => field.name)
|
const fieldNames = flattenTopLevelFields(block.fields).flatMap((field) => field.name)
|
||||||
|
|
||||||
Object.keys(table).forEach((fieldName) => {
|
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) {
|
if (fieldNames.indexOf(fieldName) === -1) {
|
||||||
throw new InvalidConfiguration(
|
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.`,
|
`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.`,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blocks = createBlocksMap(data)
|
const blocks = createBlocksMap(data)
|
||||||
|
const deletions = []
|
||||||
|
|
||||||
const result = traverseFields<T>({
|
const result = traverseFields<T>({
|
||||||
blocks,
|
blocks,
|
||||||
@@ -38,6 +39,7 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
|
|||||||
dataRef: {
|
dataRef: {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
},
|
},
|
||||||
|
deletions,
|
||||||
fieldPrefix: '',
|
fieldPrefix: '',
|
||||||
fields,
|
fields,
|
||||||
numbers,
|
numbers,
|
||||||
@@ -46,5 +48,7 @@ export const transform = <T extends TypeWithID>({ config, data, fields }: Transf
|
|||||||
table: data,
|
table: data,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
deletions.forEach((deletion) => deletion())
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ type TraverseFieldsArgs = {
|
|||||||
* The data reference to be mutated within this recursive function
|
* The data reference to be mutated within this recursive function
|
||||||
*/
|
*/
|
||||||
dataRef: Record<string, unknown>
|
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
|
* 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,
|
blocks,
|
||||||
config,
|
config,
|
||||||
dataRef,
|
dataRef,
|
||||||
|
deletions,
|
||||||
fieldPrefix,
|
fieldPrefix,
|
||||||
fields,
|
fields,
|
||||||
numbers,
|
numbers,
|
||||||
@@ -69,6 +74,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
blocks,
|
blocks,
|
||||||
config,
|
config,
|
||||||
dataRef,
|
dataRef,
|
||||||
|
deletions,
|
||||||
fieldPrefix,
|
fieldPrefix,
|
||||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||||
numbers,
|
numbers,
|
||||||
@@ -87,6 +93,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
blocks,
|
blocks,
|
||||||
config,
|
config,
|
||||||
dataRef,
|
dataRef,
|
||||||
|
deletions,
|
||||||
fieldPrefix,
|
fieldPrefix,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
numbers,
|
numbers,
|
||||||
@@ -99,7 +106,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
if (fieldAffectsData(field)) {
|
if (fieldAffectsData(field)) {
|
||||||
const fieldName = `${fieldPrefix || ''}${field.name}`
|
const fieldName = `${fieldPrefix || ''}${field.name}`
|
||||||
const fieldData = table[fieldName]
|
const fieldData = table[fieldName]
|
||||||
|
|
||||||
if (field.type === 'array') {
|
if (field.type === 'array') {
|
||||||
if (Array.isArray(fieldData)) {
|
if (Array.isArray(fieldData)) {
|
||||||
if (field.localized) {
|
if (field.localized) {
|
||||||
@@ -109,11 +115,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
const locale = row._locale
|
const locale = row._locale
|
||||||
const data = {}
|
const data = {}
|
||||||
delete row._locale
|
delete row._locale
|
||||||
|
if (row._uuid) {
|
||||||
|
row.id = row._uuid
|
||||||
|
delete row._uuid
|
||||||
|
}
|
||||||
|
|
||||||
const rowResult = traverseFields<T>({
|
const rowResult = traverseFields<T>({
|
||||||
blocks,
|
blocks,
|
||||||
config,
|
config,
|
||||||
dataRef: data,
|
dataRef: data,
|
||||||
|
deletions,
|
||||||
fieldPrefix: '',
|
fieldPrefix: '',
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
numbers,
|
numbers,
|
||||||
@@ -129,10 +140,15 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
}, {})
|
}, {})
|
||||||
} else {
|
} else {
|
||||||
result[field.name] = fieldData.map((row, i) => {
|
result[field.name] = fieldData.map((row, i) => {
|
||||||
|
if (row._uuid) {
|
||||||
|
row.id = row._uuid
|
||||||
|
delete row._uuid
|
||||||
|
}
|
||||||
return traverseFields<T>({
|
return traverseFields<T>({
|
||||||
blocks,
|
blocks,
|
||||||
config,
|
config,
|
||||||
dataRef: row,
|
dataRef: row,
|
||||||
|
deletions,
|
||||||
fieldPrefix: '',
|
fieldPrefix: '',
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
numbers,
|
numbers,
|
||||||
@@ -155,6 +171,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
result[field.name] = {}
|
result[field.name] = {}
|
||||||
|
|
||||||
blocks[blockFieldPath].forEach((row) => {
|
blocks[blockFieldPath].forEach((row) => {
|
||||||
|
if (row._uuid) {
|
||||||
|
row.id = row._uuid
|
||||||
|
delete row._uuid
|
||||||
|
}
|
||||||
if (typeof row._locale === 'string') {
|
if (typeof row._locale === 'string') {
|
||||||
if (!result[field.name][row._locale]) result[field.name][row._locale] = []
|
if (!result[field.name][row._locale]) result[field.name][row._locale] = []
|
||||||
result[field.name][row._locale].push(row)
|
result[field.name][row._locale].push(row)
|
||||||
@@ -171,6 +191,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
blocks,
|
blocks,
|
||||||
config,
|
config,
|
||||||
dataRef: row,
|
dataRef: row,
|
||||||
|
deletions,
|
||||||
fieldPrefix: '',
|
fieldPrefix: '',
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
numbers,
|
numbers,
|
||||||
@@ -189,6 +210,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
} else {
|
} else {
|
||||||
result[field.name] = blocks[blockFieldPath].map((row, i) => {
|
result[field.name] = blocks[blockFieldPath].map((row, i) => {
|
||||||
delete row._order
|
delete row._order
|
||||||
|
if (row._uuid) {
|
||||||
|
row.id = row._uuid
|
||||||
|
delete row._uuid
|
||||||
|
}
|
||||||
const block = field.blocks.find(({ slug }) => slug === row.blockType)
|
const block = field.blocks.find(({ slug }) => slug === row.blockType)
|
||||||
|
|
||||||
if (block) {
|
if (block) {
|
||||||
@@ -196,6 +221,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
blocks,
|
blocks,
|
||||||
config,
|
config,
|
||||||
dataRef: row,
|
dataRef: row,
|
||||||
|
deletions,
|
||||||
fieldPrefix: '',
|
fieldPrefix: '',
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
numbers,
|
numbers,
|
||||||
@@ -345,6 +371,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
blocks,
|
blocks,
|
||||||
config,
|
config,
|
||||||
dataRef: groupLocaleData as Record<string, unknown>,
|
dataRef: groupLocaleData as Record<string, unknown>,
|
||||||
|
deletions,
|
||||||
fieldPrefix: groupFieldPrefix,
|
fieldPrefix: groupFieldPrefix,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
numbers,
|
numbers,
|
||||||
@@ -360,6 +387,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
blocks,
|
blocks,
|
||||||
config,
|
config,
|
||||||
dataRef: groupData as Record<string, unknown>,
|
dataRef: groupData as Record<string, unknown>,
|
||||||
|
deletions,
|
||||||
fieldPrefix: groupFieldPrefix,
|
fieldPrefix: groupFieldPrefix,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
numbers,
|
numbers,
|
||||||
@@ -425,5 +453,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
return result
|
return result
|
||||||
}, dataRef)
|
}, dataRef)
|
||||||
|
|
||||||
|
if (Array.isArray(table._locales)) {
|
||||||
|
deletions.push(() => delete table._locales)
|
||||||
|
}
|
||||||
|
|
||||||
return formatted as T
|
return formatted as T
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import type { ArrayField } from 'payload/types'
|
import type { ArrayField } from 'payload/types'
|
||||||
|
|
||||||
|
import type { PostgresAdapter } from '../../types'
|
||||||
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types'
|
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types'
|
||||||
|
|
||||||
import { isArrayOfRows } from '../../utilities/isArrayOfRows'
|
import { isArrayOfRows } from '../../utilities/isArrayOfRows'
|
||||||
import { traverseFields } from './traverseFields'
|
import { traverseFields } from './traverseFields'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
|
adapter: PostgresAdapter
|
||||||
arrayTableName: string
|
arrayTableName: string
|
||||||
baseTableName: string
|
baseTableName: string
|
||||||
blocks: {
|
blocks: {
|
||||||
@@ -25,6 +27,7 @@ type Args = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const transformArray = ({
|
export const transformArray = ({
|
||||||
|
adapter,
|
||||||
arrayTableName,
|
arrayTableName,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
@@ -38,6 +41,7 @@ export const transformArray = ({
|
|||||||
selects,
|
selects,
|
||||||
}: Args) => {
|
}: Args) => {
|
||||||
const newRows: ArrayRowToInsert[] = []
|
const newRows: ArrayRowToInsert[] = []
|
||||||
|
const hasUUID = adapter.tables[arrayTableName]._uuid
|
||||||
|
|
||||||
if (isArrayOfRows(data)) {
|
if (isArrayOfRows(data)) {
|
||||||
data.forEach((arrayRow, i) => {
|
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) {
|
if (locale) {
|
||||||
newRow.locales[locale] = {
|
newRow.locales[locale] = {
|
||||||
_locale: locale,
|
_locale: locale,
|
||||||
@@ -60,6 +74,7 @@ export const transformArray = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
adapter,
|
||||||
arrays: newRow.arrays,
|
arrays: newRow.arrays,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import type { BlockField } from 'payload/types'
|
|||||||
|
|
||||||
import toSnakeCase from 'to-snake-case'
|
import toSnakeCase from 'to-snake-case'
|
||||||
|
|
||||||
|
import type { PostgresAdapter } from '../../types'
|
||||||
import type { BlockRowToInsert, RelationshipToDelete } from './types'
|
import type { BlockRowToInsert, RelationshipToDelete } from './types'
|
||||||
|
|
||||||
import { traverseFields } from './traverseFields'
|
import { traverseFields } from './traverseFields'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
|
adapter: PostgresAdapter
|
||||||
baseTableName: string
|
baseTableName: string
|
||||||
blocks: {
|
blocks: {
|
||||||
[blockType: string]: BlockRowToInsert[]
|
[blockType: string]: BlockRowToInsert[]
|
||||||
@@ -24,6 +26,7 @@ type Args = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const transformBlocks = ({
|
export const transformBlocks = ({
|
||||||
|
adapter,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
data,
|
data,
|
||||||
@@ -56,7 +59,20 @@ export const transformBlocks = ({
|
|||||||
|
|
||||||
const blockTableName = `${baseTableName}_blocks_${blockType}`
|
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({
|
traverseFields({
|
||||||
|
adapter,
|
||||||
arrays: newRow.arrays,
|
arrays: newRow.arrays,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import type { Field } from 'payload/types'
|
import type { Field } from 'payload/types'
|
||||||
|
|
||||||
|
import type { PostgresAdapter } from '../../types'
|
||||||
import type { RowToInsert } from './types'
|
import type { RowToInsert } from './types'
|
||||||
|
|
||||||
import { traverseFields } from './traverseFields'
|
import { traverseFields } from './traverseFields'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
|
adapter: PostgresAdapter
|
||||||
data: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
path?: string
|
path?: string
|
||||||
tableName: 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
|
// Split out the incoming data into rows to insert / delete
|
||||||
const rowToInsert: RowToInsert = {
|
const rowToInsert: RowToInsert = {
|
||||||
arrays: {},
|
arrays: {},
|
||||||
@@ -28,6 +36,7 @@ export const transformForWrite = ({ data, fields, path = '', tableName }: Args):
|
|||||||
// This function is responsible for building up the
|
// This function is responsible for building up the
|
||||||
// above rowToInsert
|
// above rowToInsert
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
adapter,
|
||||||
arrays: rowToInsert.arrays,
|
arrays: rowToInsert.arrays,
|
||||||
baseTableName: tableName,
|
baseTableName: tableName,
|
||||||
blocks: rowToInsert.blocks,
|
blocks: rowToInsert.blocks,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { Field } from 'payload/types'
|
|||||||
import { fieldAffectsData } from 'payload/types'
|
import { fieldAffectsData } from 'payload/types'
|
||||||
import toSnakeCase from 'to-snake-case'
|
import toSnakeCase from 'to-snake-case'
|
||||||
|
|
||||||
|
import type { PostgresAdapter } from '../../types'
|
||||||
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types'
|
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types'
|
||||||
|
|
||||||
import { isArrayOfRows } from '../../utilities/isArrayOfRows'
|
import { isArrayOfRows } from '../../utilities/isArrayOfRows'
|
||||||
@@ -14,6 +15,7 @@ import { transformRelationship } from './relationships'
|
|||||||
import { transformSelects } from './selects'
|
import { transformSelects } from './selects'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
|
adapter: PostgresAdapter
|
||||||
arrays: {
|
arrays: {
|
||||||
[tableName: string]: ArrayRowToInsert[]
|
[tableName: string]: ArrayRowToInsert[]
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,7 @@ type Args = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const traverseFields = ({
|
export const traverseFields = ({
|
||||||
|
adapter,
|
||||||
arrays,
|
arrays,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
@@ -95,6 +98,7 @@ export const traverseFields = ({
|
|||||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||||
if (Array.isArray(localeData)) {
|
if (Array.isArray(localeData)) {
|
||||||
const newRows = transformArray({
|
const newRows = transformArray({
|
||||||
|
adapter,
|
||||||
arrayTableName,
|
arrayTableName,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
@@ -114,6 +118,7 @@ export const traverseFields = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const newRows = transformArray({
|
const newRows = transformArray({
|
||||||
|
adapter,
|
||||||
arrayTableName,
|
arrayTableName,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
@@ -138,6 +143,7 @@ export const traverseFields = ({
|
|||||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||||
if (Array.isArray(localeData)) {
|
if (Array.isArray(localeData)) {
|
||||||
transformBlocks({
|
transformBlocks({
|
||||||
|
adapter,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
data: localeData,
|
data: localeData,
|
||||||
@@ -154,6 +160,7 @@ export const traverseFields = ({
|
|||||||
}
|
}
|
||||||
} else if (isArrayOfRows(fieldData)) {
|
} else if (isArrayOfRows(fieldData)) {
|
||||||
transformBlocks({
|
transformBlocks({
|
||||||
|
adapter,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
data: fieldData,
|
data: fieldData,
|
||||||
@@ -174,6 +181,7 @@ export const traverseFields = ({
|
|||||||
if (field.localized) {
|
if (field.localized) {
|
||||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
adapter,
|
||||||
arrays,
|
arrays,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
@@ -195,6 +203,7 @@ export const traverseFields = ({
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
adapter,
|
||||||
arrays,
|
arrays,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
@@ -225,6 +234,7 @@ export const traverseFields = ({
|
|||||||
if (tab.localized) {
|
if (tab.localized) {
|
||||||
Object.entries(data[tab.name]).forEach(([localeKey, localeData]) => {
|
Object.entries(data[tab.name]).forEach(([localeKey, localeData]) => {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
adapter,
|
||||||
arrays,
|
arrays,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
@@ -246,6 +256,7 @@ export const traverseFields = ({
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
adapter,
|
||||||
arrays,
|
arrays,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
@@ -267,6 +278,7 @@ export const traverseFields = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
adapter,
|
||||||
arrays,
|
arrays,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
@@ -290,6 +302,7 @@ export const traverseFields = ({
|
|||||||
|
|
||||||
if (field.type === 'row' || field.type === 'collapsible') {
|
if (field.type === 'row' || field.type === 'collapsible') {
|
||||||
traverseFields({
|
traverseFields({
|
||||||
|
adapter,
|
||||||
arrays,
|
arrays,
|
||||||
baseTableName,
|
baseTableName,
|
||||||
blocks,
|
blocks,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const upsertRow = async <T extends TypeWithID>({
|
|||||||
// Split out the incoming data into the corresponding:
|
// Split out the incoming data into the corresponding:
|
||||||
// base row, locales, relationships, blocks, and arrays
|
// base row, locales, relationships, blocks, and arrays
|
||||||
const rowToInsert = transformForWrite({
|
const rowToInsert = transformForWrite({
|
||||||
|
adapter,
|
||||||
data,
|
data,
|
||||||
fields,
|
fields,
|
||||||
path,
|
path,
|
||||||
@@ -107,6 +108,9 @@ export const upsertRow = async <T extends TypeWithID>({
|
|||||||
rowToInsert.blocks[blockName].forEach((blockRow) => {
|
rowToInsert.blocks[blockName].forEach((blockRow) => {
|
||||||
blockRow.row._parentID = insertedRow.id
|
blockRow.row._parentID = insertedRow.id
|
||||||
if (!blocksToInsert[blockName]) blocksToInsert[blockName] = []
|
if (!blocksToInsert[blockName]) blocksToInsert[blockName] = []
|
||||||
|
if (blockRow.row.uuid) {
|
||||||
|
delete blockRow.row.uuid
|
||||||
|
}
|
||||||
blocksToInsert[blockName].push(blockRow)
|
blocksToInsert[blockName].push(blockRow)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@payloadcms/eslint-config",
|
"name": "@payloadcms/eslint-config",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"description": "Payload styles for ESLint and Prettier",
|
"description": "Payload styles for ESLint and Prettier",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@payloadcms/live-preview-react",
|
"name": "@payloadcms/live-preview-react",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"description": "The official live preview React SDK for Payload",
|
"description": "The official live preview React SDK for Payload",
|
||||||
"repository": "https://github.com/payloadcms/payload",
|
"repository": "https://github.com/payloadcms/payload",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"prepublishOnly": "pnpm clean && pnpm build"
|
"prepublishOnly": "pnpm clean && pnpm build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@payloadcms/live-preview": "workspace:*"
|
"@payloadcms/live-preview": "workspace:^0.x"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@payloadcms/eslint-config": "workspace:*",
|
"@payloadcms/eslint-config": "workspace:*",
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"payload": "workspace:*"
|
"payload": "workspace:*"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"payload": "^2.0.0",
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@payloadcms/live-preview",
|
"name": "@payloadcms/live-preview",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"description": "The official live preview JavaScript SDK for Payload",
|
"description": "The official live preview JavaScript SDK for Payload",
|
||||||
"repository": "https://github.com/payloadcms/payload",
|
"repository": "https://github.com/payloadcms/payload",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -20,6 +20,9 @@
|
|||||||
"@payloadcms/eslint-config": "workspace:*",
|
"@payloadcms/eslint-config": "workspace:*",
|
||||||
"payload": "workspace:*"
|
"payload": "workspace:*"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "^2.0.0"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"default": "./src/index.ts",
|
"default": "./src/index.ts",
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { mergeData } from '.'
|
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: {
|
export const handleMessage = async <T>(args: {
|
||||||
depth: number
|
depth: number
|
||||||
event: MessageEvent
|
event: MessageEvent
|
||||||
@@ -11,9 +17,13 @@ export const handleMessage = async <T>(args: {
|
|||||||
const eventData = JSON.parse(event?.data)
|
const eventData = JSON.parse(event?.data)
|
||||||
|
|
||||||
if (eventData.type === 'payload-live-preview') {
|
if (eventData.type === 'payload-live-preview') {
|
||||||
|
if (!payloadLivePreviewFieldSchema && eventData.fieldSchemaJSON) {
|
||||||
|
payloadLivePreviewFieldSchema = eventData.fieldSchemaJSON
|
||||||
|
}
|
||||||
|
|
||||||
const mergedData = await mergeData<T>({
|
const mergedData = await mergeData<T>({
|
||||||
depth,
|
depth,
|
||||||
fieldSchema: eventData.fieldSchemaJSON,
|
fieldSchema: payloadLivePreviewFieldSchema,
|
||||||
incomingData: eventData.data,
|
incomingData: eventData.data,
|
||||||
initialData,
|
initialData,
|
||||||
serverURL,
|
serverURL,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import type { fieldSchemaToJSON } from 'payload/utilities'
|
||||||
|
|
||||||
import { traverseFields } from './traverseFields'
|
import { traverseFields } from './traverseFields'
|
||||||
|
|
||||||
export type MergeLiveDataArgs<T> = {
|
export type MergeLiveDataArgs<T> = {
|
||||||
apiRoute?: string
|
apiRoute?: string
|
||||||
depth: number
|
depth: number
|
||||||
fieldSchema: Record<string, unknown>[]
|
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||||
incomingData: T
|
incomingData: T
|
||||||
initialData: T
|
initialData: T
|
||||||
serverURL: string
|
serverURL: string
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ export const ready = (args: { serverURL: string }): void => {
|
|||||||
const { serverURL } = args
|
const { serverURL } = args
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// This subscription may have been from either an iframe `src` or `window.open()`
|
// This subscription may have been from either an iframe or a popup
|
||||||
// i.e. `window?.opener` || `window?.parent`
|
// We need to report 'ready' to the parent window, whichever it may be
|
||||||
window?.opener?.postMessage(
|
// i.e. `window?.opener` for popups, `window?.parent` for iframes
|
||||||
|
const windowToPostTo: Window = window?.opener || window?.parent
|
||||||
|
|
||||||
|
windowToPostTo?.postMessage(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
popupReady: true,
|
ready: true,
|
||||||
type: 'payload-live-preview',
|
type: 'payload-live-preview',
|
||||||
}),
|
}),
|
||||||
serverURL,
|
serverURL,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const subscribe = <T>(args: {
|
|||||||
const { callback, depth, initialData, serverURL } = args
|
const { callback, depth, initialData, serverURL } = args
|
||||||
|
|
||||||
const onMessage = async (event: MessageEvent) => {
|
const onMessage = async (event: MessageEvent) => {
|
||||||
const mergedData = await handleMessage({ depth, event, initialData, serverURL })
|
const mergedData = await handleMessage<T>({ depth, event, initialData, serverURL })
|
||||||
callback(mergedData)
|
callback(mergedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import type { fieldSchemaToJSON } from 'payload/utilities'
|
||||||
|
|
||||||
import { promise } from './promise'
|
import { promise } from './promise'
|
||||||
|
|
||||||
type Args<T> = {
|
type Args<T> = {
|
||||||
apiRoute?: string
|
apiRoute?: string
|
||||||
depth: number
|
depth: number
|
||||||
fieldSchema: Record<string, unknown>[]
|
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
|
||||||
incomingData: T
|
incomingData: T
|
||||||
populationPromises: Promise<void>[]
|
populationPromises: Promise<void>[]
|
||||||
result: T
|
result: T
|
||||||
@@ -19,12 +21,11 @@ export const traverseFields = <T>({
|
|||||||
result,
|
result,
|
||||||
serverURL,
|
serverURL,
|
||||||
}: Args<T>): void => {
|
}: Args<T>): void => {
|
||||||
fieldSchema.forEach((field) => {
|
fieldSchema.forEach((fieldJSON) => {
|
||||||
if ('name' in field && typeof field.name === 'string') {
|
if ('name' in fieldJSON && typeof fieldJSON.name === 'string') {
|
||||||
// TODO: type this
|
const fieldName = fieldJSON.name
|
||||||
const fieldName = field.name
|
|
||||||
|
|
||||||
switch (field.type) {
|
switch (fieldJSON.type) {
|
||||||
case 'array':
|
case 'array':
|
||||||
if (Array.isArray(incomingData[fieldName])) {
|
if (Array.isArray(incomingData[fieldName])) {
|
||||||
result[fieldName] = incomingData[fieldName].map((row, i) => {
|
result[fieldName] = incomingData[fieldName].map((row, i) => {
|
||||||
@@ -38,7 +39,7 @@ export const traverseFields = <T>({
|
|||||||
traverseFields({
|
traverseFields({
|
||||||
apiRoute,
|
apiRoute,
|
||||||
depth,
|
depth,
|
||||||
fieldSchema: field.fields as Record<string, unknown>[], // TODO: type this
|
fieldSchema: fieldJSON.fields,
|
||||||
incomingData: row,
|
incomingData: row,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
result: newRow,
|
result: newRow,
|
||||||
@@ -53,7 +54,7 @@ export const traverseFields = <T>({
|
|||||||
case 'blocks':
|
case 'blocks':
|
||||||
if (Array.isArray(incomingData[fieldName])) {
|
if (Array.isArray(incomingData[fieldName])) {
|
||||||
result[fieldName] = incomingData[fieldName].map((row, i) => {
|
result[fieldName] = incomingData[fieldName].map((row, i) => {
|
||||||
const matchedBlock = field.blocks[row.blockType]
|
const matchedBlock = fieldJSON.blocks[row.blockType]
|
||||||
|
|
||||||
const hasExistingRow =
|
const hasExistingRow =
|
||||||
Array.isArray(result[fieldName]) &&
|
Array.isArray(result[fieldName]) &&
|
||||||
@@ -70,7 +71,7 @@ export const traverseFields = <T>({
|
|||||||
traverseFields({
|
traverseFields({
|
||||||
apiRoute,
|
apiRoute,
|
||||||
depth,
|
depth,
|
||||||
fieldSchema: matchedBlock.fields as Record<string, unknown>[], // TODO: type this
|
fieldSchema: matchedBlock.fields,
|
||||||
incomingData: row,
|
incomingData: row,
|
||||||
populationPromises,
|
populationPromises,
|
||||||
result: newRow,
|
result: newRow,
|
||||||
@@ -82,7 +83,7 @@ export const traverseFields = <T>({
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'tab':
|
case 'tabs':
|
||||||
case 'group':
|
case 'group':
|
||||||
if (!result[fieldName]) {
|
if (!result[fieldName]) {
|
||||||
result[fieldName] = {}
|
result[fieldName] = {}
|
||||||
@@ -91,7 +92,7 @@ export const traverseFields = <T>({
|
|||||||
traverseFields({
|
traverseFields({
|
||||||
apiRoute,
|
apiRoute,
|
||||||
depth,
|
depth,
|
||||||
fieldSchema: field.fields as Record<string, unknown>[], // TODO: type this
|
fieldSchema: fieldJSON.fields,
|
||||||
incomingData: incomingData[fieldName] || {},
|
incomingData: incomingData[fieldName] || {},
|
||||||
populationPromises,
|
populationPromises,
|
||||||
result: result[fieldName],
|
result: result[fieldName],
|
||||||
@@ -102,7 +103,7 @@ export const traverseFields = <T>({
|
|||||||
|
|
||||||
case 'upload':
|
case 'upload':
|
||||||
case 'relationship':
|
case 'relationship':
|
||||||
if (field.hasMany && Array.isArray(incomingData[fieldName])) {
|
if (fieldJSON.hasMany && Array.isArray(incomingData[fieldName])) {
|
||||||
const existingValue = Array.isArray(result[fieldName]) ? [...result[fieldName]] : []
|
const existingValue = Array.isArray(result[fieldName]) ? [...result[fieldName]] : []
|
||||||
result[fieldName] = Array.isArray(result[fieldName])
|
result[fieldName] = Array.isArray(result[fieldName])
|
||||||
? [...result[fieldName]].slice(0, incomingData[fieldName].length)
|
? [...result[fieldName]].slice(0, incomingData[fieldName].length)
|
||||||
@@ -110,7 +111,7 @@ export const traverseFields = <T>({
|
|||||||
|
|
||||||
incomingData[fieldName].forEach((relation, i) => {
|
incomingData[fieldName].forEach((relation, i) => {
|
||||||
// Handle `hasMany` polymorphic
|
// Handle `hasMany` polymorphic
|
||||||
if (Array.isArray(field.relationTo)) {
|
if (Array.isArray(fieldJSON.relationTo)) {
|
||||||
const existingID = existingValue[i]?.value?.id
|
const existingID = existingValue[i]?.value?.id
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -134,7 +135,7 @@ export const traverseFields = <T>({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle `hasMany` singular
|
// Handle `hasMany` monomorphic
|
||||||
const existingID = existingValue[i]?.id
|
const existingID = existingValue[i]?.id
|
||||||
|
|
||||||
if (existingID !== relation) {
|
if (existingID !== relation) {
|
||||||
@@ -143,7 +144,7 @@ export const traverseFields = <T>({
|
|||||||
id: relation,
|
id: relation,
|
||||||
accessor: i,
|
accessor: i,
|
||||||
apiRoute,
|
apiRoute,
|
||||||
collection: String(field.relationTo),
|
collection: String(fieldJSON.relationTo),
|
||||||
depth,
|
depth,
|
||||||
ref: result[fieldName],
|
ref: result[fieldName],
|
||||||
serverURL,
|
serverURL,
|
||||||
@@ -154,7 +155,7 @@ export const traverseFields = <T>({
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Handle `hasOne` polymorphic
|
// Handle `hasOne` polymorphic
|
||||||
if (Array.isArray(field.relationTo)) {
|
if (Array.isArray(fieldJSON.relationTo)) {
|
||||||
const hasNewValue =
|
const hasNewValue =
|
||||||
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
|
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
|
||||||
const hasOldValue =
|
const hasOldValue =
|
||||||
@@ -190,31 +191,37 @@ export const traverseFields = <T>({
|
|||||||
result[fieldName] = null
|
result[fieldName] = null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const hasNewValue =
|
// Handle `hasOne` monomorphic
|
||||||
typeof incomingData[fieldName] === 'object' && incomingData[fieldName] !== null
|
const newID: string =
|
||||||
const hasOldValue =
|
(typeof incomingData[fieldName] === 'string' && incomingData[fieldName]) ||
|
||||||
typeof result[fieldName] === 'object' && result[fieldName] !== null
|
(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 (newID !== oldID) {
|
||||||
|
if (newID) {
|
||||||
if (newValue !== oldValue) {
|
|
||||||
if (newValue) {
|
|
||||||
populationPromises.push(
|
populationPromises.push(
|
||||||
promise({
|
promise({
|
||||||
id: newValue,
|
id: newID,
|
||||||
accessor: fieldName,
|
accessor: fieldName,
|
||||||
apiRoute,
|
apiRoute,
|
||||||
collection: String(field.relationTo),
|
collection: String(fieldJSON.relationTo),
|
||||||
depth,
|
depth,
|
||||||
ref: result as Record<string, unknown>,
|
ref: result as Record<string, unknown>,
|
||||||
serverURL,
|
serverURL,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
result[fieldName] = null
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
result[fieldName] = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
packages/payload/.release-it.js
Normal file
51
packages/payload/.release-it.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
module.exports = {
|
||||||
|
verbose: true,
|
||||||
|
git: {
|
||||||
|
commitMessage: 'chore(release): v${version}',
|
||||||
|
requireCleanWorkingDir: false,
|
||||||
|
tagMatch: 'v*', // payload is tagged normally, other packages are tagged with a prefix
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
release: true,
|
||||||
|
},
|
||||||
|
npm: {
|
||||||
|
skipChecks: true,
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
'before:init': ['pnpm install', 'pnpm clean', 'pnpm build'], // Assume tests have already been run
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'@release-it/conventional-changelog': {
|
||||||
|
infile: '../../CHANGELOG.md',
|
||||||
|
preset: {
|
||||||
|
name: 'conventionalcommits',
|
||||||
|
types: [
|
||||||
|
{ type: 'feat', section: 'Features' },
|
||||||
|
{ type: 'feature', section: 'Features' },
|
||||||
|
{ type: 'fix', section: 'Bug Fixes' },
|
||||||
|
{ type: 'docs', section: 'Documentation' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
writerOpts: {
|
||||||
|
commitGroupsSort: (a, b) => {
|
||||||
|
const groupOrder = ['Features', 'Bug Fixes', 'Documentation']
|
||||||
|
return groupOrder.indexOf(a.title) - groupOrder.indexOf(b.title)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Scoped commits at the end, alphabetical sort
|
||||||
|
commitsSort: (a, b) => {
|
||||||
|
if (a.scope || b.scope) {
|
||||||
|
if (!a.scope) return -1
|
||||||
|
if (!b.scope) return 1
|
||||||
|
return a.scope === b.scope
|
||||||
|
? a.subject.localeCompare(b.subject)
|
||||||
|
: a.scope.localeCompare(b.scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alphabetical sort
|
||||||
|
return a.subject.localeCompare(b.subject)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "payload",
|
"name": "payload",
|
||||||
"version": "2.0.7",
|
"version": "2.0.11",
|
||||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
@@ -40,8 +40,7 @@
|
|||||||
"lint": "eslint \"src/**/*.ts\"",
|
"lint": "eslint \"src/**/*.ts\"",
|
||||||
"prepublishOnly": "pnpm clean && pnpm build",
|
"prepublishOnly": "pnpm clean && pnpm build",
|
||||||
"pretest": "pnpm build",
|
"pretest": "pnpm build",
|
||||||
"release:beta": "release-it pre --preReleaseId=beta --npm.tag=beta --config .release-it.pre.json",
|
"release:beta": "release-it pre --preReleaseId=beta --npm.tag=beta --config .release-it.pre.js",
|
||||||
"release:canary": "release-it pre --preReleaseId=canary --npm.tag=canary --config .release-it.pre.json",
|
|
||||||
"release:major": "release-it major",
|
"release:major": "release-it major",
|
||||||
"release:minor": "release-it minor",
|
"release:minor": "release-it minor",
|
||||||
"release:patch": "release-it patch",
|
"release:patch": "release-it patch",
|
||||||
@@ -128,7 +127,7 @@
|
|||||||
"react-select": "5.7.4",
|
"react-select": "5.7.4",
|
||||||
"react-toastify": "8.2.0",
|
"react-toastify": "8.2.0",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"sass": "1.64.0",
|
"sass": "1.69.4",
|
||||||
"scheduler": "0.23.0",
|
"scheduler": "0.23.0",
|
||||||
"scmp": "2.1.0",
|
"scmp": "2.1.0",
|
||||||
"sharp": "0.31.3",
|
"sharp": "0.31.3",
|
||||||
|
|||||||
@@ -31,13 +31,14 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
|||||||
routes: { admin, api },
|
routes: { admin, api },
|
||||||
serverURL,
|
serverURL,
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
|
||||||
const { setModified } = useForm()
|
const { setModified } = useForm()
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
const { toggleModal } = useModal()
|
const { toggleModal } = useModal()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { i18n, t } = useTranslation('general')
|
const { i18n, t } = useTranslation('general')
|
||||||
const title = useTitle({ collection })
|
const title = useTitle({ collection })
|
||||||
const titleToRender = titleFromProps || title
|
const titleToRender = titleFromProps || title || id
|
||||||
|
|
||||||
const modalSlug = `delete-${id}`
|
const modalSlug = `delete-${id}`
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@import '../../../scss/styles.scss';
|
@import '../../../scss/styles.scss';
|
||||||
|
|
||||||
.doc-controls {
|
.doc-controls {
|
||||||
@include blur-bg;
|
@include blur-bg-light;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&::after {
|
&__divider {
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ export const DocumentControls: React.FC<{
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={`${baseClass}__divider`} />
|
||||||
</Gutter>
|
</Gutter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,19 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
white-space: nowrap;
|
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) {
|
&:focus:not(:focus-visible) {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export const DocumentTab: React.FC<DocumentTabProps & DocumentTabConfig> = (prop
|
|||||||
className={`${baseClass}__link`}
|
className={`${baseClass}__link`}
|
||||||
to={href}
|
to={href}
|
||||||
{...(newTab && { rel: 'noopener noreferrer', target: '_blank' })}
|
{...(newTab && { rel: 'noopener noreferrer', target: '_blank' })}
|
||||||
|
tabIndex={isActive ? -1 : 0}
|
||||||
>
|
>
|
||||||
<span className={`${baseClass}__label`}>
|
<span className={`${baseClass}__label`}>
|
||||||
{labelToRender}
|
{labelToRender}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const tabs: DocumentTabConfig[] = [
|
|||||||
// API
|
// API
|
||||||
{
|
{
|
||||||
condition: ({ collection, global }) =>
|
condition: ({ collection, global }) =>
|
||||||
!collection?.admin?.hideAPIURL || !global?.admin?.hideAPIURL,
|
(collection && !collection?.admin?.hideAPIURL) || (global && !global?.admin?.hideAPIURL),
|
||||||
href: '/api',
|
href: '/api',
|
||||||
label: 'API',
|
label: 'API',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@import '../../../scss/styles.scss';
|
@import '../../../scss/styles.scss';
|
||||||
|
|
||||||
$transTime: 200ms;
|
$transTime: 200;
|
||||||
|
|
||||||
.drawer {
|
.drawer {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -9,7 +9,7 @@ $transTime: 200ms;
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
||||||
&__blur-bg {
|
&__blur-bg {
|
||||||
@include blur-bg();
|
@include blur-bg;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -17,7 +17,7 @@ $transTime: 200ms;
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all $transTime linear;
|
transition: all #{$transTime}ms linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
@@ -27,7 +27,8 @@ $transTime: 200ms;
|
|||||||
z-index: 2;
|
z-index: 2;
|
||||||
width: calc(100% - var(--gutter-h));
|
width: calc(100% - var(--gutter-h));
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all $transTime linear;
|
transition: all #{$transTime}ms linear;
|
||||||
|
background-color: var(--theme-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content-children {
|
&__content-children {
|
||||||
@@ -40,14 +41,14 @@ $transTime: 200ms;
|
|||||||
&--is-open {
|
&--is-open {
|
||||||
.drawer {
|
.drawer {
|
||||||
&__content,
|
&__content,
|
||||||
&__blur-bg,
|
&__blur-bg {
|
||||||
&__close {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__close {
|
&__close {
|
||||||
transition: opacity $transTime linear;
|
opacity: 0.1;
|
||||||
transition-delay: $transTime;
|
transition: opacity #{$transTime}ms linear;
|
||||||
|
transition-delay: #{calc($transTime / 2)}ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
@@ -68,7 +69,7 @@ $transTime: 200ms;
|
|||||||
transition: none;
|
transition: none;
|
||||||
transition-delay: 0ms;
|
transition-delay: 0ms;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
background: transparent;
|
background: var(--theme-elevation-800);
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
@@ -120,7 +121,15 @@ $transTime: 200ms;
|
|||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='dark'] {
|
html[data-theme='dark'] {
|
||||||
.drawer__close {
|
.drawer {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
&__close {
|
||||||
|
background: var(--color-base-1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--is-open {
|
||||||
|
.drawer__close {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Modal, useModal } from '@faceless-ui/modal'
|
import { Modal, useModal } from '@faceless-ui/modal'
|
||||||
import { useWindowInfo } from '@faceless-ui/window-info'
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@@ -52,9 +51,6 @@ export const Drawer: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation('general')
|
const { t } = useTranslation('general')
|
||||||
const { closeModal, modalState } = useModal()
|
const { closeModal, modalState } = useModal()
|
||||||
const {
|
|
||||||
breakpoints: { m: midBreak },
|
|
||||||
} = useWindowInfo()
|
|
||||||
const drawerDepth = useEditDepth()
|
const drawerDepth = useEditDepth()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [animateIn, setAnimateIn] = useState(false)
|
const [animateIn, setAnimateIn] = useState(false)
|
||||||
@@ -72,7 +68,12 @@ export const Drawer: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className={[className, baseClass, animateIn && `${baseClass}--is-open`]
|
className={[
|
||||||
|
className,
|
||||||
|
baseClass,
|
||||||
|
animateIn && `${baseClass}--is-open`,
|
||||||
|
drawerDepth > 1 && `${baseClass}--nested`,
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
@@ -80,7 +81,7 @@ export const Drawer: React.FC<Props> = ({
|
|||||||
zIndex: zBase + drawerDepth,
|
zIndex: zBase + drawerDepth,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{drawerDepth === 1 && <div className={`${baseClass}__blur-bg`} />}
|
{(!drawerDepth || drawerDepth === 1) && <div className={`${baseClass}__blur-bg`} />}
|
||||||
<button
|
<button
|
||||||
aria-label={t('close')}
|
aria-label={t('close')}
|
||||||
className={`${baseClass}__close`}
|
className={`${baseClass}__close`}
|
||||||
@@ -89,7 +90,7 @@ export const Drawer: React.FC<Props> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
<div className={`${baseClass}__content`}>
|
<div className={`${baseClass}__content`}>
|
||||||
<div className={`${baseClass}__blur-bg`} />
|
<div className={`${baseClass}__blur-bg-content`} />
|
||||||
<Gutter className={`${baseClass}__content-children`} left={gutter} right={gutter}>
|
<Gutter className={`${baseClass}__content-children`} left={gutter} right={gutter}>
|
||||||
<EditDepthContext.Provider value={drawerDepth + 1}>
|
<EditDepthContext.Provider value={drawerDepth + 1}>
|
||||||
{header && header}
|
{header && header}
|
||||||
|
|||||||
@@ -97,7 +97,9 @@ export const EditUpload: React.FC<{
|
|||||||
|
|
||||||
const centerFocalPoint = () => {
|
const centerFocalPoint = () => {
|
||||||
const containerRect = focalWrapRef.current.getBoundingClientRect()
|
const containerRect = focalWrapRef.current.getBoundingClientRect()
|
||||||
const boundsRect = cropRef.current.getBoundingClientRect()
|
const boundsRect = showCrop
|
||||||
|
? cropRef.current.getBoundingClientRect()
|
||||||
|
: imageRef.current.getBoundingClientRect()
|
||||||
const xCenter =
|
const xCenter =
|
||||||
((boundsRect.left - containerRect.left + boundsRect.width / 2) / containerRect.width) * 100
|
((boundsRect.left - containerRect.left + boundsRect.width / 2) / containerRect.width) * 100
|
||||||
const yCenter =
|
const yCenter =
|
||||||
@@ -164,17 +166,19 @@ export const EditUpload: React.FC<{
|
|||||||
) : (
|
) : (
|
||||||
<img alt={t('upload:setFocalPoint')} ref={imageRef} src={fileSrcToUse} />
|
<img alt={t('upload:setFocalPoint')} ref={imageRef} src={fileSrcToUse} />
|
||||||
)}
|
)}
|
||||||
<DraggableElement
|
{showFocalPoint && (
|
||||||
boundsRef={cropRef}
|
<DraggableElement
|
||||||
checkBounds={checkBounds}
|
boundsRef={showCrop ? cropRef : imageRef}
|
||||||
className={`${baseClass}__focalPoint`}
|
checkBounds={showCrop ? checkBounds : false}
|
||||||
containerRef={focalWrapRef}
|
className={`${baseClass}__focalPoint`}
|
||||||
initialPosition={pointPosition}
|
containerRef={focalWrapRef}
|
||||||
onDragEnd={onDragEnd}
|
initialPosition={pointPosition}
|
||||||
setCheckBounds={setCheckBounds}
|
onDragEnd={onDragEnd}
|
||||||
>
|
setCheckBounds={showCrop ? setCheckBounds : false}
|
||||||
<Plus />
|
>
|
||||||
</DraggableElement>
|
<Plus />
|
||||||
|
</DraggableElement>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(showCrop || showFocalPoint) && (
|
{(showCrop || showFocalPoint) && (
|
||||||
|
|||||||
@@ -6,22 +6,37 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
position: absolute;
|
position: relative;
|
||||||
|
@include blur-bg;
|
||||||
|
|
||||||
--hamburger-padding: 5px;
|
--hamburger-padding: 8px;
|
||||||
--hamburger-size: 9px;
|
--hamburger-size: 9px;
|
||||||
--hamburger-line-gap: 3px;
|
--hamburger-line-gap: 3px;
|
||||||
padding: var(--hamburger-padding);
|
|
||||||
border: 1px solid var(--theme-elevation-350);
|
|
||||||
color: var(--theme-text);
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
&:focus {
|
color: var(--theme-text);
|
||||||
outline: none;
|
|
||||||
|
&__wrapper {
|
||||||
|
border: 1px solid var(--theme-elevation-150);
|
||||||
|
padding: var(--hamburger-padding);
|
||||||
|
border-radius: 3px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid var(--theme-elevation-500);
|
||||||
|
background-color: var(--theme-elevation-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
height: var(--hamburger-size);
|
height: var(--hamburger-size);
|
||||||
width: var(--hamburger-size);
|
width: var(--hamburger-size);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -14,32 +14,34 @@ export const Hamburger: React.FC<{
|
|||||||
const { closeIcon = 'x', isActive = false } = props
|
const { closeIcon = 'x', isActive = false } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[baseClass].filter(Boolean).join(' ')}>
|
<div className={baseClass}>
|
||||||
<div className={`${baseClass}__icon`}>
|
<div className={`${baseClass}__wrapper`}>
|
||||||
{!isActive && (
|
<div className={`${baseClass}__icon`}>
|
||||||
<div className={`${baseClass}__lines`} title={t('open')}>
|
{!isActive && (
|
||||||
<div className={`${baseClass}__line ${baseClass}__top`} />
|
<div className={`${baseClass}__lines`} title={t('open')}>
|
||||||
<div className={`${baseClass}__line ${baseClass}__middle`} />
|
<div className={`${baseClass}__line ${baseClass}__top`} />
|
||||||
<div className={`${baseClass}__line ${baseClass}__bottom`} />
|
<div className={`${baseClass}__line ${baseClass}__middle`} />
|
||||||
</div>
|
<div className={`${baseClass}__line ${baseClass}__bottom`} />
|
||||||
)}
|
</div>
|
||||||
{isActive && (
|
)}
|
||||||
<div
|
{isActive && (
|
||||||
aria-label={closeIcon === 'collapse' ? t('collapse') : t('close')}
|
<div
|
||||||
className={`${baseClass}__close-icon`}
|
aria-label={closeIcon === 'collapse' ? t('collapse') : t('close')}
|
||||||
title={closeIcon === 'collapse' ? t('collapse') : t('close')}
|
className={`${baseClass}__close-icon`}
|
||||||
>
|
title={closeIcon === 'collapse' ? t('collapse') : t('close')}
|
||||||
{closeIcon === 'x' && (
|
>
|
||||||
<React.Fragment>
|
{closeIcon === 'x' && (
|
||||||
<div className={`${baseClass}__line ${baseClass}__x-left`} />
|
<React.Fragment>
|
||||||
<div className={`${baseClass}__line ${baseClass}__x-right`} />
|
<div className={`${baseClass}__line ${baseClass}__x-left`} />
|
||||||
</React.Fragment>
|
<div className={`${baseClass}__line ${baseClass}__x-right`} />
|
||||||
)}
|
</React.Fragment>
|
||||||
{closeIcon === 'collapse' && (
|
)}
|
||||||
<Chevron className={`${baseClass}__collapse-chevron`} direction="left" />
|
{closeIcon === 'collapse' && (
|
||||||
)}
|
<Chevron className={`${baseClass}__collapse-chevron`} direction="left" />
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__bg {
|
&__bg {
|
||||||
@include blur-bg;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -58,9 +57,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__account {
|
&__account {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:focus:not(:focus-visible) {
|
&:focus:not(:focus-visible) {
|
||||||
opacity: 1;
|
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 {
|
&__controls-wrapper {
|
||||||
@@ -92,6 +110,12 @@
|
|||||||
right: base(2);
|
right: base(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--nav-open {
|
||||||
|
.app-header__localizer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__mobile-nav-toggler {
|
&__mobile-nav-toggler {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import { Hamburger } from '../Hamburger'
|
|||||||
import Localizer from '../Localizer'
|
import Localizer from '../Localizer'
|
||||||
import { LocalizerLabel } from '../Localizer/LocalizerLabel'
|
import { LocalizerLabel } from '../Localizer/LocalizerLabel'
|
||||||
import { NavToggler } from '../Nav/NavToggler'
|
import { NavToggler } from '../Nav/NavToggler'
|
||||||
|
import { useNav } from '../Nav/context'
|
||||||
import StepNav from '../StepNav'
|
import StepNav from '../StepNav'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'app-header'
|
const baseClass = 'app-header'
|
||||||
|
|
||||||
export const AppHeader: React.FC = (props) => {
|
export const AppHeader: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -21,12 +22,14 @@ export const AppHeader: React.FC = (props) => {
|
|||||||
routes: { admin: adminRoute },
|
routes: { admin: adminRoute },
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
|
||||||
|
const { navOpen } = useNav()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={[baseClass].filter(Boolean).join(' ')}>
|
<header className={[baseClass, navOpen && `${baseClass}--nav-open`].filter(Boolean).join(' ')}>
|
||||||
<div className={`${baseClass}__bg`} />
|
<div className={`${baseClass}__bg`} />
|
||||||
<div className={`${baseClass}__content`}>
|
<div className={`${baseClass}__content`}>
|
||||||
<div className={`${baseClass}__wrapper`}>
|
<div className={`${baseClass}__wrapper`}>
|
||||||
<NavToggler className={`${baseClass}__mobile-nav-toggler`}>
|
<NavToggler className={`${baseClass}__mobile-nav-toggler`} tabIndex={-1}>
|
||||||
<Hamburger />
|
<Hamburger />
|
||||||
</NavToggler>
|
</NavToggler>
|
||||||
<div className={`${baseClass}__controls-wrapper`}>
|
<div className={`${baseClass}__controls-wrapper`}>
|
||||||
@@ -43,6 +46,7 @@ export const AppHeader: React.FC = (props) => {
|
|||||||
<Link
|
<Link
|
||||||
aria-label={t('authentication:account')}
|
aria-label={t('authentication:account')}
|
||||||
className={`${baseClass}__account`}
|
className={`${baseClass}__account`}
|
||||||
|
tabIndex={0}
|
||||||
to={`${adminRoute}/account`}
|
to={`${adminRoute}/account`}
|
||||||
>
|
>
|
||||||
<Account />
|
<Account />
|
||||||
|
|||||||
@@ -8,20 +8,22 @@ import RenderCustomComponent from '../../utilities/RenderCustomComponent'
|
|||||||
|
|
||||||
const baseClass = 'nav'
|
const baseClass = 'nav'
|
||||||
|
|
||||||
const DefaultLogout = () => {
|
const DefaultLogout: React.FC<{
|
||||||
|
tabIndex?: number
|
||||||
|
}> = ({ tabIndex }) => {
|
||||||
const { t } = useTranslation('authentication')
|
const { t } = useTranslation('authentication')
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
admin: {
|
admin: { logoutRoute },
|
||||||
components: { logout },
|
|
||||||
logoutRoute,
|
|
||||||
},
|
|
||||||
routes: { admin },
|
routes: { admin },
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
aria-label={t('logOut')}
|
aria-label={t('logOut')}
|
||||||
className={`${baseClass}__log-out`}
|
className={`${baseClass}__log-out`}
|
||||||
|
tabIndex={tabIndex}
|
||||||
to={`${admin}${logoutRoute}`}
|
to={`${admin}${logoutRoute}`}
|
||||||
>
|
>
|
||||||
<LogOut />
|
<LogOut />
|
||||||
@@ -29,7 +31,9 @@ const DefaultLogout = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Logout: React.FC = () => {
|
const Logout: React.FC<{
|
||||||
|
tabIndex?: number
|
||||||
|
}> = ({ tabIndex = 0 }) => {
|
||||||
const {
|
const {
|
||||||
admin: {
|
admin: {
|
||||||
components: {
|
components: {
|
||||||
@@ -40,7 +44,15 @@ const Logout: React.FC = () => {
|
|||||||
} = {},
|
} = {},
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
|
||||||
return <RenderCustomComponent CustomComponent={CustomLogout} DefaultComponent={DefaultLogout} />
|
return (
|
||||||
|
<RenderCustomComponent
|
||||||
|
CustomComponent={CustomLogout}
|
||||||
|
DefaultComponent={DefaultLogout}
|
||||||
|
componentProps={{
|
||||||
|
tabIndex,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Logout
|
export default Logout
|
||||||
|
|||||||
@@ -6,11 +6,5 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
height: var(--app-header-height);
|
|
||||||
width: var(--gutter-h);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@include small-break {
|
|
||||||
width: calc(var(--gutter-h) * 2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ export const NavToggler: React.FC<{
|
|||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
id?: string
|
id?: string
|
||||||
|
tabIndex?: number
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { id, children, className } = props
|
const { id, children, className, tabIndex = 0 } = props
|
||||||
|
|
||||||
const { t } = useTranslation('general')
|
const { t } = useTranslation('general')
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ export const NavToggler: React.FC<{
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
tabIndex={tabIndex}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -19,11 +19,21 @@ export const NavContext = React.createContext<NavContextType>({
|
|||||||
|
|
||||||
export const useNav = () => React.useContext(NavContext)
|
export const useNav = () => React.useContext(NavContext)
|
||||||
|
|
||||||
|
const getNavPreference = async (getPreference): Promise<boolean> => {
|
||||||
|
const navPrefs = await getPreference('nav')
|
||||||
|
const preferredState = navPrefs?.open
|
||||||
|
if (typeof preferredState === 'boolean') {
|
||||||
|
return preferredState
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const NavProvider: React.FC<{
|
export const NavProvider: React.FC<{
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}> = ({ children }) => {
|
}> = ({ children }) => {
|
||||||
const {
|
const {
|
||||||
breakpoints: { l: largeBreak },
|
breakpoints: { l: largeBreak, m: midBreak, s: smallBreak },
|
||||||
} = useWindowInfo()
|
} = useWindowInfo()
|
||||||
|
|
||||||
const { getPreference } = usePreferences()
|
const { getPreference } = usePreferences()
|
||||||
@@ -40,16 +50,11 @@ export const NavProvider: React.FC<{
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (largeBreak === false) {
|
if (largeBreak === false) {
|
||||||
const setNavFromPreferences = async () => {
|
const setNavFromPreferences = async () => {
|
||||||
const navPrefs = await getPreference('nav')
|
const preferredState = await getNavPreference(getPreference)
|
||||||
const preferredState = navPrefs?.open
|
setNavOpen(preferredState)
|
||||||
if (typeof preferredState === 'boolean') {
|
|
||||||
setNavOpen(preferredState)
|
|
||||||
} else {
|
|
||||||
setNavOpen(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setNavFromPreferences()
|
setNavFromPreferences() // eslint-disable-line @typescript-eslint/no-floating-promises
|
||||||
}
|
}
|
||||||
}, [largeBreak, getPreference, setNavOpen])
|
}, [largeBreak, getPreference, setNavOpen])
|
||||||
|
|
||||||
@@ -58,7 +63,7 @@ export const NavProvider: React.FC<{
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unlisten: () => void
|
let unlisten: () => void
|
||||||
|
|
||||||
if (largeBreak) {
|
if (midBreak) {
|
||||||
unlisten = history.listen(() => {
|
unlisten = history.listen(() => {
|
||||||
setNavOpen(false)
|
setNavOpen(false)
|
||||||
})
|
})
|
||||||
@@ -67,27 +72,28 @@ export const NavProvider: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => unlisten && unlisten()
|
return () => unlisten && unlisten()
|
||||||
}, [history, setNavOpen, largeBreak])
|
}, [history, setNavOpen, midBreak])
|
||||||
|
|
||||||
// on smaller screens where the nav is a modal
|
|
||||||
// close the nav when the user resizes down to a smaller screen
|
|
||||||
useEffect(() => {
|
|
||||||
if (largeBreak) {
|
|
||||||
setNavOpen(false)
|
|
||||||
}
|
|
||||||
}, [largeBreak])
|
|
||||||
|
|
||||||
// on open and close, lock the body scroll
|
// on open and close, lock the body scroll
|
||||||
// do not do this on desktop, the sidebar is not a modal
|
// do not do this on desktop, the sidebar is not a modal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (navRef.current) {
|
if (navRef.current) {
|
||||||
if (navOpen && largeBreak) {
|
if (navOpen && midBreak) {
|
||||||
disableBodyScroll(navRef.current)
|
disableBodyScroll(navRef.current)
|
||||||
} else {
|
} else {
|
||||||
enableBodyScroll(navRef.current)
|
enableBodyScroll(navRef.current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [navOpen, largeBreak])
|
}, [navOpen, midBreak])
|
||||||
|
|
||||||
|
// on smaller screens where the nav is a modal
|
||||||
|
// close the nav when the user resizes down to mobile
|
||||||
|
// the sidebar is a modal on mobile
|
||||||
|
useEffect(() => {
|
||||||
|
if (largeBreak === false || midBreak === false || smallBreak === false) {
|
||||||
|
setNavOpen(false)
|
||||||
|
}
|
||||||
|
}, [largeBreak, midBreak, smallBreak])
|
||||||
|
|
||||||
// when the component unmounts, clear all body scroll locks
|
// when the component unmounts, clear all body scroll locks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
@include blur-bg;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -41,7 +40,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: var(--app-header-height) base(1.5) base(2) base(1.25);
|
padding: var(--app-header-height) base(1) base(2) base(1);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
// remove the scrollbar here to prevent layout shift as nav groups are toggled
|
// remove the scrollbar here to prevent layout shift as nav groups are toggled
|
||||||
@@ -128,12 +127,7 @@
|
|||||||
|
|
||||||
@include mid-break {
|
@include mid-break {
|
||||||
&__scroll {
|
&__scroll {
|
||||||
padding: var(--app-header-height) base(0.75) base(2) base(0.75);
|
padding: var(--app-header-height) base(0.5) base(2);
|
||||||
}
|
|
||||||
|
|
||||||
nav a {
|
|
||||||
font-size: base(0.875);
|
|
||||||
line-height: base(1.5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,9 +135,12 @@
|
|||||||
&__scroll {
|
&__scroll {
|
||||||
padding: var(--app-header-height) var(--gutter-h) base(2);
|
padding: var(--app-header-height) var(--gutter-h) base(2);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@include extra-small-break {
|
nav a {
|
||||||
|
font-size: base(0.875);
|
||||||
|
line-height: base(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
&--nav-open {
|
&--nav-open {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ const DefaultNav: React.FC = () => {
|
|||||||
className={`${baseClass}__link`}
|
className={`${baseClass}__link`}
|
||||||
id={id}
|
id={id}
|
||||||
key={i}
|
key={i}
|
||||||
|
tabIndex={!navOpen ? -1 : undefined}
|
||||||
to={href}
|
to={href}
|
||||||
>
|
>
|
||||||
<span className={`${baseClass}__link-icon`}>
|
<span className={`${baseClass}__link-icon`}>
|
||||||
@@ -117,7 +118,7 @@ const DefaultNav: React.FC = () => {
|
|||||||
{Array.isArray(afterNavLinks) &&
|
{Array.isArray(afterNavLinks) &&
|
||||||
afterNavLinks.map((Component, i) => <Component key={i} />)}
|
afterNavLinks.map((Component, i) => <Component key={i} />)}
|
||||||
<div className={`${baseClass}__controls`}>
|
<div className={`${baseClass}__controls`}>
|
||||||
<Logout />
|
<Logout tabIndex={!navOpen ? -1 : undefined} />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,6 +129,7 @@ const DefaultNav: React.FC = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNavOpen(false)
|
setNavOpen(false)
|
||||||
}}
|
}}
|
||||||
|
tabIndex={!navOpen ? -1 : undefined}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<Hamburger isActive />
|
<Hamburger isActive />
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import AnimateHeight from 'react-animate-height'
|
import AnimateHeight from 'react-animate-height'
|
||||||
|
|
||||||
import Chevron from '../../icons/Chevron'
|
import Chevron from '../../icons/Chevron'
|
||||||
import { usePreferences } from '../../utilities/Preferences'
|
import { usePreferences } from '../../utilities/Preferences'
|
||||||
|
import { useNav } from '../Nav/context'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'nav-group'
|
const baseClass = 'nav-group'
|
||||||
@@ -16,6 +17,7 @@ const NavGroup: React.FC<Props> = ({ children, label }) => {
|
|||||||
const [collapsed, setCollapsed] = useState(true)
|
const [collapsed, setCollapsed] = useState(true)
|
||||||
const [animate, setAnimate] = useState(false)
|
const [animate, setAnimate] = useState(false)
|
||||||
const { getPreference, setPreference } = usePreferences()
|
const { getPreference, setPreference } = usePreferences()
|
||||||
|
const { navOpen } = useNav()
|
||||||
|
|
||||||
const preferencesKey = `collapsed-${label}-groups`
|
const preferencesKey = `collapsed-${label}-groups`
|
||||||
|
|
||||||
@@ -44,13 +46,12 @@ const NavGroup: React.FC<Props> = ({ children, label }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`nav-group-${label}`}
|
|
||||||
className={[`${baseClass}`, `${label}`, collapsed && `${baseClass}--collapsed`]
|
className={[`${baseClass}`, `${label}`, collapsed && `${baseClass}--collapsed`]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
|
id={`nav-group-${label}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
className={[
|
className={[
|
||||||
`${baseClass}__toggle`,
|
`${baseClass}__toggle`,
|
||||||
`${baseClass}__toggle--${collapsed ? 'collapsed' : 'open'}`,
|
`${baseClass}__toggle--${collapsed ? 'collapsed' : 'open'}`,
|
||||||
@@ -58,6 +59,8 @@ const NavGroup: React.FC<Props> = ({ children, label }) => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
onClick={toggleCollapsed}
|
onClick={toggleCollapsed}
|
||||||
|
tabIndex={!navOpen ? -1 : 0}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__label`}>{label}</div>
|
<div className={`${baseClass}__label`}>{label}</div>
|
||||||
<div className={`${baseClass}__indicator`}>
|
<div className={`${baseClass}__indicator`}>
|
||||||
@@ -67,7 +70,7 @@ const NavGroup: React.FC<Props> = ({ children, label }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</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>
|
<div className={`${baseClass}__content`}>{children}</div>
|
||||||
</AnimateHeight>
|
</AnimateHeight>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const PopupTrigger: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={classes} onClick={() => setActive(!active)} type="button">
|
<button className={classes} onClick={() => setActive(!active)} tabIndex={0} type="button">
|
||||||
{button}
|
{button}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,27 @@
|
|||||||
background: linear-gradient(to right, transparent, var(--theme-bg));
|
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;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { getTranslation } from '../../../../utilities/getTranslation'
|
|||||||
import { useConfig } from '../../utilities/Config'
|
import { useConfig } from '../../utilities/Config'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
const baseClass = 'step-nav'
|
||||||
|
|
||||||
const Context = createContext({} as ContextType)
|
const Context = createContext({} as ContextType)
|
||||||
|
|
||||||
const StepNavProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
const StepNavProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||||
@@ -41,32 +43,34 @@ const StepNav: React.FC<{
|
|||||||
} = config
|
} = config
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={['step-nav', className].filter(Boolean).join(' ')}>
|
<Fragment>
|
||||||
{stepNav.length > 0 ? (
|
{stepNav.length > 0 ? (
|
||||||
<Fragment>
|
<nav className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||||
<Link to={admin}>
|
<Link className={`${baseClass}__home`} tabIndex={0} to={admin}>
|
||||||
<IconGraphic />
|
<IconGraphic />
|
||||||
</Link>
|
</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
</Fragment>
|
{stepNav.map((item, i) => {
|
||||||
) : (
|
const StepLabel = <span key={i}>{getTranslation(item.label, i18n)}</span>
|
||||||
<IconGraphic />
|
const Step =
|
||||||
)}
|
stepNav.length === i + 1 ? (
|
||||||
{stepNav.map((item, i) => {
|
StepLabel
|
||||||
const StepLabel = <span key={i}>{getTranslation(item.label, i18n)}</span>
|
) : (
|
||||||
const Step =
|
<Fragment key={i}>
|
||||||
stepNav.length === i + 1 ? (
|
{item.url ? <Link to={item.url}>{StepLabel}</Link> : StepLabel}
|
||||||
StepLabel
|
<span>/</span>
|
||||||
) : (
|
</Fragment>
|
||||||
<Fragment key={i}>
|
)
|
||||||
{item.url ? <Link to={item.url}>{StepLabel}</Link> : StepLabel}
|
|
||||||
<span>/</span>
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
|
|
||||||
return Step
|
return Step
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
) : (
|
||||||
|
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||||
|
<IconGraphic />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import type { Props } from './types'
|
import type { Props } from './types'
|
||||||
@@ -37,7 +37,7 @@ const RenderFields: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
const { i18n, t } = useTranslation('general')
|
const { i18n, t } = useTranslation('general')
|
||||||
const [hasRendered, setHasRendered] = useState(Boolean(forceRender))
|
const [hasRendered, setHasRendered] = useState(Boolean(forceRender))
|
||||||
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions)
|
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions, forceRender)
|
||||||
|
|
||||||
const isIntersecting = Boolean(entry?.isIntersecting)
|
const isIntersecting = Boolean(entry?.isIntersecting)
|
||||||
const isAboveViewport = entry?.boundingClientRect?.top < 0
|
const isAboveViewport = entry?.boundingClientRect?.top < 0
|
||||||
@@ -105,6 +105,7 @@ const RenderFields: React.FC<Props> = (props) => {
|
|||||||
readOnly,
|
readOnly,
|
||||||
},
|
},
|
||||||
fieldTypes,
|
fieldTypes,
|
||||||
|
forceRender,
|
||||||
indexPath:
|
indexPath:
|
||||||
'indexPath' in props ? `${props?.indexPath}.${fieldIndex}` : `${fieldIndex}`,
|
'indexPath' in props ? `${props?.indexPath}.${fieldIndex}` : `${fieldIndex}`,
|
||||||
path: field.path || (isFieldAffectingData && 'name' in field ? field.name : ''),
|
path: field.path || (isFieldAffectingData && 'name' in field ? field.name : ''),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type ArrayRowProps = UseDraggableSortableReturn &
|
|||||||
CustomRowLabel?: RowLabelType
|
CustomRowLabel?: RowLabelType
|
||||||
addRow: (rowIndex: number) => void
|
addRow: (rowIndex: number) => void
|
||||||
duplicateRow: (rowIndex: number) => void
|
duplicateRow: (rowIndex: number) => void
|
||||||
|
forceRender?: boolean
|
||||||
hasMaxRows?: boolean
|
hasMaxRows?: boolean
|
||||||
moveRow: (fromIndex: number, toIndex: number) => void
|
moveRow: (fromIndex: number, toIndex: number) => void
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
@@ -40,6 +41,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
|||||||
duplicateRow,
|
duplicateRow,
|
||||||
fieldTypes,
|
fieldTypes,
|
||||||
fields,
|
fields,
|
||||||
|
forceRender = false,
|
||||||
hasMaxRows,
|
hasMaxRows,
|
||||||
indexPath,
|
indexPath,
|
||||||
labels,
|
labels,
|
||||||
@@ -126,10 +128,11 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
|
|||||||
path: createNestedFieldPath(path, field),
|
path: createNestedFieldPath(path, field),
|
||||||
}))}
|
}))}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
|
forceRender={forceRender}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
|
margins="small"
|
||||||
permissions={permissions?.fields}
|
permissions={permissions?.fields}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
margins="small"
|
|
||||||
/>
|
/>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
admin: { className, components, condition, description, readOnly },
|
admin: { className, components, condition, description, readOnly },
|
||||||
fieldTypes,
|
fieldTypes,
|
||||||
fields,
|
fields,
|
||||||
|
forceRender = false,
|
||||||
indexPath,
|
indexPath,
|
||||||
localized,
|
localized,
|
||||||
maxRows,
|
maxRows,
|
||||||
@@ -234,6 +235,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
|||||||
duplicateRow={duplicateRow}
|
duplicateRow={duplicateRow}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
|
forceRender={forceRender}
|
||||||
hasMaxRows={hasMaxRows}
|
hasMaxRows={hasMaxRows}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { ArrayField } from '../../../../../fields/config/types'
|
|||||||
|
|
||||||
export type Props = Omit<ArrayField, 'type'> & {
|
export type Props = Omit<ArrayField, 'type'> & {
|
||||||
fieldTypes: FieldTypes
|
fieldTypes: FieldTypes
|
||||||
|
forceRender?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
label: false | string
|
label: false | string
|
||||||
path?: string
|
path?: string
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type BlockFieldProps = UseDraggableSortableReturn &
|
|||||||
addRow: (rowIndex: number, blockType: string) => void
|
addRow: (rowIndex: number, blockType: string) => void
|
||||||
blockToRender: Block
|
blockToRender: Block
|
||||||
duplicateRow: (rowIndex: number) => void
|
duplicateRow: (rowIndex: number) => void
|
||||||
|
forceRender?: boolean
|
||||||
hasMaxRows?: boolean
|
hasMaxRows?: boolean
|
||||||
moveRow: (fromIndex: number, toIndex: number) => void
|
moveRow: (fromIndex: number, toIndex: number) => void
|
||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
@@ -40,6 +41,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
|||||||
blocks,
|
blocks,
|
||||||
duplicateRow,
|
duplicateRow,
|
||||||
fieldTypes,
|
fieldTypes,
|
||||||
|
forceRender,
|
||||||
hasMaxRows,
|
hasMaxRows,
|
||||||
indexPath,
|
indexPath,
|
||||||
labels,
|
labels,
|
||||||
@@ -130,6 +132,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
|
|||||||
path: createNestedFieldPath(path, field),
|
path: createNestedFieldPath(path, field),
|
||||||
}))}
|
}))}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
|
forceRender={forceRender}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
margins="small"
|
margins="small"
|
||||||
permissions={permissions?.blocks?.[row.blockType]?.fields}
|
permissions={permissions?.blocks?.[row.blockType]?.fields}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
|||||||
admin: { className, condition, description, readOnly },
|
admin: { className, condition, description, readOnly },
|
||||||
blocks,
|
blocks,
|
||||||
fieldTypes,
|
fieldTypes,
|
||||||
|
forceRender = false,
|
||||||
indexPath,
|
indexPath,
|
||||||
label,
|
label,
|
||||||
labels: labelsFromProps,
|
labels: labelsFromProps,
|
||||||
@@ -238,6 +239,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
|||||||
blocks={blocks}
|
blocks={blocks}
|
||||||
duplicateRow={duplicateRow}
|
duplicateRow={duplicateRow}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
|
forceRender={forceRender}
|
||||||
hasMaxRows={hasMaxRows}
|
hasMaxRows={hasMaxRows}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { BlockField } from '../../../../../fields/config/types'
|
|||||||
|
|
||||||
export type Props = Omit<BlockField, 'type'> & {
|
export type Props = Omit<BlockField, 'type'> & {
|
||||||
fieldTypes: FieldTypes
|
fieldTypes: FieldTypes
|
||||||
|
forceRender?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
path?: string
|
path?: string
|
||||||
permissions: FieldPermissions
|
permissions: FieldPermissions
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import { WatchChildErrors } from '../../WatchChildErrors'
|
|||||||
import withCondition from '../../withCondition'
|
import withCondition from '../../withCondition'
|
||||||
import { useRow } from '../Row/provider'
|
import { useRow } from '../Row/provider'
|
||||||
import { useTabs } from '../Tabs/provider'
|
import { useTabs } from '../Tabs/provider'
|
||||||
|
import { fieldBaseClass } from '../shared'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { GroupProvider, useGroup } from './provider'
|
import { GroupProvider, useGroup } from './provider'
|
||||||
import { fieldBaseClass } from '../shared'
|
|
||||||
|
|
||||||
const baseClass = 'group-field'
|
const baseClass = 'group-field'
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ const Group: React.FC<Props> = (props) => {
|
|||||||
admin: { className, description, hideGutter = false, readOnly, style, width },
|
admin: { className, description, hideGutter = false, readOnly, style, width },
|
||||||
fieldTypes,
|
fieldTypes,
|
||||||
fields,
|
fields,
|
||||||
|
forceRender = false,
|
||||||
indexPath,
|
indexPath,
|
||||||
label,
|
label,
|
||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
@@ -88,6 +89,7 @@ const Group: React.FC<Props> = (props) => {
|
|||||||
path: createNestedFieldPath(path, subField),
|
path: createNestedFieldPath(path, subField),
|
||||||
}))}
|
}))}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
|
forceRender={forceRender}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
margins="small"
|
margins="small"
|
||||||
permissions={permissions?.fields}
|
permissions={permissions?.fields}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { GroupField } from '../../../../../fields/config/types'
|
|||||||
|
|
||||||
export type Props = Omit<GroupField, 'type'> & {
|
export type Props = Omit<GroupField, 'type'> & {
|
||||||
fieldTypes: FieldTypes
|
fieldTypes: FieldTypes
|
||||||
|
forceRender?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
path?: string
|
path?: string
|
||||||
permissions: FieldPermissions
|
permissions: FieldPermissions
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const Row: React.FC<Props> = (props) => {
|
|||||||
admin: { className, readOnly },
|
admin: { className, readOnly },
|
||||||
fieldTypes,
|
fieldTypes,
|
||||||
fields,
|
fields,
|
||||||
|
forceRender = false,
|
||||||
indexPath,
|
indexPath,
|
||||||
path,
|
path,
|
||||||
permissions,
|
permissions,
|
||||||
@@ -28,6 +29,7 @@ const Row: React.FC<Props> = (props) => {
|
|||||||
path: createNestedFieldPath(path, field),
|
path: createNestedFieldPath(path, field),
|
||||||
}))}
|
}))}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
|
forceRender={forceRender}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { RowField } from '../../../../../fields/config/types'
|
|||||||
|
|
||||||
export type Props = Omit<RowField, 'type'> & {
|
export type Props = Omit<RowField, 'type'> & {
|
||||||
fieldTypes: FieldTypes
|
fieldTypes: FieldTypes
|
||||||
|
forceRender?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
path?: string
|
path?: string
|
||||||
permissions: FieldPermissions
|
permissions: FieldPermissions
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
|
|||||||
import RenderFields from '../../RenderFields'
|
import RenderFields from '../../RenderFields'
|
||||||
import { WatchChildErrors } from '../../WatchChildErrors'
|
import { WatchChildErrors } from '../../WatchChildErrors'
|
||||||
import withCondition from '../../withCondition'
|
import withCondition from '../../withCondition'
|
||||||
|
import { fieldBaseClass } from '../shared'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import { TabsProvider } from './provider'
|
import { TabsProvider } from './provider'
|
||||||
import { fieldBaseClass } from '../shared'
|
|
||||||
|
|
||||||
const baseClass = 'tabs-field'
|
const baseClass = 'tabs-field'
|
||||||
|
|
||||||
@@ -72,6 +72,7 @@ const TabsField: React.FC<Props> = (props) => {
|
|||||||
const {
|
const {
|
||||||
admin: { className, readOnly },
|
admin: { className, readOnly },
|
||||||
fieldTypes,
|
fieldTypes,
|
||||||
|
forceRender = false,
|
||||||
indexPath,
|
indexPath,
|
||||||
path,
|
path,
|
||||||
permissions,
|
permissions,
|
||||||
@@ -188,7 +189,7 @@ const TabsField: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
fieldTypes={fieldTypes}
|
fieldTypes={fieldTypes}
|
||||||
forceRender
|
forceRender={forceRender}
|
||||||
indexPath={indexPath}
|
indexPath={indexPath}
|
||||||
key={String(activeTabConfig.label)}
|
key={String(activeTabConfig.label)}
|
||||||
margins="small"
|
margins="small"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { TabsField } from '../../../../../fields/config/types'
|
|||||||
|
|
||||||
export type Props = Omit<TabsField, 'type'> & {
|
export type Props = Omit<TabsField, 'type'> & {
|
||||||
fieldTypes: FieldTypes
|
fieldTypes: FieldTypes
|
||||||
|
forceRender?: boolean
|
||||||
indexPath: string
|
indexPath: string
|
||||||
path?: string
|
path?: string
|
||||||
permissions: FieldPermissions
|
permissions: FieldPermissions
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
.graphic-account {
|
.graphic-account {
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&__bg {
|
&__bg {
|
||||||
fill: var(--theme-elevation-100);
|
fill: var(--theme-elevation-50);
|
||||||
|
stroke: var(--theme-elevation-200);
|
||||||
|
stroke-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__head,
|
&__head,
|
||||||
&__body {
|
&__body {
|
||||||
fill: var(--theme-elevation-300);
|
fill: var(--theme-elevation-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
.graphic-account {
|
.graphic-account {
|
||||||
&__bg {
|
&__bg {
|
||||||
fill: var(--theme-elevation-500);
|
fill: var(--theme-elevation-500);
|
||||||
|
stroke: var(--theme-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__head,
|
&__head,
|
||||||
@@ -25,6 +32,7 @@
|
|||||||
.graphic-account {
|
.graphic-account {
|
||||||
&__bg {
|
&__bg {
|
||||||
fill: var(--theme-elevation-200);
|
fill: var(--theme-elevation-200);
|
||||||
|
stroke: var(--theme-elevation-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__head,
|
&__head,
|
||||||
@@ -41,6 +49,12 @@
|
|||||||
.graphic-account {
|
.graphic-account {
|
||||||
&__bg {
|
&__bg {
|
||||||
fill: var(--theme-elevation-300);
|
fill: var(--theme-elevation-300);
|
||||||
|
stroke: var(--theme-elevation-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head,
|
||||||
|
&__body {
|
||||||
|
fill: var(--theme-elevation-600);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ const css = `
|
|||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graphic-icon path {
|
.graphic-icon path {
|
||||||
fill: var(--theme-elevation-1000);
|
fill: var(--theme-elevation-1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.graphic-icon {
|
.graphic-icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__nav-toggler {
|
&__nav-toggler-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: var(--z-modal);
|
z-index: var(--z-modal);
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -40,113 +40,32 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__nav-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
background: var(--theme-overlay);
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
z-index: var(--z-modal);
|
|
||||||
cursor: pointer;
|
|
||||||
pointer-events: none;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
// on large break, push the document off the screen
|
|
||||||
// show an overlay to prevent clicking on the document
|
|
||||||
@include large-break {
|
|
||||||
width: calc(100% + var(--nav-width));
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
|
|
||||||
&__wrap {
|
|
||||||
transform: translate3d(calc(var(--nav-width) * -1), 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nav-overlay {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--nav-open {
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
|
|
||||||
.template-default {
|
|
||||||
&__wrap {
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
transition: transform var(--nav-trans-time) linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nav-overlay {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nav-toggler {
|
|
||||||
.hamburger {
|
|
||||||
left: calc(var(--base) * 1.25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mid-break {
|
@include mid-break {
|
||||||
&__nav-toggler {
|
&__nav-toggler-wrapper {
|
||||||
.hamburger {
|
.hamburger {
|
||||||
left: calc(var(--base) * 0.75);
|
left: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include small-break {
|
@include small-break {
|
||||||
&__nav-toggler {
|
|
||||||
.hamburger {
|
|
||||||
left: calc(var(--base) * 0.5);
|
|
||||||
}
|
|
||||||
&.nav-toggler--is-open .hamburger {
|
|
||||||
display: flex;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include small-break {
|
|
||||||
&__nav-toggler {
|
|
||||||
width: unset;
|
|
||||||
justify-content: unset;
|
|
||||||
|
|
||||||
.hamburger {
|
|
||||||
opacity: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__nav-toggler.nav-toggler--is-open .hamburger {
|
|
||||||
display: flex;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include extra-small-break {
|
|
||||||
&--nav-open {
|
&--nav-open {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__nav-toggler.nav-toggler--is-open .hamburger {
|
&__nav-toggler-wrapper {
|
||||||
display: none;
|
width: unset;
|
||||||
|
justify-content: unset;
|
||||||
|
|
||||||
|
.hamburger {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-default {
|
.template-default {
|
||||||
&__wrap {
|
&__wrap {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__nav-overlay {
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const Default: React.FC<Props> = ({ children, className }) => {
|
|||||||
|
|
||||||
const { t } = useTranslation('general')
|
const { t } = useTranslation('general')
|
||||||
|
|
||||||
const { navOpen, setNavOpen } = useNav()
|
const { navOpen } = useNav()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -35,6 +35,11 @@ const Default: React.FC<Props> = ({ children, className }) => {
|
|||||||
keywords={`${t('dashboard')}, Payload`}
|
keywords={`${t('dashboard')}, Payload`}
|
||||||
title={t('dashboard')}
|
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
|
<div
|
||||||
className={[baseClass, className, navOpen && `${baseClass}--nav-open`]
|
className={[baseClass, className, navOpen && `${baseClass}--nav-open`]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -44,17 +49,8 @@ const Default: React.FC<Props> = ({ children, className }) => {
|
|||||||
<div className={`${baseClass}__wrap`}>
|
<div className={`${baseClass}__wrap`}>
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
{children}
|
{children}
|
||||||
<button
|
|
||||||
aria-label={t('close')}
|
|
||||||
className={`${baseClass}__nav-overlay`}
|
|
||||||
onClick={() => setNavOpen(!navOpen)}
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NavToggler className={`${baseClass}__nav-toggler`} id="nav-toggler">
|
|
||||||
<Hamburger closeIcon="collapse" isActive={navOpen} />
|
|
||||||
</NavToggler>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,10 @@
|
|||||||
.global-default-edit {
|
.global-default-edit {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
--doc-sidebar-width: 325px;
|
||||||
|
|
||||||
&--has-sidebar {
|
&--has-sidebar {
|
||||||
.global-default-edit {
|
.global-default-edit {
|
||||||
&__main {
|
|
||||||
width: 66.66%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__edit {
|
&__edit {
|
||||||
[dir='ltr'] & {
|
[dir='ltr'] & {
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -40,6 +37,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__edit {
|
&__edit {
|
||||||
@@ -48,11 +46,16 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__auth {
|
||||||
|
margin-bottom: var(--base);
|
||||||
|
}
|
||||||
|
|
||||||
&__sidebar-wrap {
|
&__sidebar-wrap {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: var(--doc-controls-height);
|
top: var(--doc-controls-height);
|
||||||
width: 33.33%;
|
width: 33.33%;
|
||||||
height: calc(100vh - var(--doc-controls-height));
|
height: calc(100vh - var(--doc-controls-height));
|
||||||
|
min-width: var(--doc-sidebar-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__sidebar {
|
&__sidebar {
|
||||||
@@ -71,24 +74,16 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--base);
|
gap: var(--base);
|
||||||
padding: calc(var(--base) * 1.5) var(--gutter-h) var(--spacing-view-bottom)
|
padding-top: calc(var(--base) * 1.5);
|
||||||
calc(var(--base) * 2);
|
padding-left: calc(var(--base) * 2);
|
||||||
|
padding-right: var(--gutter-h);
|
||||||
|
padding-bottom: var(--spacing-view-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
color: var(--theme-elevation-400);
|
color: var(--theme-elevation-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include large-break {
|
|
||||||
&--no-sidebar {
|
|
||||||
.global-default-edit {
|
|
||||||
&__main {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mid-break {
|
@include mid-break {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@@ -130,6 +125,7 @@
|
|||||||
height: initial;
|
height: initial;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
margin-top: calc(var(--base) / 2);
|
margin-top: calc(var(--base) / 2);
|
||||||
|
width: var(--doc-sidebar-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__form {
|
&__form {
|
||||||
@@ -158,6 +154,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include small-break {
|
@include small-break {
|
||||||
|
&__sidebar-wrap {
|
||||||
|
min-width: initial;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&__edit {
|
&__edit {
|
||||||
padding-top: calc(var(--base) / 2);
|
padding-top: calc(var(--base) / 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ export const GlobalRoutes: React.FC<GlobalEditViewProps> = (props) => {
|
|||||||
<Unauthorized />
|
<Unauthorized />
|
||||||
)}
|
)}
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact key={`${global.slug}-api`} path={`${adminRoute}/globals/${global.slug}/api`}>
|
{global?.admin?.hideAPIURL !== true && (
|
||||||
{permissions?.read ? <CustomGlobalComponent view="API" {...props} /> : <Unauthorized />}
|
<Route exact key={`${global.slug}-api`} path={`${adminRoute}/globals/${global.slug}/api`}>
|
||||||
</Route>
|
{permissions?.read ? <CustomGlobalComponent view="API" {...props} /> : <Unauthorized />}
|
||||||
|
</Route>
|
||||||
|
)}
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
key={`${global.slug}-view-version`}
|
key={`${global.slug}-view-version`}
|
||||||
|
|||||||
@@ -3,21 +3,31 @@ import type { Dispatch } from 'react'
|
|||||||
import { createContext, useContext } from 'react'
|
import { createContext, useContext } from 'react'
|
||||||
|
|
||||||
import type { LivePreviewConfig } from '../../../../../exports/config'
|
import type { LivePreviewConfig } from '../../../../../exports/config'
|
||||||
|
import type { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
|
||||||
|
import type { usePopupWindow } from '../usePopupWindow'
|
||||||
import type { SizeReducerAction } from './sizeReducer'
|
import type { SizeReducerAction } from './sizeReducer'
|
||||||
|
|
||||||
export interface LivePreviewContextType {
|
export interface LivePreviewContextType {
|
||||||
|
appIsReady: boolean
|
||||||
breakpoint: LivePreviewConfig['breakpoints'][number]['name']
|
breakpoint: LivePreviewConfig['breakpoints'][number]['name']
|
||||||
breakpoints: LivePreviewConfig['breakpoints']
|
breakpoints: LivePreviewConfig['breakpoints']
|
||||||
|
fieldSchemaJSON?: ReturnType<typeof fieldSchemaToJSON>
|
||||||
iframeHasLoaded: boolean
|
iframeHasLoaded: boolean
|
||||||
iframeRef: React.RefObject<HTMLIFrameElement>
|
iframeRef: React.RefObject<HTMLIFrameElement>
|
||||||
|
isPopupOpen: boolean
|
||||||
measuredDeviceSize: {
|
measuredDeviceSize: {
|
||||||
height: number
|
height: number
|
||||||
width: 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
|
setBreakpoint: (breakpoint: LivePreviewConfig['breakpoints'][number]['name']) => void
|
||||||
setHeight: (height: number) => void
|
setHeight: (height: number) => void
|
||||||
setIframeHasLoaded: (loaded: boolean) => void
|
setIframeHasLoaded: (loaded: boolean) => void
|
||||||
setMeasuredDeviceSize: (size: { height: number; width: number }) => void
|
setMeasuredDeviceSize: (size: { height: number; width: number }) => void
|
||||||
|
setPreviewWindowType: (previewWindowType: 'iframe' | 'popup') => void
|
||||||
setSize: Dispatch<SizeReducerAction>
|
setSize: Dispatch<SizeReducerAction>
|
||||||
setToolbarPosition: (position: { x: number; y: number }) => void
|
setToolbarPosition: (position: { x: number; y: number }) => void
|
||||||
setWidth: (width: number) => void
|
setWidth: (width: number) => void
|
||||||
@@ -30,22 +40,31 @@ export interface LivePreviewContextType {
|
|||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
}
|
}
|
||||||
|
url: string | undefined
|
||||||
zoom: number
|
zoom: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LivePreviewContext = createContext<LivePreviewContextType>({
|
export const LivePreviewContext = createContext<LivePreviewContextType>({
|
||||||
|
appIsReady: false,
|
||||||
breakpoint: undefined,
|
breakpoint: undefined,
|
||||||
breakpoints: undefined,
|
breakpoints: undefined,
|
||||||
|
fieldSchemaJSON: undefined,
|
||||||
iframeHasLoaded: false,
|
iframeHasLoaded: false,
|
||||||
iframeRef: undefined,
|
iframeRef: undefined,
|
||||||
|
isPopupOpen: false,
|
||||||
measuredDeviceSize: {
|
measuredDeviceSize: {
|
||||||
height: 0,
|
height: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
},
|
},
|
||||||
|
openPopupWindow: () => {},
|
||||||
|
popupRef: undefined,
|
||||||
|
previewWindowType: 'iframe',
|
||||||
|
setAppIsReady: () => {},
|
||||||
setBreakpoint: () => {},
|
setBreakpoint: () => {},
|
||||||
setHeight: () => {},
|
setHeight: () => {},
|
||||||
setIframeHasLoaded: () => {},
|
setIframeHasLoaded: () => {},
|
||||||
setMeasuredDeviceSize: () => {},
|
setMeasuredDeviceSize: () => {},
|
||||||
|
setPreviewWindowType: () => {},
|
||||||
setSize: () => {},
|
setSize: () => {},
|
||||||
setToolbarPosition: () => {},
|
setToolbarPosition: () => {},
|
||||||
setWidth: () => {},
|
setWidth: () => {},
|
||||||
@@ -58,6 +77,7 @@ export const LivePreviewContext = createContext<LivePreviewContextType>({
|
|||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
|
url: undefined,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,48 @@
|
|||||||
import { DndContext } from '@dnd-kit/core'
|
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 { LivePreviewConfig } from '../../../../../exports/config'
|
||||||
|
import type { Field } from '../../../../../fields/config/types'
|
||||||
import type { EditViewProps } from '../../types'
|
import type { EditViewProps } from '../../types'
|
||||||
import type { usePopupWindow } from '../usePopupWindow'
|
import type { usePopupWindow } from '../usePopupWindow'
|
||||||
|
|
||||||
|
import { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
|
||||||
import { customCollisionDetection } from './collisionDetection'
|
import { customCollisionDetection } from './collisionDetection'
|
||||||
import { LivePreviewContext } from './context'
|
import { LivePreviewContext } from './context'
|
||||||
import { sizeReducer } from './sizeReducer'
|
import { sizeReducer } from './sizeReducer'
|
||||||
|
|
||||||
export type ToolbarProviderProps = EditViewProps & {
|
export type LivePreviewProviderProps = EditViewProps & {
|
||||||
|
appIsReady?: boolean
|
||||||
breakpoints?: LivePreviewConfig['breakpoints']
|
breakpoints?: LivePreviewConfig['breakpoints']
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
deviceSize?: {
|
deviceSize?: {
|
||||||
height: number
|
height: number
|
||||||
width: number
|
width: number
|
||||||
}
|
}
|
||||||
popupState: ReturnType<typeof usePopupWindow>
|
isPopupOpen?: boolean
|
||||||
|
openPopupWindow?: ReturnType<typeof usePopupWindow>['openPopupWindow']
|
||||||
|
popupRef?: React.MutableRefObject<Window>
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
|
export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = (props) => {
|
||||||
const { breakpoints, children } = 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 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 [size, setSize] = React.useReducer(sizeReducer, { height: 0, width: 0 })
|
||||||
|
|
||||||
const [measuredDeviceSize, setMeasuredDeviceSize] = React.useState({
|
const [measuredDeviceSize, setMeasuredDeviceSize] = useState({
|
||||||
height: 0,
|
height: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
})
|
})
|
||||||
@@ -41,6 +50,22 @@ export const LivePreviewProvider: React.FC<ToolbarProviderProps> = (props) => {
|
|||||||
const [breakpoint, setBreakpoint] =
|
const [breakpoint, setBreakpoint] =
|
||||||
React.useState<LivePreviewConfig['breakpoints'][0]['name']>('responsive')
|
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
|
// The toolbar needs to freely drag and drop around the page
|
||||||
const handleDragEnd = (ev) => {
|
const handleDragEnd = (ev) => {
|
||||||
// only update position if the toolbar is completely within the preview area
|
// 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])
|
}, [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 (
|
return (
|
||||||
<LivePreviewContext.Provider
|
<LivePreviewContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
appIsReady,
|
||||||
breakpoint,
|
breakpoint,
|
||||||
breakpoints,
|
breakpoints,
|
||||||
|
fieldSchemaJSON,
|
||||||
iframeHasLoaded,
|
iframeHasLoaded,
|
||||||
iframeRef,
|
iframeRef,
|
||||||
|
isPopupOpen,
|
||||||
measuredDeviceSize,
|
measuredDeviceSize,
|
||||||
|
openPopupWindow,
|
||||||
|
popupRef,
|
||||||
|
previewWindowType,
|
||||||
|
setAppIsReady,
|
||||||
setBreakpoint,
|
setBreakpoint,
|
||||||
setHeight,
|
setHeight,
|
||||||
setIframeHasLoaded,
|
setIframeHasLoaded,
|
||||||
setMeasuredDeviceSize,
|
setMeasuredDeviceSize,
|
||||||
|
setPreviewWindowType: handleWindowChange,
|
||||||
setSize,
|
setSize,
|
||||||
setToolbarPosition: setPosition,
|
setToolbarPosition: setPosition,
|
||||||
setWidth,
|
setWidth,
|
||||||
setZoom,
|
setZoom,
|
||||||
size,
|
size,
|
||||||
toolbarPosition: position,
|
toolbarPosition: position,
|
||||||
|
url,
|
||||||
zoom,
|
zoom,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@import '../../../../scss/styles.scss';
|
@import '../../../../scss/styles.scss';
|
||||||
|
|
||||||
.live-preview-iframe {
|
.live-preview-iframe {
|
||||||
|
background-color: white;
|
||||||
border: 0;
|
border: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@import '../../../../scss/styles.scss';
|
@import '../../../../scss/styles.scss';
|
||||||
|
|
||||||
.live-preview-window {
|
.live-preview-window {
|
||||||
|
background-color: var(--theme-bg);
|
||||||
width: 60%;
|
width: 60%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
|||||||
@@ -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 { EditViewProps } from '../../types'
|
||||||
import type { usePopupWindow } from '../usePopupWindow'
|
|
||||||
|
|
||||||
import { fieldSchemaToJSON } from '../../../../../utilities/fieldSchemaToJSON'
|
|
||||||
import { useAllFormFields } from '../../../forms/Form/context'
|
import { useAllFormFields } from '../../../forms/Form/context'
|
||||||
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
|
import reduceFieldsToValues from '../../../forms/Form/reduceFieldsToValues'
|
||||||
import { LivePreviewProvider } from '../Context'
|
|
||||||
import { useLivePreviewContext } from '../Context/context'
|
import { useLivePreviewContext } from '../Context/context'
|
||||||
import { DeviceContainer } from '../Device'
|
import { DeviceContainer } from '../Device'
|
||||||
import { IFrame } from '../IFrame'
|
import { IFrame } from '../IFrame'
|
||||||
@@ -17,93 +12,81 @@ import './index.scss'
|
|||||||
|
|
||||||
const baseClass = 'live-preview-window'
|
const baseClass = 'live-preview-window'
|
||||||
|
|
||||||
const Preview: React.FC<
|
export const LivePreview: React.FC<EditViewProps> = (props) => {
|
||||||
EditViewProps & {
|
|
||||||
popupState: ReturnType<typeof usePopupWindow>
|
|
||||||
url?: string
|
|
||||||
}
|
|
||||||
> = (props) => {
|
|
||||||
const {
|
const {
|
||||||
popupState: { isPopupOpen, popupHasLoaded, popupRef },
|
appIsReady,
|
||||||
|
iframeHasLoaded,
|
||||||
|
iframeRef,
|
||||||
|
popupRef,
|
||||||
|
previewWindowType,
|
||||||
|
setIframeHasLoaded,
|
||||||
url,
|
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 [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
|
// The preview could either be an iframe embedded on the page
|
||||||
// Or it could be a separate popup window
|
// Or it could be a separate popup window
|
||||||
// We need to transmit data to both accordingly
|
// We need to transmit data to both accordingly
|
||||||
useEffect(() => {
|
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)
|
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({
|
const message = JSON.stringify({
|
||||||
data: values,
|
data: values,
|
||||||
fieldSchemaJSON,
|
fieldSchemaJSON: shouldSendSchema ? fieldSchemaJSON : undefined,
|
||||||
type: 'payload-live-preview',
|
type: 'payload-live-preview',
|
||||||
})
|
})
|
||||||
|
|
||||||
// external window
|
// Post message to external popup window
|
||||||
if (isPopupOpen) {
|
if (previewWindowType === 'popup' && popupRef.current) {
|
||||||
setIframeHasLoaded(false)
|
popupRef.current.postMessage(message, url)
|
||||||
if (popupRef.current) {
|
|
||||||
popupRef.current.postMessage(message, url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// embedded iframe
|
// Post message to embedded iframe
|
||||||
if (!isPopupOpen) {
|
if (previewWindowType === 'iframe' && iframeRef.current) {
|
||||||
if (iframeHasLoaded && iframeRef.current) {
|
iframeRef.current.contentWindow?.postMessage(message, url)
|
||||||
iframeRef.current.contentWindow?.postMessage(message, url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
fields,
|
fields,
|
||||||
url,
|
url,
|
||||||
iframeHasLoaded,
|
iframeHasLoaded,
|
||||||
isPopupOpen,
|
previewWindowType,
|
||||||
popupRef,
|
popupRef,
|
||||||
popupHasLoaded,
|
appIsReady,
|
||||||
iframeRef,
|
iframeRef,
|
||||||
setIframeHasLoaded,
|
setIframeHasLoaded,
|
||||||
fieldSchemaJSON,
|
fieldSchemaJSON,
|
||||||
])
|
])
|
||||||
|
|
||||||
if (!isPopupOpen) {
|
if (previewWindowType === 'iframe') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
baseClass,
|
baseClass,
|
||||||
isPopupOpen && `${baseClass}--popup-open`,
|
|
||||||
breakpoint && breakpoint !== 'responsive' && `${baseClass}--has-breakpoint`,
|
breakpoint && breakpoint !== 'responsive' && `${baseClass}--has-breakpoint`,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__wrapper`}>
|
<div className={`${baseClass}__wrapper`}>
|
||||||
<LivePreviewToolbar {...props} iframeRef={iframeRef} url={url} />
|
<LivePreviewToolbar {...props} />
|
||||||
<div className={`${baseClass}__main`}>
|
<div className={`${baseClass}__main`}>
|
||||||
<DeviceContainer>
|
<DeviceContainer>
|
||||||
<IFrame ref={iframeRef} setIframeHasLoaded={setIframeHasLoaded} url={url} />
|
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { LivePreviewToolbarProps } from '..'
|
import type { EditViewProps } from '../../../types'
|
||||||
|
|
||||||
import { X } from '../../../..'
|
import { X } from '../../../..'
|
||||||
import { ExternalLinkIcon } from '../../../../graphics/ExternalLink'
|
import { ExternalLinkIcon } from '../../../../graphics/ExternalLink'
|
||||||
@@ -10,19 +10,16 @@ import './index.scss'
|
|||||||
|
|
||||||
const baseClass = 'live-preview-toolbar-controls'
|
const baseClass = 'live-preview-toolbar-controls'
|
||||||
|
|
||||||
export const ToolbarControls: React.FC<LivePreviewToolbarProps> = (props) => {
|
export const ToolbarControls: React.FC<EditViewProps> = () => {
|
||||||
const { breakpoint, breakpoints, setBreakpoint, setZoom, zoom } = useLivePreviewContext()
|
const { breakpoint, breakpoints, setBreakpoint, setPreviewWindowType, setZoom, url, zoom } =
|
||||||
|
useLivePreviewContext()
|
||||||
const {
|
|
||||||
popupState: { openPopupWindow },
|
|
||||||
url,
|
|
||||||
} = props
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
{breakpoints?.length > 0 && (
|
{breakpoints?.length > 0 && (
|
||||||
<select
|
<select
|
||||||
className={`${baseClass}__breakpoint`}
|
className={`${baseClass}__breakpoint`}
|
||||||
|
name="live-preview-breakpoint"
|
||||||
onChange={(e) => setBreakpoint(e.target.value)}
|
onChange={(e) => setBreakpoint(e.target.value)}
|
||||||
value={breakpoint}
|
value={breakpoint}
|
||||||
>
|
>
|
||||||
@@ -57,7 +54,15 @@ export const ToolbarControls: React.FC<LivePreviewToolbarProps> = (props) => {
|
|||||||
<option value={150}>150%</option>
|
<option value={150}>150%</option>
|
||||||
<option value={200}>200%</option>
|
<option value={200}>200%</option>
|
||||||
</select>
|
</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 />
|
<ExternalLinkIcon />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export const PreviewFrameSizeInput: React.FC<{
|
|||||||
<input
|
<input
|
||||||
className={baseClass}
|
className={baseClass}
|
||||||
min={0}
|
min={0}
|
||||||
|
name={axis === 'x' ? 'live-preview-width' : 'live-preview-height'}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
step={1}
|
step={1}
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useDraggable } from '@dnd-kit/core'
|
import { useDraggable } from '@dnd-kit/core'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { ToolbarProviderProps } from '../Context'
|
import type { EditViewProps } from '../../types'
|
||||||
|
|
||||||
import DragHandle from '../../../icons/Drag'
|
import DragHandle from '../../../icons/Drag'
|
||||||
import { useLivePreviewContext } from '../Context/context'
|
import { useLivePreviewContext } from '../Context/context'
|
||||||
@@ -10,11 +10,7 @@ import './index.scss'
|
|||||||
|
|
||||||
const baseClass = 'live-preview-toolbar'
|
const baseClass = 'live-preview-toolbar'
|
||||||
|
|
||||||
export type LivePreviewToolbarProps = Omit<ToolbarProviderProps, 'children'> & {
|
const DraggableToolbar: React.FC<EditViewProps> = (props) => {
|
||||||
iframeRef: React.RefObject<HTMLIFrameElement>
|
|
||||||
}
|
|
||||||
|
|
||||||
const DraggableToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
|
|
||||||
const { toolbarPosition } = useLivePreviewContext()
|
const { toolbarPosition } = useLivePreviewContext()
|
||||||
|
|
||||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
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 (
|
return (
|
||||||
<div className={[baseClass, `${baseClass}--static`].join(' ')}>
|
<div className={[baseClass, `${baseClass}--static`].join(' ')}>
|
||||||
<ToolbarControls {...props} />
|
<ToolbarControls {...props} />
|
||||||
@@ -59,7 +55,7 @@ const StaticToolbar: React.FC<LivePreviewToolbarProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LivePreviewToolbar: React.FC<
|
export const LivePreviewToolbar: React.FC<
|
||||||
LivePreviewToolbarProps & {
|
EditViewProps & {
|
||||||
draggable?: boolean
|
draggable?: boolean
|
||||||
}
|
}
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user