Compare commits
59 Commits
v3.0.0-bet
...
fix/form-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e37897f7a5 | ||
|
|
dbf1a1bd8b | ||
|
|
4f0ddcf632 | ||
|
|
693621a6e3 | ||
|
|
5b201392cc | ||
|
|
a70bcf81c0 | ||
|
|
ed880d5018 | ||
|
|
ea84e82ad5 | ||
|
|
4216d69ccb | ||
|
|
dcad5003f5 | ||
|
|
bd9c06a99d | ||
|
|
48932ef54d | ||
|
|
4aeefc5a1a | ||
|
|
e96ff90029 | ||
|
|
f6e77b845b | ||
|
|
a0bb02d05a | ||
|
|
354ad7092c | ||
|
|
f9d862d854 | ||
|
|
f41576dd65 | ||
|
|
ffa20aa7d0 | ||
|
|
f7a2cf96b9 | ||
|
|
cfeac79b99 | ||
|
|
821bed0ea6 | ||
|
|
9e9111666b | ||
|
|
5065322d31 | ||
|
|
ad4796cdb2 | ||
|
|
43b7ba82da | ||
|
|
3785c79ac9 | ||
|
|
4384e9eb0e | ||
|
|
9364f8da2e | ||
|
|
a4ef359660 | ||
|
|
848c05f247 | ||
|
|
ec556360b6 | ||
|
|
d99b426e3b | ||
|
|
19a78297b4 | ||
|
|
e95eea694c | ||
|
|
4c6aaafe88 | ||
|
|
dc8c099d9e | ||
|
|
259ae674a1 | ||
|
|
731f023c6d | ||
|
|
86b19d4c74 | ||
|
|
17b8c29799 | ||
|
|
29af2849ba | ||
|
|
a7ac5efd70 | ||
|
|
15c7a9dcf8 | ||
|
|
8e55a2a866 | ||
|
|
0f306da63b | ||
|
|
ba9ea5c752 | ||
|
|
53b7d6f89f | ||
|
|
f5fb095df4 | ||
|
|
721919fae9 | ||
|
|
d3e27e87fe | ||
|
|
e1ff92e8c6 | ||
|
|
ac5d744914 | ||
|
|
b94a265fad | ||
|
|
1ba3a92745 | ||
|
|
9814fd705e | ||
|
|
20455f4fc2 | ||
|
|
9f37bf7397 |
@@ -9,6 +9,7 @@ module.exports = {
|
||||
rules: {
|
||||
'payload/no-jsx-import-statements': 'warn',
|
||||
'payload/no-relative-monorepo-imports': 'error',
|
||||
'payload/no-imports-from-exports-dir': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
13
.github/CODEOWNERS
vendored
13
.github/CODEOWNERS
vendored
@@ -3,22 +3,19 @@
|
||||
### Package Exports ###
|
||||
/**/exports/ @denolfe @jmikrut
|
||||
|
||||
### Adapters ###
|
||||
### Packages ###
|
||||
/packages/richtext-*/ @AlessioGr
|
||||
|
||||
### Plugins ###
|
||||
/packages/plugin-cloud*/ @denolfe
|
||||
/packages/email-*/ @denolfe
|
||||
/packages/storage-*/ @denolfe
|
||||
/packages/create-payload-app/ @denolfe
|
||||
/packages/eslint-*/ @denolfe
|
||||
|
||||
### Templates ###
|
||||
/templates/ @jacobsfletch @denolfe
|
||||
|
||||
### Misc ###
|
||||
/packages/create-payload-app/ @denolfe
|
||||
/packages/eslint-*/ @denolfe
|
||||
|
||||
### Build Files ###
|
||||
/**/package.json @denolfe
|
||||
|
||||
/tsconfig.json @denolfe
|
||||
/**/tsconfig*.json @denolfe
|
||||
|
||||
|
||||
48
.github/actions/setup/action.yml
vendored
Normal file
48
.github/actions/setup/action.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Setup node and pnpm
|
||||
description: Configure the Node.js and pnpm versions
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
description: 'The Node.js version to use'
|
||||
required: true
|
||||
default: 18.20.2
|
||||
pnpm-version:
|
||||
description: 'The pnpm version to use'
|
||||
required: true
|
||||
default: 8.15.7
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
shell: bash
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ inputs.pnpm-version }}
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-store-
|
||||
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- shell: bash
|
||||
run: pnpm install
|
||||
68
.github/workflows/main.yml
vendored
68
.github/workflows/main.yml
vendored
@@ -2,9 +2,14 @@ name: build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
push:
|
||||
branches: ['main', 'beta']
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -49,6 +54,50 @@ jobs:
|
||||
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
|
||||
echo "templates: ${{ steps.filter.outputs.templates }}"
|
||||
|
||||
lint:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 720
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-store-
|
||||
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- run: pnpm install
|
||||
- name: Lint staged
|
||||
run: |
|
||||
git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF}...${GITHUB_SHA}
|
||||
npx lint-staged --diff="origin/${GITHUB_BASE_REF}...${GITHUB_SHA}"
|
||||
|
||||
build:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.needs_build == 'true' }}
|
||||
@@ -441,3 +490,18 @@ jobs:
|
||||
yarn install
|
||||
yarn build
|
||||
yarn generate:types
|
||||
|
||||
all-green:
|
||||
name: All Green
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- lint
|
||||
- build
|
||||
- tests-unit
|
||||
- tests-int
|
||||
- tests-e2e
|
||||
|
||||
steps:
|
||||
- if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
|
||||
run: exit 1
|
||||
|
||||
36
.github/workflows/release-canary.yml
vendored
Normal file
36
.github/workflows/release-canary.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: release-canary
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches:
|
||||
- beta
|
||||
|
||||
env:
|
||||
NODE_VERSION: 18.20.2
|
||||
PNPM_VERSION: 8.15.7
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
jobs:
|
||||
release:
|
||||
permissions:
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
- name: Load npm token
|
||||
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Canary release script
|
||||
# dry run hard-coded to true for testing and no npm token provided
|
||||
run: pnpm tsx ./scripts/publish-canary.ts
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
@@ -12,3 +12,4 @@
|
||||
tsconfig.json
|
||||
packages/payload/*.js
|
||||
packages/payload/*.d.ts
|
||||
payload-types.ts
|
||||
|
||||
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -16,6 +16,14 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/dev.js storage-uploadthing",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Uploadthing",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"envFile": "${workspaceFolder}/test/storage-uploadthing/.env"
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/dev.js live-preview",
|
||||
"cwd": "${workspaceFolder}",
|
||||
|
||||
@@ -57,6 +57,7 @@ In addition to the default [field admin config](/docs/fields/overview#admin-conf
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`initCollapsed`** | Set the initial collapsed state |
|
||||
| **`components.RowLabel`** | Function or React component to be rendered as the label on the array row. Receives `({ data, index, path })` as args |
|
||||
| **`isSortable`** | Disable order sorting by setting this value to `false` |
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
@@ -53,9 +53,10 @@ _\* An asterisk denotes that a property is required._
|
||||
|
||||
In addition to the default [field admin config](/docs/fields/overview#admin-config), you can adjust the following properties:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ------------------------------- |
|
||||
| **`initCollapsed`** | Set the initial collapsed state |
|
||||
| Option | Description |
|
||||
| ------------------- | ---------------------------------- |
|
||||
| **`initCollapsed`** | Set the initial collapsed state |
|
||||
| **`isSortable`** | Disable order sorting by setting this value to `false` |
|
||||
|
||||
### Block configs
|
||||
|
||||
|
||||
@@ -163,19 +163,21 @@ Example:
|
||||
|
||||
In addition to each field's base configuration, you can define specific traits and properties for fields that only have effect on how they are rendered in the Admin panel. The following properties are available for all fields within the `admin` property:
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | You can programmatically show / hide fields based on what other fields are doing. [Click here](#conditional-logic) for more info. |
|
||||
| `components` | All field components can be completely and easily swapped out for custom components that you define. [Click here](#custom-components) for more info. |
|
||||
| `description` | Helper text to display with the field to provide more information for the editor user. [Click here](#description) for more info. |
|
||||
| `position` | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. |
|
||||
| `width` | Restrict the width of a field. you can pass any string-based value here, be it pixels, percentages, etc. This property is especially useful when fields are nested within a `Row` type where they can be organized horizontally. |
|
||||
| `style` | Attach raw CSS style properties to the root DOM element of a field. |
|
||||
| `className` | Attach a CSS class name to the root DOM element of a field. |
|
||||
| `readOnly` | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. |
|
||||
| `disabled` | If a field is `disabled`, it is completely omitted from the Admin panel. |
|
||||
| `disableBulkEdit` | Set `disableBulkEdit` to `true` to prevent fields from appearing in the select options when making edits for multiple documents. |
|
||||
| `hidden` | Setting a field's `hidden` property on its `admin` config will transform it into a `hidden` input type. Its value will still submit with the Admin panel's requests, but the field itself will not be visible to editors. |
|
||||
| Option | Description |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `condition` | You can programmatically show / hide fields based on what other fields are doing. [Click here](#conditional-logic) for more info. |
|
||||
| `components` | All field components can be completely and easily swapped out for custom components that you define. [Click here](#custom-components) for more info. |
|
||||
| `description` | Helper text to display with the field to provide more information for the editor user. [Click here](#description) for more info. |
|
||||
| `position` | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. |
|
||||
| `width` | Restrict the width of a field. you can pass any string-based value here, be it pixels, percentages, etc. This property is especially useful when fields are nested within a `Row` type where they can be organized horizontally. |
|
||||
| `style` | Attach raw CSS style properties to the root DOM element of a field. |
|
||||
| `className` | Attach a CSS class name to the root DOM element of a field. |
|
||||
| `readOnly` | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. |
|
||||
| `disabled` | If a field is `disabled`, it is completely omitted from the Admin panel. |
|
||||
| `disableBulkEdit` | Set `disableBulkEdit` to `true` to prevent fields from appearing in the select options when making edits for multiple documents. |
|
||||
| `disableListColumn` | Set `disableListColumn` to `true` to prevent fields from appearing in the list view column selector. |
|
||||
| `disableListFilter` | Set `disableListFilter` to `true` to prevent fields from appearing in the list view filter options. |
|
||||
| `hidden` | Setting a field's `hidden` property on its `admin` config will transform it into a `hidden` input type. Its value will still submit with the Admin panel's requests, but the field itself will not be visible to editors. |
|
||||
|
||||
### Custom components
|
||||
|
||||
|
||||
@@ -26,28 +26,28 @@ keywords: relationship, fields, config, configuration, documentation, Content Ma
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`relationTo`** \* | Provide one or many collection `slug`s to be able to assign relationships to. |
|
||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-relationship-options). |
|
||||
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many relations instead of only one. |
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with `hasMany`. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with `hasMany`. |
|
||||
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](/docs/getting-started/concepts#depth) |
|
||||
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| Option | Description |
|
||||
|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||
| **`relationTo`** \* | Provide one or many collection `slug`s to be able to assign relationships to. |
|
||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-relationship-options). |
|
||||
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many relations instead of only one. |
|
||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with `hasMany`. |
|
||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with `hasMany`. |
|
||||
| **`maxDepth`** | Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth) |
|
||||
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
|
||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -156,6 +156,28 @@ To populate `user.author.department` in it's entirety you could specify `?depth=
|
||||
}
|
||||
```
|
||||
|
||||
#### Field-level max depth
|
||||
|
||||
Fields like relationships or uploads can have a `maxDepth` property that limits the depth of the population for that field. Here are some examples:
|
||||
|
||||
Depth: 10
|
||||
Current depth when field is accessed: 1
|
||||
`maxDepth`: undefined
|
||||
|
||||
In this case, the field would be populated to 9 levels of population.
|
||||
|
||||
Depth: 10
|
||||
Current depth when field is accessed: 0
|
||||
`maxDepth`: 2
|
||||
|
||||
In this case, the field would be populated to 2 levels of population, despite there being a remaining depth of 8.
|
||||
|
||||
Depth: 10
|
||||
Current depth when field is accessed: 2
|
||||
`maxDepth`: 1
|
||||
|
||||
In this case, the field would not be populated, as the current depth (2) has exceeded the `maxDepth` for this field (1).
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
|
||||
284
docs/live-preview/client.mdx
Normal file
284
docs/live-preview/client.mdx
Normal file
@@ -0,0 +1,284 @@
|
||||
---
|
||||
title: Client-side Live Preview
|
||||
label: Client-side
|
||||
order: 40
|
||||
desc: Learn how to implement Live Preview in your client-side front-end application.
|
||||
keywords: live preview, frontend, react, next.js, vue, nuxt.js, svelte, hook, useLivePreview
|
||||
---
|
||||
|
||||
<Banner type="info">
|
||||
If your front-end application supports Server Components like the [Next.js App Router](https://nextjs.org/docs/app), etc., we suggest setting up [server-side Live Preview](./server).
|
||||
</Banner>
|
||||
|
||||
While using Live Preview, the Admin panel emits a new `window.postMessage` event every time your document has changed. Your front-end application can listen for these events and re-render accordingly.
|
||||
|
||||
If your front-end application is built with [React](#react) or [Vue](#vue), use the `useLivePreview` hooks that Payload provides. In the future, all other major frameworks like Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
|
||||
|
||||
By default, all hooks accept the following args:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------ | -------------------------------------------------------------------------------------- |
|
||||
| **`serverURL`** \* | The URL of your Payload server. |
|
||||
| **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. |
|
||||
| **`depth`** | The depth of the relationships to fetch. Defaults to `0`. |
|
||||
| **`apiRoute`** | The path of your API route as defined in `routes.api`. Defaults to `/api`. |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
And return the following values:
|
||||
|
||||
| Path | Description |
|
||||
| --------------- | ---------------------------------------------------------------- |
|
||||
| **`data`** | The live data of the document, merged with the initial data. |
|
||||
| **`isLoading`** | A boolean that indicates whether or not the document is loading. |
|
||||
|
||||
<Banner type="info">
|
||||
If your front-end is tightly coupled to required fields, you should ensure that your UI does not
|
||||
break when these fields are removed. For example, if you are rendering something like
|
||||
`data.relatedPosts[0].title`, your page will break once you remove the first related post. To get
|
||||
around this, use conditional logic, optional chaining, or default values in your UI where needed.
|
||||
For example, `data?.relatedPosts?.[0]?.title`.
|
||||
</Banner>
|
||||
|
||||
<Banner type="info">
|
||||
It is important that the `depth` argument matches exactly with the depth of your initial page request. The depth property is used to populated relationships and uploads beyond their IDs. See [Depth](../getting-started/concepts#depth) for more information.
|
||||
</Banner>
|
||||
|
||||
### React
|
||||
|
||||
If your front-end application is built with client-side [React](https://react.dev) like [Next.js Pages Router](https://nextjs.org/docs/pages), you can use the `useLivePreview` hook that Payload provides.
|
||||
|
||||
First, install the `@payloadcms/live-preview-react` package:
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/live-preview-react
|
||||
```
|
||||
|
||||
Then, use the `useLivePreview` hook in your React component:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useLivePreview } from '@payloadcms/live-preview-react'
|
||||
import { Page as PageType } from '@/payload-types'
|
||||
|
||||
// Fetch the page in a server component, pass it to the client component, then thread it through the hook
|
||||
// The hook will take over from there and keep the preview in sync with the changes you make
|
||||
// The `data` property will contain the live data of the document
|
||||
export const PageClient: React.FC<{
|
||||
page: {
|
||||
title: string
|
||||
}
|
||||
}> = ({ page: initialPage }) => {
|
||||
const { data } = useLivePreview<PageType>({
|
||||
initialData: initialPage,
|
||||
serverURL: PAYLOAD_SERVER_URL,
|
||||
depth: 2,
|
||||
})
|
||||
|
||||
return <h1>{data.title}</h1>
|
||||
}
|
||||
```
|
||||
|
||||
### Vue
|
||||
|
||||
If your front-end application is built with [Vue 3](https://vuejs.org) or [Nuxt 3](https://nuxt.js), you can use the `useLivePreview` composable that Payload provides.
|
||||
|
||||
First, install the `@payloadcms/live-preview-vue` package:
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/live-preview-vue
|
||||
```
|
||||
|
||||
Then, use the `useLivePreview` hook in your Vue component:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { PageData } from '~/types';
|
||||
import { defineProps } from 'vue';
|
||||
import { useLivePreview } from '@payloadcms/live-preview-vue';
|
||||
|
||||
// Fetch the initial data on the parent component or using async state
|
||||
const props = defineProps<{ initialData: PageData }>();
|
||||
|
||||
// The hook will take over from here and keep the preview in sync with the changes you make.
|
||||
// The `data` property will contain the live data of the document only when viewed from the Preview view of the Admin UI.
|
||||
const { data } = useLivePreview<PageData>({
|
||||
initialData: props.initialData,
|
||||
serverURL: "<PAYLOAD_SERVER_URL>",
|
||||
depth: 2,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ data.title }}</h1>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Building your own hook
|
||||
|
||||
No matter what front-end framework you are using, you can build your own hook using the same underlying tooling that Payload provides.
|
||||
|
||||
First, install the base `@payloadcms/live-preview` package:
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/live-preview
|
||||
```
|
||||
|
||||
This package provides the following functions:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`subscribe`** | Subscribes to the Admin panel's `window.postMessage` events and calls the provided callback function. |
|
||||
| **`unsubscribe`** | Unsubscribes from the Admin panel's `window.postMessage` events. |
|
||||
| **`ready`** | Sends a `window.postMessage` event to the Admin panel to indicate that the front-end is ready to receive messages. |
|
||||
| **`isLivePreviewEvent`** | Checks if a `MessageEvent` originates from the Admin panel and is a Live Preview event, i.e. debounced form state. |
|
||||
|
||||
The `subscribe` function takes the following args:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------- |
|
||||
| **`callback`** \* | A callback function that is called with `data` every time a change is made to the document. |
|
||||
| **`serverURL`** \* | The URL of your Payload server. |
|
||||
| **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. |
|
||||
| **`depth`** | The depth of the relationships to fetch. Defaults to `0`. |
|
||||
|
||||
With these functions, you can build your own hook using your front-end framework of choice:
|
||||
|
||||
```tsx
|
||||
import { subscribe, unsubscribe } from '@payloadcms/live-preview'
|
||||
|
||||
// To build your own hook, subscribe to Live Preview events using the `subscribe` function
|
||||
// It handles everything from:
|
||||
// 1. Listening to `window.postMessage` events
|
||||
// 2. Merging initial data with active form state
|
||||
// 3. Populating relationships and uploads
|
||||
// 4. Calling the `onChange` callback with the result
|
||||
// Your hook should also:
|
||||
// 1. Tell the Admin panel when it is ready to receive messages
|
||||
// 2. Handle the results of the `onChange` callback to update the UI
|
||||
// 3. Unsubscribe from the `window.postMessage` events when it unmounts
|
||||
```
|
||||
|
||||
Here is an example of what the same `useLivePreview` React hook from above looks like under the hood:
|
||||
|
||||
```tsx
|
||||
import { subscribe, unsubscribe, ready } from '@payloadcms/live-preview'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||
|
||||
export const useLivePreview = <T extends any>(props: {
|
||||
depth?: number
|
||||
initialData: T
|
||||
serverURL: string
|
||||
}): {
|
||||
data: T
|
||||
isLoading: boolean
|
||||
} => {
|
||||
const { depth = 0, initialData, serverURL } = props
|
||||
const [data, setData] = useState<T>(initialData)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const hasSentReadyMessage = useRef<boolean>(false)
|
||||
|
||||
const onChange = useCallback((mergedData) => {
|
||||
// When a change is made, the `onChange` callback will be called with the merged data
|
||||
// Set this merged data into state so that React will re-render the UI
|
||||
setData(mergedData)
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for `window.postMessage` events from the Admin panel
|
||||
// When a change is made, the `onChange` callback will be called with the merged data
|
||||
const subscription = subscribe({
|
||||
callback: onChange,
|
||||
depth,
|
||||
initialData,
|
||||
serverURL,
|
||||
})
|
||||
|
||||
// Once subscribed, send a `ready` message back up to the Admin panel
|
||||
// This will indicate that the front-end is ready to receive messages
|
||||
if (!hasSentReadyMessage.current) {
|
||||
hasSentReadyMessage.current = true
|
||||
|
||||
ready({
|
||||
serverURL,
|
||||
})
|
||||
}
|
||||
|
||||
// When the component unmounts, unsubscribe from the `window.postMessage` events
|
||||
return () => {
|
||||
unsubscribe(subscription)
|
||||
}
|
||||
}, [serverURL, onChange, depth, initialData])
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
When building your own hook, ensure that the args and return values are consistent with the ones
|
||||
listed at the top of this document. This will ensure that all hooks follow the same API.
|
||||
</Banner>
|
||||
|
||||
## Example
|
||||
|
||||
For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). There you will find examples of various front-end frameworks and how to integrate each one of them, including:
|
||||
|
||||
- [Next.js App Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-app)
|
||||
- [Next.js Pages Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-pages)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
#### Relationships and/or uploads are not populating
|
||||
|
||||
If you are using relationships or uploads in your front-end application, and your front-end application runs on a different domain than your Payload server, you may need to configure [CORS](../configuration/overview) to allow requests to be made between the two domains. This includes sites that are running on a different port or subdomain. Similarly, if you are protecting resources behind user authentication, you may also need to configure [CSRF](../authentication/overview#csrf-protection) to allow cookies to be sent between the two domains. For example:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
{
|
||||
// ...
|
||||
// If your site is running on a different domain than your Payload server,
|
||||
// This will allows requests to be made between the two domains
|
||||
cors: {
|
||||
[
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
},
|
||||
// If you are protecting resources behind user authentication,
|
||||
// This will allow cookies to be sent between the two domains
|
||||
csrf: {
|
||||
[
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Relationships and/or uploads disappear after editing a document
|
||||
|
||||
It is possible that either you are setting an improper [`depth`](../getting-started/concepts#depth) in your initial request and/or your `useLivePreview` hook, or they're mismatched. Ensure that the `depth` parameter is set to the correct value, and that it matches exactly in both places. For example:
|
||||
|
||||
```tsx
|
||||
// Your initial request
|
||||
const { docs } = await payload.find({
|
||||
collection: 'pages',
|
||||
depth: 1, // Ensure this is set to the proper depth for your application
|
||||
where: {
|
||||
slug: {
|
||||
equals: 'home',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Your hook
|
||||
const { data } = useLivePreview<PageType>({
|
||||
initialData: initialPage,
|
||||
serverURL: PAYLOAD_SERVER_URL,
|
||||
depth: 1, // Ensure this matches the depth of your initial request
|
||||
})
|
||||
```
|
||||
@@ -1,279 +1,16 @@
|
||||
---
|
||||
title: Implementing Live Preview in your app
|
||||
label: Frontend Implementation
|
||||
title: Implementing Live Preview in your frontend
|
||||
label: Frontend
|
||||
order: 20
|
||||
desc: Learn how to implement Live Preview in your front-end application.
|
||||
keywords: live preview, frontend, react, next.js, vue, nuxt.js, svelte, hook, useLivePreview
|
||||
---
|
||||
|
||||
While using Live Preview, the Admin panel emits a new `window.postMessage` event every time a change is made to the document. Your front-end application can listen for these events and re-render accordingly.
|
||||
There are two ways to use Live Preview in your own application depending on whether your front-end framework supports server components:
|
||||
|
||||
Wiring your front-end into Live Preview is easy. If your front-end application is built with React, Next.js, Vue or Nuxt.js, use the `useLivePreview` hook that Payload provides. In the future, all other major frameworks like Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information.
|
||||
|
||||
By default, all hooks accept the following args:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------ | -------------------------------------------------------------------------------------- |
|
||||
| **`serverURL`** \* | The URL of your Payload server. |
|
||||
| **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. |
|
||||
| **`depth`** | The depth of the relationships to fetch. Defaults to `0`. |
|
||||
| **`apiRoute`** | The path of your API route as defined in `routes.api`. Defaults to `/api`. |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
And return the following values:
|
||||
|
||||
| Path | Description |
|
||||
| --------------- | ---------------------------------------------------------------- |
|
||||
| **`data`** | The live data of the document, merged with the initial data. |
|
||||
| **`isLoading`** | A boolean that indicates whether or not the document is loading. |
|
||||
- [Server-side Live Preview (suggested)](./server)
|
||||
- [Client-side Live Preview](./client)
|
||||
|
||||
<Banner type="info">
|
||||
If your front-end is tightly coupled to required fields, you should ensure that your UI does not
|
||||
break when these fields are removed. For example, if you are rendering something like
|
||||
`data.relatedPosts[0].title`, your page will break once you remove the first related post. To get
|
||||
around this, use conditional logic, optional chaining, or default values in your UI where needed.
|
||||
For example, `data?.relatedPosts?.[0]?.title`.
|
||||
We suggest using server-side Live Preview if your framework supports it, it is both simpler to setup and more performant to run than the client-side alternative.
|
||||
</Banner>
|
||||
|
||||
<Banner type="info">
|
||||
It is important that the `depth` argument matches exactly with the depth of your initial page request. The depth property is used to populated relationships and uploads beyond their IDs. See [Depth](../getting-started/concepts#depth) for more information.
|
||||
</Banner>
|
||||
|
||||
### React
|
||||
|
||||
If your front-end application is built with React or Next.js, you can use the `useLivePreview` hook that Payload provides.
|
||||
|
||||
First, install the `@payloadcms/live-preview-react` package:
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/live-preview-react
|
||||
```
|
||||
|
||||
Then, use the `useLivePreview` hook in your React component:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useLivePreview } from '@payloadcms/live-preview-react'
|
||||
import { Page as PageType } from '@/payload-types'
|
||||
|
||||
// Fetch the page in a server component, pass it to the client component, then thread it through the hook
|
||||
// The hook will take over from there and keep the preview in sync with the changes you make
|
||||
// The `data` property will contain the live data of the document
|
||||
export const PageClient: React.FC<{
|
||||
page: {
|
||||
title: string
|
||||
}
|
||||
}> = ({ page: initialPage }) => {
|
||||
const { data } = useLivePreview<PageType>({
|
||||
initialData: initialPage,
|
||||
serverURL: PAYLOAD_SERVER_URL,
|
||||
depth: 2,
|
||||
})
|
||||
|
||||
return <h1>{data.title}</h1>
|
||||
}
|
||||
```
|
||||
|
||||
### Vue
|
||||
|
||||
If your front-end application is built with Vue 3 or Nuxt 3, you can use the `useLivePreview` composable that Payload provides.
|
||||
|
||||
First, install the `@payloadcms/live-preview-vue` package:
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/live-preview-vue
|
||||
```
|
||||
|
||||
Then, use the `useLivePreview` hook in your Vue component:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { PageData } from '~/types';
|
||||
import { defineProps } from 'vue';
|
||||
import { useLivePreview } from '@payloadcms/live-preview-vue';
|
||||
|
||||
// Fetch the initial data on the parent component or using async state
|
||||
const props = defineProps<{ initialData: PageData }>();
|
||||
|
||||
// The hook will take over from here and keep the preview in sync with the changes you make.
|
||||
// The `data` property will contain the live data of the document only when viewed from the Preview view of the Admin UI.
|
||||
const { data } = useLivePreview<PageData>({
|
||||
initialData: props.initialData,
|
||||
serverURL: "<PAYLOAD_SERVER_URL>",
|
||||
depth: 2,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ data.title }}</h1>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Building your own hook
|
||||
|
||||
No matter what front-end framework you are using, you can build your own hook using the same underlying tooling that Payload provides.
|
||||
|
||||
First, install the base `@payloadcms/live-preview` package:
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/live-preview
|
||||
```
|
||||
|
||||
This package provides the following functions:
|
||||
|
||||
| Path | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`subscribe`** | Subscribes to the Admin panel's `window.postMessage` events and calls the provided callback function. |
|
||||
| **`unsubscribe`** | Unsubscribes from the Admin panel's `window.postMessage` events. |
|
||||
| **`ready`** | Sends a `window.postMessage` event to the Admin panel to indicate that the front-end is ready to receive messages. |
|
||||
|
||||
The `subscribe` function takes the following args:
|
||||
|
||||
| Path | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------- |
|
||||
| **`callback`** \* | A callback function that is called with `data` every time a change is made to the document. |
|
||||
| **`serverURL`** \* | The URL of your Payload server. |
|
||||
| **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. |
|
||||
| **`depth`** | The depth of the relationships to fetch. Defaults to `0`. |
|
||||
|
||||
With these functions, you can build your own hook using your front-end framework of choice:
|
||||
|
||||
```tsx
|
||||
import { subscribe, unsubscribe } from '@payloadcms/live-preview'
|
||||
|
||||
// To build your own hook, subscribe to Live Preview events using the`subscribe` function
|
||||
// It handles everything from:
|
||||
// 1. Listening to `window.postMessage` events
|
||||
// 2. Merging initial data with active form state
|
||||
// 3. Populating relationships and uploads
|
||||
// 4. Calling the `onChange` callback with the result
|
||||
// Your hook should also:
|
||||
// 1. Tell the Admin panel when it is ready to receive messages
|
||||
// 2. Handle the results of the `onChange` callback to update the UI
|
||||
// 3. Unsubscribe from the `window.postMessage` events when it unmounts
|
||||
```
|
||||
|
||||
Here is an example of what the same `useLivePreview` React hook from above looks like under the hood:
|
||||
|
||||
```tsx
|
||||
import { subscribe, unsubscribe, ready } from '@payloadcms/live-preview'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||
|
||||
export const useLivePreview = <T extends any>(props: {
|
||||
depth?: number
|
||||
initialData: T
|
||||
serverURL: string
|
||||
}): {
|
||||
data: T
|
||||
isLoading: boolean
|
||||
} => {
|
||||
const { depth = 0, initialData, serverURL } = props
|
||||
const [data, setData] = useState<T>(initialData)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const hasSentReadyMessage = useRef<boolean>(false)
|
||||
|
||||
const onChange = useCallback((mergedData) => {
|
||||
// When a change is made, the `onChange` callback will be called with the merged data
|
||||
// Set this merged data into state so that React will re-render the UI
|
||||
setData(mergedData)
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for `window.postMessage` events from the Admin panel
|
||||
// When a change is made, the `onChange` callback will be called with the merged data
|
||||
const subscription = subscribe({
|
||||
callback: onChange,
|
||||
depth,
|
||||
initialData,
|
||||
serverURL,
|
||||
})
|
||||
|
||||
// Once subscribed, send a `ready` message back up to the Admin panel
|
||||
// This will indicate that the front-end is ready to receive messages
|
||||
if (!hasSentReadyMessage.current) {
|
||||
hasSentReadyMessage.current = true
|
||||
|
||||
ready({
|
||||
serverURL,
|
||||
})
|
||||
}
|
||||
|
||||
// When the component unmounts, unsubscribe from the `window.postMessage` events
|
||||
return () => {
|
||||
unsubscribe(subscription)
|
||||
}
|
||||
}, [serverURL, onChange, depth, initialData])
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
When building your own hook, ensure that the args and return values are consistent with the ones
|
||||
listed at the top of this document. This will ensure that all hooks follow the same API.
|
||||
</Banner>
|
||||
|
||||
## Example
|
||||
|
||||
For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). There you will find examples of various front-end frameworks and how to integrate each one of them, including:
|
||||
|
||||
- [Next.js App Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-app)
|
||||
- [Next.js Pages Router](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-pages)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
#### Relationships and/or uploads are not populating
|
||||
|
||||
If you are using relationships or uploads in your front-end application, and your front-end application runs on a different domain than your Payload server, you may need to configure [CORS](../configuration/overview) to allow requests to be made between the two domains. This includes sites that are running on a different port or subdomain. Similarly, if you are protecting resources behind user authentication, you may also need to configure [CSRF](../authentication/overview#csrf-protection) to allow cookies to be sent between the two domains. For example:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
{
|
||||
// ...
|
||||
// If your site is running on a different domain than your Payload server,
|
||||
// This will allows requests to be made between the two domains
|
||||
cors: {
|
||||
[
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
},
|
||||
// If you are protecting resources behind user authentication,
|
||||
// This will allow cookies to be sent between the two domains
|
||||
csrf: {
|
||||
[
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Relationships and/or uploads disappear after editing a document
|
||||
|
||||
It is possible that either you are setting an improper [`depth`](../getting-started/concepts#depth) in your initial request and/or your `useLivePreview` hook, or they're mismatched. Ensure that the `depth` parameter is set to the correct value, and that it matches exactly in both places. For example:
|
||||
|
||||
```tsx
|
||||
// Your initial request
|
||||
const { docs } = await payload.find({
|
||||
collection: 'pages',
|
||||
depth: 1, // Ensure this is set to the proper depth for your application
|
||||
where: {
|
||||
slug: {
|
||||
equals: 'home',
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Your hook
|
||||
const { data } = useLivePreview<PageType>({
|
||||
initialData: initialPage,
|
||||
serverURL: PAYLOAD_SERVER_URL,
|
||||
depth: 1, // Ensure this matches the depth of your initial request
|
||||
})
|
||||
```
|
||||
|
||||
@@ -8,7 +8,7 @@ keywords: live preview, preview, live, iframe, iframe preview, visual editing, d
|
||||
|
||||
**With Live Preview you can render your front-end application directly within the Admin panel. As you type, your changes take effect in real-time. No need to save a draft or publish your changes.**
|
||||
|
||||
Live Preview works by rendering an iframe on the page that loads your front-end application. The Admin panel communicates with your app through [`window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) events. These events are emitted every time a change is made to the document. Your app then listens for these events and re-renders itself with the data it receives.
|
||||
Live Preview works by rendering an iframe on the page that loads your front-end application. The Admin panel communicates with your app through [`window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) events. These events are emitted every time a change is made to the document. Your app then listens for these events and re-renders itself with the data it receives. Live Preview works in both server-side as well as client-side environments. See [Front-End](./frontend) for more details.
|
||||
|
||||
{/* IMAGE OF LIVE PREVIEW HERE */}
|
||||
|
||||
|
||||
175
docs/live-preview/server.mdx
Normal file
175
docs/live-preview/server.mdx
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
title: Server-side Live Preview
|
||||
label: Server-side
|
||||
order: 30
|
||||
desc: Learn how to implement Live Preview in your server-side front-end application.
|
||||
keywords: live preview, frontend, react, next.js, vue, nuxt.js, svelte, hook, useLivePreview
|
||||
---
|
||||
|
||||
<Banner type="info">
|
||||
Server-side Live Preview is only for front-end frameworks that support the concept of Server Components, i.e. [React Server Components](https://react.dev/reference/rsc/server-components). If your front-end application is built with a client-side framework like the [Next.js Pages Router](https://nextjs.org/docs/pages), [React Router](https://reactrouter.com), [Vue 3](https://vuejs.org), etc., see [client-side Live Preview](./client).
|
||||
</Banner>
|
||||
|
||||
Server-side Live Preview works by making a roundtrip to the server every time your document is saved, i.e. draft save, autosave, or publish. While using Live Preview, the Admin panel emits a new `window.postMessage` event which your front-end application can use to invoke this process. In Next.js, this means simply calling `router.refresh()` which will hydrate the HTML using new data straight from the [Local API](../local-api/overview).
|
||||
|
||||
<Banner type="warning">
|
||||
It is recommended that you enable [Autosave](../versions/autosave) alongside Live Preview to make the experience feel more responsive.
|
||||
</Banner>
|
||||
|
||||
If your front-end application is built with [React](#react), you can use the `RefreshRouteOnChange` function that Payload provides. In the future, all other major frameworks like Vue and Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own router refresh component](#building-your-own-router-refresh-component) for more information.
|
||||
|
||||
### React
|
||||
|
||||
If your front-end application is built with [React](https://react.dev) or [Next.js](https://nextjs.org), you can use the `RefreshRouteOnSave` component that Payload provides.
|
||||
|
||||
First, install the `@payloadcms/live-preview-react` package:
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/live-preview-react
|
||||
```
|
||||
|
||||
Then, render `RefreshRouteOnSave` anywhere in your `page.tsx`. Here's an example:
|
||||
|
||||
`page.tsx`:
|
||||
|
||||
```tsx
|
||||
import { RefreshRouteOnSave } from './RefreshRouteOnSave.tsx'
|
||||
import { getPayloadHMR } from '@payloadcms/next'
|
||||
import config from '../payload.config'
|
||||
|
||||
export default async function Page() {
|
||||
const payload = await getPayloadHMR({ config })
|
||||
|
||||
const page = await payload.find({
|
||||
collection: 'pages',
|
||||
draft: true
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<RefreshRouteOnSave />
|
||||
<h1>{page.title}</h1>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`RefreshRouteOnSave.tsx`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
export const RefreshRouteOnSave: React.FC = () => {
|
||||
const router = useRouter()
|
||||
return <PayloadLivePreview refresh={router.refresh} serverURL={process.env.PAYLOAD_SERVER_URL} />
|
||||
}
|
||||
```
|
||||
|
||||
## Building your own router refresh component
|
||||
|
||||
No matter what front-end framework you are using, you can build your own component using the same underlying tooling that Payload provides.
|
||||
|
||||
First, install the base `@payloadcms/live-preview` package:
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/live-preview
|
||||
```
|
||||
|
||||
This package provides the following functions:
|
||||
|
||||
| Path | Description |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`ready`** | Sends a `window.postMessage` event to the Admin panel to indicate that the front-end is ready to receive messages. |
|
||||
| **`isDocumentEvent`** | Checks if a `MessageEvent` originates from the Admin panel and is a document-level event, i.e. draft save, autosave, publish, etc. |
|
||||
|
||||
With these functions, you can build your own hook using your front-end framework of choice:
|
||||
|
||||
```tsx
|
||||
import { ready, isDocumentEvent } from '@payloadcms/live-preview'
|
||||
|
||||
// To build your own component:
|
||||
// 1. Listen for document-level `window.postMessage` events sent from the Admin panel
|
||||
// 2. Tell the Admin panel when it is ready to receive messages
|
||||
// 3. Refresh the route every time a new document-level event is received
|
||||
// 4. Unsubscribe from the `window.postMessage` events when it unmounts
|
||||
```
|
||||
|
||||
Here is an example of what the same `RefreshRouteOnSave` React component from above looks like under the hood:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
|
||||
import { isDocumentEvent, ready } from '@payloadcms/live-preview'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
export const RefreshRouteOnSave: React.FC<{
|
||||
apiRoute?: string
|
||||
depth?: number
|
||||
refresh: () => void
|
||||
serverURL: string
|
||||
}> = (props) => {
|
||||
const { apiRoute, depth, refresh, serverURL } = props
|
||||
const hasSentReadyMessage = useRef<boolean>(false)
|
||||
|
||||
const onMessage = useCallback(
|
||||
(event: MessageEvent) => {
|
||||
if (isDocumentEvent(event, serverURL)) {
|
||||
if (typeof refresh === 'function') {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
},
|
||||
[refresh, serverURL],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('message', onMessage)
|
||||
}
|
||||
|
||||
if (!hasSentReadyMessage.current) {
|
||||
hasSentReadyMessage.current = true
|
||||
|
||||
ready({
|
||||
serverURL,
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('message', onMessage)
|
||||
}
|
||||
}
|
||||
}, [serverURL, onMessage, depth, apiRoute])
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
{/* TODO: add example once beta has been release and an example can be created */}
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
#### Updates do not appear as fast as client-side Live Preview
|
||||
|
||||
If you are noticing that updates feel less snappy than client-side Live Preview (i.e. the `useLivePreview` hook), this is because of how the two differ in how they work—instead of emitting events against form state as you type, server-side Live Preview refreshes the route after a new document is saved. You can use autosave to mimic this effect. Try decreasing the value of `versions.autoSave.interval` to make the experience feel more responsive:
|
||||
|
||||
```ts
|
||||
// collection.ts
|
||||
{
|
||||
versions: {
|
||||
drafts: {
|
||||
autosave: {
|
||||
interval: 375,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -85,7 +85,7 @@ The following custom endpoints are automatically opened for you:
|
||||
|
||||
##### Stripe REST Proxy
|
||||
|
||||
If `rest` is true, proxies the [Stripe REST API](https://stripe.com/docs/api) behind [Payload access control](https://payloadcms.com/docs/access-control/overview) and returns the result. If you need to proxy the API server-side, use the [stripeProxy](#node) function.
|
||||
If `rest` is true, proxies the [Stripe REST API](https://stripe.com/docs/api) behind [Payload access control](https://payloadcms.com/docs/access-control/overview) and returns the result. This flag should only be used for local development, see the security note below for more information.
|
||||
|
||||
```ts
|
||||
const res = await fetch(`/api/stripe/rest`, {
|
||||
@@ -106,6 +106,8 @@ const res = await fetch(`/api/stripe/rest`, {
|
||||
})
|
||||
```
|
||||
|
||||
If you need to proxy the API server-side, use the [stripeProxy](#node) function.
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
@@ -113,6 +115,12 @@ const res = await fetch(`/api/stripe/rest`, {
|
||||
config.
|
||||
</Banner>
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Warning:</strong>
|
||||
<br />
|
||||
Opening the REST proxy endpoint in production is a potential security risk. Authenticated users will have open access to the Stripe REST API. In production, open your own endpoint and use the [stripeProxy](#node) function to proxy the Stripe API server-side.
|
||||
</Banner>
|
||||
|
||||
## Webhooks
|
||||
|
||||
[Stripe webhooks](https://stripe.com/docs/webhooks) are used to sync from Stripe to Payload. Webhooks listen for events on your Stripe account so you can trigger reactions to them. Follow the steps below to enable webhooks.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: "typescript",
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
arrowParens: "avoid",
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: "typescript",
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
arrowParens: "avoid",
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: "typescript",
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
arrowParens: "avoid",
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"semi": false
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -4,18 +4,21 @@ This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextj
|
||||
|
||||
> 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).
|
||||
|
||||
**IMPORTANT—This application runs on a different server as Payload and establishes a connection from another domain or port over HTTP.** For an integrated setup that runs on a single server and uses the [Local API](https://payloadcms.com/docs/local-api/overview#local-api), check out [how to serve Payload alongside Next.js](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). To learn more about this, check out [how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Payload
|
||||
|
||||
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/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.
|
||||
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 live preview.
|
||||
|
||||
### Next.js
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
4. `pnpm dev`, `yarn dev`, or `npm run dev` to start the server
|
||||
5. `open http://localhost:3001` to see the result
|
||||
|
||||
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload) for full details.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/live-preview-react": "latest",
|
||||
"@payloadcms/live-preview-react": "3.0.0-beta.28",
|
||||
"escape-html": "^1.0.3",
|
||||
"next": "^13.5.1",
|
||||
"payload-admin-bar": "^1.0.6",
|
||||
|
||||
2769
examples/live-preview/next-app/pnpm-lock.yaml
generated
Normal file
2769
examples/live-preview/next-app/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -4,18 +4,21 @@ This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nex
|
||||
|
||||
> 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).
|
||||
|
||||
**IMPORTANT—This application runs on a different server as Payload and establishes a connection from another domain or port over HTTP.** For an integrated setup that runs on a single server and uses the [Local API](https://payloadcms.com/docs/local-api/overview#local-api), check out [how to serve Payload alongside Next.js](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). To learn more about this, check out [how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Payload
|
||||
|
||||
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/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.
|
||||
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 live preview.
|
||||
|
||||
### Next.js
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server
|
||||
4. `pnpm dev`, `yarn dev`, or `npm run dev` to start the server
|
||||
5. `open http://localhost:3001` to see the result
|
||||
|
||||
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload) for full details.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/live-preview-react": "latest",
|
||||
"@payloadcms/live-preview-react": "3.0.0-beta.28",
|
||||
"@types/escape-html": "^1.0.2",
|
||||
"escape-html": "^1.0.3",
|
||||
"next": "^13.5.1",
|
||||
|
||||
2325
examples/live-preview/next-pages/pnpm-lock.yaml
generated
Normal file
2325
examples/live-preview/next-pages/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
DATABASE_URI=mongodb://127.0.0.1/payload-example-live-preview
|
||||
PAYLOAD_SECRET=ENTER-STRING-HERE
|
||||
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
|
||||
PAYLOAD_PUBLIC_SEED=true
|
||||
PAYLOAD_DROP_DATABASE=true
|
||||
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
||||
|
||||
12
examples/live-preview/payload/.eslintignore
Normal file
12
examples/live-preview/payload/.eslintignore
Normal file
@@ -0,0 +1,12 @@
|
||||
.tmp
|
||||
**/.git
|
||||
**/.hg
|
||||
**/.pnp.*
|
||||
**/.svn
|
||||
**/.yarn/**
|
||||
**/build
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
playwright.config.ts
|
||||
jest.config.js
|
||||
@@ -1,4 +1,15 @@
|
||||
module.exports = {
|
||||
extends: ['plugin:@next/next/core-web-vitals', '@payloadcms'],
|
||||
ignorePatterns: ['**/payload-types.ts'],
|
||||
overrides: [
|
||||
{
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
root: true,
|
||||
extends: ['@payloadcms'],
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -2,21 +2,31 @@
|
||||
|
||||
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:
|
||||
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.** If you are working on an application running on an entirely separate server, there are various fully working, separately running front-ends made explicitly for this example, including:
|
||||
|
||||
- [Next.js App Router](../next-app)
|
||||
- [Next.js Pages Router](../next-pages)
|
||||
|
||||
Follow the instructions in each respective README to get started. If you are setting up Live Preview for another front-end, please consider contributing to this repo with your own example!
|
||||
Those applications run directly alongside this one. Follow the instructions in each respective README to get started. If you are setting up authentication for another front-end, please consider contributing to this repo with your own example!
|
||||
|
||||
To learn more about this, [check out how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Clone this repo
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
|
||||
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
|
||||
1. `cp .env.example .env` to copy the example environment variables
|
||||
|
||||
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
|
||||
|
||||
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
1. `open http://localhost:3000` to access the home page
|
||||
1. `open http://localhost:3000/admin` to access the admin panel
|
||||
- Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -64,9 +74,82 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
While using Live Preview, the Admin panel emits a new `window.postMessage` event every time a change is made to the document. Your front-end application can listen for these events and re-render accordingly.
|
||||
|
||||
### React
|
||||
There are two ways to use Live Preview in your own application depending on whether your front-end framework supports server components:
|
||||
|
||||
If your front-end application is built with React or Next.js, use the [`useLivePreview`](#react) React hook that Payload provides.
|
||||
- [Server-side Live Preview (suggested)](#server)
|
||||
- [Client-side Live Preview](#client)
|
||||
|
||||
<Banner type="info">
|
||||
We suggest using server-side Live Preview if your framework supports it, it is both simpler to setup and more performant to run than the client-side alternative.
|
||||
</Banner>
|
||||
|
||||
### Server
|
||||
|
||||
> Server-side Live Preview is only for front-end frameworks that support the concept of Server Components, i.e. [React Server Components](https://react.dev/reference/rsc/server-components). If your front-end application is built with a client-side framework like the [Next.js Pages Router](https://nextjs.org/docs/pages), [React Router](https://reactrouter.com), [Vue 3](https://vuejs.org), etc., see [client-side Live Preview](#client).
|
||||
|
||||
Server-side Live Preview works by making a roundtrip to the server every time your document is saved, i.e. draft save, autosave, or publish. While using Live Preview, the Admin panel emits a new `window.postMessage` event which your front-end application can use to invoke this process. In Next.js, this means simply calling `router.refresh()` which will hydrate the HTML using new data straight from the [Local API](../local-api/overview).
|
||||
|
||||
If your server-side front-end application is built with [React](#react), you can use the `RefreshRouteOnChange` function that Payload provides. In the future, all other major frameworks like Vue and Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own router refresh component](https://payloadcms.com/docs/live-preview/server#building-your-own-router-refresh-component) for more information.
|
||||
|
||||
#### React
|
||||
|
||||
If your front-end application is built with server-side [React](https://react.dev), i.e. [Next.js App Router](https://nextjs.org/docs/app), you can use the `RefreshRouteOnSave` component that Payload provides and thread it your framework's refresh function.
|
||||
|
||||
First, install the `@payloadcms/live-preview-react` package:
|
||||
|
||||
```bash
|
||||
npm install @payloadcms/live-preview-react
|
||||
```
|
||||
|
||||
Then, render `RefreshRouteOnSave` anywhere in your `page.tsx`. Here's an example:
|
||||
|
||||
`page.tsx`:
|
||||
|
||||
```tsx
|
||||
import { RefreshRouteOnSave } from './RefreshRouteOnSave.tsx'
|
||||
import { getPayloadHMR } from '@payloadcms/next'
|
||||
import config from '../payload.config'
|
||||
|
||||
export default async function Page() {
|
||||
const payload = await getPayloadHMR({ config })
|
||||
|
||||
const page = await payload.find({
|
||||
collection: 'pages',
|
||||
draft: true,
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<RefreshRouteOnSave />
|
||||
<h1>{page.title}</h1>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
`RefreshRouteOnSave.tsx`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
export const RefreshRouteOnSave: React.FC = () => {
|
||||
const router = useRouter()
|
||||
return <PayloadLivePreview refresh={router.refresh} serverURL={process.env.PAYLOAD_SERVER_URL} />
|
||||
}
|
||||
```
|
||||
|
||||
For more details on how to setup server-side Live Preview, see the [server-side Live Preview](https://payloadcms.com/docs/live-preview/server) docs.
|
||||
|
||||
### Client
|
||||
|
||||
> If your front-end application is supports Server Components like the [Next.js App Router](https://nextjs.org/docs/app), etc., we suggest setting up [server-side Live Preview](#server).
|
||||
|
||||
#### React
|
||||
|
||||
If your front-end application is built with client-side React such as Next.js Pages Router, React Router, etc., use the [`useLivePreview`](#react) React hook that Payload provides.
|
||||
|
||||
First, install the `@payloadcms/live-preview-react` package:
|
||||
|
||||
@@ -77,8 +160,8 @@ npm install @payloadcms/live-preview-react
|
||||
Then, use the `useLivePreview` hook in your React component:
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
import { useLivePreview } from '@payloadcms/live-preview-react';
|
||||
'use client'
|
||||
import { useLivePreview } from '@payloadcms/live-preview-react'
|
||||
import { Page as PageType } from '@/payload-types'
|
||||
|
||||
// Fetch the page in a server component, pass it to the client component, then thread it through the hook
|
||||
@@ -95,13 +178,11 @@ export const PageClient: React.FC<{
|
||||
depth: 2, // Ensure that the depth matches the request for `initialPage`
|
||||
})
|
||||
|
||||
return (
|
||||
<h1>{data.title}</h1>
|
||||
)
|
||||
return <h1>{data.title}</h1>
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
#### JavaScript
|
||||
|
||||
In the future, all other major frameworks like Vue, Svelte, etc will be officially supported. If you are using any of these framework today, you can still integrate with Live Preview yourself using the tooling that Payload provides.
|
||||
|
||||
@@ -114,7 +195,7 @@ npm install @payloadcms/live-preview
|
||||
Then, build your own hook:
|
||||
|
||||
```tsx
|
||||
import { subscribe, unsubscribe } from '@payloadcms/live-preview';
|
||||
import { subscribe, unsubscribe } from '@payloadcms/live-preview'
|
||||
|
||||
// Build your own hook to subscribe to the live preview events
|
||||
// This function will handle everything for you like
|
||||
@@ -125,6 +206,8 @@ import { subscribe, unsubscribe } from '@payloadcms/live-preview';
|
||||
|
||||
See [building your own Live Preview hook](https://payloadcms.com/docs/live-preview/frontend#building-your-own-hook) for more details.
|
||||
|
||||
For more details on how to setup client-side Live Preview, see the [client-side Live Preview](https://payloadcms.com/docs/live-preview/client) docs.
|
||||
|
||||
## Development
|
||||
|
||||
To spin up this example locally, follow the [Quick Start](#quick-start).
|
||||
|
||||
5
examples/live-preview/payload/next-env.d.ts
vendored
Normal file
5
examples/live-preview/payload/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
8
examples/live-preview/payload/next.config.mjs
Normal file
8
examples/live-preview/payload/next.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Your Next.js config here
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig)
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nodemon.json",
|
||||
"ext": "ts",
|
||||
"exec": "ts-node src/server.ts -- -I",
|
||||
"stdin": false
|
||||
}
|
||||
@@ -1,49 +1,44 @@
|
||||
{
|
||||
"name": "payload-example-live-preview",
|
||||
"description": "Payload Live Preview example.",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/server.js",
|
||||
"description": "Payload Live Preview example.",
|
||||
"license": "MIT",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
|
||||
"seed": "rm -rf media && cross-env PAYLOAD_PUBLIC_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts ts-node src/server.ts",
|
||||
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
|
||||
"build:server": "tsc",
|
||||
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
|
||||
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"build": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts NODE_OPTIONS=--no-deprecation next build",
|
||||
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts && pnpm seed && cross-env NODE_OPTIONS=--no-deprecation next dev",
|
||||
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src"
|
||||
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
|
||||
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
|
||||
"lint:fix": "eslint --fix --ext .ts,.tsx src",
|
||||
"payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload",
|
||||
"seed": "npm run payload migrate:fresh",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/bundler-webpack": "latest",
|
||||
"@payloadcms/db-mongodb": "latest",
|
||||
"@payloadcms/richtext-slate": "latest",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"payload": "latest"
|
||||
"@payloadcms/db-mongodb": "3.0.0-beta.28",
|
||||
"@payloadcms/live-preview-react": "3.0.0-beta.28",
|
||||
"@payloadcms/next": "3.0.0-beta.28",
|
||||
"@payloadcms/richtext-slate": "3.0.0-beta.28",
|
||||
"@payloadcms/ui": "3.0.0-beta.28",
|
||||
"cross-env": "^7.0.3",
|
||||
"next": "14.3.0-canary.7",
|
||||
"payload": "3.0.0-beta.28",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "^0.0.2",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/node": "18.11.3",
|
||||
"@types/react": "18.0.21",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-filenames": "^1.3.2",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"nodemon": "^2.0.6",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.8.4"
|
||||
"@next/eslint-plugin-next": "^13.1.6",
|
||||
"@payloadcms/eslint-config": "^1.1.1",
|
||||
"@swc/core": "^1.4.14",
|
||||
"@swc/types": "^0.1.6",
|
||||
"@types/node": "^20.11.25",
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.57.0",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "5.4.4"
|
||||
}
|
||||
}
|
||||
|
||||
6441
examples/live-preview/payload/pnpm-lock.yaml
generated
Normal file
6441
examples/live-preview/payload/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
export const RefreshRouteOnSave: React.FC = () => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<PayloadLivePreview
|
||||
refresh={() => router.refresh()}
|
||||
serverURL={process.env.NEXT_PUBLIC_SERVER_URL || ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.page {
|
||||
margin-top: calc(var(--base) * 2);
|
||||
}
|
||||
70
examples/live-preview/payload/src/app/(app)/[slug]/page.tsx
Normal file
70
examples/live-preview/payload/src/app/(app)/[slug]/page.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable no-restricted-exports */
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import { notFound } from 'next/navigation'
|
||||
import React from 'react'
|
||||
import { Fragment } from 'react'
|
||||
|
||||
import type { Page as PageType } from '../../../payload-types'
|
||||
|
||||
import config from '../../../payload.config'
|
||||
import { Gutter } from '../_components/Gutter'
|
||||
import RichText from '../_components/RichText'
|
||||
import { RefreshRouteOnSave } from './RefreshRouteOnSave'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
interface PageParams {
|
||||
params: { slug: string }
|
||||
}
|
||||
|
||||
export default async function Page({ params: { slug = 'home' } }: PageParams) {
|
||||
const payload = await getPayloadHMR({ config })
|
||||
|
||||
const pageRes = await payload.find({
|
||||
collection: 'pages',
|
||||
draft: true,
|
||||
limit: 1,
|
||||
where: {
|
||||
slug: {
|
||||
equals: slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const data = pageRes?.docs?.[0] as PageType | null
|
||||
|
||||
if (data === null) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<RefreshRouteOnSave />
|
||||
<main className={classes.page}>
|
||||
<Gutter>
|
||||
<RichText content={data?.richText} />
|
||||
</Gutter>
|
||||
</main>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const payload = await getPayloadHMR({ config })
|
||||
|
||||
const pagesRes = await payload.find({
|
||||
collection: 'pages',
|
||||
depth: 0,
|
||||
draft: true,
|
||||
limit: 100,
|
||||
})
|
||||
|
||||
const pages = pagesRes?.docs
|
||||
|
||||
return pages.map(({ slug }) =>
|
||||
slug !== 'home'
|
||||
? {
|
||||
slug,
|
||||
}
|
||||
: {},
|
||||
) // eslint-disable-line function-paren-newline
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
.button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
svg {
|
||||
margin-right: calc(var(--base) / 2);
|
||||
width: var(--base);
|
||||
height: var(--base);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.primary--white {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary--black {
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.secondary--white {
|
||||
background-color: white;
|
||||
box-shadow: inset 0 0 0 1px black;
|
||||
}
|
||||
|
||||
.secondary--black {
|
||||
background-color: black;
|
||||
box-shadow: inset 0 0 0 1px white;
|
||||
}
|
||||
|
||||
.appearance--default {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { ElementType } from 'react';
|
||||
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type Props = {
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
el?: 'a' | 'button' | 'link'
|
||||
href?: string
|
||||
label?: string
|
||||
newTab?: boolean
|
||||
onClick?: () => void
|
||||
type?: 'button' | 'submit'
|
||||
}
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
type = 'button',
|
||||
appearance,
|
||||
className: classNameFromProps,
|
||||
disabled,
|
||||
el: elFromProps = 'link',
|
||||
href,
|
||||
label,
|
||||
newTab,
|
||||
onClick,
|
||||
}) => {
|
||||
let el = elFromProps
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
const className = [
|
||||
classes.button,
|
||||
classNameFromProps,
|
||||
classes[`appearance--${appearance}`],
|
||||
classes.button,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const content = (
|
||||
<div className={classes.content}>
|
||||
{/* <Chevron /> */}
|
||||
<span className={classes.label}>{label}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (onClick || type === 'submit') el = 'button'
|
||||
|
||||
if (el === 'link') {
|
||||
return (
|
||||
<Link className={className} href={href || ''} {...newTabProps} onClick={onClick}>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Element: ElementType = el
|
||||
|
||||
return (
|
||||
<Element
|
||||
className={className}
|
||||
href={href}
|
||||
type={type}
|
||||
{...newTabProps}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{content}
|
||||
</Element>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '../../../payload-types'
|
||||
|
||||
import { Button } from '../Button'
|
||||
|
||||
export type CMSLinkType = {
|
||||
appearance?: 'default' | 'primary' | 'secondary'
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
label?: string
|
||||
newTab?: boolean
|
||||
reference?: {
|
||||
relationTo: 'pages'
|
||||
value: Page | string
|
||||
}
|
||||
type?: 'custom' | 'reference'
|
||||
url?: string
|
||||
}
|
||||
|
||||
export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
type,
|
||||
appearance,
|
||||
children,
|
||||
className,
|
||||
label,
|
||||
newTab,
|
||||
reference,
|
||||
url,
|
||||
}) => {
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `/${reference.value.slug === 'home' ? '' : reference.value.slug}`
|
||||
: url
|
||||
|
||||
if (!appearance) {
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
|
||||
if (type === 'custom') {
|
||||
return (
|
||||
<a href={url} {...newTabProps} className={className}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} {...newTabProps} className={className} prefetch={false}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const buttonProps = {
|
||||
appearance,
|
||||
href,
|
||||
label,
|
||||
newTab,
|
||||
}
|
||||
|
||||
return <Button className={className} {...buttonProps} el="link" />
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
.gutter {
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.gutterLeft {
|
||||
padding-left: var(--gutter-h);
|
||||
}
|
||||
|
||||
.gutterRight {
|
||||
padding-right: var(--gutter-h);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Ref } from 'react';
|
||||
|
||||
import React, { forwardRef } from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
left?: boolean
|
||||
ref?: Ref<HTMLDivElement>
|
||||
right?: boolean
|
||||
}
|
||||
|
||||
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const { children, className, left = true, right = true } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
classes.gutter,
|
||||
left && classes.gutterLeft,
|
||||
right && classes.gutterRight,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
Gutter.displayName = 'Gutter'
|
||||
@@ -0,0 +1,32 @@
|
||||
.header {
|
||||
padding: var(--base) 0;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: calc(var(--base) / 2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--base);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
gap: 0 calc(var(--base) / 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../../../payload.config'
|
||||
import { CMSLink } from '../CMSLink'
|
||||
import { Gutter } from '../Gutter'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const Header = async () => {
|
||||
const payload = await getPayloadHMR({ config })
|
||||
|
||||
const mainMenu = await payload.findGlobal({
|
||||
slug: 'main-menu',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
const { navItems } = mainMenu
|
||||
|
||||
const hasNavItems = navItems && Array.isArray(navItems) && navItems.length > 0
|
||||
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link className={classes.logo} href="/">
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcSet="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-light.svg"
|
||||
/>
|
||||
<Image
|
||||
alt="Payload Logo"
|
||||
height={30}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-dark.svg"
|
||||
width={150}
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
{hasNavItems && (
|
||||
<nav className={classes.nav}>
|
||||
{navItems.map(({ link }, i) => {
|
||||
return <CMSLink key={i} {...link} />
|
||||
})}
|
||||
</nav>
|
||||
)}
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.richText {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
import serialize from './serialize'
|
||||
|
||||
const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => {
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
||||
{serialize(content)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RichText
|
||||
@@ -0,0 +1,92 @@
|
||||
import escapeHTML from 'escape-html'
|
||||
import React, { Fragment } from 'react'
|
||||
import { Text } from 'slate'
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
type Children = Leaf[]
|
||||
|
||||
type Leaf = {
|
||||
[key: string]: unknown
|
||||
children: Children
|
||||
type: string
|
||||
url?: string
|
||||
value?: {
|
||||
alt: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
const serialize = (children: Children): React.ReactNode[] =>
|
||||
children.map((node, i) => {
|
||||
if (Text.isText(node)) {
|
||||
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
|
||||
|
||||
if (node.bold) {
|
||||
text = <strong key={i}>{text}</strong>
|
||||
}
|
||||
|
||||
if (node.code) {
|
||||
text = <code key={i}>{text}</code>
|
||||
}
|
||||
|
||||
if (node.italic) {
|
||||
text = <em key={i}>{text}</em>
|
||||
}
|
||||
|
||||
if (node.underline) {
|
||||
text = (
|
||||
<span key={i} style={{ textDecoration: 'underline' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (node.strikethrough) {
|
||||
text = (
|
||||
<span key={i} style={{ textDecoration: 'line-through' }}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return <Fragment key={i}>{text}</Fragment>
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'h1':
|
||||
return <h1 key={i}>{serialize(node.children)}</h1>
|
||||
case 'h2':
|
||||
return <h2 key={i}>{serialize(node.children)}</h2>
|
||||
case 'h3':
|
||||
return <h3 key={i}>{serialize(node.children)}</h3>
|
||||
case 'h4':
|
||||
return <h4 key={i}>{serialize(node.children)}</h4>
|
||||
case 'h5':
|
||||
return <h5 key={i}>{serialize(node.children)}</h5>
|
||||
case 'h6':
|
||||
return <h6 key={i}>{serialize(node.children)}</h6>
|
||||
case 'blockquote':
|
||||
return <blockquote key={i}>{serialize(node.children)}</blockquote>
|
||||
case 'ul':
|
||||
return <ul key={i}>{serialize(node.children)}</ul>
|
||||
case 'ol':
|
||||
return <ol key={i}>{serialize(node.children)}</ol>
|
||||
case 'li':
|
||||
return <li key={i}>{serialize(node.children)}</li>
|
||||
case 'link':
|
||||
return (
|
||||
<a href={escapeHTML(node.url)} key={i}>
|
||||
{serialize(node.children)}
|
||||
</a>
|
||||
)
|
||||
|
||||
default:
|
||||
return <p key={i}>{serialize(node.children)}</p>
|
||||
}
|
||||
})
|
||||
|
||||
export default serialize
|
||||
107
examples/live-preview/payload/src/app/(app)/app.scss
Normal file
107
examples/live-preview/payload/src/app/(app)/app.scss
Normal file
@@ -0,0 +1,107 @@
|
||||
$breakpoint: 1000px;
|
||||
|
||||
:root {
|
||||
--max-width: 1600px;
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-rgb: 255, 255, 255;
|
||||
--block-spacing: 2rem;
|
||||
--gutter-h: 4rem;
|
||||
--base: 1rem;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
--block-spacing: 1rem;
|
||||
--gutter-h: 2rem;
|
||||
--base: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-rgb: 7, 7, 7;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: rgb(var(--background-rgb));
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2.5rem 0;
|
||||
|
||||
@media (max-width: $breakpoint) {
|
||||
font-size: 3rem;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 3.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2.5rem 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
23
examples/live-preview/payload/src/app/(app)/layout.tsx
Normal file
23
examples/live-preview/payload/src/app/(app)/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable no-restricted-exports */
|
||||
import React from 'react'
|
||||
|
||||
import { Header } from './_components/Header'
|
||||
import './app.scss'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Generated by create next app',
|
||||
title: 'Create Next App',
|
||||
}
|
||||
|
||||
export default function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Header />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
3
examples/live-preview/payload/src/app/(app)/page.tsx
Normal file
3
examples/live-preview/payload/src/app/(app)/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import Page from './[slug]/page'
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,22 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
@@ -0,0 +1,22 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,10 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
16
examples/live-preview/payload/src/app/(payload)/layout.tsx
Normal file
16
examples/live-preview/payload/src/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import configPromise from '@payload-config'
|
||||
import '@payloadcms/next/css'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import React from 'react'
|
||||
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
|
||||
|
||||
export default Layout
|
||||
@@ -8,7 +8,7 @@ const format = (val: string): string =>
|
||||
|
||||
const formatSlug =
|
||||
(fallback: string): FieldHook =>
|
||||
({ operation, value, originalDoc, data }) => {
|
||||
({ data, operation, originalDoc, value }) => {
|
||||
if (typeof value === 'string') {
|
||||
return format(value)
|
||||
}
|
||||
|
||||
@@ -6,19 +6,21 @@ import formatSlug from './hooks/formatSlug'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
access: {
|
||||
create: loggedIn,
|
||||
delete: loggedIn,
|
||||
read: () => true,
|
||||
update: loggedIn,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', 'updatedAt'],
|
||||
livePreview: {
|
||||
url: ({ data }) =>
|
||||
`${process.env.PAYLOAD_PUBLIC_SITE_URL}${data.slug !== 'home' ? `/${data.slug}` : ''}`,
|
||||
url: ({ data }) => {
|
||||
const isHomePage = data.slug === 'home'
|
||||
return `${process.env.PAYLOAD_PUBLIC_SITE_URL}${!isHomePage ? `/${data.slug}` : ''}`
|
||||
},
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
create: loggedIn,
|
||||
update: loggedIn,
|
||||
delete: loggedIn,
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
@@ -28,16 +30,23 @@ export const Pages: CollectionConfig = {
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
label: 'Slug',
|
||||
type: 'text',
|
||||
index: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [formatSlug('title')],
|
||||
},
|
||||
index: true,
|
||||
label: 'Slug',
|
||||
},
|
||||
richText(),
|
||||
],
|
||||
versions: {
|
||||
drafts: {
|
||||
autosave: {
|
||||
interval: 375,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { CollectionConfig } from 'payload/types'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
fields: [],
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import type { Field } from 'payload/types'
|
||||
import deepMerge from '../utilities/deepMerge'
|
||||
|
||||
export const appearanceOptions = {
|
||||
default: {
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
primary: {
|
||||
label: 'Primary Button',
|
||||
value: 'primary',
|
||||
@@ -11,13 +15,9 @@ export const appearanceOptions = {
|
||||
label: 'Secondary Button',
|
||||
value: 'secondary',
|
||||
},
|
||||
default: {
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
}
|
||||
|
||||
export type LinkAppearances = 'primary' | 'secondary' | 'default'
|
||||
export type LinkAppearances = 'default' | 'primary' | 'secondary'
|
||||
|
||||
type LinkType = (options?: {
|
||||
appearances?: LinkAppearances[] | false
|
||||
@@ -39,6 +39,11 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
{
|
||||
name: 'type',
|
||||
type: 'radio',
|
||||
admin: {
|
||||
layout: 'horizontal',
|
||||
width: '50%',
|
||||
},
|
||||
defaultValue: 'reference',
|
||||
options: [
|
||||
{
|
||||
label: 'Internal link',
|
||||
@@ -49,22 +54,17 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
defaultValue: 'reference',
|
||||
admin: {
|
||||
layout: 'horizontal',
|
||||
width: '50%',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'newTab',
|
||||
label: 'Open in new tab',
|
||||
type: 'checkbox',
|
||||
admin: {
|
||||
width: '50%',
|
||||
style: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
width: '50%',
|
||||
},
|
||||
label: 'Open in new tab',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -74,23 +74,23 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
const linkTypes: Field[] = [
|
||||
{
|
||||
name: 'reference',
|
||||
label: 'Document to link to',
|
||||
type: 'relationship',
|
||||
relationTo: ['pages'],
|
||||
required: true,
|
||||
maxDepth: 1,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'reference',
|
||||
},
|
||||
label: 'Document to link to',
|
||||
maxDepth: 1,
|
||||
relationTo: ['pages'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
label: 'Custom URL',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
condition: (_, siblingData) => siblingData?.type === 'custom',
|
||||
},
|
||||
label: 'Custom URL',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -104,12 +104,12 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
...linkTypes,
|
||||
{
|
||||
name: 'label',
|
||||
label: 'Label',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
width: '50%',
|
||||
},
|
||||
label: 'Label',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -131,11 +131,11 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
|
||||
linkResult.fields.push({
|
||||
name: 'appearance',
|
||||
type: 'select',
|
||||
defaultValue: 'default',
|
||||
options: appearanceOptionsToUse,
|
||||
admin: {
|
||||
description: 'Choose how the link should be rendered.',
|
||||
},
|
||||
defaultValue: 'default',
|
||||
options: appearanceOptionsToUse,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
import type { RichTextElement, RichTextLeaf } from '@payloadcms/richtext-slate/dist/types'
|
||||
import type { RichTextField } from 'payload/types'
|
||||
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
|
||||
import deepMerge from '../../utilities/deepMerge'
|
||||
import link from '../link'
|
||||
import elements from './elements'
|
||||
@@ -26,27 +27,28 @@ const richText: RichText = (
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
required: true,
|
||||
editor: slateEditor({
|
||||
admin: {
|
||||
elements: [...elements, ...(additions.elements || [])],
|
||||
leaves: [...leaves, ...(additions.leaves || [])],
|
||||
upload: {
|
||||
collections: {
|
||||
media: {
|
||||
fields: [
|
||||
{
|
||||
type: 'richText',
|
||||
name: 'caption',
|
||||
label: 'Caption',
|
||||
type: 'richText',
|
||||
editor: slateEditor({
|
||||
admin: {
|
||||
elements: [...elements],
|
||||
leaves: [...leaves],
|
||||
},
|
||||
}),
|
||||
label: 'Caption',
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
name: 'alignment',
|
||||
type: 'radio',
|
||||
label: 'Alignment',
|
||||
options: [
|
||||
{
|
||||
@@ -81,10 +83,9 @@ const richText: RichText = (
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: [...elements, ...(additions.elements || [])],
|
||||
leaves: [...leaves, ...(additions.leaves || [])],
|
||||
},
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
overrides,
|
||||
)
|
||||
|
||||
@@ -11,12 +11,12 @@ export const MainMenu: GlobalConfig = {
|
||||
{
|
||||
name: 'navItems',
|
||||
type: 'array',
|
||||
maxRows: 6,
|
||||
fields: [
|
||||
link({
|
||||
appearances: false,
|
||||
}),
|
||||
],
|
||||
maxRows: 6,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
130
examples/live-preview/payload/src/migrations/seed.ts
Normal file
130
examples/live-preview/payload/src/migrations/seed.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
|
||||
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const home: Partial<Page> = {
|
||||
slug: 'home',
|
||||
richText: [
|
||||
{
|
||||
type: 'h1',
|
||||
children: [
|
||||
{
|
||||
text: 'Payload Live Preview Example',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{ text: 'This is a ' },
|
||||
{
|
||||
type: 'link',
|
||||
children: [{ text: 'Next.js' }],
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: 'https://nextjs.org',
|
||||
},
|
||||
{ text: " app made explicitly for Payload's " },
|
||||
{
|
||||
type: 'link',
|
||||
children: [{ text: 'Live Preview Example' }],
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: 'https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload',
|
||||
},
|
||||
{ text: '. With ' },
|
||||
{
|
||||
type: 'link',
|
||||
children: [{ text: 'Live Preview' }],
|
||||
newTab: true,
|
||||
url: 'https://payloadcms.com/docs/live-preview',
|
||||
},
|
||||
{
|
||||
text: ' you can edit this page in the admin panel and see the changes reflected here in real time.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
title: 'Home',
|
||||
}
|
||||
|
||||
export const examplePage: Partial<Page> = {
|
||||
slug: 'example-page',
|
||||
richText: [
|
||||
{
|
||||
type: 'h1',
|
||||
children: [
|
||||
{
|
||||
text: 'Example Page',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
text: 'This is an example page. You can edit this page in the Admin panel and see the changes reflected here in real time.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
title: 'Example Page',
|
||||
}
|
||||
|
||||
export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'demo@payloadcms.com',
|
||||
password: 'demo',
|
||||
},
|
||||
})
|
||||
|
||||
const { id: examplePageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
data: examplePage as any, // eslint-disable-line
|
||||
})
|
||||
|
||||
const homepageJSON = JSON.parse(JSON.stringify(home))
|
||||
|
||||
const { id: homePageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
data: homepageJSON,
|
||||
})
|
||||
|
||||
await payload.updateGlobal({
|
||||
slug: 'main-menu',
|
||||
data: {
|
||||
navItems: [
|
||||
{
|
||||
link: {
|
||||
type: 'reference',
|
||||
label: 'Home',
|
||||
reference: {
|
||||
relationTo: 'pages',
|
||||
value: homePageID,
|
||||
},
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
link: {
|
||||
type: 'reference',
|
||||
label: 'Example Page',
|
||||
reference: {
|
||||
relationTo: 'pages',
|
||||
value: examplePageID,
|
||||
},
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
link: {
|
||||
type: 'custom',
|
||||
label: 'Dashboard',
|
||||
reference: undefined,
|
||||
url: 'http://localhost:3000/admin',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
@@ -9,31 +9,34 @@ import { Users } from './collections/Users'
|
||||
import BeforeLogin from './components/BeforeLogin'
|
||||
import { MainMenu } from './globals/MainMenu'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Pages, Users],
|
||||
admin: {
|
||||
bundler: webpackBundler(), // bundler-config
|
||||
components: {
|
||||
beforeLogin: [BeforeLogin],
|
||||
},
|
||||
livePreview: {
|
||||
breakpoints: [
|
||||
{
|
||||
label: 'Mobile',
|
||||
name: 'mobile',
|
||||
width: 375,
|
||||
height: 667,
|
||||
label: 'Mobile',
|
||||
width: 375,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
|
||||
globals: [MainMenu],
|
||||
editor: slateEditor({}),
|
||||
collections: [Pages, Users],
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI,
|
||||
url: process.env.DATABASE_URI || '',
|
||||
}),
|
||||
editor: slateEditor({}),
|
||||
globals: [MainMenu],
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
|
||||
typescript: {
|
||||
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const home: Partial<Page> = {
|
||||
title: 'Home',
|
||||
slug: 'home',
|
||||
richText: [
|
||||
{
|
||||
type: 'h1',
|
||||
children: [
|
||||
{
|
||||
text: 'Payload Live Preview Example',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{ text: 'This is a ' },
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
url: 'https://nextjs.org',
|
||||
newTab: true,
|
||||
children: [{ text: 'Next.js' }],
|
||||
},
|
||||
{ text: " app made explicitly for Payload's " },
|
||||
{
|
||||
type: 'link',
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: 'https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload',
|
||||
children: [{ text: 'Live Preview Example' }],
|
||||
},
|
||||
{ text: '. With ' },
|
||||
{
|
||||
type: 'link',
|
||||
newTab: true,
|
||||
url: 'https://payloadcms.com/docs/live-preview',
|
||||
children: [{ text: 'Live Preview' }],
|
||||
},
|
||||
{
|
||||
text: ' you can edit this page in the admin panel and see the changes reflected here in real time.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { home } from './home'
|
||||
import { examplePage } from './page'
|
||||
|
||||
export const seed = async (payload: Payload): Promise<void> => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'demo@payloadcms.com',
|
||||
password: 'demo',
|
||||
},
|
||||
})
|
||||
|
||||
const { id: examplePageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
data: examplePage as any, // eslint-disable-line
|
||||
})
|
||||
|
||||
const homepageJSON = JSON.parse(JSON.stringify(home))
|
||||
|
||||
const { id: homePageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
data: homepageJSON,
|
||||
})
|
||||
|
||||
await payload.updateGlobal({
|
||||
slug: 'main-menu',
|
||||
data: {
|
||||
navItems: [
|
||||
{
|
||||
link: {
|
||||
type: 'reference',
|
||||
reference: {
|
||||
relationTo: 'pages',
|
||||
value: homePageID,
|
||||
},
|
||||
label: 'Home',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
link: {
|
||||
type: 'reference',
|
||||
reference: {
|
||||
relationTo: 'pages',
|
||||
value: examplePageID,
|
||||
},
|
||||
label: 'Example Page',
|
||||
url: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
link: {
|
||||
type: 'custom',
|
||||
reference: null,
|
||||
label: 'Dashboard',
|
||||
url: 'http://localhost:3000/admin',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { Page } from '../payload-types'
|
||||
|
||||
export const examplePage: Partial<Page> = {
|
||||
title: 'Example Page',
|
||||
slug: 'example-page',
|
||||
richText: [
|
||||
{
|
||||
type: 'h1',
|
||||
children: [
|
||||
{
|
||||
text: 'Example Page',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
text: 'This is an example page. You can edit this page in the Admin panel and see the changes reflected here in real time.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
|
||||
dotenv.config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
})
|
||||
|
||||
import express from 'express'
|
||||
import payload from 'payload'
|
||||
|
||||
import { seed } from './seed'
|
||||
|
||||
const app = express()
|
||||
|
||||
app.get('/', (_, res) => {
|
||||
res.redirect('/admin')
|
||||
})
|
||||
|
||||
const start = async (): Promise<void> => {
|
||||
await payload.init({
|
||||
secret: process.env.PAYLOAD_SECRET,
|
||||
express: app,
|
||||
onInit: () => {
|
||||
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
|
||||
},
|
||||
})
|
||||
|
||||
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
|
||||
payload.logger.info('---- SEEDING DATABASE ----')
|
||||
await seed(payload)
|
||||
}
|
||||
|
||||
app.listen(3000)
|
||||
}
|
||||
|
||||
start()
|
||||
@@ -1,33 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"node_modules/*": ["./node_modules/*"]
|
||||
},
|
||||
"@/*": ["./src/*"],
|
||||
"@payload-config": ["src/payload.config.ts"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
],
|
||||
"ts-node": {
|
||||
"transpileOnly": true
|
||||
}
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: "typescript",
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
arrowParens: "avoid",
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
parser: 'typescript',
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"semi": false
|
||||
}
|
||||
@@ -1,42 +1,25 @@
|
||||
# Payload Blank Template
|
||||
# Payload with TailwindCSS and shadcn/ui
|
||||
|
||||
A blank template for [Payload](https://github.com/payloadcms/payload) to help you get up and running quickly. This repo may have been created by running `npx create-payload-app@latest` and selecting the "blank" template or by cloning this template on [Payload Cloud](https://payloadcms.com/new/clone/blank).
|
||||
This is an example repo of Payload being setup with TailwindCSS and shadcn/ui components ready to be used in the admin panel itself.
|
||||
|
||||
See the official [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples) for details on how to use Payload in a variety of different ways.
|
||||
Checkout our [tutorial](https://payloadcms.com/blog/how-to-setup-tailwindcss-and-shadcn-ui-in-payload) on our website
|
||||
|
||||
## Development
|
||||
## Quick Start
|
||||
|
||||
To spin up the project locally, follow these steps:
|
||||
To spin up this example locally, follow these steps:
|
||||
|
||||
1. First clone the repo
|
||||
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
|
||||
1. Next `yarn && yarn dev` (or `docker-compose up`, see [Docker](#docker))
|
||||
1. Now `open http://localhost:3000/admin` to access the admin panel
|
||||
1. Create your first admin user using the form on the page
|
||||
1. Clone this repo
|
||||
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, or `npm install`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app.
|
||||
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
|
||||
|
||||
### Docker
|
||||
1. `cp .env.example .env` to copy the example environment variables
|
||||
|
||||
Alternatively, you can use [Docker](https://www.docker.com) to spin up this project locally. To do so, follow these steps:
|
||||
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
|
||||
|
||||
1. Follow [steps 1 and 2 from above](#development), the docker-compose file will automatically use the `.env` file in your project root
|
||||
1. Next run `docker-compose up`
|
||||
1. Follow [steps 4 and 5 from above](#development) to login and create your first admin user
|
||||
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
1. `open http://localhost:3000` to access the home page
|
||||
1. `open http://localhost:3000/admin` to access the admin panel
|
||||
|
||||
That's it! The Docker instance will help you get up and running quickly while also standardizing the development environment across your teams.
|
||||
|
||||
## Production
|
||||
|
||||
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
|
||||
|
||||
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
|
||||
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
|
||||
|
||||
### Deployment
|
||||
|
||||
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/production/deployment) for full details.
|
||||
|
||||
## Questions
|
||||
|
||||
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/payload) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -9,7 +9,7 @@ const withBundleAnalyzer = bundleAnalyzer({
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default withBundleAnalyzer(
|
||||
withPayload({
|
||||
reactStrictMode: false,
|
||||
// reactStrictMode: false,
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.25",
|
||||
"version": "3.0.0-beta.30",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -25,6 +25,7 @@
|
||||
"build:plugin-form-builder": "turbo build --filter plugin-form-builder",
|
||||
"build:plugin-nested-docs": "turbo build --filter plugin-nested-docs",
|
||||
"build:plugin-redirects": "turbo build --filter plugin-redirects",
|
||||
"build:plugin-relationship-object-ids": "turbo build --filter plugin-relationship-object-ids",
|
||||
"build:plugin-search": "turbo build --filter plugin-search",
|
||||
"build:plugin-sentry": "turbo build --filter plugin-sentry",
|
||||
"build:plugin-seo": "turbo build --filter plugin-seo",
|
||||
@@ -32,6 +33,11 @@
|
||||
"build:plugins": "turbo build --filter \"@payloadcms/plugin-*\"",
|
||||
"build:richtext-lexical": "turbo build --filter richtext-lexical",
|
||||
"build:richtext-slate": "turbo build --filter richtext-slate",
|
||||
"build:storage-azure": "turbo build --filter storage-azure",
|
||||
"build:storage-gcs": "turbo build --filter storage-gcs",
|
||||
"build:storage-s3": "turbo build --filter storage-s3",
|
||||
"build:storage-uploadthing": "turbo build --filter storage-uploadthing",
|
||||
"build:storage-vercel-blob": "turbo build --filter storage-vercel-blob",
|
||||
"build:tests": "pnpm --filter payload-test-suite run typecheck",
|
||||
"build:translations": "turbo build --filter translations",
|
||||
"build:ui": "turbo build --filter ui",
|
||||
@@ -127,7 +133,7 @@
|
||||
"lint-staged": "^14.0.1",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^9.0",
|
||||
"next": "^14.3.0-canary.7",
|
||||
"next": "^14.3.0-canary.37",
|
||||
"node-mocks-http": "^1.14.1",
|
||||
"nodemon": "3.0.3",
|
||||
"open": "^10.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.25",
|
||||
"version": "3.0.0-beta.30",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.25",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.25",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.25",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.25",
|
||||
"version": "3.0.0-beta.30",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Disallow imports from an exports directory',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create: function (context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value
|
||||
|
||||
// Match imports starting with any number of "../" followed by "exports/"
|
||||
const regex = /^(\.?\.\/)*exports\//
|
||||
|
||||
if (regex.test(importPath)) {
|
||||
context.report({
|
||||
node: node.source,
|
||||
message:
|
||||
'Import from relative "exports/" is not allowed. Import directly to the source instead.',
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
'no-jsx-import-statements': require('./customRules/no-jsx-import-statements'),
|
||||
'no-non-retryable-assertions': require('./customRules/no-non-retryable-assertions'),
|
||||
'no-relative-monorepo-imports': require('./customRules/no-relative-monorepo-imports'),
|
||||
'no-imports-from-exports-dir': require('./customRules/no-imports-from-exports-dir'),
|
||||
'no-flaky-assertions': require('./customRules/no-flaky-assertions'),
|
||||
'no-wait-function': {
|
||||
create: function (context) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user