Compare commits
14 Commits
richtext-s
...
richtext-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6f02765eb | ||
|
|
156ffdd18c | ||
|
|
fe888b5f6c | ||
|
|
bea79feaea | ||
|
|
293cee6f90 | ||
|
|
3e745e91da | ||
|
|
4243048fc5 | ||
|
|
ef84a2cfff | ||
|
|
c00cbaabbc | ||
|
|
02f407e995 | ||
|
|
74e8051bb6 | ||
|
|
ee670b2b20 | ||
|
|
2f8bcc977b | ||
|
|
0cc91d7377 |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,3 +1,13 @@
|
||||
## [2.3.1](https://github.com/payloadcms/payload/compare/v2.3.0...v2.3.1) (2023-12-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensure doc controls are not hidden behind lexical field ([#4345](https://github.com/payloadcms/payload/issues/4345)) ([bea79fe](https://github.com/payloadcms/payload/commit/bea79feaeaee18bf94dd04262f134483f1468494))
|
||||
* query validation on relationship fields ([#4353](https://github.com/payloadcms/payload/issues/4353)) ([fe888b5](https://github.com/payloadcms/payload/commit/fe888b5f6ceaa3969eac759cbdfb109b106dae05))
|
||||
* **richtext-lexical:** blocks content may be hidden behind components outside of the editor ([#4325](https://github.com/payloadcms/payload/issues/4325)) ([3e745e9](https://github.com/payloadcms/payload/commit/3e745e91da620a00e3f0f91892ee3ec66ba72bc0))
|
||||
* **richtext-lexical:** Blocks node: incorrect conversion from v1 node to v2 node ([ef84a2c](https://github.com/payloadcms/payload/commit/ef84a2cfffbb1be52dd948e59eeec0ce324e9046))
|
||||
|
||||
## [2.3.0](https://github.com/payloadcms/payload/compare/v2.2.2...v2.3.0) (2023-11-30)
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ keywords: deployment, production, config, configuration, documentation, Content
|
||||
launch. <strong>Awesome! Great work!</strong> Now, what's next?
|
||||
</Banner>
|
||||
|
||||
There are many ways to deploy Payload to a production environment. When evaluating how you will deploy Payload, you need to consider these main aspects:
|
||||
There are many ways to deploy Payload to a production environment. When evaluating how you will deploy Payload, you need
|
||||
to consider these main aspects:
|
||||
|
||||
1. [Basics](#basics)
|
||||
1. [Security](#security)
|
||||
@@ -21,19 +22,26 @@ There are many ways to deploy Payload to a production environment. When evaluati
|
||||
|
||||
## Basics
|
||||
|
||||
In order for Payload to run, it requires both the server code and the built admin panel. These will be the `dist` and `build` directories by default. If you've used `create-payload-app` to create your project, executing the `build` npm script will build both and output these directories.
|
||||
In order for Payload to run, it requires both the server code and the built admin panel. These will be the `dist`
|
||||
and `build` directories by default. If you've used `create-payload-app` to create your project, executing the `build`
|
||||
npm script will build both and output these directories.
|
||||
|
||||
## Security
|
||||
|
||||
Payload features a suite of security features that you can rely on to strengthen your application's security. When deploying to Production, it's a good idea to double-check that you are making proper use of each of them.
|
||||
Payload features a suite of security features that you can rely on to strengthen your application's security. When
|
||||
deploying to Production, it's a good idea to double-check that you are making proper use of each of them.
|
||||
|
||||
##### The Secret Key
|
||||
|
||||
When you initialize Payload, you provide it with a `secret` property. This property should be impossible to guess and extremely difficult for brute-force attacks to crack. Make sure your Production `secret` is a long, complex string. It's often best practice to store it in an `env` file which is not checked into your Git repository, using `dotenv` to supply it to your `payload.init` call.
|
||||
When you initialize Payload, you provide it with a `secret` property. This property should be impossible to guess and
|
||||
extremely difficult for brute-force attacks to crack. Make sure your Production `secret` is a long, complex string. It's
|
||||
often best practice to store it in an `env` file which is not checked into your Git repository, using `dotenv` to supply
|
||||
it to your `payload.init` call.
|
||||
|
||||
##### Double-check and thoroughly test all Access Control
|
||||
|
||||
Because _**you**_ are in complete control of who can do what with your data, you should double and triple-check that you wield that power responsibly before deploying to Production.
|
||||
Because _**you**_ are in complete control of who can do what with your data, you should double and triple-check that you
|
||||
wield that power responsibly before deploying to Production.
|
||||
|
||||
<Banner type="error">
|
||||
<strong>By default, all Access Control functions require that a user is successfully logged in to Payload to create, read, update, or delete data.</strong>{' '}
|
||||
@@ -44,7 +52,8 @@ Because _**you**_ are in complete control of who can do what with your data, you
|
||||
|
||||
##### Building the Admin panel
|
||||
|
||||
Before running in Production, you need to have built a production-ready copy of the Payload Admin panel. To do this, Payload provides the `build` NPM script. You can use it by adding a `script` to your `package.json` file like this:
|
||||
Before running in Production, you need to have built a production-ready copy of the Payload Admin panel. To do this,
|
||||
Payload provides the `build` NPM script. You can use it by adding a `script` to your `package.json` file like this:
|
||||
|
||||
`package.json`:
|
||||
|
||||
@@ -60,19 +69,26 @@ Before running in Production, you need to have built a production-ready copy of
|
||||
}
|
||||
```
|
||||
|
||||
Then, to build Payload, you would run `npm run build` in your project folder. A production-ready Admin bundle will be created in the `build` directory.
|
||||
Then, to build Payload, you would run `npm run build` in your project folder. A production-ready Admin bundle will be
|
||||
created in the `build` directory.
|
||||
|
||||
##### Setting Node to Production
|
||||
|
||||
Make sure you set the environment variable `NODE_ENV` to `production`. Based on this variable, many Node packages automatically optimize themselves. In production, Payload automatically disables the [GraphQL Playground](/docs/graphql/overview#graphql-playground), serves a production-ready version of the Admin panel, and other changes.
|
||||
Make sure you set the environment variable `NODE_ENV` to `production`. Based on this variable, many Node packages
|
||||
automatically optimize themselves. In production, Payload automatically disables
|
||||
the [GraphQL Playground](/docs/graphql/overview#graphql-playground), serves a production-ready version of the Admin
|
||||
panel, and other changes.
|
||||
|
||||
##### Secure Cookie Settings
|
||||
|
||||
You should be using an SSL certificate for production Payload instances, which means you can [enable secure cookies](/docs/authentication/config) in your Authentication-enabled Collection configs.
|
||||
You should be using an SSL certificate for production Payload instances, which means you
|
||||
can [enable secure cookies](/docs/authentication/config) in your Authentication-enabled Collection configs.
|
||||
|
||||
##### Preventing API Abuse
|
||||
|
||||
Payload comes with a robust set of built-in anti-abuse measures, such as locking out users after X amount of failed login attempts, request rate limiting, GraphQL query complexity limits, max `depth` settings, and more. [Click here to learn more](/docs/production/preventing-abuse).
|
||||
Payload comes with a robust set of built-in anti-abuse measures, such as locking out users after X amount of failed
|
||||
login attempts, request rate limiting, GraphQL query complexity limits, max `depth` settings, and
|
||||
more. [Click here to learn more](/docs/production/preventing-abuse).
|
||||
|
||||
## MongoDB
|
||||
|
||||
@@ -80,11 +96,18 @@ Payload can be used with any MongoDB compatible database including AWS DocumentD
|
||||
|
||||
##### Managing MongoDB yourself
|
||||
|
||||
If you are using a [persistent filesystem-based cloud host](#persistent-vs-ephemeral-filesystems) such as a [DigitalOcean Droplet](https://www.digitalocean.com/products/droplets/) or an [Amazon EC2](https://aws.amazon.com/ec2/?ec2-whats-new.sort-by=item.additionalFields.postDateTime&ec2-whats-new.sort-order=desc) server, you might opt to install MongoDB directly on that server itself so that Node can communicate with it locally. With this approach, you can benefit from faster response times, but scaling can become more involved as your app's user base grows.
|
||||
If you are using a [persistent filesystem-based cloud host](#persistent-vs-ephemeral-filesystems) such as
|
||||
a [DigitalOcean Droplet](https://www.digitalocean.com/products/droplets/) or
|
||||
an [Amazon EC2](https://aws.amazon.com/ec2/?ec2-whats-new.sort-by=item.additionalFields.postDateTime&ec2-whats-new.sort-order=desc)
|
||||
server, you might opt to install MongoDB directly on that server itself so that Node can communicate with it locally.
|
||||
With this approach, you can benefit from faster response times, but scaling can become more involved as your app's user
|
||||
base grows.
|
||||
|
||||
##### Letting someone else do it
|
||||
|
||||
Alternatively, you can rely on a third-party MongoDB host such as [MongoDB Atlas](https://www.mongodb.com/). With Atlas or a similar cloud provider, you can trust them to take care of your database's availability, security, redundancy, and backups.
|
||||
Alternatively, you can rely on a third-party MongoDB host such as [MongoDB Atlas](https://www.mongodb.com/). With Atlas
|
||||
or a similar cloud provider, you can trust them to take care of your database's availability, security, redundancy, and
|
||||
backups.
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
@@ -98,21 +121,31 @@ Alternatively, you can rely on a third-party MongoDB host such as [MongoDB Atlas
|
||||
|
||||
##### DocumentDB
|
||||
|
||||
When using AWS DocumentDB, you will need to configure connection options for authentication in the `mongoOptions` passed to `payload.init`. You also need to set `mongoOptions.useFacet` to `false` to disable use of the unsupported `$facet` aggregation.
|
||||
When using AWS DocumentDB, you will need to configure connection options for authentication in the `connectOptions`
|
||||
passed to the `mongooseAdapter` . You also need to set `connectOptions.useFacet` to `false` to disable use of the
|
||||
unsupported `$facet` aggregation.
|
||||
|
||||
##### CosmosDB
|
||||
|
||||
When using Azure Cosmos DB, an index is needed for any field you may want to sort on. To add the sort index for all fields that may be sorted in the admin UI use the <a href="/docs/configuration/overview">indexSortableFields</a> configuration option.
|
||||
When using Azure Cosmos DB, an index is needed for any field you may want to sort on. To add the sort index for all
|
||||
fields that may be sorted in the admin UI use the <a href="/docs/configuration/overview">indexSortableFields</a>
|
||||
configuration option.
|
||||
|
||||
## File storage
|
||||
|
||||
If you are using Payload to [manage file uploads](/docs/upload/overview), you need to consider where your uploaded files will be permanently stored. If you do not use Payload for file uploads, then this section does not impact your app whatsoever.
|
||||
If you are using Payload to [manage file uploads](/docs/upload/overview), you need to consider where your uploaded files
|
||||
will be permanently stored. If you do not use Payload for file uploads, then this section does not impact your app
|
||||
whatsoever.
|
||||
|
||||
#### Persistent vs Ephemeral Filesystems
|
||||
|
||||
Some cloud app hosts such as [Heroku](https://heroku.com) use `ephemeral` file systems, which means that any files uploaded to your server only last until the server restarts or shuts down. Heroku and similar providers schedule restarts and shutdowns without your control, meaning your uploads will accidentally disappear without any way to get them back.
|
||||
Some cloud app hosts such as [Heroku](https://heroku.com) use `ephemeral` file systems, which means that any files
|
||||
uploaded to your server only last until the server restarts or shuts down. Heroku and similar providers schedule
|
||||
restarts and shutdowns without your control, meaning your uploads will accidentally disappear without any way to get
|
||||
them back.
|
||||
|
||||
Alternatively, persistent filesystems will never delete your files and can be trusted to reliably host uploads perpetually.
|
||||
Alternatively, persistent filesystems will never delete your files and can be trusted to reliably host uploads
|
||||
perpetually.
|
||||
|
||||
**Popular cloud providers with ephemeral filesystems:**
|
||||
|
||||
@@ -135,21 +168,26 @@ Alternatively, persistent filesystems will never delete your files and can be tr
|
||||
|
||||
##### Using ephemeral filesystem providers like Heroku
|
||||
|
||||
If you don't use Payload's `upload` functionality, you can go ahead and use Heroku or similar platform easily. Everything will work exactly as you want it to.
|
||||
If you don't use Payload's `upload` functionality, you can go ahead and use Heroku or similar platform easily.
|
||||
Everything will work exactly as you want it to.
|
||||
|
||||
But, if you do, and you still want to use an ephemeral filesystem provider, you can write a hook-based solution to _copy_ the files your users upload to a more permanent storage solution like Amazon S3 or DigitalOcean Spaces.
|
||||
But, if you do, and you still want to use an ephemeral filesystem provider, you can write a hook-based solution to
|
||||
_copy_ the files your users upload to a more permanent storage solution like Amazon S3 or DigitalOcean Spaces.
|
||||
|
||||
**To automatically send uploaded files to S3 or similar, you could:**
|
||||
|
||||
- Write an asynchronous `beforeChange` hook for all Collections that support Uploads, which takes any uploaded `file` from the Express `req` and sends it to an S3 bucket
|
||||
- Write an `afterRead` hook to save a `s3URL` field that automatically takes the `filename` stored and formats a full S3 URL
|
||||
- Write an asynchronous `beforeChange` hook for all Collections that support Uploads, which takes any uploaded `file`
|
||||
from the Express `req` and sends it to an S3 bucket
|
||||
- Write an `afterRead` hook to save a `s3URL` field that automatically takes the `filename` stored and formats a full S3
|
||||
URL
|
||||
- Write an `afterDelete` hook that automatically deletes files from the S3 bucket
|
||||
|
||||
With the above configuration, deploying to Heroku or similar becomes no problem.
|
||||
|
||||
## DigitalOcean Tutorials
|
||||
|
||||
DigitalOcean provides extremely helpful documentation that can walk you through the entire process of creating a production-ready Droplet to host your Payload app:
|
||||
DigitalOcean provides extremely helpful documentation that can walk you through the entire process of creating a
|
||||
production-ready Droplet to host your Payload app:
|
||||
|
||||
1. Create a new Ubuntu 20.04 droplet on [DigitalOcean](https://digitalocean.com)
|
||||
1. [Initial server setup](https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-20-04)
|
||||
@@ -160,18 +198,25 @@ DigitalOcean provides extremely helpful documentation that can walk you through
|
||||
|
||||
### Swap Space
|
||||
|
||||
Swap refers to a section of storage on the hard drive that is reserved to temporarily store data that can no longer fit within RAM. This allows for the expansion of your server's working memory, with some limitations. Swap space comes into play when available RAM can no longer accommodate actively used application data, enabling the system to continue functioning.
|
||||
Swap refers to a section of storage on the hard drive that is reserved to temporarily store data that can no longer fit
|
||||
within RAM. This allows for the expansion of your server's working memory, with some limitations. Swap space comes into
|
||||
play when available RAM can no longer accommodate actively used application data, enabling the system to continue
|
||||
functioning.
|
||||
|
||||
Insufficient space can lead to deployment errors and memory-related issues, resulting in application crashes, sluggish performance, or an unresponsive server.
|
||||
Insufficient space can lead to deployment errors and memory-related issues, resulting in application crashes, sluggish
|
||||
performance, or an unresponsive server.
|
||||
|
||||
Common deployment error due to **space limitations** (as reported by users):
|
||||
|
||||
- `Error: Command failed with exit code 1`
|
||||
|
||||
To configure swap, we recommend following this tutorial on [How To Add Swap Space](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04).
|
||||
To configure swap, we recommend following this tutorial
|
||||
on [How To Add Swap Space](https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04).
|
||||
|
||||
## Docker
|
||||
|
||||
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
|
||||
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment
|
||||
variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
|
||||
|
||||
```dockerfile
|
||||
FROM node:18-alpine as base
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
|
||||
@@ -120,24 +120,28 @@ export async function validateSearchParam({
|
||||
if (segments[0] === 'parent' || segments[0] === 'version') {
|
||||
segments.shift()
|
||||
} else {
|
||||
segments.forEach((segment, pathIndex) => {
|
||||
if (fieldAccess[segment]) {
|
||||
if (pathIndex === segments.length - 1) {
|
||||
fieldAccess = fieldAccess[segment]
|
||||
} else if ('fields' in fieldAccess[segment]) {
|
||||
fieldAccess = fieldAccess[segment].fields
|
||||
} else if ('blocks' in fieldAccess[segment]) {
|
||||
fieldAccess = fieldAccess[segment]
|
||||
if (['json', 'relationship', 'richText'].includes(field.type)) {
|
||||
fieldAccess = fieldAccess[field.name]
|
||||
} else {
|
||||
segments.forEach((segment, pathIndex) => {
|
||||
if (fieldAccess[segment]) {
|
||||
if (pathIndex === segments.length - 1) {
|
||||
fieldAccess = fieldAccess[segment]
|
||||
} else if ('fields' in fieldAccess[segment]) {
|
||||
fieldAccess = fieldAccess[segment].fields
|
||||
} else if ('blocks' in fieldAccess[segment]) {
|
||||
fieldAccess = fieldAccess[segment]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fieldAccess = fieldAccess.read.permission
|
||||
} else {
|
||||
fieldAccess = policies[entityType][entitySlug].fields
|
||||
|
||||
if (['json', 'richText'].includes(field.type)) {
|
||||
if (['json', 'relationship', 'richText'].includes(field.type)) {
|
||||
fieldAccess = fieldAccess[field.name]
|
||||
} else {
|
||||
segments.forEach((segment, pathIndex) => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm build:swc && pnpm build:types",
|
||||
"_build": "pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -70,7 +70,7 @@ export class BlockNode extends DecoratorBlockNode {
|
||||
serializedNode = {
|
||||
...serializedNode,
|
||||
fields: {
|
||||
...(serializedNode as any).data.fields,
|
||||
...(serializedNode as any).fields.data,
|
||||
},
|
||||
version: 2,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
@import 'payload/scss';
|
||||
|
||||
.rich-text-lexical {
|
||||
.editor {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.editor-shell {
|
||||
position: relative;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Config } from '../../payload/payload-types'
|
||||
import { ORDER } from '../_graphql/orders'
|
||||
import { PAGE } from '../_graphql/pages'
|
||||
import { PRODUCT } from '../_graphql/products'
|
||||
import { GRAPHQL_API_URL } from './shared'
|
||||
import { payloadToken } from './token'
|
||||
|
||||
const queryMap = {
|
||||
@@ -38,7 +39,7 @@ export const fetchDoc = async <T>(args: {
|
||||
token = cookies().get(payloadToken)
|
||||
}
|
||||
|
||||
const doc: T = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const doc: T = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Config } from '../../payload/payload-types'
|
||||
import { ORDERS } from '../_graphql/orders'
|
||||
import { PAGES } from '../_graphql/pages'
|
||||
import { PRODUCTS } from '../_graphql/products'
|
||||
import { GRAPHQL_API_URL } from './shared'
|
||||
import { payloadToken } from './token'
|
||||
|
||||
const queryMap = {
|
||||
@@ -34,7 +35,7 @@ export const fetchDocs = async <T>(
|
||||
token = cookies().get(payloadToken)
|
||||
}
|
||||
|
||||
const docs: T[] = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const docs: T[] = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Footer, Header, Settings } from '../../payload/payload-types'
|
||||
import { FOOTER_QUERY, HEADER_QUERY, SETTINGS_QUERY } from '../_graphql/globals'
|
||||
import { GRAPHQL_API_URL } from './shared'
|
||||
|
||||
export async function fetchSettings(): Promise<Settings> {
|
||||
if (!process.env.NEXT_PUBLIC_SERVER_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const settings = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const settings = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -27,9 +28,9 @@ export async function fetchSettings(): Promise<Settings> {
|
||||
}
|
||||
|
||||
export async function fetchHeader(): Promise<Header> {
|
||||
if (!process.env.NEXT_PUBLIC_SERVER_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
if (!GRAPHQL_API_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const header = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const header = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -52,9 +53,9 @@ export async function fetchHeader(): Promise<Header> {
|
||||
}
|
||||
|
||||
export async function fetchFooter(): Promise<Footer> {
|
||||
if (!process.env.NEXT_PUBLIC_SERVER_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
if (!GRAPHQL_API_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const footer = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const footer = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'
|
||||
|
||||
import type { User } from '../../payload/payload-types'
|
||||
import { ME_QUERY } from '../_graphql/me'
|
||||
import { GRAPHQL_API_URL } from './shared'
|
||||
|
||||
export const getMe = async (args?: {
|
||||
nullUserRedirect?: string
|
||||
@@ -15,7 +16,7 @@ export const getMe = async (args?: {
|
||||
const cookieStore = cookies()
|
||||
const token = cookieStore.get('payload-token')?.value
|
||||
|
||||
const meUserReq = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const meUserReq = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `JWT ${token}`,
|
||||
|
||||
3
templates/ecommerce/src/app/_api/shared.ts
Normal file
3
templates/ecommerce/src/app/_api/shared.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const GRAPHQL_API_URL = process.env.NEXT_BUILD
|
||||
? `http://127.0.0.1:${process.env.PORT || 3000}`
|
||||
: process.env.NEXT_PUBLIC_SERVER_URL
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Comment, Post, User } from '../../payload/payload-types'
|
||||
import { COMMENTS_BY_DOC, COMMENTS_BY_USER } from '../_graphql/comments'
|
||||
import { GRAPHQL_API_URL } from './shared'
|
||||
|
||||
export const fetchComments = async (args: {
|
||||
user?: User['id']
|
||||
@@ -7,7 +8,7 @@ export const fetchComments = async (args: {
|
||||
}): Promise<Comment[]> => {
|
||||
const { user, doc } = args || {}
|
||||
|
||||
const docs: Comment[] = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const docs: Comment[] = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Config } from '../../payload/payload-types'
|
||||
import { PAGE } from '../_graphql/pages'
|
||||
import { POST } from '../_graphql/posts'
|
||||
import { PROJECT } from '../_graphql/projects'
|
||||
import { GRAPHQL_API_URL } from './shared'
|
||||
import { payloadToken } from './token'
|
||||
|
||||
const queryMap = {
|
||||
@@ -38,7 +39,7 @@ export const fetchDoc = async <T>(args: {
|
||||
token = cookies().get(payloadToken)
|
||||
}
|
||||
|
||||
const doc: T = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const doc: T = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Config } from '../../payload/payload-types'
|
||||
import { PAGES } from '../_graphql/pages'
|
||||
import { POSTS } from '../_graphql/posts'
|
||||
import { PROJECTS } from '../_graphql/projects'
|
||||
import { GRAPHQL_API_URL } from './shared'
|
||||
import { payloadToken } from './token'
|
||||
|
||||
const queryMap = {
|
||||
@@ -35,7 +36,7 @@ export const fetchDocs = async <T>(
|
||||
token = cookies().get(payloadToken)
|
||||
}
|
||||
|
||||
const docs: T[] = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const docs: T[] = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Footer, Header, Settings } from '../../payload/payload-types'
|
||||
import { FOOTER_QUERY, HEADER_QUERY, SETTINGS_QUERY } from '../_graphql/globals'
|
||||
import { GRAPHQL_API_URL } from './shared'
|
||||
|
||||
export async function fetchSettings(): Promise<Settings> {
|
||||
if (!process.env.NEXT_PUBLIC_SERVER_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
if (!GRAPHQL_API_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const settings = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const settings = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -27,9 +28,9 @@ export async function fetchSettings(): Promise<Settings> {
|
||||
}
|
||||
|
||||
export async function fetchHeader(): Promise<Header> {
|
||||
if (!process.env.NEXT_PUBLIC_SERVER_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
if (!GRAPHQL_API_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const header = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const header = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -52,9 +53,9 @@ export async function fetchHeader(): Promise<Header> {
|
||||
}
|
||||
|
||||
export async function fetchFooter(): Promise<Footer> {
|
||||
if (!process.env.NEXT_PUBLIC_SERVER_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
if (!GRAPHQL_API_URL) throw new Error('NEXT_PUBLIC_SERVER_URL not found')
|
||||
|
||||
const footer = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const footer = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'
|
||||
|
||||
import type { User } from '../../payload/payload-types'
|
||||
import { ME_QUERY } from '../_graphql/me'
|
||||
import { GRAPHQL_API_URL } from './shared'
|
||||
|
||||
export const getMe = async (args?: {
|
||||
nullUserRedirect?: string
|
||||
@@ -15,7 +16,7 @@ export const getMe = async (args?: {
|
||||
const cookieStore = cookies()
|
||||
const token = cookieStore.get('payload-token')?.value
|
||||
|
||||
const meUserReq = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/graphql`, {
|
||||
const meUserReq = await fetch(`${GRAPHQL_API_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `JWT ${token}`,
|
||||
|
||||
3
templates/website/src/app/_api/shared.ts
Normal file
3
templates/website/src/app/_api/shared.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const GRAPHQL_API_URL = process.env.NEXT_BUILD
|
||||
? `http://127.0.0.1:${process.env.PORT || 3000}`
|
||||
: process.env.NEXT_PUBLIC_SERVER_URL
|
||||
@@ -205,6 +205,65 @@ describe('lexical', () => {
|
||||
expect(textContent).toBe('')
|
||||
})
|
||||
|
||||
test('ensure blocks content is not hidden behind components outside of the editor', async () => {
|
||||
// This test expects there to be a TreeView below the editor
|
||||
|
||||
// This test makes sure there are no z-index issues here
|
||||
await navigateToLexicalFields()
|
||||
const richTextField = page.locator('.rich-text-lexical').first()
|
||||
await richTextField.scrollIntoViewIfNeeded()
|
||||
await expect(richTextField).toBeVisible()
|
||||
|
||||
// Find span in contentEditable with text "Some text below relationship node"
|
||||
const contentEditable = richTextField.locator('.ContentEditable__root').first()
|
||||
await expect(contentEditable).toBeVisible()
|
||||
await contentEditable.click() // Use click, because focus does not work
|
||||
|
||||
await page.keyboard.press('/')
|
||||
|
||||
const slashMenuPopover = page.locator('#slash-menu .slash-menu-popup')
|
||||
await expect(slashMenuPopover).toBeVisible()
|
||||
|
||||
// Heading 2 should be the last, most bottom popover button element which should be initially visible, if not hidden by something (e.g. another block)
|
||||
const popoverSelectButton = slashMenuPopover
|
||||
.locator('button.slash-menu-popup__item-block-select')
|
||||
.first()
|
||||
await expect(popoverSelectButton).toBeVisible()
|
||||
await popoverSelectButton.click()
|
||||
|
||||
const newSelectBlock = richTextField.locator('.lexical-block').first()
|
||||
await newSelectBlock.scrollIntoViewIfNeeded()
|
||||
await expect(newSelectBlock).toBeVisible()
|
||||
|
||||
await page.mouse.wheel(0, 300) // Scroll down so that the future react-select menu popover is displayed below and not above
|
||||
|
||||
const reactSelect = newSelectBlock.locator('.rs__control').first()
|
||||
await reactSelect.click()
|
||||
|
||||
const popover = page.locator('.rs__menu').first()
|
||||
const popoverOption3 = popover.locator('.rs__option').nth(2)
|
||||
|
||||
const popoverOption3BoundingBox = await popoverOption3.boundingBox()
|
||||
expect(popoverOption3BoundingBox).not.toBeNull()
|
||||
expect(popoverOption3BoundingBox).not.toBeUndefined()
|
||||
expect(popoverOption3BoundingBox.height).toBeGreaterThan(0)
|
||||
expect(popoverOption3BoundingBox.width).toBeGreaterThan(0)
|
||||
|
||||
// Now click the button to see if it actually works. Simulate an actual mouse click instead of using .click()
|
||||
// by using page.mouse and the correct coordinates
|
||||
// .isVisible() and .click() might work fine EVEN if the slash menu is not actually visible by humans
|
||||
// see: https://github.com/microsoft/playwright/issues/9923
|
||||
// This is why we use page.mouse.click() here. It's the most effective way of detecting such a z-index issue
|
||||
// and usually the only method which works.
|
||||
|
||||
const x = popoverOption3BoundingBox.x
|
||||
const y = popoverOption3BoundingBox.y
|
||||
|
||||
await page.mouse.click(x, y, { button: 'left' })
|
||||
|
||||
await expect(reactSelect.locator('.rs__value-container').first()).toHaveText('Option 3')
|
||||
})
|
||||
|
||||
describe('nested lexical editor in block', () => {
|
||||
test('should type and save typed text', async () => {
|
||||
await navigateToLexicalFields()
|
||||
|
||||
@@ -42,6 +42,8 @@ export const defaultAccessRelSlug = 'strict-access'
|
||||
export const chainedRelSlug = 'chained'
|
||||
export const customIdSlug = 'custom-id'
|
||||
export const customIdNumberSlug = 'custom-id-number'
|
||||
export const polymorphicRelationshipsSlug = 'polymorphic-relationships'
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [
|
||||
{
|
||||
@@ -232,6 +234,16 @@ export default buildConfigWithDefaults({
|
||||
|
||||
slug: 'movieReviews',
|
||||
},
|
||||
{
|
||||
slug: polymorphicRelationshipsSlug,
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'polymorphic',
|
||||
relationTo: ['movies'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
|
||||
@@ -563,6 +563,46 @@ describe('Relationships', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Polymorphic Relationships', () => {
|
||||
it('should allow REST querying on polymorphic relationships', async () => {
|
||||
const movie = await payload.create({
|
||||
collection: 'movies',
|
||||
data: {
|
||||
name: 'Pulp Fiction 2',
|
||||
},
|
||||
})
|
||||
await payload.create({
|
||||
collection: 'polymorphic-relationships',
|
||||
data: {
|
||||
polymorphic: {
|
||||
value: movie.id,
|
||||
relationTo: 'movies',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const query = await client.find({
|
||||
slug: 'polymorphic-relationships',
|
||||
query: {
|
||||
and: [
|
||||
{
|
||||
'polymorphic.value': {
|
||||
equals: movie.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'polymorphic.relationTo': {
|
||||
equals: 'movies',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(query.result.docs).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createPost(overrides?: Partial<Post>) {
|
||||
|
||||
Reference in New Issue
Block a user