Compare commits
4 Commits
alpha-post
...
2_0/docs/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a326ed817c | ||
|
|
172684345a | ||
|
|
5abb0ca8e6 | ||
|
|
9478cfecbc |
@@ -23,7 +23,3 @@ indent_size = 2
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -8,6 +8,3 @@
|
||||
**/dist/**
|
||||
**/node_modules
|
||||
**/temp
|
||||
playwright.config.ts
|
||||
jest.config.js
|
||||
test/live-preview/next-app
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ['@payloadcms'],
|
||||
ignorePatterns: ['README.md', 'packages/**/*.spec.ts'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['packages/**'],
|
||||
plugins: ['payload'],
|
||||
rules: {
|
||||
'payload/no-jsx-import-statements': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['scripts/**'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'no-console': 'off',
|
||||
'perfectionist/sort-object-types': 'off',
|
||||
'perfectionist/sort-objects': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
|
||||
@@ -52,13 +34,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: __dirname,
|
||||
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
|
||||
EXPERIMENTAL_useProjectService: true,
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
root: true,
|
||||
}
|
||||
|
||||
@@ -10,12 +10,3 @@ cdaa0acd61d3001407609915bd573b78565d5571
|
||||
|
||||
# prettier write again
|
||||
dfac7395fed95fc5d8ebca21b786ce70821942bb
|
||||
|
||||
# lint and format plugin-cloud
|
||||
fb7d1be2f3325d076b7c967b1730afcef37922c2
|
||||
|
||||
# lint and format create-payload-app
|
||||
5fd3d430001efe86515262ded5e26f00c1451181
|
||||
|
||||
# 3.0 prettier & lint everywhere
|
||||
6789e61488a1d3de56f472ac3214faf344030005
|
||||
|
||||
41
.github/CODEOWNERS
vendored
41
.github/CODEOWNERS
vendored
@@ -1,41 +0,0 @@
|
||||
# Order matters. The last matching pattern takes precedence.
|
||||
|
||||
### Core ###
|
||||
/packages/payload/src/uploads/ @denolfe
|
||||
/packages/payload/src/admin/ @jmikrut @jacobsfletch @JarrodMFlesch
|
||||
|
||||
### Adapters ###
|
||||
/packages/db-*/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/richtext-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Plugins ###
|
||||
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/plugin-cloud*/ @denolfe
|
||||
/packages/plugin-form-builder/ @jacobsfletch
|
||||
/packages/plugin-live-preview*/ @jacobsfletch
|
||||
/packages/plugin-nested-docs/ @jacobsfletch
|
||||
/packages/plugin-redirects/ @jacobsfletch
|
||||
/packages/plugin-search/ @jacobsfletch
|
||||
/packages/plugin-sentry/ @JessChowdhury
|
||||
/packages/plugin-seo/ @jacobsfletch
|
||||
/packages/plugin-stripe/ @jacobsfletch
|
||||
|
||||
### Examples ###
|
||||
/examples/ @jacobsfletch
|
||||
/examples/testing/ @JarrodMFlesch
|
||||
/examples/email/ @JessChowdhury
|
||||
/examples/whitelabel/ @JessChowdhury
|
||||
|
||||
### Templates ###
|
||||
/templates/ @jacobsfletch @denolfe
|
||||
|
||||
### Misc ###
|
||||
/packages/create-payload-app/ @denolfe
|
||||
/packages/eslint-config-payload/ @denolfe
|
||||
/packages/payload-admin-bar/ @jacobsfletch
|
||||
|
||||
### Root ###
|
||||
/package.json @denolfe
|
||||
/scripts/ @denolfe
|
||||
/.github/ @denolfe
|
||||
/.github/CODEOWNERS @denolfe
|
||||
19
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
19
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Create a bug report for Payload
|
||||
labels: ['[possible-bug]']
|
||||
labels: ['possible-bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -10,12 +10,7 @@ body:
|
||||
id: reproduction-link
|
||||
attributes:
|
||||
label: Link to reproduction
|
||||
description: Want us to look into your issue faster? Follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
description: Please add a link to a reproduction. See the fork [reproduction-guide](https://github.com/payloadcms/payload/blob/master/.github/reproduction-guide.md) for more information.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -24,6 +19,11 @@ body:
|
||||
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Bug
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
@@ -31,11 +31,6 @@ body:
|
||||
description: What version of Payload are you running?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: adapters-plugins
|
||||
attributes:
|
||||
label: Adapters and Plugins
|
||||
description: What adapters and plugins are you using? ie. db-mongodb, db-postgres, bundler-webpack, etc.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
<!-- Please include a summary of the pull request and any related issues it fixes. Please also include relevant motivation and context. -->
|
||||
|
||||
- [ ] I have read and understand the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository.
|
||||
- [ ] I have read and understand the [CONTRIBUTING.md](../CONTRIBUTING.md) document in this repository.
|
||||
|
||||
## Type of change
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Change to the [templates](https://github.com/payloadcms/payload/tree/main/templates) directory (does not affect core functionality)
|
||||
- [ ] Change to the [examples](https://github.com/payloadcms/payload/tree/main/examples) directory (does not affect core functionality)
|
||||
- [ ] Change to the [templates](../templates/) directory (does not affect core functionality)
|
||||
- [ ] Change to the [examples](../examples/) directory (does not affect core functionality)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
## Checklist:
|
||||
|
||||
21
.github/reproduction-guide.md
vendored
21
.github/reproduction-guide.md
vendored
@@ -1,11 +1,10 @@
|
||||
# Reproduction Guide
|
||||
|
||||
1. [Fork](https://github.com/payloadcms/payload/fork) this repo
|
||||
2. Optionally, create a new branch for your reproduction
|
||||
3. Run `pnpm install` to install dependencies
|
||||
4. Open up the `test/_community` directory
|
||||
5. Add any necessary `collections/globals/fields` in this directory to recreate the issue you are experiencing
|
||||
6. Run `pnpm dev _community` to start the admin panel
|
||||
1. [fork](https://github.com/payloadcms/payload/fork) this repo
|
||||
2. run `yarn` to install dependencies
|
||||
3. open up the `test/_community` directory
|
||||
4. add any necessary `collections/globals/fields` in this directory to recreate the issue you are experiencing
|
||||
5. run `yarn dev _community` to start the admin panel
|
||||
|
||||
**NOTE:** The goal is to isolate the problem by reducing the number of `collections/globals/fields` you add to the `test/_community` folder. This folder is _not_ meant for you to copy your project into, but rather recreate the issue you are experiencing with minimal config.
|
||||
|
||||
@@ -22,7 +21,7 @@
|
||||
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
|
||||
- `int.spec.ts` [Optional] - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
|
||||
- `e2e.spec.ts` [Optional] - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests.
|
||||
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types _community`.
|
||||
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `yarn dev:generate-types _community`.
|
||||
|
||||
The directory split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config. You should modify the files in `test/_community` to get started.
|
||||
|
||||
@@ -40,12 +39,12 @@ There are a couple ways run integration tests:
|
||||
|
||||
- **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/github/int-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/github/int-debug.png" />
|
||||
|
||||
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
|
||||
|
||||
```bash
|
||||
pnpm test:int _community
|
||||
yarn test:int _community
|
||||
```
|
||||
|
||||
### Running E2E tests (Admin Panel UI tests)
|
||||
@@ -57,8 +56,8 @@ The easiest way to run E2E tests is to install
|
||||
|
||||
Once they are installed you can open the `testing` tab in vscode sidebar and drill down to the test you want to run, i.e. `/test/_community/e2e.spec.ts`
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/github/e2e-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/github/e2e-debug.png" />
|
||||
|
||||
#### Notes
|
||||
|
||||
The default credentials are `dev@payloadcms.com` as email and `test` as password. They can be found in `test/credentials.ts`. By default, these will be autofilled, so no log-in is required.
|
||||
- It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart. The default credentials are `dev@payloadcms.com` as email and `test` as password.
|
||||
|
||||
384
.github/workflows/main.yml
vendored
384
.github/workflows/main.yml
vendored
@@ -4,40 +4,10 @@ on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
push:
|
||||
branches: ['main', 'alpha']
|
||||
branches: ['master', '2.0']
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
needs_build: ${{ steps.filter.outputs.needs_build }}
|
||||
templates: ${{ steps.filter.outputs.templates }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
needs_build:
|
||||
- '.github/workflows/**'
|
||||
- 'packages/**'
|
||||
- 'test/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'package.json'
|
||||
templates:
|
||||
- 'templates/**'
|
||||
- name: Log all filter results
|
||||
run: |
|
||||
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
|
||||
echo "templates: ${{ steps.filter.outputs.templates }}"
|
||||
|
||||
core-build:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.needs_build == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -46,12 +16,12 @@ jobs:
|
||||
fetch-depth: 25
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
@@ -61,7 +31,7 @@ jobs:
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
@@ -71,222 +41,120 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm run build:core
|
||||
- run: pnpm run build
|
||||
|
||||
- name: Cache build
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
plugins-build:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.needs_build == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm run build:plugins
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
database:
|
||||
- mongodb
|
||||
- postgres
|
||||
- postgres-custom-schema
|
||||
- postgres-uuid
|
||||
- supabase
|
||||
database: [mongoose, postgres]
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: payloadtests
|
||||
AWS_ENDPOINT_URL: http://127.0.0.1:4566
|
||||
AWS_ACCESS_KEY_ID: localstack
|
||||
AWS_SECRET_ACCESS_KEY: localstack
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Start LocalStack
|
||||
run: pnpm docker:start
|
||||
|
||||
- name: Start PostgreSQL
|
||||
uses: CasperWA/postgresql-action@v1.2
|
||||
with:
|
||||
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
|
||||
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
|
||||
postgresql db: ${{ env.POSTGRES_DB }}
|
||||
postgresql user: ${{ env.POSTGRES_USER }}
|
||||
postgresql password: ${{ env.POSTGRES_PASSWORD }}
|
||||
if: startsWith(matrix.database, 'postgres')
|
||||
|
||||
- name: Install Supabase CLI
|
||||
uses: supabase/setup-cli@v1
|
||||
with:
|
||||
version: latest
|
||||
if: matrix.database == 'supabase'
|
||||
|
||||
- name: Initialize Supabase
|
||||
run: |
|
||||
supabase init
|
||||
supabase start
|
||||
if: matrix.database == 'supabase'
|
||||
|
||||
- name: Wait for PostgreSQL
|
||||
run: sleep 30
|
||||
if: startsWith(matrix.database, 'postgres')
|
||||
if: matrix.database == 'postgres'
|
||||
|
||||
- run: sleep 30
|
||||
- name: Configure PostgreSQL
|
||||
run: |
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();"
|
||||
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
|
||||
if: startsWith(matrix.database, 'postgres')
|
||||
if: matrix.database == 'postgres'
|
||||
|
||||
- name: Configure PostgreSQL with custom schema
|
||||
run: |
|
||||
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE SCHEMA custom;"
|
||||
if: matrix.database == 'postgres-custom-schema'
|
||||
|
||||
- name: Configure Supabase
|
||||
run: |
|
||||
echo "POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres" >> $GITHUB_ENV
|
||||
if: matrix.database == 'supabase'
|
||||
- name: Component Tests
|
||||
run: pnpm test:components
|
||||
|
||||
- name: Integration Tests
|
||||
run: pnpm test:int --testPathIgnorePatterns=test/fields # Ignore fields tests until reworked
|
||||
run: pnpm test:int
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8096
|
||||
PAYLOAD_DATABASE: ${{ matrix.database }}
|
||||
POSTGRES_URL: ${{ env.POSTGRES_URL }}
|
||||
|
||||
tests-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {}
|
||||
suite:
|
||||
- _community
|
||||
- access-control
|
||||
# - admin
|
||||
- auth
|
||||
# - field-error-states
|
||||
# - fields-relationship
|
||||
# - fields
|
||||
- fields/lexical
|
||||
- live-preview
|
||||
# - localization
|
||||
# - plugin-nested-docs
|
||||
# - plugin-seo
|
||||
# - refresh-permissions
|
||||
# - uploads
|
||||
# - versions
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Install Playwright
|
||||
run: pnpm exec playwright install
|
||||
|
||||
- name: E2E Tests
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
retry_on: error
|
||||
max_attempts: 2
|
||||
timeout_minutes: 15
|
||||
command: pnpm test:e2e ${{ matrix.suite }}
|
||||
run: pnpm test:e2e --bail
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: test-results
|
||||
path: test/test-results/
|
||||
retention-days: 1
|
||||
path: test-results/
|
||||
retention-days: 30
|
||||
|
||||
tests-type-generation:
|
||||
if: false # This should be replaced with gen on a real Payload project
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
@@ -297,74 +165,186 @@ jobs:
|
||||
- name: Generate GraphQL schema file
|
||||
run: pnpm dev:generate-graphql-schema graphql-schema-gen
|
||||
|
||||
plugins:
|
||||
# DB Adapters
|
||||
|
||||
build-db-mongodb:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pkg:
|
||||
- create-payload-app
|
||||
- plugin-cloud
|
||||
- plugin-cloud-storage
|
||||
- plugin-form-builder
|
||||
- plugin-nested-docs
|
||||
- plugin-search
|
||||
- plugin-sentry
|
||||
- plugin-seo
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Build ${{ matrix.pkg }}
|
||||
run: pnpm turbo run build --filter=${{ matrix.pkg }}
|
||||
- name: Build db-mongodb
|
||||
run: pnpm turbo run build --filter=db-mongodb
|
||||
|
||||
- name: Test ${{ matrix.pkg }}
|
||||
run: pnpm --filter ${{ matrix.pkg }} run test
|
||||
|
||||
templates:
|
||||
needs: changes
|
||||
if: false # Disable until templates are updated for 3.0
|
||||
build-db-postgres:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
template: [blank, website, ecommerce]
|
||||
needs: core-build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 25
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Start MongoDB
|
||||
uses: supercharge/mongodb-github-action@1.10.0
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
mongodb-version: 6.0
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Build Template
|
||||
run: |
|
||||
cd templates/${{ matrix.template }}
|
||||
cp .env.example .env
|
||||
yarn install
|
||||
yarn build
|
||||
yarn generate:types
|
||||
- name: Restore build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Build db-postgres
|
||||
run: pnpm turbo run build --filter=db-postgres
|
||||
|
||||
# Bundlers
|
||||
|
||||
build-bundler-webpack:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Build bundler-webpack
|
||||
run: pnpm turbo run build --filter=bundler-webpack
|
||||
|
||||
build-bundler-vite:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Build bundler-vite
|
||||
run: pnpm turbo run build --filter=bundler-vite
|
||||
|
||||
# Other Plugins
|
||||
|
||||
build-plugin-richtext-slate:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Build richtext-slate
|
||||
run: pnpm turbo run build --filter=richtext-slate
|
||||
|
||||
build-plugin-richtext-lexical:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Build richtext-lexical
|
||||
run: pnpm turbo run build --filter=richtext-lexical
|
||||
|
||||
build-plugin-live-preview:
|
||||
runs-on: ubuntu-latest
|
||||
needs: core-build
|
||||
|
||||
steps:
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Build live-preview
|
||||
run: pnpm turbo run build --filter=live-preview
|
||||
|
||||
- name: Build live-preview-react
|
||||
run: pnpm turbo run build --filter=live-preview-react
|
||||
|
||||
114
.gitignore
vendored
114
.gitignore
vendored
@@ -1,21 +1,10 @@
|
||||
coverage
|
||||
package-lock.json
|
||||
dist
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
|
||||
.idea
|
||||
test-results
|
||||
.devcontainer
|
||||
.localstack
|
||||
/migrations
|
||||
.localstack
|
||||
.turbo
|
||||
|
||||
.turbo
|
||||
|
||||
# Ignore test directory media folder/files
|
||||
/media
|
||||
/versions
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,windows,webstorm,sublimetext,visualstudiocode
|
||||
@@ -26,6 +15,9 @@ test-results
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
@@ -241,24 +233,119 @@ GitHub.sublime-settings
|
||||
.history
|
||||
.ionide
|
||||
|
||||
### WebStorm ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### WebStorm Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||
.idea/**/sonarlint/
|
||||
|
||||
# SonarQube Plugin
|
||||
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||
.idea/**/sonarIssues.xml
|
||||
|
||||
# Markdown Navigator plugin
|
||||
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||
.idea/**/markdown-navigator.xml
|
||||
.idea/**/markdown-navigator-enh.xml
|
||||
.idea/**/markdown-navigator/
|
||||
|
||||
# Cache file creation bug
|
||||
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||
.idea/$CACHE_FILE$
|
||||
|
||||
# CodeStream plugin
|
||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||
.idea/codestream.xml
|
||||
|
||||
# Azure Toolkit for IntelliJ plugin
|
||||
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
||||
.idea/**/azureSettings.xml
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
@@ -287,5 +374,4 @@ $RECYCLE.BIN/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,webstorm,sublimetext,visualstudiocode
|
||||
|
||||
/build
|
||||
.swc
|
||||
/build
|
||||
8
.idea/runConfigurations/Run_Dev_Fields.xml
generated
8
.idea/runConfigurations/Run_Dev_Fields.xml
generated
@@ -1,8 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Dev Fields" type="NodeJSConfigurationType" application-parameters="--no-deprecation fields" path-to-js-file="test/dev.js" working-dir="$PROJECT_DIR$">
|
||||
<envs>
|
||||
<env name="NODE_OPTIONS" value="--no-deprecation" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
8
.idea/runConfigurations/Run_Dev__community.xml
generated
8
.idea/runConfigurations/Run_Dev__community.xml
generated
@@ -1,8 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Dev _community" type="NodeJSConfigurationType" application-parameters="--no-deprecation _community" path-to-js-file="test/dev.js" working-dir="$PROJECT_DIR$">
|
||||
<envs>
|
||||
<env name="NODE_OPTIONS" value="--no-deprecation" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1 +1 @@
|
||||
v18.19.1
|
||||
v18.17.1
|
||||
|
||||
@@ -9,4 +9,3 @@
|
||||
**/node_modules
|
||||
**/temp
|
||||
**/docs/**
|
||||
tsconfig.json
|
||||
|
||||
22
.release-it.json
Normal file
22
.release-it.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"verbose": true,
|
||||
"git": {
|
||||
"commitMessage": "chore(release): v${version}",
|
||||
"requireCleanWorkingDir": true
|
||||
},
|
||||
"github": {
|
||||
"release": true
|
||||
},
|
||||
"npm": {
|
||||
"skipChecks": true
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": ["pnpm i", "pnpm clean", "pnpm test"]
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/conventional-changelog": {
|
||||
"preset": "angular",
|
||||
"infile": "CHANGELOG.md"
|
||||
}
|
||||
}
|
||||
}
|
||||
25
.release-it.pre.json
Normal file
25
.release-it.pre.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"verbose": true,
|
||||
"git": {
|
||||
"requireCleanWorkingDir": false,
|
||||
"commit": false,
|
||||
"push": false,
|
||||
"tag": false
|
||||
},
|
||||
"github": {
|
||||
"release": true
|
||||
},
|
||||
"npm": {
|
||||
"skipChecks": true,
|
||||
"tag": "canary"
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": ["pnpm i", "pnpm clean", "pnpm test"]
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/conventional-changelog": {
|
||||
"preset": "angular",
|
||||
"infile": "CHANGELOG.md"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
.swcrc
15
.swcrc
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": "inline",
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true,
|
||||
"dts": true
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "es6"
|
||||
}
|
||||
}
|
||||
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@@ -1,8 +1,3 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"firsttris.vscode-jest-runner",
|
||||
"ms-playwright.playwright"
|
||||
]
|
||||
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
|
||||
67
.vscode/launch.json
vendored
67
.vscode/launch.json
vendored
@@ -3,36 +3,12 @@
|
||||
// Hover to view descriptions of existing attributes.
|
||||
"configurations": [
|
||||
{
|
||||
"command": "pnpm generate:types",
|
||||
"name": "Generate Types CLI",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"command": "node --no-deprecation test/dev.js fields",
|
||||
"command": "pnpm run dev _community",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Community",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev live-preview -- --no-turbo",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Live Preview",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev plugin-cloud-storage",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev - plugin-cloud-storage",
|
||||
"request": "launch",
|
||||
"type": "node-terminal",
|
||||
"env": {
|
||||
"PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER": "s3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev fields",
|
||||
"cwd": "${workspaceFolder}",
|
||||
@@ -41,7 +17,7 @@
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev:postgres versions",
|
||||
"command": "pnpm run dev:postgres collections-graphql",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Postgres",
|
||||
"request": "launch",
|
||||
@@ -54,20 +30,6 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev localization",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Localization",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run dev uploads",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Uploads",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "PAYLOAD_BUNDLER=vite pnpm run dev fields",
|
||||
"cwd": "${workspaceFolder}",
|
||||
@@ -78,20 +40,6 @@
|
||||
"NODE_ENV": "production"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "pnpm run test:int live-preview",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Live Preview Int Tests",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm run test:int plugin-search",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Search Plugin Int Tests",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts build",
|
||||
"env": {
|
||||
@@ -116,6 +64,17 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts generate:types",
|
||||
"env": {
|
||||
"PAYLOAD_CONFIG_PATH": "test/_community/config.ts",
|
||||
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
|
||||
},
|
||||
"name": "Generate Types CLI",
|
||||
"outputCapture": "std",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:status",
|
||||
"env": {
|
||||
|
||||
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
@@ -5,21 +5,21 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
},
|
||||
"[json]": {
|
||||
@@ -35,10 +35,5 @@
|
||||
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
// Load .git-blame-ignore-revs file
|
||||
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
|
||||
"[javascript][typescript][typescriptreact]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"]
|
||||
}
|
||||
|
||||
1041
CHANGELOG.md
1041
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ If you find a vulnerability within the core Payload repository, and we determine
|
||||
|
||||
## Documentation edits
|
||||
|
||||
Payload documentation can be found directly within its codebase, and you can feel free to make changes / improvements to any of it through opening a PR. We utilize these files directly in our website and will periodically deploy documentation updates as necessary.
|
||||
Payload documentation can be found directly within its codebase and you can feel free to make changes / improvements to any of it through opening a PR. We utilize these files directly in our website and will periodically deploy documentation updates as necessary.
|
||||
|
||||
## Building additional features
|
||||
|
||||
@@ -30,17 +30,9 @@ Our design review ensures that proposed changes fit seamlessly with other compon
|
||||
|
||||
To help us work on new features, you can create a new feature request post in [GitHub Discussion](https://github.com/payloadcms/payload/discussions) or discuss it in our [Discord](https://discord.com/invite/payload). New functionality often has large implications across the entire Payload repo, so it is best to discuss the architecture and approach before starting work on a pull request.
|
||||
|
||||
### Installation & Requirements
|
||||
|
||||
Payload is structured as a Monorepo, encompassing not only the core Payload platform but also various plugins and packages. To install all required dependencies, you have to run `pnpm install` once in the root directory. **PNPM IS REQUIRED!** Yarn or npm will not work - you will have to use pnpm to develop in the core repository. In most systems, the easiest way to install pnpm is to run `corepack enable` in your terminal.
|
||||
|
||||
If you're coming from a very outdated version of payload, it is recommended to nuke the node_modules folder before running pnpm install. On UNIX systems, you can easily do that using the `pnpm clean:unix` command, which will delete all node_modules folders and build artefacts.
|
||||
|
||||
It is also recommended to use at least Node v18 or higher. You can check your current node version by typing `node --version` in your terminal. The easiest way to switch between different node versions is to use [nvm](https://github.com/nvm-sh/nvm#intro).
|
||||
|
||||
### Code
|
||||
|
||||
Most new functionality should keep testing in mind. All top-level directories within the `test/` directory are for testing a specific category: `fields`, `collections`, etc.
|
||||
Most new functionality should keep testing in mind. With 1.0, testability of new features has been vastly improved. All top-level directories within the `test/` directory are for testing a specific category: `fields`, `collections`, etc.
|
||||
|
||||
If it makes sense to add your feature to an existing test directory, please do so.
|
||||
|
||||
@@ -57,35 +49,21 @@ A typical directory with `test/` will be structured like this:
|
||||
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
|
||||
- `int.spec.ts` - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
|
||||
- `e2e.spec.ts` - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests. These tests are typically only needed if a large change is being made to the Admin UI.
|
||||
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types my-test-dir`. Replace `my-test-dir` with the name of your testing directory.
|
||||
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types my-test-dir`.
|
||||
|
||||
Each test directory is split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config.
|
||||
The directory split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config.
|
||||
|
||||
The following command will start Payload with your config: `pnpm dev my-test-dir`. Example: `pnpm dev fields` for the test/`fields` test suite. This command will start up Payload using your config and refresh a test database on every restart. If you're using VS Code, the most common run configs are automatically added to your editor - you should be able to find them in your VS Code launch tab.
|
||||
The following command will start Payload with your config: `pnpm dev my-test-dir`. This command will start up Payload using your config and refresh a test database on every restart.
|
||||
|
||||
By default, payload will [automatically log you in](https://payloadcms.com/docs/authentication/config#admin-autologin) with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
|
||||
By default, it will automatically log you in with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
|
||||
|
||||
The default credentials are `dev@payloadcms.com` as E-Mail and `test` as password. These are used in the auto-login.
|
||||
|
||||
### Testing with your own MongoDB database
|
||||
|
||||
If you wish to use your own MongoDB database for the `test` directory instead of using the in memory database, all you need to do is add the following env vars to the `test/dev.ts` file:
|
||||
If you wish to use to your own Mongo database for the `test` directory instead of using the in memory database, all you need to do is add the following env vars to the `test/dev.ts` file:
|
||||
|
||||
- `process.env.NODE_ENV`
|
||||
- `process.env.PAYLOAD_TEST_MONGO_URL`
|
||||
- Simply set `process.env.NODE_ENV` to `test` and set `process.env.PAYLOAD_TEST_MONGO_URL` to your MongoDB URL e.g. `mongodb://127.0.0.1/your-test-db`.
|
||||
- Simply set `process.env.NODE_ENV` to `test` and set `process.env.PAYLOAD_TEST_MONGO_URL` to your mongo url e.g. `mongodb://127.0.0.1/your-test-db`.
|
||||
|
||||
### Using Postgres
|
||||
|
||||
If you have postgres installed on your system, you can also run the test suites using postgres. By default, mongodb is used.
|
||||
|
||||
To do that, simply set the `PAYLOAD_DATABASE` environment variable to `postgres`.
|
||||
|
||||
### Running the e2e and int tests
|
||||
|
||||
You can run the entire test suite using `pnpm test`. If you wish to only run e2e tests, you can use `pnpm test:e2e`. If you wish to only run int tests, you can use `pnpm test:int`.
|
||||
|
||||
By default, `pnpm test:int` will only run int test against MongoDB. To run int tests against postgres, you can use `pnpm test:int:postgres`. You will have to have postgres installed on your system for this to work.
|
||||
NOTE: It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart. The default credentials are `dev@payloadcms.com` as E-Mail and `test` as password.
|
||||
|
||||
### Commits
|
||||
|
||||
@@ -111,14 +89,3 @@ If you are committing to [templates](./templates) or [examples](./examples), use
|
||||
## Pull Requests
|
||||
|
||||
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR message.
|
||||
|
||||
## Previewing docs
|
||||
|
||||
This is how you can preview changes you made locally to the docs:
|
||||
|
||||
1. Clone our [website repository](https://github.com/payloadcms/website)
|
||||
2. Run `yarn install`
|
||||
3. Duplicate the `.env.example` file and rename it to `.env`
|
||||
4. Add a `DOCS_DIR` environment variable to the `.env` file which points to the absolute path of your modified docs folder. For example `DOCS_DIR=/Users/yourname/Documents/GitHub/payload/docs`
|
||||
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: *Docs successfully written to /.../website/src/app/docs.json*. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
|
||||
6. You're done! Now you can start the website locally using `yarn run dev` and preview the docs under [http://localhost:3000/docs/](http://localhost:3000/docs/)
|
||||
|
||||
@@ -45,7 +45,7 @@ There are a couple ways to do this:
|
||||
|
||||
- **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/src/admin/assets/images/github/int-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/github/int-debug.png" />
|
||||
|
||||
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
|
||||
|
||||
@@ -62,7 +62,7 @@ The easiest way to run E2E tests is to install
|
||||
|
||||
Once they are installed you can open the `testing` tab in vscode sidebar and drill down to the test you want to run, i.e. `/test/_community/e2e.spec.ts`
|
||||
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/src/admin/assets/images/github/e2e-debug.png" />
|
||||
<img src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/github/e2e-debug.png" />
|
||||
|
||||
#### Notes
|
||||
|
||||
|
||||
51
README.md
51
README.md
@@ -1,24 +1,30 @@
|
||||
<a href="https://payloadcms.com"><img width="100%" src="https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/assets/images/github-banner-alt.jpg?raw=true" alt="Payload headless CMS Admin panel built with React" /></a>
|
||||
<a href="https://payloadcms.com">
|
||||
<img width="100%" src="src/admin/assets/images/github-banner-alt.jpg" alt="Payload headless CMS Admin panel built with React" />
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<p align="left">
|
||||
<a href="https://github.com/payloadcms/payload/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/payloadcms/payload/main.yml?style=flat-square"></a>
|
||||
<a href="https://github.com/payloadcms/payload/actions">
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/payloadcms/payload/tests.yml?style=flat-square">
|
||||
</a>
|
||||
|
||||
<a href="https://discord.gg/payload"><img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord&color=7289da&style=flat-square" /></a>
|
||||
<a href="https://discord.gg/payload">
|
||||
<img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord&color=7289da&style=flat-square" />
|
||||
</a>
|
||||
|
||||
<a href="https://www.npmjs.com/package/payload"><img alt="npm" src="https://img.shields.io/npm/v/payload?style=flat-square" /></a>
|
||||
<a href="https://www.npmjs.com/package/payload">
|
||||
<img alt="npm" src="https://img.shields.io/npm/v/payload?style=flat-square" />
|
||||
</a>
|
||||
|
||||
<a href="https://twitter.com/payloadcms"><img src="https://img.shields.io/badge/follow-payloadcms-1DA1F2?logo=twitter&style=flat-square" alt="Payload Twitter" /></a>
|
||||
<a href="https://twitter.com/payloadcms">
|
||||
<img src="https://img.shields.io/badge/follow-payloadcms-1DA1F2?logo=twitter&style=flat-square" alt="Payload Twitter" />
|
||||
</a>
|
||||
</p>
|
||||
<hr/>
|
||||
<h4>
|
||||
<a target="_blank" href="https://payloadcms.com/docs/getting-started/what-is-payload" rel="dofollow"><strong>Explore the Docs</strong></a> · <a target="_blank" href="https://payloadcms.com/community-help" rel="dofollow"><strong>Community Help</strong></a> · <a target="_blank" href="https://demo.payloadcms.com/" rel="dofollow"><strong>Try Live Demo</strong></a> · <a target="_blank" href="https://github.com/payloadcms/payload/discussions/1539" rel="dofollow"><strong>Roadmap</strong></a> · <a target="_blank" href="https://www.g2.com/products/payload-cms/reviews#reviews" rel="dofollow"><strong>View G2 Reviews</strong></a>
|
||||
</h4>
|
||||
<hr/>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 🎉 <strong>Payload 2.0 is now available!</strong> Read more in the <a target="_blank" href="https://payloadcms.com/blog/payload-2-0" rel="dofollow"><strong>announcement post</strong></a>.
|
||||
|
||||
<h3>Benefits over a regular CMS</h3>
|
||||
<ul>
|
||||
<li>Don’t hit some third-party SaaS API, hit your own API</li>
|
||||
@@ -41,27 +47,20 @@ Create a cloud account, connect your GitHub, and [deploy in minutes](https://pay
|
||||
Before beginning to work with Payload, make sure you have all of the [required software](https://payloadcms.com/docs/getting-started/installation).
|
||||
|
||||
```text
|
||||
npx create-payload-app@latest
|
||||
npx create-payload-app
|
||||
```
|
||||
|
||||
Alternatively, it only takes about five minutes to [create an app from scratch](https://payloadcms.com/docs/getting-started/installation#from-scratch).
|
||||
|
||||
## 🖱️ One-click templates
|
||||
|
||||
Jumpstart your next project by starting with a pre-made template. These are production-ready, end-to-end solutions designed to get you to market as fast as possible.
|
||||
### 🛒 [E-Commerce](https://github.com/payloadcms/payload/tree/master/templates/ecommerce)
|
||||
|
||||
### [🛒 E-Commerce](https://github.com/payloadcms/payload/tree/main/templates/ecommerce)
|
||||
Eliminate the need to combine Shopify and a CMS, and instead do it all with Payload + Stripe. Best of all, you can extend it as much as you need.
|
||||
|
||||
Eliminate the need to combine Shopify and a CMS, and instead do it all with Payload + Stripe. Comes with a beautiful, fully functional front-end complete with shopping cart, checkout, orders, and much more.
|
||||
[All Official Templates](https://github.com/orgs/payloadcms/repositories?q=topic%3Apayload-template) · [Community Templates](https://github.com/topics/payload-template)
|
||||
|
||||
### [🌐 Website](https://github.com/payloadcms/payload/tree/main/templates/website)
|
||||
|
||||
Build any kind of website, blog, or portfolio from small to enterprise. Comes with a beautiful, fully functional front-end complete with posts, projects, comments, and much more.
|
||||
|
||||
We're constantly adding more templates to our [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates). If you maintain your own template, consider adding the `payload-template` topic to your GitHub repository for others to find.
|
||||
|
||||
- [Official Templates](https://github.com/payloadcms/payload/tree/main/templates)
|
||||
- [Community Templates](https://github.com/topics/payload-template)
|
||||
**If you maintain your own template, consider adding the `payload-template` topic to your GitHub repository for others to find.**
|
||||
|
||||
## ✨ Features
|
||||
|
||||
@@ -89,19 +88,15 @@ We're constantly adding more templates to our [Templates Directory](https://gith
|
||||
|
||||
Check out the [Payload website](https://payloadcms.com/docs/getting-started/what-is-payload) to find in-depth documentation for everything that Payload offers.
|
||||
|
||||
Migrating from v1 to v2? Check out the [2.0 Release Notes](https://github.com/payloadcms/payload/releases/tag/v2.0.0) on how to do it.
|
||||
|
||||
## 🙋 Contributing
|
||||
|
||||
If you want to add contributions to this repository, please follow the instructions in [contributing.md](./CONTRIBUTING.md).
|
||||
If you want to add contributions to this repository, please follow the instructions in [contributing.md](./contributing.md).
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
The [Examples Directory](./examples) is a great resource for learning how to setup Payload in a variety of different ways, but you can also find great examples in our blog and throughout our social media.
|
||||
The examples directory is a great resource for learning how to setup Payload in a variety of different ways.
|
||||
|
||||
- [Examples Directory](./examples)
|
||||
- [Payload Blog](https://payloadcms.com/blog)
|
||||
- [Payload YouTube](https://www.youtube.com/@payloadcms)
|
||||
[Examples Directory](./examples)
|
||||
|
||||
## 🔌 Plugins
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { NotFoundView } from '@payloadcms/next/views/NotFound/index.js'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) => NotFoundView({ config, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
@@ -1,22 +0,0 @@
|
||||
/* 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/Root/index.js'
|
||||
|
||||
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
|
||||
@@ -1,9 +0,0 @@
|
||||
/* 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_PATCH, REST_POST } from '@payloadcms/next/routes/index.js'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
@@ -1,6 +0,0 @@
|
||||
/* 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/index.js'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
@@ -1,6 +0,0 @@
|
||||
/* 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/index.js'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
@@ -1,15 +0,0 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import configPromise from '@payload-config'
|
||||
import { RootLayout } from '@payloadcms/next/layouts/Root/index.js'
|
||||
/* 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
|
||||
@@ -1,43 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useLivePreview } from '@payloadcms/live-preview-react'
|
||||
import React from 'react'
|
||||
|
||||
import type { Page as PageType } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from '../../_api/serverURL.js'
|
||||
import { Blocks } from '../../_components/Blocks/index.js'
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import { Hero } from '../../_components/Hero/index.js'
|
||||
|
||||
export const PageClient: React.FC<{
|
||||
page: PageType
|
||||
}> = ({ page: initialPage }) => {
|
||||
const { data } = useLivePreview<PageType>({
|
||||
depth: 2,
|
||||
initialData: initialPage,
|
||||
serverURL: PAYLOAD_SERVER_URL,
|
||||
})
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Gutter>
|
||||
<h1 id="page-title">{data.title}</h1>
|
||||
</Gutter>
|
||||
<Hero {...data?.hero} />
|
||||
<Blocks
|
||||
blocks={[
|
||||
...(data?.layout ?? []),
|
||||
{
|
||||
blockName: 'Relationships',
|
||||
blockType: 'relationships',
|
||||
data,
|
||||
},
|
||||
]}
|
||||
disableTopPadding={
|
||||
!data?.hero || data?.hero?.type === 'none' || data?.hero?.type === 'lowImpact'
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { fetchDoc } from '../../_api/fetchDoc.js'
|
||||
import { fetchDocs } from '../../_api/fetchDocs.js'
|
||||
import { PageClient } from './page.client.js'
|
||||
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default async function Page({ params: { slug = 'home' } }) {
|
||||
let page: Page | null = null
|
||||
|
||||
try {
|
||||
page = await fetchDoc<Page>({
|
||||
slug,
|
||||
collection: 'pages',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return <PageClient page={page} />
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const pages = await fetchDocs<Page>('pages')
|
||||
return pages?.map(({ slug }) => slug)
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useLivePreview } from '@payloadcms/live-preview-react'
|
||||
import React from 'react'
|
||||
|
||||
import type { Post as PostType } from '../../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
|
||||
import { Blocks } from '../../../_components/Blocks/index.js'
|
||||
import { PostHero } from '../../../_heros/PostHero/index.js'
|
||||
|
||||
export const PostClient: React.FC<{
|
||||
post: PostType
|
||||
}> = ({ post: initialPost }) => {
|
||||
const { data } = useLivePreview<PostType>({
|
||||
depth: 2,
|
||||
initialData: initialPost,
|
||||
serverURL: PAYLOAD_SERVER_URL,
|
||||
})
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<PostHero post={data} />
|
||||
<Blocks blocks={data?.layout} />
|
||||
<Blocks
|
||||
blocks={[
|
||||
{
|
||||
blockName: 'Related Posts',
|
||||
blockType: 'relatedPosts',
|
||||
docs: data?.relatedPosts,
|
||||
introContent: [
|
||||
{
|
||||
type: 'h4',
|
||||
children: [
|
||||
{
|
||||
text: 'Related posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'p',
|
||||
children: [
|
||||
{
|
||||
text: 'The posts displayed here are individually selected for this page. Admins can select any number of related posts to display here and the layout will adjust accordingly. Alternatively, you could swap this out for the "Archive" block to automatically populate posts by category complete with pagination. To manage related posts, ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
children: [
|
||||
{
|
||||
text: 'navigate to the admin dashboard',
|
||||
},
|
||||
],
|
||||
url: `/admin/collections/posts/${data?.id}`,
|
||||
},
|
||||
{
|
||||
text: '.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
relationTo: 'posts',
|
||||
},
|
||||
]}
|
||||
disableTopPadding
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
import type { Post } from '../../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { fetchDoc } from '../../../_api/fetchDoc.js'
|
||||
import { fetchDocs } from '../../../_api/fetchDocs.js'
|
||||
import { PostClient } from './page.client.js'
|
||||
|
||||
export default async function Post({ params: { slug = '' } }) {
|
||||
let post: Post | null = null
|
||||
|
||||
try {
|
||||
post = await fetchDoc<Post>({
|
||||
slug,
|
||||
collection: 'posts',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return <PostClient post={post} />
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const posts = await fetchDocs<Post>('posts')
|
||||
return posts?.map(({ slug }) => slug)
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import QueryString from 'qs'
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from './serverURL.js'
|
||||
|
||||
export const fetchDoc = async <T>(args: {
|
||||
collection: string
|
||||
depth?: number
|
||||
id?: string
|
||||
slug?: string
|
||||
}): Promise<T> => {
|
||||
const { id, slug, collection, depth = 2 } = args || {}
|
||||
|
||||
const queryString = QueryString.stringify(
|
||||
{
|
||||
...(slug ? { 'where[slug][equals]': slug } : {}),
|
||||
...(depth ? { depth } : {}),
|
||||
},
|
||||
{ addQueryPrefix: true },
|
||||
)
|
||||
|
||||
const doc: T = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}${queryString}`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
})
|
||||
?.then((res) => res.json())
|
||||
?.then((res) => {
|
||||
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching doc')
|
||||
return res?.docs?.[0]
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { PAYLOAD_SERVER_URL } from './serverURL.js'
|
||||
|
||||
export const fetchDocs = async <T>(collection: string): Promise<T[]> => {
|
||||
const docs: T[] = await fetch(`${PAYLOAD_SERVER_URL}/api/${collection}?depth=0&limit=100`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
})
|
||||
?.then((res) => res.json())
|
||||
?.then((res) => {
|
||||
if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching docs')
|
||||
|
||||
return res?.docs
|
||||
})
|
||||
|
||||
return docs
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { ArchiveBlockProps } from './types.js'
|
||||
|
||||
import { CollectionArchive } from '../../_components/CollectionArchive/index.js'
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import RichText from '../../_components/RichText/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const ArchiveBlock: React.FC<
|
||||
ArchiveBlockProps & {
|
||||
id?: string
|
||||
}
|
||||
> = (props) => {
|
||||
const {
|
||||
id,
|
||||
categories,
|
||||
introContent,
|
||||
limit,
|
||||
populateBy,
|
||||
populatedDocs,
|
||||
populatedDocsTotal,
|
||||
relationTo,
|
||||
selectedDocs,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className={classes.archiveBlock} id={`block-${id}`}>
|
||||
{introContent && (
|
||||
<Gutter className={classes.introContent}>
|
||||
<RichText content={introContent} />
|
||||
</Gutter>
|
||||
)}
|
||||
<CollectionArchive
|
||||
categories={categories}
|
||||
limit={limit}
|
||||
populateBy={populateBy}
|
||||
populatedDocs={populatedDocs}
|
||||
populatedDocsTotal={populatedDocsTotal}
|
||||
relationTo={relationTo}
|
||||
selectedDocs={selectedDocs}
|
||||
sort="-publishedDate"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { Page } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
export type ArchiveBlockProps = Extract<
|
||||
Exclude<Page['layout'], undefined>[0],
|
||||
{ blockType: 'archive' }
|
||||
>
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import { CMSLink } from '../../_components/Link/index.js'
|
||||
import RichText from '../../_components/RichText/index.js'
|
||||
import { VerticalPadding } from '../../_components/VerticalPadding/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'cta' }>
|
||||
|
||||
export const CallToActionBlock: React.FC<
|
||||
Props & {
|
||||
id?: string
|
||||
}
|
||||
> = ({ invertBackground, links, richText }) => {
|
||||
return (
|
||||
<Gutter>
|
||||
<VerticalPadding
|
||||
className={[classes.callToAction, invertBackground && classes.invert]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={classes.wrap}>
|
||||
<div className={classes.content}>
|
||||
<RichText className={classes.richText} content={richText} />
|
||||
</div>
|
||||
<div className={classes.linkGroup}>
|
||||
{(links || []).map(({ link }, i) => {
|
||||
return <CMSLink key={i} {...link} invert={invertBackground} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</VerticalPadding>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Page } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import { CMSLink } from '../../_components/Link/index.js'
|
||||
import RichText from '../../_components/RichText/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'content' }>
|
||||
|
||||
export const ContentBlock: React.FC<
|
||||
Props & {
|
||||
id?: string
|
||||
}
|
||||
> = (props) => {
|
||||
const { columns } = props
|
||||
|
||||
return (
|
||||
<Gutter className={classes.content}>
|
||||
<div className={classes.grid}>
|
||||
{columns && columns.length > 0 ? (
|
||||
<Fragment>
|
||||
{columns.map((col, index) => {
|
||||
const { enableLink, link, richText, size } = col
|
||||
|
||||
return (
|
||||
<div className={[classes.column, classes[`column--${size}`]].join(' ')} key={index}>
|
||||
<RichText content={richText} />
|
||||
{enableLink && <CMSLink className={classes.link} {...link} />}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Fragment>
|
||||
) : null}
|
||||
</div>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { StaticImageData } from 'next/image.js'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import { Media } from '../../_components/Media/index.js'
|
||||
import RichText from '../../_components/RichText/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = Extract<Exclude<Page['layout'], undefined>[0], { blockType: 'mediaBlock' }> & {
|
||||
id?: string
|
||||
staticImage?: StaticImageData
|
||||
}
|
||||
|
||||
export const MediaBlock: React.FC<Props> = (props) => {
|
||||
const { media, position = 'default', staticImage } = props
|
||||
|
||||
let caption
|
||||
if (media && typeof media === 'object') caption = media.caption
|
||||
|
||||
return (
|
||||
<div className={classes.mediaBlock}>
|
||||
{position === 'fullscreen' && (
|
||||
<div className={classes.fullscreen}>
|
||||
<Media resource={media} src={staticImage} />
|
||||
</div>
|
||||
)}
|
||||
{position === 'default' && (
|
||||
<Gutter>
|
||||
<Media resource={media} src={staticImage} />
|
||||
</Gutter>
|
||||
)}
|
||||
{caption && (
|
||||
<Gutter className={classes.caption}>
|
||||
<RichText content={caption} />
|
||||
</Gutter>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { Post } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { Card } from '../../_components/Card/index.js'
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import RichText from '../../_components/RichText/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type RelatedPostsProps = {
|
||||
blockName: string
|
||||
blockType: 'relatedPosts'
|
||||
docs?: (Post | string)[] | null
|
||||
introContent?: any
|
||||
relationTo: 'posts'
|
||||
}
|
||||
|
||||
export const RelatedPosts: React.FC<RelatedPostsProps> = (props) => {
|
||||
const { docs, introContent, relationTo } = props
|
||||
|
||||
return (
|
||||
<div className={classes.relatedPosts}>
|
||||
{introContent && (
|
||||
<Gutter className={classes.introContent}>
|
||||
<RichText content={introContent} />
|
||||
</Gutter>
|
||||
)}
|
||||
<Gutter>
|
||||
<div className={classes.grid}>
|
||||
{docs?.map((doc, index) => {
|
||||
if (typeof doc === 'string') return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
classes.column,
|
||||
docs.length === 2 && classes['cols-half'],
|
||||
docs.length >= 3 && classes['cols-thirds'],
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
key={index}
|
||||
>
|
||||
<Card doc={doc} relationTo={relationTo} showCategories />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Gutter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
@import '../../_css/common';
|
||||
|
||||
.relationshipsBlock {
|
||||
}
|
||||
|
||||
.array {
|
||||
border: 1px solid var(--color-base-100);
|
||||
padding: var(--base);
|
||||
|
||||
& > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Page } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import RichText from '../../_components/RichText/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type RelationshipsBlockProps = {
|
||||
blockName: string
|
||||
blockType: 'relationships'
|
||||
data: Page
|
||||
}
|
||||
|
||||
export const RelationshipsBlock: React.FC<RelationshipsBlockProps> = (props) => {
|
||||
const { data } = props
|
||||
|
||||
return (
|
||||
<div className={classes.relationshipsBlock}>
|
||||
<Gutter>
|
||||
<p>
|
||||
This block is for testing purposes only. It renders every possible type of relationship.
|
||||
</p>
|
||||
<p>
|
||||
<b>Rich Text — Slate:</b>
|
||||
</p>
|
||||
{data?.richTextSlate && <RichText content={data.richTextSlate} renderUploadFilenameOnly />}
|
||||
<p>
|
||||
<b>Rich Text — Lexical:</b>
|
||||
</p>
|
||||
{data?.richTextLexical && (
|
||||
<RichText content={data.richTextLexical} renderUploadFilenameOnly serializer="lexical" />
|
||||
)}
|
||||
<p>
|
||||
<b>Upload:</b>
|
||||
</p>
|
||||
{data?.relationshipAsUpload ? (
|
||||
<div>
|
||||
{typeof data?.relationshipAsUpload === 'string'
|
||||
? data?.relationshipAsUpload
|
||||
: data?.relationshipAsUpload.filename}
|
||||
</div>
|
||||
) : (
|
||||
<div>None</div>
|
||||
)}
|
||||
<p>
|
||||
<b>Monomorphic Has One:</b>
|
||||
</p>
|
||||
{data?.relationshipMonoHasOne ? (
|
||||
<div>
|
||||
{typeof data?.relationshipMonoHasOne === 'string'
|
||||
? data?.relationshipMonoHasOne
|
||||
: data?.relationshipMonoHasOne.title}
|
||||
</div>
|
||||
) : (
|
||||
<div>None</div>
|
||||
)}
|
||||
<p>
|
||||
<b>Monomorphic Has Many:</b>
|
||||
</p>
|
||||
{data?.relationshipMonoHasMany ? (
|
||||
<Fragment>
|
||||
{data?.relationshipMonoHasMany.length
|
||||
? data?.relationshipMonoHasMany?.map((item, index) =>
|
||||
item ? (
|
||||
<div key={index}>{typeof item === 'string' ? item : item.title}</div>
|
||||
) : (
|
||||
'null'
|
||||
),
|
||||
)
|
||||
: 'None'}
|
||||
</Fragment>
|
||||
) : (
|
||||
<div>None</div>
|
||||
)}
|
||||
<p>
|
||||
<b>Polymorphic Has One:</b>
|
||||
</p>
|
||||
{data?.relationshipPolyHasOne ? (
|
||||
<div>
|
||||
{typeof data?.relationshipPolyHasOne.value === 'string'
|
||||
? data?.relationshipPolyHasOne.value
|
||||
: data?.relationshipPolyHasOne.value.title}
|
||||
</div>
|
||||
) : (
|
||||
<div>None</div>
|
||||
)}
|
||||
<p>
|
||||
<b>Polymorphic Has Many:</b>
|
||||
</p>
|
||||
{data?.relationshipPolyHasMany ? (
|
||||
<Fragment>
|
||||
{data?.relationshipPolyHasMany.length
|
||||
? data?.relationshipPolyHasMany?.map((item, index) =>
|
||||
item.value ? (
|
||||
<div key={index}>
|
||||
{typeof item.value === 'string' ? item.value : item.value.title}
|
||||
</div>
|
||||
) : (
|
||||
'null'
|
||||
),
|
||||
)
|
||||
: 'None'}
|
||||
</Fragment>
|
||||
) : (
|
||||
<div>None</div>
|
||||
)}
|
||||
<p>
|
||||
<b>Array of Relationships:</b>
|
||||
</p>
|
||||
{data?.arrayOfRelationships?.map((item, index) => (
|
||||
<div className={classes.array} key={index}>
|
||||
<p>
|
||||
<b>Rich Text:</b>
|
||||
</p>
|
||||
{item?.richTextInArray && <RichText content={item.richTextInArray} />}
|
||||
<p>
|
||||
<b>Upload:</b>
|
||||
</p>
|
||||
{item?.uploadInArray ? (
|
||||
<div>
|
||||
{typeof item?.uploadInArray === 'string'
|
||||
? item?.uploadInArray
|
||||
: item?.uploadInArray.filename}
|
||||
</div>
|
||||
) : (
|
||||
<div>None</div>
|
||||
)}
|
||||
<p>
|
||||
<b>Monomorphic Has One:</b>
|
||||
</p>
|
||||
{item?.relationshipInArrayMonoHasOne ? (
|
||||
<div>
|
||||
{typeof item?.relationshipInArrayMonoHasOne === 'string'
|
||||
? item?.relationshipInArrayMonoHasOne
|
||||
: item?.relationshipInArrayMonoHasOne.title}
|
||||
</div>
|
||||
) : (
|
||||
<div>None</div>
|
||||
)}
|
||||
<p>
|
||||
<b>Monomorphic Has Many:</b>
|
||||
</p>
|
||||
{item?.relationshipInArrayMonoHasMany ? (
|
||||
<Fragment>
|
||||
{item?.relationshipInArrayMonoHasMany.length
|
||||
? item?.relationshipInArrayMonoHasMany?.map((rel, relIndex) =>
|
||||
rel ? (
|
||||
<div key={relIndex}>{typeof rel === 'string' ? rel : rel.title}</div>
|
||||
) : (
|
||||
'null'
|
||||
),
|
||||
)
|
||||
: 'None'}
|
||||
</Fragment>
|
||||
) : (
|
||||
<div>None</div>
|
||||
)}
|
||||
<p>
|
||||
<b>Polymorphic Has One:</b>
|
||||
</p>
|
||||
{item?.relationshipInArrayPolyHasOne ? (
|
||||
<div>
|
||||
{typeof item?.relationshipInArrayPolyHasOne.value === 'string'
|
||||
? item?.relationshipInArrayPolyHasOne.value
|
||||
: item?.relationshipInArrayPolyHasOne.value.title}
|
||||
</div>
|
||||
) : (
|
||||
<div>None</div>
|
||||
)}
|
||||
<p>
|
||||
<b>Polymorphic Has Many:</b>
|
||||
</p>
|
||||
{item?.relationshipInArrayPolyHasMany ? (
|
||||
<Fragment>
|
||||
{item?.relationshipInArrayPolyHasMany.length
|
||||
? item?.relationshipInArrayPolyHasMany?.map((rel, relIndex) =>
|
||||
rel.value ? (
|
||||
<div key={relIndex}>
|
||||
{typeof rel.value === 'string' ? rel.value : rel.value.title}
|
||||
</div>
|
||||
) : (
|
||||
'null'
|
||||
),
|
||||
)
|
||||
: 'None'}
|
||||
</Fragment>
|
||||
) : (
|
||||
<div>None</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Gutter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
id?: string
|
||||
invert?: boolean | null
|
||||
}
|
||||
|
||||
export const BackgroundColor: React.FC<Props> = (props) => {
|
||||
const { id, children, className, invert } = props
|
||||
|
||||
return (
|
||||
<div className={[invert && classes.invert, className].filter(Boolean).join(' ')} id={id}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Page } from '../../../../test/live-preview/payload-types.js'
|
||||
import type { RelationshipsBlockProps } from '../../_blocks/Relationships/index.js'
|
||||
import type { VerticalPaddingOptions } from '../VerticalPadding/index.js'
|
||||
|
||||
import { ArchiveBlock } from '../../_blocks/ArchiveBlock/index.js'
|
||||
import { CallToActionBlock } from '../../_blocks/CallToAction/index.js'
|
||||
import { ContentBlock } from '../../_blocks/Content/index.js'
|
||||
import { MediaBlock } from '../../_blocks/MediaBlock/index.js'
|
||||
import { RelatedPosts, type RelatedPostsProps } from '../../_blocks/RelatedPosts/index.js'
|
||||
import { RelationshipsBlock } from '../../_blocks/Relationships/index.js'
|
||||
import { toKebabCase } from '../../_utilities/toKebabCase.js'
|
||||
import { BackgroundColor } from '../BackgroundColor/index.js'
|
||||
import { VerticalPadding } from '../VerticalPadding/index.js'
|
||||
|
||||
const blockComponents = {
|
||||
archive: ArchiveBlock,
|
||||
content: ContentBlock,
|
||||
cta: CallToActionBlock,
|
||||
mediaBlock: MediaBlock,
|
||||
relatedPosts: RelatedPosts,
|
||||
relationships: RelationshipsBlock,
|
||||
}
|
||||
|
||||
type Block = NonNullable<Page['layout']>[number]
|
||||
|
||||
export const Blocks: React.FC<{
|
||||
blocks?: (Block | RelatedPostsProps | RelationshipsBlockProps)[] | null
|
||||
disableTopPadding?: boolean
|
||||
}> = (props) => {
|
||||
const { blocks, disableTopPadding } = props
|
||||
|
||||
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
|
||||
|
||||
if (hasBlocks) {
|
||||
return (
|
||||
<Fragment>
|
||||
{blocks.map((block, index) => {
|
||||
const { blockName, blockType } = block
|
||||
|
||||
if (blockType && blockType in blockComponents) {
|
||||
const Block = blockComponents[blockType]
|
||||
|
||||
// the cta block is containerized, so we don't consider it to be inverted at the block-level
|
||||
const blockIsInverted =
|
||||
'invertBackground' in block && blockType !== 'cta' ? block.invertBackground : false
|
||||
const prevBlock = blocks[index - 1]
|
||||
|
||||
const prevBlockInverted =
|
||||
prevBlock && 'invertBackground' in prevBlock && prevBlock?.invertBackground
|
||||
|
||||
const isPrevSame = Boolean(blockIsInverted) === Boolean(prevBlockInverted)
|
||||
|
||||
let paddingTop: VerticalPaddingOptions = 'large'
|
||||
let paddingBottom: VerticalPaddingOptions = 'large'
|
||||
|
||||
if (prevBlock && isPrevSame) {
|
||||
paddingTop = 'none'
|
||||
}
|
||||
|
||||
if (index === blocks.length - 1) {
|
||||
paddingBottom = 'large'
|
||||
}
|
||||
|
||||
if (disableTopPadding && index === 0) {
|
||||
paddingTop = 'none'
|
||||
}
|
||||
|
||||
if (Block) {
|
||||
return (
|
||||
<BackgroundColor invert={blockIsInverted} key={index}>
|
||||
<VerticalPadding bottom={paddingBottom} top={paddingTop}>
|
||||
{/* @ts-expect-error */}
|
||||
<Block id={toKebabCase(blockName)} {...block} />
|
||||
</VerticalPadding>
|
||||
</BackgroundColor>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ElementType } from 'react'
|
||||
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
|
||||
|
||||
export type Props = {
|
||||
appearance?: 'default' | 'none' | 'primary' | 'secondary'
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
el?: 'a' | 'button' | 'link'
|
||||
href?: string
|
||||
invert?: boolean
|
||||
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,
|
||||
invert,
|
||||
label,
|
||||
newTab,
|
||||
onClick,
|
||||
}) => {
|
||||
let el = elFromProps
|
||||
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
|
||||
const className = [
|
||||
classes.button,
|
||||
classNameFromProps,
|
||||
classes[`appearance--${appearance}`],
|
||||
invert && classes[`${appearance}--invert`],
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const content = (
|
||||
<div className={classes.content}>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Post } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { Media } from '../Media/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
|
||||
|
||||
export const Card: React.FC<{
|
||||
alignItems?: 'center'
|
||||
className?: string
|
||||
doc?: Post
|
||||
hideImagesOnMobile?: boolean
|
||||
orientation?: 'horizontal' | 'vertical'
|
||||
relationTo?: 'posts'
|
||||
showCategories?: boolean
|
||||
title?: string
|
||||
}> = (props) => {
|
||||
const {
|
||||
className,
|
||||
doc,
|
||||
orientation = 'vertical',
|
||||
relationTo,
|
||||
showCategories,
|
||||
title: titleFromProps,
|
||||
} = props
|
||||
|
||||
const { slug, categories, meta, title } = doc || {}
|
||||
const { description, image: metaImage } = meta || {}
|
||||
|
||||
const hasCategories = categories && Array.isArray(categories) && categories.length > 0
|
||||
const titleToUse = titleFromProps || title
|
||||
const sanitizedDescription = description?.replace(/\s/g, ' ') // replace non-breaking space with white space
|
||||
const href = `/live-preview/${relationTo}/${slug}`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[classes.card, className, orientation && classes[orientation]]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<Link className={classes.mediaWrapper} href={href}>
|
||||
{!metaImage && <div className={classes.placeholder}>No image</div>}
|
||||
{metaImage && typeof metaImage !== 'string' && (
|
||||
<Media fill imgClassName={classes.image} resource={metaImage} />
|
||||
)}
|
||||
</Link>
|
||||
<div className={classes.content}>
|
||||
{showCategories && hasCategories && (
|
||||
<div className={classes.leader}>
|
||||
{showCategories && hasCategories && (
|
||||
<div>
|
||||
{categories?.map((category, index) => {
|
||||
const titleFromCategory = typeof category === 'string' ? category : category.title
|
||||
|
||||
const categoryTitle = titleFromCategory || 'Untitled category'
|
||||
|
||||
const isLast = index === categories.length - 1
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{categoryTitle}
|
||||
{!isLast && <Fragment>, </Fragment>}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{titleToUse && (
|
||||
<h4 className={classes.title}>
|
||||
<Link className={classes.titleLink} href={href}>
|
||||
{titleToUse}
|
||||
</Link>
|
||||
</h4>
|
||||
)}
|
||||
{description && (
|
||||
<div className={classes.body}>
|
||||
{description && <p className={classes.description}>{sanitizedDescription}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export const Chevron: React.FC<{
|
||||
className?: string
|
||||
rotate?: number
|
||||
}> = ({ className, rotate }) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
height="100%"
|
||||
style={{
|
||||
transform: typeof rotate === 'number' ? `rotate(${rotate || 0}deg)` : undefined,
|
||||
}}
|
||||
viewBox="0 0 24 24"
|
||||
width="100%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M23.245 4l-11.245 14.374-11.219-14.374-.781.619 12 15.381 12-15.391-.755-.609z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import qs from 'qs'
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import type { Post } from '../../../../../test/live-preview/payload-types.js'
|
||||
import type { ArchiveBlockProps } from '../../../_blocks/ArchiveBlock/types.js'
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
|
||||
import { Card } from '../../Card/index.js'
|
||||
import { Gutter } from '../../Gutter/index.js'
|
||||
import { PageRange } from '../../PageRange/index.js'
|
||||
import { Pagination } from '../../Pagination/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
type Result = {
|
||||
docs: (Post | string)[]
|
||||
hasNextPage: boolean
|
||||
hasPrevPage: boolean
|
||||
nextPage: number
|
||||
page: number
|
||||
prevPage: number
|
||||
totalDocs: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export type Props = Omit<ArchiveBlockProps, 'blockType'> & {
|
||||
className?: string
|
||||
onResultChange?: (result: Result) => void // eslint-disable-line no-unused-vars
|
||||
showPageRange?: boolean
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export const CollectionArchiveByCollection: React.FC<Props> = (props) => {
|
||||
const {
|
||||
categories: catsFromProps,
|
||||
className,
|
||||
limit = 10,
|
||||
onResultChange,
|
||||
populatedDocs,
|
||||
populatedDocsTotal,
|
||||
relationTo,
|
||||
showPageRange,
|
||||
sort = '-createdAt',
|
||||
} = props
|
||||
|
||||
const [results, setResults] = useState<Result>({
|
||||
docs: populatedDocs?.map((doc) => doc.value) || [],
|
||||
hasNextPage: false,
|
||||
hasPrevPage: false,
|
||||
nextPage: 1,
|
||||
page: 1,
|
||||
prevPage: 1,
|
||||
totalDocs: typeof populatedDocsTotal === 'number' ? populatedDocsTotal : 0,
|
||||
totalPages: 1,
|
||||
})
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | undefined>(undefined)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const hasHydrated = useRef(false)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const scrollToRef = useCallback(() => {
|
||||
const { current } = scrollRef
|
||||
if (current) {
|
||||
// current.scrollIntoView({
|
||||
// behavior: 'smooth',
|
||||
// })
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && typeof results.page !== 'undefined') {
|
||||
// scrollToRef()
|
||||
}
|
||||
}, [isLoading, scrollToRef, results])
|
||||
|
||||
useEffect(() => {
|
||||
// hydrate the block with fresh content after first render
|
||||
// don't show loader unless the request takes longer than x ms
|
||||
// and don't show it during initial hydration
|
||||
const timer = setTimeout(() => {
|
||||
if (hasHydrated) {
|
||||
setIsLoading(true)
|
||||
}
|
||||
}, 500)
|
||||
|
||||
const searchQuery = qs.stringify(
|
||||
{
|
||||
depth: 1,
|
||||
limit,
|
||||
page,
|
||||
sort,
|
||||
where: {
|
||||
...(catsFromProps && catsFromProps?.length > 0
|
||||
? {
|
||||
categories: {
|
||||
in:
|
||||
typeof catsFromProps === 'string'
|
||||
? [catsFromProps]
|
||||
: catsFromProps
|
||||
.map((cat) => (typeof cat === 'object' && cat !== null ? cat.id : cat))
|
||||
.join(','),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
{ encode: false },
|
||||
)
|
||||
|
||||
const makeRequest = async () => {
|
||||
try {
|
||||
const req = await fetch(`${PAYLOAD_SERVER_URL}/api/${relationTo}?${searchQuery}`)
|
||||
const json = await req.json()
|
||||
clearTimeout(timer)
|
||||
hasHydrated.current = true
|
||||
|
||||
const { docs } = json as { docs: Post[] }
|
||||
|
||||
if (docs && Array.isArray(docs)) {
|
||||
setResults(json)
|
||||
setIsLoading(false)
|
||||
if (typeof onResultChange === 'function') {
|
||||
onResultChange(json)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err) // eslint-disable-line no-console
|
||||
setIsLoading(false)
|
||||
setError(`Unable to load "${relationTo} archive" data at this time.`)
|
||||
}
|
||||
}
|
||||
|
||||
void makeRequest()
|
||||
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
}, [page, catsFromProps, relationTo, onResultChange, sort, limit])
|
||||
|
||||
return (
|
||||
<div className={[classes.collectionArchive, className].filter(Boolean).join(' ')}>
|
||||
<div className={classes.scrollRef} ref={scrollRef} />
|
||||
{!isLoading && error && <Gutter>{error}</Gutter>}
|
||||
<Fragment>
|
||||
{showPageRange !== false && (
|
||||
<Gutter>
|
||||
<div className={classes.pageRange}>
|
||||
<PageRange
|
||||
collection={relationTo}
|
||||
currentPage={results.page}
|
||||
limit={limit}
|
||||
totalDocs={results.totalDocs}
|
||||
/>
|
||||
</div>
|
||||
</Gutter>
|
||||
)}
|
||||
<Gutter>
|
||||
<div className={classes.grid}>
|
||||
{results.docs?.map((result, index) => {
|
||||
if (typeof result === 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.column} key={index}>
|
||||
<Card doc={result} relationTo="posts" showCategories />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{results.totalPages > 1 && (
|
||||
<Pagination
|
||||
className={classes.pagination}
|
||||
onClick={setPage}
|
||||
page={results.page}
|
||||
totalPages={results.totalPages}
|
||||
/>
|
||||
)}
|
||||
</Gutter>
|
||||
</Fragment>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { ArchiveBlockProps } from '../../../_blocks/ArchiveBlock/types.js'
|
||||
|
||||
import { Card } from '../../Card/index.js'
|
||||
import { Gutter } from '../../Gutter/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type Props = {
|
||||
className?: string
|
||||
selectedDocs?: ArchiveBlockProps['selectedDocs']
|
||||
}
|
||||
|
||||
export const CollectionArchiveBySelection: React.FC<Props> = (props) => {
|
||||
const { className, selectedDocs } = props
|
||||
|
||||
const result = selectedDocs?.map((doc) => doc.value)
|
||||
|
||||
return (
|
||||
<div className={[classes.collectionArchive, className].filter(Boolean).join(' ')}>
|
||||
<Fragment>
|
||||
<Gutter>
|
||||
<div className={classes.grid}>
|
||||
{result?.map((result, index) => {
|
||||
if (typeof result === 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.column} key={index}>
|
||||
<Card doc={result} relationTo="posts" showCategories />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Gutter>
|
||||
</Fragment>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { ArchiveBlockProps } from '../../_blocks/ArchiveBlock/types.js'
|
||||
|
||||
import { CollectionArchiveByCollection } from './PopulateByCollection/index.js'
|
||||
import { CollectionArchiveBySelection } from './PopulateBySelection/index.js'
|
||||
|
||||
export type Props = Omit<ArchiveBlockProps, 'blockType'> & {
|
||||
className?: string
|
||||
sort?: string
|
||||
}
|
||||
|
||||
export const CollectionArchive: React.FC<Props> = (props) => {
|
||||
const { className, populateBy, selectedDocs } = props
|
||||
|
||||
if (populateBy === 'selection') {
|
||||
return <CollectionArchiveBySelection className={className} selectedDocs={selectedDocs} />
|
||||
}
|
||||
|
||||
if (populateBy === 'collection') {
|
||||
return <CollectionArchiveByCollection {...props} className={className} />
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import React from 'react'
|
||||
|
||||
import { fetchFooter } from '../../_api/fetchFooter.js'
|
||||
import { Gutter } from '../Gutter/index.js'
|
||||
import { CMSLink } from '../Link/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
|
||||
|
||||
export async function Footer() {
|
||||
const footer = await fetchFooter()
|
||||
|
||||
const navItems = footer?.navItems || []
|
||||
|
||||
return (
|
||||
<footer className={classes.footer}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link href="/">
|
||||
<picture>
|
||||
<img
|
||||
alt="Payload Logo"
|
||||
className={classes.logo}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-light.svg"
|
||||
/>
|
||||
</picture>
|
||||
</Link>
|
||||
<nav className={classes.nav}>
|
||||
{navItems.map(({ link }, i) => {
|
||||
return <CMSLink key={i} {...link} />
|
||||
})}
|
||||
<Link href="/admin">Admin</Link>
|
||||
<Link href="https://github.com/payloadcms/payload/tree/main/templates/ecommerce">
|
||||
Source Code
|
||||
</Link>
|
||||
<Link href="https://github.com/payloadcms/payload">Payload</Link>
|
||||
</nav>
|
||||
</Gutter>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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'
|
||||
@@ -1,20 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import type { Header as HeaderType } from '../../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { CMSLink } from '../../Link/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const HeaderNav: React.FC<{ header: HeaderType }> = ({ header }) => {
|
||||
const navItems = header?.navItems || []
|
||||
|
||||
return (
|
||||
<nav className={classes.nav}>
|
||||
{navItems.map(({ link }, i) => {
|
||||
return <CMSLink key={i} {...link} />
|
||||
})}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import React from 'react'
|
||||
|
||||
import { fetchHeader } from '../../_api/fetchHeader.js'
|
||||
import { Gutter } from '../Gutter/index.js'
|
||||
import { HeaderNav } from './Nav/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
|
||||
|
||||
export async function Header() {
|
||||
const header = await fetchHeader()
|
||||
|
||||
return (
|
||||
<header className={classes.header}>
|
||||
<Gutter className={classes.wrap}>
|
||||
<Link href="/live-preview">
|
||||
<img
|
||||
alt="Payload Logo"
|
||||
className={classes.logo}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/admin/assets/images/payload-logo-dark.svg"
|
||||
/>
|
||||
</Link>
|
||||
<HeaderNav header={header} />
|
||||
</Gutter>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { HighImpactHero } from '../../_heros/HighImpact/index.js'
|
||||
import { LowImpactHero } from '../../_heros/LowImpact/index.js'
|
||||
|
||||
const heroes = {
|
||||
highImpact: HighImpactHero,
|
||||
lowImpact: LowImpactHero,
|
||||
}
|
||||
|
||||
export const Hero: React.FC<Page['hero']> = (props) => {
|
||||
const { type } = props || {}
|
||||
|
||||
if (!type || type === 'none') return null
|
||||
|
||||
const HeroToRender = heroes[type]
|
||||
|
||||
if (!HeroToRender) return null
|
||||
|
||||
return <HeroToRender {...props} />
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import React from 'react'
|
||||
|
||||
import type { Page, Post } from '../../../../test/live-preview/payload-types.js'
|
||||
import type { Props as ButtonProps } from '../Button/index.js'
|
||||
|
||||
import { Button } from '../Button/index.js'
|
||||
|
||||
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
|
||||
|
||||
type CMSLinkType = {
|
||||
appearance?: ButtonProps['appearance']
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
invert?: ButtonProps['invert']
|
||||
label?: string
|
||||
newTab?: boolean
|
||||
reference?: {
|
||||
relationTo: 'pages' | 'posts'
|
||||
value: Page | Post | string
|
||||
}
|
||||
type?: 'custom' | 'reference'
|
||||
url?: string
|
||||
}
|
||||
|
||||
export const CMSLink: React.FC<CMSLinkType> = ({
|
||||
type,
|
||||
appearance,
|
||||
children,
|
||||
className,
|
||||
invert,
|
||||
label,
|
||||
newTab,
|
||||
reference,
|
||||
url,
|
||||
}) => {
|
||||
const href =
|
||||
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
|
||||
? `/${reference.value.slug}`
|
||||
: url
|
||||
|
||||
if (!href) return null
|
||||
|
||||
if (!appearance) {
|
||||
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
|
||||
|
||||
if (href || url) {
|
||||
return (
|
||||
<Link {...newTabProps} className={className} href={href || url || ''}>
|
||||
{label && label}
|
||||
{children && children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
appearance={appearance}
|
||||
className={className}
|
||||
href={href}
|
||||
invert={invert}
|
||||
label={label}
|
||||
newTab={newTab}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { StaticImageData } from 'next/image.js'
|
||||
|
||||
import NextImageWithDefault from 'next/image.js'
|
||||
import React from 'react'
|
||||
|
||||
import type { Props as MediaProps } from '../types.js'
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
|
||||
import cssVariables from '../../../cssVariables.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const { breakpoints } = cssVariables
|
||||
|
||||
const NextImage = (NextImageWithDefault.default ||
|
||||
NextImageWithDefault) as typeof NextImageWithDefault.default
|
||||
|
||||
export const Image: React.FC<MediaProps> = (props) => {
|
||||
const {
|
||||
alt: altFromProps,
|
||||
fill,
|
||||
imgClassName,
|
||||
onClick,
|
||||
onLoad: onLoadFromProps,
|
||||
priority,
|
||||
resource,
|
||||
src: srcFromProps,
|
||||
} = props
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState(true)
|
||||
|
||||
let width: number | undefined
|
||||
let height: number | undefined
|
||||
let alt = altFromProps
|
||||
let src: StaticImageData | string = srcFromProps || ''
|
||||
|
||||
if (!src && resource && typeof resource !== 'string') {
|
||||
const {
|
||||
alt: altFromResource,
|
||||
filename: fullFilename,
|
||||
height: fullHeight,
|
||||
width: fullWidth,
|
||||
} = resource
|
||||
|
||||
width = fullWidth || undefined
|
||||
height = fullHeight || undefined
|
||||
alt = altFromResource
|
||||
|
||||
const filename = fullFilename
|
||||
|
||||
src = `${PAYLOAD_SERVER_URL}/api/media/file/${filename}`
|
||||
}
|
||||
|
||||
// NOTE: this is used by the browser to determine which image to download at different screen sizes
|
||||
const sizes = Object.entries(breakpoints)
|
||||
.map(([, value]) => `(max-width: ${value}px) ${value}px`)
|
||||
.join(', ')
|
||||
|
||||
return (
|
||||
<NextImage
|
||||
alt={alt || ''}
|
||||
className={[isLoading && classes.placeholder, classes.image, imgClassName]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
fill={fill}
|
||||
height={!fill ? height : undefined}
|
||||
onClick={onClick}
|
||||
onLoad={() => {
|
||||
setIsLoading(false)
|
||||
if (typeof onLoadFromProps === 'function') {
|
||||
onLoadFromProps()
|
||||
}
|
||||
}}
|
||||
priority={priority}
|
||||
sizes={sizes}
|
||||
src={src}
|
||||
width={!fill ? width : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
|
||||
import type { Props as MediaProps } from '../types.js'
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from '../../../_api/serverURL.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const Video: React.FC<MediaProps> = (props) => {
|
||||
const { onClick, resource, videoClassName } = props
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
// const [showFallback] = useState<boolean>()
|
||||
|
||||
useEffect(() => {
|
||||
const { current: video } = videoRef
|
||||
if (video) {
|
||||
video.addEventListener('suspend', () => {
|
||||
// setShowFallback(true);
|
||||
// console.warn('Video was suspended, rendering fallback image.')
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (resource && typeof resource !== 'string') {
|
||||
const { filename } = resource
|
||||
|
||||
return (
|
||||
<video
|
||||
autoPlay
|
||||
className={[classes.video, videoClassName].filter(Boolean).join(' ')}
|
||||
controls={false}
|
||||
loop
|
||||
muted
|
||||
onClick={onClick}
|
||||
playsInline
|
||||
ref={videoRef}
|
||||
>
|
||||
<source src={`${PAYLOAD_SERVER_URL}/media/${filename}`} />
|
||||
</video>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { ElementType } from 'react'
|
||||
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Props } from './types.js'
|
||||
|
||||
import { Image } from './Image/index.js'
|
||||
import { Video } from './Video/index.js'
|
||||
|
||||
export const Media: React.FC<Props> = (props) => {
|
||||
const { className, htmlElement = 'div', resource } = props
|
||||
|
||||
const isVideo = typeof resource !== 'string' && resource?.mimeType?.includes('video')
|
||||
const Tag = (htmlElement as ElementType) || Fragment
|
||||
|
||||
return (
|
||||
<Tag
|
||||
{...(htmlElement !== null
|
||||
? {
|
||||
className,
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
{isVideo ? (
|
||||
<Video {...props} />
|
||||
) : (
|
||||
<Image {...props} /> // eslint-disable-line
|
||||
)}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { StaticImageData } from 'next/image'
|
||||
import type { ElementType, Ref } from 'react'
|
||||
|
||||
import type { Media as MediaType } from '../../../payload-types'
|
||||
|
||||
export interface Props {
|
||||
alt?: string
|
||||
className?: string
|
||||
fill?: boolean // for NextImage only
|
||||
htmlElement?: ElementType | null
|
||||
imgClassName?: string
|
||||
onClick?: () => void
|
||||
onLoad?: () => void
|
||||
priority?: boolean // for NextImage only
|
||||
ref?: Ref<HTMLImageElement | HTMLVideoElement | null>
|
||||
resource?: MediaType | string // for Payload media
|
||||
size?: string // for NextImage only
|
||||
src?: StaticImageData // for static media
|
||||
videoClassName?: string
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const defaultLabels = {
|
||||
plural: 'Docs',
|
||||
singular: 'Doc',
|
||||
}
|
||||
|
||||
const defaultCollectionLabels = {
|
||||
products: {
|
||||
plural: 'Products',
|
||||
singular: 'Product',
|
||||
},
|
||||
}
|
||||
|
||||
export const PageRange: React.FC<{
|
||||
className?: string
|
||||
collection?: string
|
||||
collectionLabels?: {
|
||||
plural?: string
|
||||
singular?: string
|
||||
}
|
||||
currentPage?: number
|
||||
limit?: number
|
||||
totalDocs?: number
|
||||
}> = (props) => {
|
||||
const {
|
||||
className,
|
||||
collection,
|
||||
collectionLabels: collectionLabelsFromProps,
|
||||
currentPage,
|
||||
limit,
|
||||
totalDocs,
|
||||
} = props
|
||||
|
||||
const indexStart = (currentPage ? currentPage - 1 : 1) * (limit || 1) + 1
|
||||
let indexEnd = (currentPage || 1) * (limit || 1)
|
||||
if (totalDocs && indexEnd > totalDocs) indexEnd = totalDocs
|
||||
|
||||
const { plural, singular } =
|
||||
collectionLabelsFromProps || defaultCollectionLabels[collection || ''] || defaultLabels || {}
|
||||
|
||||
return (
|
||||
<div className={[className, classes.pageRange].filter(Boolean).join(' ')}>
|
||||
{(typeof totalDocs === 'undefined' || totalDocs === 0) && 'Search produced no results.'}
|
||||
{typeof totalDocs !== 'undefined' &&
|
||||
totalDocs > 0 &&
|
||||
`Showing ${indexStart} - ${indexEnd} of ${totalDocs} ${totalDocs > 1 ? plural : singular}`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Chevron } from '../Chevron/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const Pagination: React.FC<{
|
||||
className?: string
|
||||
onClick: (page: number) => void
|
||||
page: number
|
||||
totalPages: number
|
||||
}> = (props) => {
|
||||
const { className, onClick, page, totalPages } = props
|
||||
const hasNextPage = page < totalPages
|
||||
const hasPrevPage = page > 1
|
||||
|
||||
return (
|
||||
<div className={[classes.pagination, className].filter(Boolean).join(' ')}>
|
||||
<button
|
||||
className={classes.button}
|
||||
disabled={!hasPrevPage}
|
||||
onClick={() => {
|
||||
onClick(page - 1)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Chevron className={classes.icon} rotate={90} />
|
||||
</button>
|
||||
<div className={classes.pageRange}>
|
||||
<span className={classes.pageRangeLabel}>
|
||||
Page {page} of {totalPages}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className={classes.button}
|
||||
disabled={!hasNextPage}
|
||||
onClick={() => {
|
||||
onClick(page + 1)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Chevron className={classes.icon} rotate={-90} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
import serializeLexical from './serializeLexical.js'
|
||||
import serializeSlate from './serializeSlate.js'
|
||||
|
||||
const RichText: React.FC<{
|
||||
className?: string
|
||||
content: any
|
||||
renderUploadFilenameOnly?: boolean
|
||||
serializer?: 'lexical' | 'slate'
|
||||
}> = ({ className, content, renderUploadFilenameOnly, serializer = 'slate' }) => {
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
|
||||
{serializer === 'slate'
|
||||
? serializeSlate(content, renderUploadFilenameOnly)
|
||||
: serializeLexical(content, renderUploadFilenameOnly)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RichText
|
||||
@@ -1,92 +0,0 @@
|
||||
import type { SerializedEditorState } from 'lexical'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { CMSLink } from '../Link/index.js'
|
||||
import { Media } from '../Media/index.js'
|
||||
|
||||
const serializer = (
|
||||
content?: SerializedEditorState['root']['children'],
|
||||
renderUploadFilenameOnly?: boolean,
|
||||
): React.ReactNode | React.ReactNode[] =>
|
||||
content?.map((node, i) => {
|
||||
switch (node.type) {
|
||||
case 'h1':
|
||||
return <h1 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h1>
|
||||
|
||||
case 'h2':
|
||||
return <h2 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h2>
|
||||
|
||||
case 'h3':
|
||||
return <h3 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h3>
|
||||
|
||||
case 'h4':
|
||||
return <h4 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h4>
|
||||
|
||||
case 'h5':
|
||||
return <h5 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h5>
|
||||
|
||||
case 'h6':
|
||||
return <h6 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h6>
|
||||
|
||||
case 'quote':
|
||||
return (
|
||||
<blockquote key={i}>
|
||||
{serializeLexical(node?.children, renderUploadFilenameOnly)}
|
||||
</blockquote>
|
||||
)
|
||||
|
||||
case 'ul':
|
||||
return <ul key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</ul>
|
||||
|
||||
case 'ol':
|
||||
return <ol key={i}>{serializeLexical(node.children, renderUploadFilenameOnly)}</ol>
|
||||
|
||||
case 'li':
|
||||
return <li key={i}>{serializeLexical(node.children, renderUploadFilenameOnly)}</li>
|
||||
|
||||
case 'relationship':
|
||||
return (
|
||||
<span key={i}>
|
||||
{node.value && typeof node.value === 'object'
|
||||
? node.value.title || node.value.id
|
||||
: node.value}
|
||||
</span>
|
||||
)
|
||||
|
||||
case 'link':
|
||||
return (
|
||||
<CMSLink
|
||||
key={i}
|
||||
newTab={Boolean(node?.newTab)}
|
||||
reference={node.doc as any}
|
||||
type={node.linkType === 'internal' ? 'reference' : 'custom'}
|
||||
url={node.url}
|
||||
>
|
||||
{serializer(node?.children, renderUploadFilenameOnly)}
|
||||
</CMSLink>
|
||||
)
|
||||
|
||||
case 'upload':
|
||||
if (renderUploadFilenameOnly) {
|
||||
return <span key={i}>{node.value.filename}</span>
|
||||
}
|
||||
|
||||
return <Media key={i} resource={node?.value} />
|
||||
|
||||
case 'paragraph':
|
||||
return <p key={i}>{serializer(node?.children, renderUploadFilenameOnly)}</p>
|
||||
|
||||
case 'text':
|
||||
return <span key={i}>{node.text}</span>
|
||||
}
|
||||
})
|
||||
|
||||
const serializeLexical = (
|
||||
content?: SerializedEditorState,
|
||||
renderUploadFilenameOnly?: boolean,
|
||||
): React.ReactNode | React.ReactNode[] => {
|
||||
return serializer(content?.root?.children, renderUploadFilenameOnly)
|
||||
}
|
||||
|
||||
export default serializeLexical
|
||||
@@ -1,131 +0,0 @@
|
||||
import escapeHTML from 'escape-html'
|
||||
import React, { Fragment } from 'react'
|
||||
import { Text } from 'slate'
|
||||
|
||||
import { CMSLink } from '../Link/index.js'
|
||||
import { Media } from '../Media/index.js'
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
type Children = Leaf[]
|
||||
|
||||
type Leaf = {
|
||||
[key: string]: unknown
|
||||
children?: Children
|
||||
type: string
|
||||
url?: string
|
||||
value?: any
|
||||
}
|
||||
|
||||
const serializeSlate = (
|
||||
children?: Children,
|
||||
renderUploadFilenameOnly?: boolean,
|
||||
): 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}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h1>
|
||||
|
||||
case 'h2':
|
||||
return <h2 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h2>
|
||||
|
||||
case 'h3':
|
||||
return <h3 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h3>
|
||||
|
||||
case 'h4':
|
||||
return <h4 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h4>
|
||||
|
||||
case 'h5':
|
||||
return <h5 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h5>
|
||||
|
||||
case 'h6':
|
||||
return <h6 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h6>
|
||||
|
||||
case 'quote':
|
||||
return (
|
||||
<blockquote key={i}>
|
||||
{serializeSlate(node?.children, renderUploadFilenameOnly)}
|
||||
</blockquote>
|
||||
)
|
||||
|
||||
case 'ul':
|
||||
return <ul key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</ul>
|
||||
|
||||
case 'ol':
|
||||
return <ol key={i}>{serializeSlate(node.children, renderUploadFilenameOnly)}</ol>
|
||||
|
||||
case 'li':
|
||||
return <li key={i}>{serializeSlate(node.children, renderUploadFilenameOnly)}</li>
|
||||
|
||||
case 'relationship':
|
||||
return (
|
||||
<span key={i}>
|
||||
{node.value && typeof node.value === 'object'
|
||||
? node.value.title || node.value.id
|
||||
: node.value}
|
||||
</span>
|
||||
)
|
||||
|
||||
case 'link':
|
||||
return (
|
||||
<CMSLink
|
||||
key={i}
|
||||
newTab={Boolean(node?.newTab)}
|
||||
reference={node.doc as any}
|
||||
type={node.linkType === 'internal' ? 'reference' : 'custom'}
|
||||
url={node.url}
|
||||
>
|
||||
{serializeSlate(node?.children, renderUploadFilenameOnly)}
|
||||
</CMSLink>
|
||||
)
|
||||
|
||||
case 'upload':
|
||||
if (renderUploadFilenameOnly) {
|
||||
return <span key={i}>{node.value.filename}</span>
|
||||
}
|
||||
|
||||
return <Media key={i} resource={node?.value} />
|
||||
|
||||
default:
|
||||
return <p key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</p>
|
||||
}
|
||||
}) || []
|
||||
|
||||
export default serializeSlate
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export type VerticalPaddingOptions = 'large' | 'medium' | 'none'
|
||||
|
||||
type Props = {
|
||||
bottom?: VerticalPaddingOptions
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
top?: VerticalPaddingOptions
|
||||
}
|
||||
|
||||
export const VerticalPadding: React.FC<Props> = ({
|
||||
bottom = 'medium',
|
||||
children,
|
||||
className,
|
||||
top = 'medium',
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={[className, classes[`top-${top}`], classes[`bottom-${bottom}`]]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
@use './queries.scss' as *;
|
||||
@use './colors.scss' as *;
|
||||
@use './type.scss' as *;
|
||||
|
||||
:root {
|
||||
--base: 24px;
|
||||
--font-body: system-ui;
|
||||
--font-mono: 'Roboto Mono', monospace;
|
||||
|
||||
--gutter-h: 180px;
|
||||
--block-padding: 120px;
|
||||
|
||||
--theme-text: var(--color-base-750);
|
||||
|
||||
@include large-break {
|
||||
--gutter-h: 144px;
|
||||
--block-padding: 96px;
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
--gutter-h: 24px;
|
||||
--block-padding: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
@extend %body;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
margin: 0;
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--color-success-500);
|
||||
color: var(--color-base-800);
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(--color-success-500);
|
||||
color: var(--color-base-800);
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@extend %h1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@extend %h2;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@extend %h3;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@extend %h4;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@extend %h5;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@extend %h6;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--base) 0;
|
||||
|
||||
@include mid-break {
|
||||
margin: calc(var(--base) * 0.75) 0;
|
||||
}
|
||||
}
|
||||
|
||||
#page-title {
|
||||
@extend %h6;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--base);
|
||||
margin: 0 0 var(--base);
|
||||
}
|
||||
|
||||
a {
|
||||
color: currentColor;
|
||||
|
||||
&:focus {
|
||||
opacity: 0.8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Page } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import { Media } from '../../_components/Media/index.js'
|
||||
import RichText from '../../_components/RichText/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const HighImpactHero: React.FC<Page['hero']> = ({ media, richText }) => {
|
||||
return (
|
||||
<Gutter className={classes.hero}>
|
||||
<div className={classes.content}>
|
||||
<RichText content={richText} />
|
||||
</div>
|
||||
<div className={classes.media}>
|
||||
{typeof media === 'object' && media !== null && (
|
||||
<Fragment>
|
||||
<Media
|
||||
// fill
|
||||
imgClassName={classes.image}
|
||||
priority
|
||||
resource={media}
|
||||
/>
|
||||
{media?.caption && <RichText className={classes.caption} content={media.caption} />}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { Page } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import RichText from '../../_components/RichText/index.js'
|
||||
import { VerticalPadding } from '../../_components/VerticalPadding/index.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
export const LowImpactHero: React.FC<Page['hero']> = ({ richText }) => {
|
||||
return (
|
||||
<Gutter className={classes.lowImpactHero}>
|
||||
<div className={classes.content}>
|
||||
<VerticalPadding>
|
||||
<RichText className={classes.richText} content={richText} />
|
||||
</VerticalPadding>
|
||||
</div>
|
||||
</Gutter>
|
||||
)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
@use '../../_css/common.scss' as *;
|
||||
|
||||
.postHero {
|
||||
display: flex;
|
||||
gap: calc(var(--base) * 2);
|
||||
|
||||
@include mid-break {
|
||||
flex-direction: column;
|
||||
gap: var(--base);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: var(--base);
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
gap: calc(var(--base) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin-bottom: calc(var(--base) * 1.5);
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.media {
|
||||
width: 50%;
|
||||
|
||||
@include mid-break {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.mediaWrapper {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
position: relative;
|
||||
aspect-ratio: 5 / 4;
|
||||
margin-bottom: calc(var(--base) / 2);
|
||||
width: calc(100% + calc(var(--gutter-h) / 2));
|
||||
|
||||
@include mid-break {
|
||||
margin-left: calc(var(--gutter-h) * -1);
|
||||
width: calc(100% + var(--gutter-h) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
background-color: var(--color-base-50);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.caption {
|
||||
color: var(--color-base-500);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import type { Post } from '../../../../test/live-preview/payload-types.js'
|
||||
|
||||
import { PAYLOAD_SERVER_URL } from '../../_api/serverURL.js'
|
||||
import { Gutter } from '../../_components/Gutter/index.js'
|
||||
import { Media } from '../../_components/Media/index.js'
|
||||
import RichText from '../../_components/RichText/index.js'
|
||||
import { formatDateTime } from '../../_utilities/formatDateTime.js'
|
||||
import classes from './index.module.scss'
|
||||
|
||||
const Link = (LinkWithDefault.default || LinkWithDefault) as typeof LinkWithDefault.default
|
||||
|
||||
export const PostHero: React.FC<{
|
||||
post: Post
|
||||
}> = ({ post }) => {
|
||||
const { id, createdAt, meta: { description, image: metaImage } = {} } = post
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Gutter className={classes.postHero}>
|
||||
<div className={classes.content}>
|
||||
<RichText className={classes.richText} content={post?.hero?.richText} />
|
||||
<p className={classes.meta}>
|
||||
{createdAt && (
|
||||
<Fragment>
|
||||
{'Created on '}
|
||||
{formatDateTime(createdAt)}
|
||||
</Fragment>
|
||||
)}
|
||||
</p>
|
||||
<div>
|
||||
<p className={classes.description}>
|
||||
{`${description ? `${description} ` : ''}To edit this post, `}
|
||||
<Link href={`${PAYLOAD_SERVER_URL}/admin/collections/posts/${id}`}>
|
||||
navigate to the admin dashboard
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.media}>
|
||||
<div className={classes.mediaWrapper}>
|
||||
{!metaImage && <div className={classes.placeholder}>No image</div>}
|
||||
{metaImage && typeof metaImage !== 'string' && (
|
||||
<Media fill imgClassName={classes.image} resource={metaImage} />
|
||||
)}
|
||||
</div>
|
||||
{metaImage && typeof metaImage !== 'string' && metaImage?.caption && (
|
||||
<RichText className={classes.caption} content={metaImage.caption} />
|
||||
)}
|
||||
</div>
|
||||
</Gutter>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Keep these in sync with the CSS variables in the `_css` directory
|
||||
export default {
|
||||
breakpoints: {
|
||||
s: 768,
|
||||
m: 1024,
|
||||
l: 1440,
|
||||
},
|
||||
colors: {
|
||||
base0: 'rgb(255, 255, 255)',
|
||||
base100: 'rgb(235, 235, 235)',
|
||||
base500: 'rgb(128, 128, 128)',
|
||||
base850: 'rgb(34, 34, 34)',
|
||||
base1000: 'rgb(0, 0, 0)',
|
||||
error500: 'rgb(255, 111, 118)',
|
||||
},
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { Footer } from './_components/Footer/index.js'
|
||||
import { Header } from './_components/Header/index.js'
|
||||
import './_css/app.scss'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: 'Payload Live Preview',
|
||||
title: 'Payload Live Preview',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { Gutter } from './_components/Gutter/index.js'
|
||||
import { VerticalPadding } from './_components/VerticalPadding/index.js'
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main>
|
||||
<VerticalPadding bottom="medium" top="none">
|
||||
<Gutter>
|
||||
<h1>404</h1>
|
||||
<p>This page could not be found.</p>
|
||||
</Gutter>
|
||||
</VerticalPadding>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import PageTemplate from './(pages)/[slug]/page.js'
|
||||
|
||||
export default PageTemplate
|
||||
@@ -1,5 +0,0 @@
|
||||
export const GET = () => {
|
||||
return Response.json({
|
||||
hello: 'elliot',
|
||||
})
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
module.exports = {
|
||||
// gitRawCommitsOpts: {
|
||||
// from: 'v2.0.9',
|
||||
// path: 'packages/payload',
|
||||
// },
|
||||
// infile: 'CHANGELOG.md',
|
||||
options: {
|
||||
preset: {
|
||||
name: 'conventionalcommits',
|
||||
types: [
|
||||
{ section: 'Features', type: 'feat' },
|
||||
{ section: 'Features', type: 'feature' },
|
||||
{ section: 'Bug Fixes', type: 'fix' },
|
||||
{ section: 'Documentation', type: 'docs' },
|
||||
],
|
||||
},
|
||||
},
|
||||
// outfile: 'NEW.md',
|
||||
writerOpts: {
|
||||
commitGroupsSort: (a, b) => {
|
||||
const groupOrder = ['Features', 'Bug Fixes', 'Documentation']
|
||||
return groupOrder.indexOf(a.title) - groupOrder.indexOf(b.title)
|
||||
},
|
||||
|
||||
// Scoped commits at the end, alphabetical sort
|
||||
commitsSort: (a, b) => {
|
||||
if (a.scope || b.scope) {
|
||||
if (!a.scope) return -1
|
||||
if (!b.scope) return 1
|
||||
return a.scope === b.scope
|
||||
? a.subject.localeCompare(b.subject)
|
||||
: a.scope.localeCompare(b.scope)
|
||||
}
|
||||
|
||||
// Alphabetical sort
|
||||
return a.subject.localeCompare(b.subject)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2,53 +2,5 @@
|
||||
title: Bundlers
|
||||
label: Bundlers
|
||||
order: 60
|
||||
desc: Bundlers are used to bundle the code that serves Payload's Admin Panel.
|
||||
---
|
||||
|
||||
Payload has two official bundlers, the [Webpack Bundler](/docs/admin/webpack) and the [Vite Bundler](/docs/admin/vite). You must install a bundler to use the admin panel.
|
||||
|
||||
##### Install a bundler
|
||||
|
||||
Webpack (recommended):
|
||||
|
||||
```text
|
||||
yarn add @payloadcms/bundler-webpack
|
||||
```
|
||||
|
||||
Vite (beta):
|
||||
|
||||
```text
|
||||
yarn add @payloadcms/bundler-vite
|
||||
```
|
||||
|
||||
##### Configure the bundler
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
// import { viteBundler } from '@payloadcms/bundler-vite'
|
||||
|
||||
export default buildConfig({
|
||||
// highlight-start
|
||||
admin: {
|
||||
bundler: webpackBundler(), // or viteBundler()
|
||||
},
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
### What are bundlers?
|
||||
|
||||
At their core, a bundler's main goal is to take a bunch of files and turn them into a few optimized files that you ship to the browser. The admin UI has a root `index.html` entry point, and from there the bundler traverses the dependency tree, bundling all of the files that are required from that point on.
|
||||
|
||||
Since the bundled file is sent to the browser, it can't include any server-only code. You will need to remove any server-only code from your admin UI before bundling it. You can learn more about [excluding server code](/docs/admin/excluding-server-code) section.
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Using environment variables in the admin UI</strong>
|
||||
<br />
|
||||
Bundles should not contain sensitive information. By default, Payload excludes env variables from
|
||||
the bundle. If you need to use env variables in your payload config, you need to prefix them with
|
||||
`PAYLOAD_PUBLIC_` to make them available to the client-side code.
|
||||
</Banner>
|
||||
desc: NEEDS TO BE WRITTEN
|
||||
---
|
||||
@@ -6,44 +6,44 @@ desc: Fully customize your Admin Panel by swapping in your own React components.
|
||||
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
While designing the Payload Admin panel, we determined it should be as minimal and straightforward as possible to allow easy customization and control. There are many times where you may want to completely control how a whole view or a field works. You might even want to add in new views entirely. In order for Payload to support this level of customization without introducing versioning / future-proofing issues, Payload provides for a pattern to supply your own React components via your Payload config.
|
||||
While designing the Payload Admin panel, we determined it should be as minimal and straightforward as possible to allow easy customization and control. There are many times where you may want to completely control how a whole view or a field works. You might even want to add in your own routes entirely. In order for Payload to support that level of customization without introducing versioning / future-proofing issues, Payload provides for a pattern to supply your own React components via your Payload config.
|
||||
|
||||
To swap in your own React component, first, consult the list of available component overrides below. Determine the scope that corresponds to what you are trying to accomplish, and then author your React component accordingly.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
<br />
|
||||
Custom components will automatically be provided with all props that the default component
|
||||
normally accepts.
|
||||
Custom components will automatically be provided with all props that the default component would
|
||||
accept.
|
||||
</Banner>
|
||||
|
||||
### Base Component Overrides
|
||||
|
||||
You can override a set of admin panel-wide components by providing a component to your base Payload config's `admin.components` property. The following options are available:
|
||||
|
||||
| Path | Description |
|
||||
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Nav`** | Contains the sidebar / mobile menu in its entirety. |
|
||||
| **`BeforeNavLinks`** | Array of components to inject into the built-in Nav, _before_ the links themselves. |
|
||||
| **`AfterNavLinks`** | Array of components to inject into the built-in Nav, _after_ the links. |
|
||||
| **`BeforeDashboard`** | Array of components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
|
||||
| **`AfterDashboard`** | Array of components to inject into the built-in Dashboard, _after_ the default dashboard contents. [Demo](https://github.com/payloadcms/payload/tree/main/test/admin/components/AfterDashboard/index.tsx) |
|
||||
| **`BeforeLogin`** | Array of components to inject into the built-in Login, _before_ the default login form. |
|
||||
| **`AfterLogin`** | Array of components to inject into the built-in Login, _after_ the default login form. |
|
||||
| **`logout.Button`** | A custom React component. |
|
||||
| **`graphics.Icon`** | Used as a graphic within the `Nav` component. Often represents a condensed version of a full logo. |
|
||||
| **`graphics.Logo`** | The full logo to be used in contexts like the `Login` view. |
|
||||
| **`providers`** | Define your own provider components that will wrap the Payload Admin UI. [More](#custom-providers) |
|
||||
| **`actions`** | Array of custom components to be rendered in the Payload Admin UI header, providing additional interactivity and functionality. |
|
||||
| **`views`** | Override or create new views within the Payload Admin UI. [More](#views) |
|
||||
| Path | Description |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Nav`** | Contains the sidebar and mobile Nav in its entirety. |
|
||||
| **`logout.Button`** | A custom React component. |
|
||||
| **`BeforeDashboard`** | Array of components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
|
||||
| **`AfterDashboard`** | Array of components to inject into the built-in Dashboard, _after_ the default dashboard contents. [Demo](https://github.com/payloadcms/payload/tree/master/test/admin/components/AfterDashboard/index.tsx) |
|
||||
| **`BeforeLogin`** | Array of components to inject into the built-in Login, _before_ the default login form. |
|
||||
| **`AfterLogin`** | Array of components to inject into the built-in Login, _after_ the default login form. |
|
||||
| **`BeforeNavLinks`** | Array of components to inject into the built-in Nav, _before_ the links themselves. |
|
||||
| **`AfterNavLinks`** | Array of components to inject into the built-in Nav, _after_ the links. |
|
||||
| **`views.Account`** | The Account view is used to show the currently logged in user's Account page. |
|
||||
| **`views.Dashboard`** | The main landing page of the Admin panel. |
|
||||
| **`graphics.Icon`** | Used as a graphic within the `Nav` component. Often represents a condensed version of a full logo. |
|
||||
| **`graphics.Logo`** | The full logo to be used in contexts like the `Login` view. |
|
||||
| **`routes`** | Define your own routes to add to the Payload Admin UI. [More](#custom-routes) |
|
||||
| **`providers`** | Define your own provider components that will wrap the Payload Admin UI. [More](#custom-providers) |
|
||||
|
||||
Here is a full example showing how to swap some of these components for your own.
|
||||
#### Full example:
|
||||
|
||||
`payload.config.js`
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
import {
|
||||
MyCustomNav,
|
||||
MyCustomLogo,
|
||||
@@ -51,7 +51,6 @@ import {
|
||||
MyCustomAccount,
|
||||
MyCustomDashboard,
|
||||
MyProvider,
|
||||
MyCustomAdminAction,
|
||||
} from './customComponents'
|
||||
|
||||
export default buildConfig({
|
||||
@@ -62,7 +61,6 @@ export default buildConfig({
|
||||
Icon: MyCustomIcon,
|
||||
Logo: MyCustomLogo,
|
||||
},
|
||||
actions: [MyCustomAdminAction],
|
||||
views: {
|
||||
Account: MyCustomAccount,
|
||||
Dashboard: MyCustomDashboard,
|
||||
@@ -73,101 +71,31 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
#### Views
|
||||
|
||||
You can easily swap entire views with your own by using the `admin.components.views` property. At the root level, Payload renders the following views by default, all of which can be overridden:
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | ----------------------------------------------------------------------------- |
|
||||
| **`Account`** | The Account view is used to show the currently logged in user's Account page. |
|
||||
| **`Dashboard`** | The main landing page of the Admin panel. |
|
||||
|
||||
To swap out any of these views, simply pass in your custom component to the `admin.components.views` property of your Payload config. For example:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Account: MyCustomAccountView,
|
||||
Dashboard: MyCustomDashboardView,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
For more granular control, pass a configuration object instead. Each view corresponds to its own `<Route />` component in [React Router v5](https://v5.reactrouter.com). Payload exposes all of the properties of React Router:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Component`** \* | Pass in the component that should be rendered when a user navigates to this route. |
|
||||
| **`path`** \* | React Router `path`. [See the React Router docs](https://v5.reactrouter.com/web/api/Route/path-string-string) for more info. |
|
||||
| **`exact`** | React Router `exact` property. [More](https://v5.reactrouter.com/web/api/Route/exact-bool) |
|
||||
| **`strict`** | React Router `strict` property. [More](https://v5.reactrouter.com/web/api/Route/strict-bool) |
|
||||
| **`sensitive`** | React Router `sensitive` property. [More](https://v5.reactrouter.com/web/api/Route/sensitive-bool) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
#### Adding new views
|
||||
|
||||
To add a _new_ view to the Admin Panel, simply add another key to the `views` object with at least a `path` and `Component` property. For example:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
MyCustomView: {
|
||||
Component: MyCustomView,
|
||||
path: '/my-custom-view',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
Routes are cascading. This means that unless explicitly given the `exact` property, they will
|
||||
match on URLs that simply _start_ with the route's path. This is helpful when creating catch-all
|
||||
routes in your application. Alternatively, you could define your nested route _before_ your parent
|
||||
route.
|
||||
</Banner>
|
||||
|
||||
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/main/test/admin/components)._
|
||||
|
||||
For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component).
|
||||
_For more examples regarding how to customize components, look at the following [examples](https://github.com/payloadcms/payload/tree/master/test/admin/components)._
|
||||
|
||||
### Collections
|
||||
|
||||
You can override components on a collection-by-collection basis via the `admin.components` property.
|
||||
You can override components on a Collection-by-Collection basis via each Collection's `admin` property.
|
||||
|
||||
| Path | Description |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`BeforeList`** | Array of components to inject _before_ the built-in List view |
|
||||
| **`BeforeListTable`** | Array of components to inject _before_ the built-in List view's table |
|
||||
| **`AfterList`** | Array of components to inject _after_ the built-in List view |
|
||||
| **`AfterListTable`** | Array of components to inject _after_ the built-in List view's table |
|
||||
| **`views.Edit`** | Used while a document within this Collection is being edited. |
|
||||
| **`views.List`** | The `List` view is used to render a paginated, filterable table of Documents in this Collection. |
|
||||
| **`edit.SaveButton`** | Replace the default `Save` button with a custom component. Drafts must be disabled |
|
||||
| **`edit.SaveDraftButton`** | Replace the default `Save Draft` button with a custom component. Drafts must be enabled and autosave must be disabled. |
|
||||
| **`edit.PublishButton`** | Replace the default `Publish` button with a custom component. Drafts must be enabled. |
|
||||
| **`edit.PreviewButton`** | Replace the default `Preview` button with a custom component. |
|
||||
| **`views`** | Override or create new views within the Payload Admin UI. [More](#collection-views) |
|
||||
| **`BeforeList`** | Array of components to inject _before_ the built-in List view |
|
||||
| **`BeforeListTable`** | Array of components to inject _before_ the built-in List view's table |
|
||||
| **`AfterListTable`** | Array of components to inject _after_ the built-in List view's table |
|
||||
| **`AfterList`** | Array of components to inject _after_ the built-in List view |
|
||||
|
||||
Here is a full example showing how to swap some of these components for your own:
|
||||
|
||||
`Collection.ts`
|
||||
#### Examples
|
||||
|
||||
```tsx
|
||||
import * as React from 'react'
|
||||
// Custom Buttons
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
CustomSaveButtonProps,
|
||||
CustomSaveDraftButtonProps,
|
||||
@@ -175,8 +103,8 @@ import {
|
||||
CustomPreviewButtonProps,
|
||||
} from 'payload/types'
|
||||
|
||||
export const CustomSaveButton: CustomSaveButtonProps = ({ DefaultButton, label, save }) => {
|
||||
return <DefaultButton label={label} save={save} />
|
||||
export const CustomSaveButton: CustomSaveButtonProps = ({ DefaultButton, label }) => {
|
||||
return <DefaultButton label={label} />
|
||||
}
|
||||
|
||||
export const CustomSaveDraftButton: CustomSaveDraftButtonProps = ({
|
||||
@@ -205,270 +133,56 @@ export const CustomPreviewButton: CustomPreviewButtonProps = ({
|
||||
}) => {
|
||||
return <DefaultButton label={label} disabled={disabled} preview={preview} />
|
||||
}
|
||||
|
||||
export const MyCollection: SanitizedCollectionConfig = {
|
||||
slug: 'my-collection',
|
||||
admin: {
|
||||
components: {
|
||||
edit: {
|
||||
SaveButton: CustomSaveButton,
|
||||
SaveDraftButton: CustomSaveDraftButton,
|
||||
PublishButton: CustomPublishButton,
|
||||
PreviewButton: CustomPreviewButton,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Collection views
|
||||
##### Custom Collection List View Example
|
||||
|
||||
To swap out entire views on collections, you can use the `admin.components.views` property on the collection's config. Payload renders the following views by default, all of which can be overridden:
|
||||
Collection.ts
|
||||
|
||||
| Property | Description |
|
||||
| ---------- | ------------------------------------------------------------------------- |
|
||||
| **`Edit`** | The Edit view is used to edit a single document for a given collection. |
|
||||
| **`List`** | The List view is used to show a list of documents for a given collection. |
|
||||
|
||||
To swap out any of these views, simply pass in your custom component to the `admin.components.views` property of your Payload config. This will replace the entire view, including the page breadcrumbs, title, tabs, etc, _as well as all nested routes_.
|
||||
|
||||
```ts
|
||||
// Collection.ts
|
||||
{
|
||||
// ...
|
||||
```tsx
|
||||
import { MyListComponent } from './MyListComponent';
|
||||
export const MyCollection: CollectionConfig = {
|
||||
slug: 'mycollection',
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: MyCustomEditView,
|
||||
List: MyCustomListView,
|
||||
List: MyListComponent,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
fields: [
|
||||
...
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
_For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component)._
|
||||
MyListComponent.tsx
|
||||
|
||||
**Customizing Nested Views within 'Edit' in Collections**
|
||||
|
||||
The `Edit` view in collections consists of several nested views, each serving a unique purpose. You can customize these nested views using the `admin.components.views.Edit` property in the collection's configuration. This approach allows you to replace specific nested views while keeping the overall structure of the `Edit` view intact, including the page breadcrumbs, title, tabs, etc.
|
||||
|
||||
Here's an example of how you can customize nested views within the `Edit` view in collections, including the use of the `actions` property:
|
||||
|
||||
```ts
|
||||
// Collection.ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
Default: {
|
||||
Component: MyCustomDefaultTab,
|
||||
actions: [CollectionEditButton], // Custom actions for the default edit view
|
||||
},
|
||||
API: {
|
||||
Component: MyCustomAPIView,
|
||||
actions: [CollectionAPIButton], // Custom actions for API view
|
||||
},
|
||||
LivePreview: {
|
||||
Component: MyCustomLivePreviewView,
|
||||
actions: [CollectionLivePreviewButton], // Custom actions for Live Preview
|
||||
},
|
||||
Version: {
|
||||
Component: MyCustomVersionView,
|
||||
actions: [CollectionVersionButton], // Custom actions for Version view
|
||||
},
|
||||
Versions: {
|
||||
Component: MyCustomVersionsView,
|
||||
actions: [CollectionVersionsButton], // Custom actions for Versions view
|
||||
},
|
||||
},
|
||||
List: {
|
||||
actions: [CollectionListButton],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { List, type Props } from 'payload/components/views/List' // Payload's default List view component and its props
|
||||
export const MyListComponent: React.FC<Props> = (props) => (
|
||||
<div>
|
||||
<p>
|
||||
Some text before the default list view component. If you just want to do that, you can also
|
||||
use the admin.components.list.BeforeList hook
|
||||
</p>
|
||||
<List {...props} />
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
**Adding New Tabs to 'Edit' View**
|
||||
|
||||
You can also add _new_ tabs to the `Edit` view by adding another key to the `components.views.Edit[key]` object with a `path` and `Component` property. See [Custom Tabs](#custom-tabs) for more information.
|
||||
|
||||
### Globals
|
||||
|
||||
As with Collections, you can override components on a global-by-global basis via the `admin.components` property.
|
||||
As with Collections, You can override components on a global-by-global basis via their `admin` property.
|
||||
|
||||
| Path | Description |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`elements.SaveButton`** | Replace the default `Save` button with a custom component. Drafts must be disabled |
|
||||
| **`elements.SaveDraftButton`** | Replace the default `Save Draft` button with a custom component. Drafts must be enabled and autosave must be disabled. |
|
||||
| **`elements.PublishButton`** | Replace the default `Publish` button with a custom component. Drafts must be enabled. |
|
||||
| **`elements.PreviewButton`** | Replace the default `Preview` button with a custom component. |
|
||||
| **`views`** | Override or create new views within the Payload Admin UI. [More](#global-views) |
|
||||
|
||||
#### Global views
|
||||
|
||||
To swap out views for globals, you can use the `admin.components.views` property on the global's config. Payload renders the following views by default, all of which can be overridden:
|
||||
|
||||
| Property | Description |
|
||||
| ---------- | ------------------------------------------------------------------- |
|
||||
| **`Edit`** | The Edit view is used to edit a single document for a given Global. |
|
||||
|
||||
To swap out any of these views, simply pass in your custom component to the `admin.components.views` property of your Payload config. This will replace the entire view, including the page breadcrumbs, title, and tabs, _as well as all nested views_.
|
||||
|
||||
```ts
|
||||
// Global.ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: MyCustomEditView,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
_For help on how to build your own custom view components, see [building a custom view component](#building-a-custom-view-component)._
|
||||
|
||||
**Customizing Nested Views within 'Edit' in Globals**
|
||||
|
||||
Similar to collections, Globals allow for detailed customization within the `Edit` view. This includes the ability to swap specific nested views while maintaining the overall structure of the `Edit` view. You can use the `admin.components.views.Edit` property in the Globals configuration to achieve this, and this will only replace the nested view, leaving the page breadcrumbs, title, and tabs intact.
|
||||
|
||||
Here's how you can customize nested views within the `Edit` view in Globals, including the use of the `actions` property:
|
||||
|
||||
```ts
|
||||
// Global.ts
|
||||
{
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
Default: {
|
||||
Component: MyCustomGlobalDefaultTab,
|
||||
actions: [GlobalEditButton], // Custom actions for the default edit view
|
||||
},
|
||||
API: {
|
||||
Component: MyCustomGlobalAPIView,
|
||||
actions: [GlobalAPIButton], // Custom actions for API view
|
||||
},
|
||||
LivePreview: {
|
||||
Component: MyCustomGlobalLivePreviewView,
|
||||
actions: [GlobalLivePreviewButton], // Custom actions for Live Preview
|
||||
},
|
||||
Version: {
|
||||
Component: MyCustomGlobalVersionView,
|
||||
actions: [GlobalVersionButton], // Custom actions for Version view
|
||||
},
|
||||
Versions: {
|
||||
Component: MyCustomGlobalVersionsView,
|
||||
actions: [GlobalVersionsButton], // Custom actions for Versions view
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
You can also add _new_ tabs to the `Edit` view by adding another key to the `components.views.Edit[key]` object with a `path` and `Component` property. See [Custom Tabs](#custom-tabs) for more information.
|
||||
|
||||
### Custom Tabs
|
||||
|
||||
You can easily swap individual collection or global edit views. To do this, pass an _object_ to the `admin.components.views.Edit` property of the config. Payload renders the following views by default, all of which can be overridden:
|
||||
|
||||
| Property | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Default`** | The Default view is the primary view in which your document is edited. |
|
||||
| **`Versions`** | The Versions view is used to view the version history of a single document. [More details](../versions) |
|
||||
| **`Version`** | The Version view is used to view a single version of a single document for a given collection. [More details](../versions). |
|
||||
| **`API`** | The API view is used to display the REST API JSON response for a given document. |
|
||||
| **`LivePreview`** | The LivePreview view is used to display the Live Preview interface. [More details](../live-preview) |
|
||||
|
||||
Here is an example:
|
||||
|
||||
```ts
|
||||
// Collection.ts or Global.ts
|
||||
export const MyCollection: SanitizedCollectionConfig = {
|
||||
slug: 'my-collection',
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
// You can also define `components.views.Edit` as a component, this will override _all_ nested views
|
||||
Default: MyCustomDefaultTab,
|
||||
Versions: MyCustomVersionsTab,
|
||||
Version: MyCustomVersionTab,
|
||||
API: MyCustomAPITab,
|
||||
LivePreview: MyCustomLivePreviewTab,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
To add a _new_ tab to the `Edit` view, simply add another key to `components.views.Edit[key]` with at least a `path` and `Component` property. For example:
|
||||
|
||||
```ts
|
||||
// `Collection.ts` or `Global.ts`
|
||||
export const MyCollection: SanitizedCollectionConfig = {
|
||||
slug: 'my-collection',
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
MyCustomTab: {
|
||||
Component: MyCustomTab,
|
||||
path: '/my-custom-tab',
|
||||
// You an swap the entire tab component out for your own
|
||||
Tab: MyCustomTab,
|
||||
},
|
||||
AnotherCustomView: {
|
||||
Component: AnotherCustomView,
|
||||
path: '/another-custom-view',
|
||||
// Or you can use the default tab component and just pass in your own label and href
|
||||
Tab: {
|
||||
label: 'Another Custom View',
|
||||
href: '/another-custom-view',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Building a custom view component
|
||||
|
||||
Your custom view components will be given all the props that a React Router `<Route />` typically would receive, as well as two props from Payload:
|
||||
|
||||
| Prop | Description |
|
||||
| ----------------------- | ---------------------------------------------------------------------------- |
|
||||
| **`user`** | The currently logged in user. Will be `null` if no user is logged in. |
|
||||
| **`canAccessAdmin`** \* | If the currently logged in user is allowed to access the admin panel or not. |
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
It's up to you to secure your custom views. If your view requires a user to be logged in or to
|
||||
have certain access rights, you should handle that within your view component yourself.
|
||||
</Banner>
|
||||
|
||||
#### Example
|
||||
|
||||
You can find examples of custom views in the [Payload source code `/test/admin/components/views` folder](https://github.com/payloadcms/payload/tree/main/test/admin/components/views). There, you'll find two custom views:
|
||||
|
||||
1. A custom view that uses the `DefaultTemplate`, which is the built-in Payload template that displays the sidebar and "eyebrow nav"
|
||||
1. A custom view that uses the `MinimalTemplate` - which is just a centered template used for things like logging in or out
|
||||
|
||||
To see how to pass in your custom views to create custom views of your own, take a look at the `admin.components.views` property of the [Payload test admin config](https://github.com/payloadcms/payload/blob/main/test/admin/config.ts).
|
||||
| Path | Description |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`views.Edit`** | Used while this Global is being edited. |
|
||||
| **`edit.SaveButton`** | Replace the default `Save` button with a custom component. Drafts must be disabled |
|
||||
| **`edit.SaveDraftButton`** | Replace the default `Save Draft` button with a custom component. Drafts must be enabled and autosave must be disabled. |
|
||||
| **`edit.PublishButton`** | Replace the default `Publish` button with a custom component. Drafts must be enabled. |
|
||||
| **`edit.PreviewButton`** | Replace the default `Preview` button with a custom component. |
|
||||
|
||||
### Fields
|
||||
|
||||
@@ -490,15 +204,6 @@ All Payload fields support the ability to swap in your own React components. So,
|
||||
| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. [More](#cell-component) |
|
||||
| **`Field`** | Swap out the field itself within all `Edit` views. [More](#field-component) |
|
||||
|
||||
As an alternative to replacing the entire Field component, you may want to keep the majority of the default Field component and only swap components within. This allows you to replace the **`Label`** or **`Error`** within a field component or add additional components inside the field with **`beforeInput`** or **`afterInput`**. **`beforeInput`** and **`afterInput`** are allowed in any fields that don't contain other fields, except [UI](/docs/fields/ui) and [Rich Text](/docs/fields/rich-text).
|
||||
|
||||
| Component | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Label`** | Override the default Label in the Field Component. [More](#label-component) |
|
||||
| **`Error`** | Override the default Label in the Field Component. [More](#error-component) |
|
||||
| **`beforeInput`** | An array of elements that will be added before `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
|
||||
| **`afterInput`** | An array of elements that will be added after `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
|
||||
|
||||
## Cell Component
|
||||
|
||||
These are the props that will be passed to your custom Cell to use in your own components.
|
||||
@@ -515,9 +220,7 @@ These are the props that will be passed to your custom Cell to use in your own c
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import type { Props } from 'payload/components/views/Cell'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'custom-cell'
|
||||
|
||||
const CustomCell: React.FC<Props> = (props) => {
|
||||
@@ -538,12 +241,14 @@ When swapping out the `Field` component, you'll be responsible for sending and r
|
||||
```tsx
|
||||
import { useField } from 'payload/components/forms'
|
||||
|
||||
const CustomTextField: React.FC<{ path: string }> = ({ path }) => {
|
||||
type Props = { path: string }
|
||||
|
||||
const CustomTextField: React.FC<Props> = ({ path }) => {
|
||||
// highlight-start
|
||||
const { value, setValue } = useField<string>({ path })
|
||||
const { value, setValue } = useField<Props>({ path })
|
||||
// highlight-end
|
||||
|
||||
return <input onChange={(e) => setValue(e.target.value)} value={value} />
|
||||
return <input onChange={(e) => setValue(e.target.value)} value={value.path} />
|
||||
}
|
||||
```
|
||||
|
||||
@@ -552,112 +257,46 @@ const CustomTextField: React.FC<{ path: string }> = ({ path }) => {
|
||||
components, including the <strong>useField</strong> hook, [click here](/docs/admin/hooks).
|
||||
</Banner>
|
||||
|
||||
## Label Component
|
||||
## Custom routes
|
||||
|
||||
These are the props that will be passed to your custom Label.
|
||||
You can easily add your own custom routes to the Payload Admin panel using the `admin.components.routes` property. Payload currently uses the extremely powerful React Router v5.x and custom routes support all the properties of the React Router `<Route />` component.
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`htmlFor`** | Property used to set `for` attribute for label. |
|
||||
| **`label`** | Label value provided in field, it can be used with i18n. |
|
||||
| **`required`** | A boolean value that represents if the field is required or not. |
|
||||
**Custom routes support the following properties:**
|
||||
|
||||
| Property | Description |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Component`** \* | Pass in the component that should be rendered when a user navigates to this route. |
|
||||
| **`path`** \* | React Router `path`. [See the React Router docs](https://v5.reactrouter.com/web/api/Route/path-string-string) for more info. |
|
||||
| **`exact`** | React Router `exact` property. [More](https://v5.reactrouter.com/web/api/Route/exact-bool) |
|
||||
| **`strict`** | React Router `strict` property. [More](https://v5.reactrouter.com/web/api/Route/strict-bool) |
|
||||
| **`sensitive`** | React Router `sensitive` property. [More](https://v5.reactrouter.com/web/api/Route/sensitive-bool) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
|
||||
#### Custom route components
|
||||
|
||||
Your custom route components will be given all the props that a React Router `<Route />` typically would receive, as well as two props from Payload:
|
||||
|
||||
| Prop | Description |
|
||||
| ----------------------- | ---------------------------------------------------------------------------- |
|
||||
| **`user`** | The currently logged in user. Will be `null` if no user is logged in. |
|
||||
| **`canAccessAdmin`** \* | If the currently logged in user is allowed to access the admin panel or not. |
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
It's up to you to secure your custom routes. If your route requires a user to be logged in or to
|
||||
have certain access rights, you should handle that within your route component yourself.
|
||||
</Banner>
|
||||
|
||||
#### Example
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
You can find examples of custom route views in the [Payload source code `/test/admin/components/views` folder](https://github.com/payloadcms/payload/tree/master/test/admin/components/views). There, you'll find two custom routes:
|
||||
|
||||
import { getTranslation } from 'payload/utilities/getTranslation'
|
||||
1. A custom view that uses the `DefaultTemplate`, which is the built-in Payload template that displays the sidebar and "eyebrow nav"
|
||||
1. A custom view that uses the `MinimalTemplate` - which is just a centered template used for things like logging in or out
|
||||
|
||||
type Props = {
|
||||
htmlFor?: string
|
||||
label?: Record<string, string> | false | string
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
const CustomLabel: React.FC<Props> = (props) => {
|
||||
const { htmlFor, label, required = false } = props
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
if (label) {
|
||||
return (
|
||||
<span>
|
||||
{getTranslation(label, i18n)}
|
||||
{required && <span className="required">*</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
## Error Component
|
||||
|
||||
These are the props that will be passed to your custom Error.
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | ------------------------------------------------------------- |
|
||||
| **`message`** | The error message. |
|
||||
| **`showError`** | A boolean value that represents if the error should be shown. |
|
||||
|
||||
#### Example
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
message: string
|
||||
showError?: boolean
|
||||
}
|
||||
|
||||
const CustomError: React.FC<Props> = (props) => {
|
||||
const { message, showError } = props
|
||||
|
||||
if (showError) {
|
||||
return <p style={{ color: 'red' }}>{message}</p>
|
||||
} else return null
|
||||
}
|
||||
```
|
||||
|
||||
## afterInput and beforeInput
|
||||
|
||||
With these properties you can add multiple components before and after the input element. For example, you can add an absolutely positioned button to clear the current field value.
|
||||
|
||||
#### Example
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { Field } from 'payload/types'
|
||||
|
||||
import './style.scss'
|
||||
|
||||
const ClearButton: React.FC = () => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
/* ... */
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const titleField: Field = {
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
afterInput: [ClearButton],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default titleField
|
||||
```
|
||||
To see how to pass in your custom views to create custom routes of your own, take a look at the `admin.components.routes` property of the [Payload test admin config](https://github.com/payloadcms/payload/blob/master/test/admin/config.ts).
|
||||
|
||||
## Custom providers
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ To make it as easy as possible for you to override our styles, Payload uses [BEM
|
||||
|
||||
In addition to adding your own style definitions, you can also override Payload's built-in CSS variables. We use as much as possible behind the scenes, and you can override any of them that you'd like to.
|
||||
|
||||
You can find the built-in Payload CSS variables within [`./src/admin/scss/app.scss`](https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/scss/app.scss) and [`./src/admin/scss/colors.scss`](https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/scss/colors.scss). The following variables are defined and can be overridden:
|
||||
You can find the built-in Payload CSS variables within [`./src/admin/scss/app.scss`](https://github.com/payloadcms/payload/blob/master/src/admin/scss/app.scss) and [`./src/admin/scss/colors.scss`](https://github.com/payloadcms/payload/blob/master/src/admin/scss/colors.scss). The following variables are defined and can be overridden:
|
||||
|
||||
- Breakpoints
|
||||
- Base color shades (white to black by default)
|
||||
|
||||
@@ -5,6 +5,8 @@ order: 100
|
||||
desc: NEEDS TO BE WRITTEN
|
||||
---
|
||||
|
||||
TODO: expand on this and make sure it looks nice, talk about how if you use a `process.env.SERVER_URL`, it might not be usable in your Payload config in your admin UI, but it needs to be, so it should be prefixed like `process.env.PAYLOAD_PUBLIC_SERVER_URL` to work in both places
|
||||
|
||||
## Admin environment vars
|
||||
|
||||
<Banner type="warning">
|
||||
@@ -12,11 +14,10 @@ desc: NEEDS TO BE WRITTEN
|
||||
<br />
|
||||
Be careful about what variables you provide to your client-side code. Analyze every single one to
|
||||
make sure that you're not accidentally leaking anything that an attacker could exploit. Only keys
|
||||
that are safe for anyone to read in plain text should be provided to your Admin panel.
|
||||
that are safe to be available to everyone in plain text should be provided to your Admin panel.
|
||||
</Banner>
|
||||
|
||||
By default, `env` variables are **not** provided to the Admin panel for security and safety reasons.
|
||||
But, Payload provides you with a way to still provide `env` vars to your frontend code.
|
||||
By default, `env` variables are **not** provided to the Admin panel for security and safety reasons. But, Payload provides you with a way to still provide `env` vars to your frontend code.
|
||||
|
||||
**Payload will automatically supply any present `env` variables that are prefixed with `PAYLOAD_PUBLIC_` directly to the Admin panel.**
|
||||
|
||||
|
||||
@@ -2,205 +2,8 @@
|
||||
title: Excluding server-only code from admin UI
|
||||
label: Excluding server code
|
||||
order: 70
|
||||
desc: Learn how to exclude server-only code from the Payload Admin UI bundle
|
||||
desc: NEEDS TO BE WRITTEN
|
||||
---
|
||||
|
||||
Because the Admin Panel browser bundle includes your Payload Config file, files using server-only modules need to be excluded.
|
||||
It's common for your config to rely on server only modules to perform logic in access control functions, hooks, and other contexts.
|
||||
|
||||
Any file that imports a server-only module such as `fs`, `stripe`, `authorizenet`, `nodemailer`, etc. **cannot** be included in the browser bundle.
|
||||
|
||||
#### Example Scenario
|
||||
|
||||
Say we have a collection called `Subscriptions` that has a `beforeChange` hook that creates a Stripe subscription whenever a Subscription document is created in Payload.
|
||||
|
||||
**Collection config**:
|
||||
|
||||
```ts
|
||||
// collections/Subscriptions/index.ts
|
||||
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
import createStripeSubscription from './hooks/createStripeSubscription'
|
||||
|
||||
export const Subscription: CollectionConfig = {
|
||||
slug: 'subscriptions',
|
||||
hooks: {
|
||||
beforeChange: [createStripeSubscription],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'stripeSubscriptionID',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**Collection hook**:
|
||||
|
||||
```ts
|
||||
// collections/Subscriptions/hooks/createStripeSubscription.ts
|
||||
|
||||
// highlight-start
|
||||
import Stripe from 'stripe' // <-- server-only module
|
||||
// highlight-end
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
|
||||
|
||||
export const createStripeSubscription = async ({ data, operation }) => {
|
||||
if (operation === 'create') {
|
||||
const dataWithStripeID = { ...data }
|
||||
|
||||
// use Stripe to create a Stripe subscription
|
||||
const subscription = await stripe.subscriptions.create({
|
||||
// Configure the subscription accordingly
|
||||
})
|
||||
|
||||
// Automatically add the Stripe subscription ID
|
||||
// to the data that will be saved to this Subscription doc
|
||||
dataWithStripeID.stripeSubscriptionID = subscription.id
|
||||
|
||||
return dataWithStripeID
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="error">
|
||||
<strong>Warning:</strong>
|
||||
<br />
|
||||
The above code is NOT production-ready and should not be referenced to create Stripe
|
||||
subscriptions. Although creating a beforeChange hook is a completely valid spot to do things like
|
||||
create subscriptions, the code above is incomplete and insecure, meant for explanation purposes
|
||||
only.
|
||||
</Banner>
|
||||
|
||||
**As-is, this collection will prevent your Admin panel from bundling or loading correctly, because Stripe relies on some Node-only packages.**
|
||||
|
||||
#### How to fix this
|
||||
|
||||
You need to make sure that you use `alias`es to tell your bundler to import "safe" files vs. attempting to import any server-side code that you need to get rid of. Depending on your bundler (Webpack, Vite, etc.) the steps involved may be slightly different.
|
||||
|
||||
The basic idea is to create a file that exports an empty object, and then alias import paths of any files that import server-only modules to that empty object file.
|
||||
|
||||
This way when your bundler goes to import a file that contains server-only modules, it will instead import the empty object file, which will not break the browser bundle.
|
||||
|
||||
### Aliasing server-only modules
|
||||
|
||||
To remove files that contain server-only modules from your bundle, you can use an `alias`.
|
||||
|
||||
In the Subscriptions config file above, we are importing the hook like so:
|
||||
|
||||
```ts
|
||||
// collections/Subscriptions/index.ts
|
||||
|
||||
import createStripeSubscription from './hooks/createStripeSubscription'
|
||||
```
|
||||
|
||||
By default the browser bundle will now include all the code from that file and any files down the tree. We know that the file imports `stripe`.
|
||||
|
||||
To fix this, we need to alias the `createStripeSubscription` file to a different file that can safely be included in the browser bundle.
|
||||
|
||||
First, we will create a mock file to replace the server-only file when bundling:
|
||||
|
||||
```js
|
||||
// mocks/modules.js
|
||||
|
||||
export default {}
|
||||
|
||||
/**
|
||||
* NOTE: if you are destructuring an import
|
||||
* the mock file will need to export matching
|
||||
* variables as the destructured object.
|
||||
*
|
||||
* export const namedExport = {}
|
||||
*/
|
||||
```
|
||||
|
||||
Aliasing with [Webpack](/docs/admin/webpack) can be done by:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
|
||||
import { Subscriptions } from './collections/Subscriptions'
|
||||
|
||||
const mockModulePath = path.resolve(__dirname, 'mocks/emptyObject.js')
|
||||
const fullFilePath = path.resolve(
|
||||
__dirname,
|
||||
'collections/Subscriptions/hooks/createStripeSubscription',
|
||||
)
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Subscriptions],
|
||||
admin: {
|
||||
bundler: webpackBundler(),
|
||||
webpack: (config) => {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
// highlight-start
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
[fullFilePath]: mockModulePath,
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Aliasing with [Vite](/docs/admin/vite) can be done by:
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { viteBundler } from '@payloadcms/bundler-vite'
|
||||
|
||||
import { Subscriptions } from './collections/Subscriptions'
|
||||
|
||||
const mockModulePath = path.resolve(__dirname, 'mocks/emptyObject.js')
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Subscriptions],
|
||||
admin: {
|
||||
bundler: viteBundler(),
|
||||
vite: (incomingViteConfig) => {
|
||||
const existingAliases = incomingViteConfig?.resolve?.alias || {}
|
||||
let aliasArray: { find: string | RegExp; replacement: string }[] = []
|
||||
|
||||
// Pass the existing Vite aliases
|
||||
if (Array.isArray(existingAliases)) {
|
||||
aliasArray = existingAliases
|
||||
} else {
|
||||
aliasArray = Object.values(existingAliases)
|
||||
}
|
||||
|
||||
// highlight-start
|
||||
// Add your own aliases using the find and replacement keys
|
||||
// remember, vite aliases are exact-match only
|
||||
aliasArray.push({
|
||||
find: '../server-only-module',
|
||||
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js'),
|
||||
})
|
||||
// highlight-end
|
||||
|
||||
return {
|
||||
...incomingViteConfig,
|
||||
resolve: {
|
||||
...(incomingViteConfig?.resolve || {}),
|
||||
alias: aliasArray,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
- Talk about the theory behind using aliases to exclude files
|
||||
- Talk about environment variables maybe?
|
||||
@@ -126,7 +126,7 @@ You can send the following actions to the `dispatchFields` function.
|
||||
| **`REPLACE_STATE`** | Completely replaces form state |
|
||||
| **`UPDATE`** | Update any property of a specific field's state |
|
||||
|
||||
To see types for each action supported within the `dispatchFields` hook, check out the Form types [here](https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/components/forms/Form/types.ts).
|
||||
To see types for each action supported within the `dispatchFields` hook, check out the Form types [here](https://github.com/payloadcms/payload/blob/master/src/admin/components/forms/Form/types.ts).
|
||||
|
||||
### useForm
|
||||
|
||||
@@ -324,7 +324,7 @@ The `useForm` hook returns an object with the following properties: |
|
||||
},
|
||||
{
|
||||
drawerTitle: 'addFieldRow',
|
||||
drawerDescription: 'A useful method to programmatically add a row to an array or block field.',
|
||||
drawerDescription: 'A useful method to programtically add a row to an array or block field.',
|
||||
drawerSlug: 'addFieldRow',
|
||||
drawerContent: (
|
||||
<>
|
||||
@@ -347,7 +347,7 @@ The `useForm` hook returns an object with the following properties: |
|
||||
value: <strong><code>rowIndex</code></strong>,
|
||||
},
|
||||
{
|
||||
value: "The index of the row to add. If omitted, the row will be added to the end of the array.",
|
||||
value: "The index of the row to add",
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -434,7 +434,7 @@ export const CustomArrayManager = () => {
|
||||
},
|
||||
{
|
||||
drawerTitle: 'removeFieldRow',
|
||||
drawerDescription: 'A useful method to programmatically remove a row from an array or block field.',
|
||||
drawerDescription: 'A useful method to programtically remove a row from an array or block field.',
|
||||
drawerSlug: 'removeFieldRow',
|
||||
drawerContent: (
|
||||
<>
|
||||
@@ -531,7 +531,7 @@ export const CustomArrayManager = () => {
|
||||
},
|
||||
{
|
||||
drawerTitle: 'replaceFieldRow',
|
||||
drawerDescription: 'A useful method to programmatically replace a row from an array or block field.',
|
||||
drawerDescription: 'A useful method to programtically replace a row from an array or block field.',
|
||||
drawerSlug: 'replaceFieldRow',
|
||||
drawerContent: (
|
||||
<>
|
||||
@@ -635,43 +635,12 @@ export const CustomArrayManager = () => {
|
||||
]}
|
||||
/>
|
||||
|
||||
### useCollapsible
|
||||
|
||||
The `useCollapsible` hook allows you to control parent collapsibles:
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------ | --- |
|
||||
| **`collapsed`** | State of the collapsible. `true` if open, `false` if collapsed |
|
||||
| **`isVisible`** | If nested, determine if the nearest collapsible is visible. `true` if no parent is closed, `false` otherwise |
|
||||
| **`toggle`** | Toggles the state of the nearest collapsible |
|
||||
| **`withinCollapsible`** | Determine when you are within another collaspible | |
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
|
||||
import { useCollapsible } from 'payload/components/utilities'
|
||||
|
||||
const CustomComponent: React.FC = () => {
|
||||
const { collapsed, toggle } = useCollapsible()
|
||||
return (
|
||||
<div>
|
||||
<p className="field-type">I am {collapsed ? 'closed' : 'open'}</p>
|
||||
<button onClick={toggle} type="button">
|
||||
Toggle
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useDocumentInfo
|
||||
|
||||
The `useDocumentInfo` hook provides lots of information about the document currently being edited, including the following:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
|---------------------------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| **`collection`** | If the doc is a collection, its collection config will be returned |
|
||||
| **`global`** | If the doc is a global, its global config will be returned |
|
||||
| **`id`** | If the doc is a collection, its ID will be returned |
|
||||
@@ -789,81 +758,3 @@ const MyComponent: React.FC = () => {
|
||||
### usePreferences
|
||||
|
||||
Returns methods to set and get user preferences. More info can be found [here](https://payloadcms.com/docs/admin/preferences).
|
||||
|
||||
### useTheme
|
||||
|
||||
Returns the currently selected theme (`light`, `dark` or `auto`), a set function to update it and a boolean `autoMode`, used to determine if the theme value should be set automatically based on the user's device preferences.
|
||||
|
||||
```tsx
|
||||
import { useTheme } from 'payload/components/utilities'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const { autoMode, setTheme, theme } = useTheme()
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
The current theme is {theme} and autoMode is {autoMode}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))}
|
||||
>
|
||||
Toggle theme
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useTableColumns
|
||||
|
||||
Returns methods to manipulate table columns
|
||||
|
||||
```tsx
|
||||
import { useTableColumns } from 'payload/components/hooks'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const { setActiveColumns } = useTableColumns()
|
||||
|
||||
const resetColumns = () => {
|
||||
setActiveColumns(['id', 'createdAt', 'updatedAt'])
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<button type="button" onClick={resetColumns}>
|
||||
Reset columns
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useDocumentEvents
|
||||
|
||||
The `useDocumentEvents` hook provides a way of subscribing to cross-document events, such as updates made to nested documents within a drawer. This hook will report document events that are outside the scope of the document currently being edited. This hook provides the following:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`mostRecentUpdate`** | An object containing the most recently updated document. It contains the `entitySlug`, `id` (if collection), and `updatedAt` properties |
|
||||
| **`reportUpdate`** | A method used to report updates to documents. It accepts the same arguments as the `mostRecentUpdate` property. |
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
import { useDocumentEvents } from 'payload/components/hooks'
|
||||
|
||||
const ListenForUpdates: React.FC = () => {
|
||||
const { mostRecentUpdate } = useDocumentEvents()
|
||||
|
||||
return <span>{JSON.stringify(mostRecentUpdate)}</span>
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
Right now the `useDocumentEvents` hook only tracks recently updated documents, but in the future
|
||||
it will track more document-related events as needed, such as document creation, deletion, etc.
|
||||
</Banner>
|
||||
|
||||
@@ -8,8 +8,8 @@ keywords: admin, components, custom, customize, documentation, Content Managemen
|
||||
|
||||
Payload dynamically generates a beautiful, fully functional React admin panel to manage your data. It's extremely powerful and can be customized / extended upon easily by swapping in your own React components. You can add additional views, modify how built-in views look / work, swap out Payload branding for your client's, build your own field types and much more.
|
||||
|
||||
The Payload Admin panel can be bundled with our officially supported [Vite](/docs/admin/vite) and [webpack](/docs/admin/webpack) bundlers or you can integrate another bundler following our adapter pattern approach.
|
||||
When bundled, it is code-split, highly performant (even with 100+ fields), and written fully in TypeScript.
|
||||
TODO: change the sentence below to reference our adapter pattern, and officially supported bundlers, linking to Vite and Webpack docs pages specifically
|
||||
The Payload Admin panel is built with Webpack, code-split, highly performant (even with 100+ fields), and written fully in TypeScript.
|
||||
|
||||
<Banner type="success">
|
||||
The Admin panel is meant to be simple enough to give you a starting point but not bring too much
|
||||
@@ -28,25 +28,25 @@ When bundled, it is code-split, highly performant (even with 100+ fields), and w
|
||||
|
||||
All options for the Admin panel are defined in your base Payload config file.
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `bundler` | The bundler that you would like to use to bundle the admin panel. Officially supported bundlers: [Webpack](/docs/admin/webpack) and [Vite](/docs/admin/vite). |
|
||||
| `user` | The `slug` of a Collection that you want be used to log in to the Admin dashboard. [More](/docs/admin/overview#the-admin-user-collection) |
|
||||
| `buildPath` | Specify an absolute path for where to store the built Admin panel bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| `meta` | Base meta data to use for the Admin panel. Included properties are `titleSuffix`, `ogImage`, and `favicon`. |
|
||||
| `disable` | If set to `true`, the entire Admin panel will be disabled. |
|
||||
| `indexHTML` | Optionally replace the entirety of the `index.html` file used by the Admin panel. Reference the [base index.html file](https://github.com/payloadcms/payload/blob/main/packages/payload/src/admin/index.html) to ensure your replacement has the appropriate HTML elements. |
|
||||
| `css` | Absolute path to a stylesheet that you can use to override / customize the Admin panel styling. [More](/docs/admin/customizing-css). |
|
||||
| `scss` | Absolute path to a Sass variables / mixins stylesheet meant to override Payload styles to make for an easy re-skinning of the Admin panel. [More](/docs/admin/customizing-css#overriding-scss-variables). |
|
||||
| `dateFormat` | Global date format that will be used for all dates in the Admin panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `autoLogin` | Used to automate admin log-in for dev and demonstration convenience. [More](/docs/authentication/config). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
|
||||
| `components` | Component overrides that affect the entirety of the Admin panel. [More](/docs/admin/components) |
|
||||
| `webpack` | Customize the Webpack config that's used to generate the Admin panel. [More](/docs/admin/webpack) |
|
||||
| `vite` | Customize the Vite config that's used to generate the Admin panel. [More](/docs/admin/vite) |
|
||||
| `logoutRoute` | The route for the `logout` page. |
|
||||
| `inactivityRoute` | The route for the `logout` inactivity page. |
|
||||
TODO: add `vite` and `bundler` properties to the table below
|
||||
|
||||
| Option | Description |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `user` | The `slug` of a Collection that you want be used to log in to the Admin dashboard. [More](/docs/admin/overview#the-admin-user-collection) |
|
||||
| `buildPath` | Specify an absolute path for where to store the built Admin panel bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
|
||||
| `meta` | Base meta data to use for the Admin panel. Included properties are `titleSuffix`, `ogImage`, and `favicon`. |
|
||||
| `disable` | If set to `true`, the entire Admin panel will be disabled. |
|
||||
| `indexHTML` | Optionally replace the entirety of the `index.html` file used by the Admin panel. Reference the [base index.html file](https://github.com/payloadcms/payload/blob/master/src/admin/index.html) to ensure your replacement has the appropriate HTML elements. |
|
||||
| `css` | Absolute path to a stylesheet that you can use to override / customize the Admin panel styling. [More](/docs/admin/customizing-css). |
|
||||
| `scss` | Absolute path to a Sass variables / mixins stylesheet meant to override Payload styles to make for an easy re-skinning of the Admin panel. [More](/docs/admin/customizing-css#overriding-scss-variables). |
|
||||
| `dateFormat` | Global date format that will be used for all dates in the Admin panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
|
||||
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
|
||||
| `autoLogin` | Used to automate admin log-in for dev and demonstration convenience. [More](/docs/authentication/config). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
|
||||
| `components` | Component overrides that affect the entirety of the Admin panel. [More](/docs/admin/components) |
|
||||
| `webpack` | Customize the Webpack config that's used to generate the Admin panel. [More](/docs/admin/webpack) |
|
||||
| **`logoutRoute`** | The route for the `logout` page. |
|
||||
| **`inactivityRoute`** | The route for the `logout` inactivity page. |
|
||||
|
||||
### The Admin User Collection
|
||||
|
||||
|
||||
@@ -5,159 +5,11 @@ order: 90
|
||||
desc: NEEDS TO BE WRITTEN
|
||||
---
|
||||
|
||||
<Banner type="info">
|
||||
The Vite bundler is currently in beta. If you would like to help us test this package, we'd love
|
||||
to hear from you if you find any [bugs or issues](https://github.com/payloadcms/payload/issues/)!
|
||||
</Banner>
|
||||
- Show how to install official bundler
|
||||
- Give banner "warning" that this is in beta
|
||||
- Talk about how you should see performance improvements in both build times and HMR
|
||||
- Initial load might not be substantially faster, because Vite pre-bundles dependencies
|
||||
- Talk about the new `admin.vite` property
|
||||
- Talk about how for compatibility, the Vite config still automatically adds aliases that come in from the `admin.webpack` property
|
||||
|
||||
Payload has a Vite bundler that you can install and bundle the Admin Panel with. This is an alternative to the [Webpack](/docs/admin/webpack) bundler and might give some performance boosts to your development workflow.
|
||||
|
||||
To use Vite as your bundler, first you need to install the package:
|
||||
|
||||
```bash
|
||||
yarn add @payloadcms/bundler-vite
|
||||
```
|
||||
|
||||
Then you will need to add the [bundler](/docs/admin/bundlers) to your Payload config:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from '@payloadcms/config'
|
||||
import { viteBundler } from '@payloadcms/bundler-vite'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [],
|
||||
admin: {
|
||||
bundler: viteBundler(),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Vite works fundamentally differently than Webpack. In development mode, it will first pre-bundle any of your dependencies that are CommonJS-only, and then it'll leverage ESM directly in your browser for a better HMR experience.
|
||||
|
||||
It then uses Rollup to create production builds of your admin UI. With Vite, you should see a decent performance boost—especially after your first cold start. However, that first cold start might take a few more seconds.
|
||||
|
||||
<Banner type="warning">
|
||||
In most cases, Vite should work out of the box. But existing Payload plugins may need to make
|
||||
compatibility changes to support Vite.
|
||||
</Banner>
|
||||
|
||||
This is because Vite aliases work fundamentally differently than Webpack aliases, and Payload relies on aliasing server-only code out of the Payload config to ensure that the bundled admin JS works within your browser.
|
||||
|
||||
Here are the main differences between how Vite aliases work and how Webpack aliases work.
|
||||
|
||||
**Vite aliases do not work with absolute paths.**
|
||||
|
||||
In Vite, alias keys must <strong>exactly match</strong> a import paths. If you have 2 files that import the same server-only module, but have different import paths, you would need to add 2 aliases to support both import paths.
|
||||
|
||||
```ts
|
||||
// File A
|
||||
import serverOnlyModule from '../server-only-module'
|
||||
|
||||
// File B
|
||||
import serverOnlyModule from '../../server-only-module'
|
||||
|
||||
// payload.config.ts
|
||||
// You would need to add 2 aliases to support both import paths
|
||||
export const buildConfig({
|
||||
collections: [],
|
||||
admin: {
|
||||
bundler: viteBundler(),
|
||||
vite: (incomingViteConfig) => {
|
||||
const existingAliases = incomingViteConfig?.resolve?.alias || {};
|
||||
let aliasArray: { find: string | RegExp; replacement: string; }[] = [];
|
||||
|
||||
// Pass the existing Vite aliases
|
||||
if (Array.isArray(existingAliases)) {
|
||||
aliasArray = existingAliases;
|
||||
} else {
|
||||
aliasArray = Object.values(existingAliases);
|
||||
}
|
||||
|
||||
// Add your own aliases using the find and replacement keys
|
||||
aliasArray.push({
|
||||
find: '../server-only-module',
|
||||
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
|
||||
find: '../../server-only-module',
|
||||
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
|
||||
});
|
||||
|
||||
return {
|
||||
...incomingViteConfig,
|
||||
resolve: {
|
||||
...(incomingViteConfig?.resolve || {}),
|
||||
alias: aliasArray,
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Vite aliases do not get applied to pre-bundled dependencies.**
|
||||
|
||||
This especially affects plugins, as plugins will be pre-bundled by Vite using `esbuild`. To get around this and support Vite, plugin authors need to configure an alias to their plugin at the top level, so that the alias will work accordingly.
|
||||
|
||||
Here's an example. Say your plugin is called `payload-plugin-cool`. It's imported as follows:
|
||||
|
||||
```ts
|
||||
import { myCoolPlugin } from 'payload-plugin-cool'
|
||||
```
|
||||
|
||||
That plugin should create an alias to support Vite as follows:
|
||||
|
||||
```ts
|
||||
{
|
||||
// aliases go here
|
||||
find: 'payload-plugin-cool',
|
||||
replacement: path.resolve(__dirname, './my-admin-plugin.js')
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This will effectively alias the entire plugin and work with Vite. If the plugin requires admin-specific code, then the `./my-admin-plugin.js` alias target file should reflect any changes necessary to the admin UI that the main server-side plugin performs.
|
||||
|
||||
### Extending the Vite config
|
||||
|
||||
The Payload config supports a new property for plugins to be able to extend the Vite config specifically. That property exists on the main Payload config under `admin.vite`. You can check out the [Vite docs](https://vitejs.dev/config/shared-options.html) for more information on what you can do with the Vite config.
|
||||
|
||||
It's a function that takes a Vite config, and returns an updated Vite config. Here's an example:
|
||||
|
||||
```ts
|
||||
export const buildConfig({
|
||||
collections: [],
|
||||
admin: {
|
||||
bundler: viteBundler(),
|
||||
vite: (incomingViteConfig) => {
|
||||
const existingAliases = incomingViteConfig?.resolve?.alias || {};
|
||||
let aliasArray: { find: string | RegExp; replacement: string; }[] = [];
|
||||
|
||||
// Pass the existing Vite aliases
|
||||
if (Array.isArray(existingAliases)) {
|
||||
aliasArray = existingAliases;
|
||||
} else {
|
||||
aliasArray = Object.values(existingAliases);
|
||||
}
|
||||
|
||||
// Add your own aliases using the find and replacement keys
|
||||
aliasArray.push({
|
||||
find: '../server-only-module',
|
||||
replacement: path.resolve(__dirname, './path/to/browser-safe-module.js')
|
||||
});
|
||||
|
||||
return {
|
||||
...incomingViteConfig,
|
||||
resolve: {
|
||||
...(incomingViteConfig?.resolve || {}),
|
||||
alias: aliasArray,
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Learn more about [aliasing server-only modules](https://payloadcms.com/docs/admin/excluding-server-code#aliasing-server-only-modules).
|
||||
|
||||
Even though there is a new property for Vite configs specifically, we have implemented some "compatibility" between Webpack and Vite out-of-the-box.
|
||||
|
||||
If your config specifies Webpack aliases, we attempt to leverage them automatically within the Vite config. They are merged into the Vite alias configuration seamlessly and may work out-of-the-box.
|
||||
- Aliases work differently in Vite vs. Webpack (you can't pass Vite an absolute path to your alias source, it needs to match perfectly)
|
||||
|
||||
@@ -6,58 +6,181 @@ desc: The Payload admin panel uses Webpack 5 and supports many common functional
|
||||
keywords: admin, webpack, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
Payload has a Webpack (v5) bundler that you can build the Admin panel with. For now, we recommended using it because it is stable. If you are feeling a bit more adventurous you can give the [Vite](/docs/admin/vite) bundler a shot.
|
||||
TODO: rewrite this
|
||||
- Talk about how Webpack is stable, and should be used unless you are adventurous.
|
||||
- Show a code example about how to install and pass the Webpack bundler to the Payload config
|
||||
|
||||
Out of the box, the Webpack bundler supports common functionalities such as SCSS and Typescript, but there are many cases where you may need to add support for additional functionalities.
|
||||
Payload allows usage of Webpack 5 to build the Admin panel. It comes with support for many common functionalities such as SCSS and Typescript out of the box, but there are many cases where you may need to add support for additional functionalities.
|
||||
|
||||
#### Installation
|
||||
To use webpack as your bundler, you must import the `@payloadcms/bundler-webpack` package and define it as the bundler in your Payload config.
|
||||
|
||||
```bash
|
||||
yarn add @payloadcms/bundler-webpack
|
||||
```
|
||||
To extend the Webpack config, add the `webpack` key to your base Payload config, and provide a function that accepts the default Webpack config as its only argument:
|
||||
|
||||
#### Import the bundler
|
||||
`payload.config.ts`
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
|
||||
export default buildConfig({
|
||||
// highlight-start
|
||||
admin: {
|
||||
bundler: webpackBundler(),
|
||||
},
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
### Extending Webpack
|
||||
|
||||
If you need to extend the Webpack config, you can do so by passing a function to the `admin.webpack` property on your Payload config.
|
||||
The function will receive the Webpack config as an argument and should return the modified config.
|
||||
|
||||
```ts
|
||||
// payload.config.ts
|
||||
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
bundler: webpackBundler()
|
||||
// highlight-start
|
||||
webpack: (config) => {
|
||||
// full control of the Webpack config
|
||||
// Do something with the config here
|
||||
|
||||
return config
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
bunder: webpackBundler(),
|
||||
```
|
||||
|
||||
TODO: move the aliasing section to its own page
|
||||
- Talk about how Webpack is stable, and should be used unless you are adventurous.
|
||||
- Show a code example about how to install and pass the Webpack bundler to the Payload config
|
||||
|
||||
### Aliasing server-only modules
|
||||
|
||||
A common use case for extending the Payload config is to alias server-only modules, thus preventing them from inclusion into the browser JavaScript bundle.
|
||||
|
||||
As the Payload config is used in both server **and** client contexts, you may find yourself writing code in your Payload config that may be incompatible with browser environments.
|
||||
|
||||
Examples of **non** browser-friendly packages:
|
||||
|
||||
- `fs`
|
||||
- `stripe`
|
||||
- `authorizenet`
|
||||
- `nodemailer`
|
||||
|
||||
You may rely on server-only packages such as the above to perform logic in access control functions, hooks, and other contexts (which is great!) but when you boot up your Payload app and try to view it in the browser, you'll likely run into missing dependency issues or other general incompatibilities.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
<br />
|
||||
To avoid problems with server code making it to your Webpack bundle, you can use the{' '}
|
||||
<strong>alias</strong> Webpack feature to tell Webpack to avoid importing the modules you want to
|
||||
restrict to server-only.
|
||||
</Banner>
|
||||
|
||||
<strong>
|
||||
For example, let's say that you have a Collection called `Subscriptions` which relies on Stripe:
|
||||
</strong>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
`collections/Subscriptions/index.js`
|
||||
|
||||
```ts
|
||||
import { CollectionConfig } from 'payload/types'
|
||||
import createStripeSubscription from './hooks/createStripeSubscription'
|
||||
|
||||
export const Subscription: CollectionConfig = {
|
||||
slug: 'subscriptions',
|
||||
hooks: {
|
||||
beforeChange: [createStripeSubscription],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'stripeSubscriptionID',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
The collection above features a `beforeChange` hook that creates a Stripe subscription whenever a Subscription document is created in Payload.
|
||||
|
||||
<strong>That hook might look something like this:</strong>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
`collections/Subscriptions/hooks/createStripeSubscription.js`
|
||||
|
||||
```js
|
||||
import Stripe from 'stripe'
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
|
||||
|
||||
const createStripeSubscription = async ({ data, operation }) => {
|
||||
if (operation === 'create') {
|
||||
const dataWithStripeID = { ...data }
|
||||
|
||||
// use Stripe to create a Stripe subscription
|
||||
const subscription = await stripe.subscriptions.create({
|
||||
// Configure the subscription accordingly
|
||||
})
|
||||
|
||||
// Automatically add the Stripe subscription ID
|
||||
// to the data that will be saved to this Subscription doc
|
||||
dataWithStripeID.stripeSubscriptionID = subscription.id
|
||||
|
||||
return dataWithStripeID
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export default createStripeSubscription
|
||||
```
|
||||
|
||||
<Banner type="error">
|
||||
<strong>Warning:</strong>
|
||||
<br />
|
||||
The above code is NOT production-ready and should not be referenced to create Stripe
|
||||
subscriptions. Although creating a beforeChange hook is a completely valid spot to do things like
|
||||
create subscriptions, the code above is incomplete and insecure, meant for explanation purposes
|
||||
only.
|
||||
</Banner>
|
||||
|
||||
**As-is, this collection will prevent your Admin panel from bundling or loading correctly, because Stripe relies on some Node-only packages.**
|
||||
|
||||
To remedy this issue you can extend the Payload Webpack config to alias your entire `createStripeSubscription` hook to a separate, empty mock file.
|
||||
|
||||
Example in `payload.config.js`:
|
||||
|
||||
```js
|
||||
import { buildConfig } from 'payload/config'
|
||||
import path from 'path'
|
||||
import Subscription from './collections/Subscription'
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
|
||||
const createStripeSubscriptionPath = path.resolve(
|
||||
__dirname,
|
||||
'collections/Subscription/hooks/createStripeSubscription.js',
|
||||
)
|
||||
const mockModulePath = path.resolve(__dirname, 'mocks/emptyObject.js')
|
||||
|
||||
export default buildConfig({
|
||||
collections: [Subscription],
|
||||
admin: {
|
||||
webpack: (config) => ({
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
[createStripeSubscriptionPath]: mockModulePath,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
bundler: webpackBundler(),
|
||||
})
|
||||
```
|
||||
|
||||
The above code will alias the file at path `createStripeSubscriptionPath` to a mocked module, which might look like this:
|
||||
|
||||
`mocks/emptyObject.js`
|
||||
|
||||
```js
|
||||
export default {}
|
||||
```
|
||||
|
||||
Now, when Webpack sees that you're attempting to import your `createStripeSubscriptionPath` file, it'll disregard that actual file and load your mock file instead. Not only will your Admin panel now bundle successfully, you will have optimized its filesize by removing unnecessary code! And you might have learned something about Webpack, too.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
<br />
|
||||
|
||||
@@ -45,14 +45,6 @@ To enable API keys on a collection, set the `useAPIKey` auth option to `true`. F
|
||||
your API keys will not be.
|
||||
</Banner>
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Important:</strong>
|
||||
If you change your `PAYLOAD_SECRET`, you will need to regenerate your API keys.
|
||||
<br />
|
||||
The secret key is used to encrypt the API keys, so if you change the secret, existing API keys will
|
||||
no longer be valid.
|
||||
</Banner>
|
||||
|
||||
#### Authenticating via API Key
|
||||
|
||||
To authenticate REST or GraphQL API requests using an API key, set the `Authorization` header. The header is case-sensitive and needs the slug of the `auth.useAPIKey` enabled collection, then " API-Key ", followed by the `apiKey` that has been assigned. Payload's built-in middleware will then assign the user document to `req.user` and handle requests with the proper access control. By doing this, Payload recognizes the request being made as a request by the user associated with that API key.
|
||||
|
||||
@@ -57,7 +57,12 @@ export const Admins: CollectionConfig = {
|
||||
name: 'role',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: ['user', 'admin', 'editor', 'developer'],
|
||||
options: [
|
||||
'user',
|
||||
'admin',
|
||||
'editor',
|
||||
'developer',
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -77,7 +82,7 @@ Once enabled, each document that is created within the Collection can be thought
|
||||
|
||||
### Token-based auth
|
||||
|
||||
Successfully logging in returns a `JWT` (JSON web token) which is how a user will identify themselves to Payload. By providing this JWT via either an HTTP-only cookie or an `Authorization: JWT` or `Authorization: Bearer` header, Payload will automatically identify the user and add its user JWT data to the Express `req`, which is available throughout Payload including within access control, hooks, and more.
|
||||
Successfully logging in returns a `JWT` (JSON web token) which is how a user will identify themselves to Payload. By providing this JWT via either an HTTP-only cookie or an `Authorization` header, Payload will automatically identify the user and add its user JWT data to the Express `req`, which is available throughout Payload including within access control, hooks, and more.
|
||||
|
||||
You can specify what data gets encoded to the JWT token by setting `saveToJWT` to true in your auth collection fields. If you wish to use a different key other than the field `name`, you can provide it to `saveToJWT` as a string. It is also possible to use `saveToJWT` on fields that are nested in inside groups and tabs. If a group has a `saveToJWT` set it will include the object with all sub-fields in the token. You can set `saveToJWT: false` for any fields you wish to omit. If a field inside a group has `saveToJWT` set, but the group does not, the field will be included at the top level of the token.
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ const app = express()
|
||||
const start = async () => {
|
||||
await payload.init({
|
||||
secret: 'PAYLOAD_SECRET_KEY',
|
||||
mongoURL: 'mongodb://localhost/payload',
|
||||
express: app,
|
||||
})
|
||||
|
||||
|
||||
@@ -30,26 +30,6 @@ Payload Cloud gives you S3 file storage backed by Cloudflare as a CDN, and this
|
||||
|
||||
AWS Cognito is used for authentication to your S3 bucket. The [Payload Cloud Plugin](https://github.com/payloadcms/plugin-cloud) will automatically pick up these values. These values are only if you'd like to access your files directly, outside of Payload Cloud.
|
||||
|
||||
#### Accessing Files Outside of Payload Cloud
|
||||
|
||||
If you'd like to access your files outside of Payload Cloud, you'll need to retrieve some values from your project's settings and put them into your environment variables. In Payload Cloud, navigate to the File Storage tab and copy the values using the copy button. Put these values in your .env file. Also copy the Cognito Password value separately and put into your .env file as well.
|
||||
|
||||
When you are done, you should have the following values in your .env file:
|
||||
|
||||
```env
|
||||
PAYLOAD_CLOUD=true
|
||||
PAYLOAD_CLOUD_ENVIRONMENT=prod
|
||||
PAYLOAD_CLOUD_COGNITO_USER_POOL_CLIENT_ID=
|
||||
PAYLOAD_CLOUD_COGNITO_USER_POOL_ID=
|
||||
PAYLOAD_CLOUD_COGNITO_IDENTITY_POOL_ID=
|
||||
PAYLOAD_CLOUD_PROJECT_ID=
|
||||
PAYLOAD_CLOUD_BUCKET=
|
||||
PAYLOAD_CLOUD_BUCKET_REGION=
|
||||
PAYLOAD_CLOUD_COGNITO_PASSWORD=
|
||||
```
|
||||
|
||||
The plugin will pick up these values and use them to access your files.
|
||||
|
||||
### Build Settings
|
||||
|
||||
You can update settings from your Project’s Settings tab. Changes to your build settings will trigger a redeployment of your project.
|
||||
@@ -63,13 +43,12 @@ From the Environment Variables page of the Settings tab, you can add, update and
|
||||
with `PAYLOAD_PUBLIC_`. Learn more
|
||||
[here](https://payloadcms.com/docs/admin/webpack#admin-environment-vars).
|
||||
</Banner>
|
||||
|
||||
### Custom Domains
|
||||
|
||||
With Payload Cloud, you can add custom domain names to your project. To do so, first go to the Domains page of the Settings tab of your project. Here you can see your default domain. To add a new domain, type in the domain name you wish to use.
|
||||
|
||||
<Banner>
|
||||
Note: do not include the protocol (http:// or https://) or any paths (/page). Only include the
|
||||
Note: do not include the protocol (http:// or https://) or any routes (/page). Only include the
|
||||
domain name and extension, and optionally a subdomain. - your-domain.com - backend.your-domain.com
|
||||
</Banner>
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ desc: Structure your Collections for your needs by defining fields, adding slugs
|
||||
keywords: collections, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
Payload Collections are defined through configs of their own, and you can define as many as your application needs. Each
|
||||
Collection will scaffold a new collection automatically in your database of choice, based on fields that you define.
|
||||
Payload Collections are defined through configs of their own, and you can define as many as your application needs. Each Collection will scaffold a new collection automatically in your database of choice, based on fields that you define.
|
||||
|
||||
It's often best practice to write your Collections in separate files and then import them into the main Payload config.
|
||||
|
||||
@@ -17,6 +16,7 @@ It's often best practice to write your Collections in separate files and then im
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`slug`** \* | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| **`fields`** \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
|
||||
| **`indexes`** \* | Array of database indexes to create, including compound indexes that have multiple fields. |
|
||||
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
|
||||
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
|
||||
@@ -29,6 +29,7 @@ It's often best practice to write your Collections in separate files and then im
|
||||
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. Set to `false` to disable GraphQL. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`defaultSort`** | Pass a top-level field to sort by default in the collection List view. Prefix the name of the field with a minus symbol ("-") to sort in descending order. |
|
||||
| **`pagination`** | Set pagination-specific options for this collection. [More](#pagination) |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
@@ -58,14 +59,11 @@ export const Orders: CollectionConfig = {
|
||||
|
||||
#### More collection config examples
|
||||
|
||||
You can find an assortment
|
||||
of [example collection configs](https://github.com/payloadcms/public-demo/tree/master/src/payload/collections) in the Public
|
||||
Demo source code on GitHub.
|
||||
You can find an assortment of [example collection configs](https://github.com/payloadcms/public-demo/tree/master/src/collections) in the Public Demo source code on GitHub.
|
||||
|
||||
### Admin options
|
||||
|
||||
You can customize the way that the Admin panel behaves on a collection-by-collection basis by defining the `admin`
|
||||
property on a collection's config.
|
||||
You can customize the way that the Admin panel behaves on a collection-by-collection basis by defining the `admin` property on a collection's config.
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -83,15 +81,12 @@ property on a collection's config.
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
|
||||
| `components` | Swap in your own React components to be used within this collection. [More](/docs/admin/components#collections) |
|
||||
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More](#list-searchable-fields) |
|
||||
| **`pagination`** | Set pagination-specific options for this collection. [More](#pagination) |
|
||||
|
||||
### Preview
|
||||
|
||||
Collection `admin` options can accept a `preview` function that will be used to generate a link pointing to the frontend
|
||||
of your app to preview data.
|
||||
Collection `admin` options can accept a `preview` function that will be used to generate a link pointing to the frontend of your app to preview data.
|
||||
|
||||
If the function is specified, a Preview button will automatically appear in the corresponding collection's Edit view.
|
||||
Clicking the Preview button will link to the URL that is generated by the function.
|
||||
If the function is specified, a Preview button will automatically appear in the corresponding collection's Edit view. Clicking the Preview button will link to the URL that is generated by the function.
|
||||
|
||||
**The preview function accepts two arguments:**
|
||||
|
||||
@@ -135,30 +130,21 @@ Here are a few options that you can specify options for pagination on a collecti
|
||||
|
||||
### Access control
|
||||
|
||||
You can specify extremely granular access control (what users can do with documents in a collection) on a collection by
|
||||
collection basis. To learn more, go to the [Access Control](/docs/access-control/overview) docs.
|
||||
You can specify extremely granular access control (what users can do with documents in a collection) on a collection by collection basis. To learn more, go to the [Access Control](/docs/access-control/overview) docs.
|
||||
|
||||
### Hooks
|
||||
|
||||
Hooks are a powerful way to extend collection functionality and execute your own logic, and can be defined on a
|
||||
collection by collection basis. To learn more, go to the [Hooks](/docs/hooks/overview) documentation.
|
||||
Hooks are a powerful way to extend collection functionality and execute your own logic, and can be defined on a collection by collection basis. To learn more, go to the [Hooks](/docs/hooks/overview) documentation.
|
||||
|
||||
### Field types
|
||||
|
||||
Collections support all field types that Payload has to offer—including simple fields like text and checkboxes all the
|
||||
way to more complicated layout-building field groups like Blocks. [Click here](/docs/fields/overview) to learn more
|
||||
about field types.
|
||||
Collections support all field types that Payload has to offer—including simple fields like text and checkboxes all the way to more complicated layout-building field groups like Blocks. [Click here](/docs/fields/overview) to learn more about field types.
|
||||
|
||||
### List Searchable Fields
|
||||
|
||||
In the List view, there is a "search" box that allows you to quickly find a document with a search. By default, it
|
||||
searches on the ID field. If you have `admin.useAsTitle` defined, the list search will use that field. However, you can
|
||||
define more than one field to search to make it easier on your admin editors to find the data they need.
|
||||
In the List view, there is a "search" box that allows you to quickly find a document with a search. By default, it searches on the ID field. If you have `admin.useAsTitle` defined, the list search will use that field. However, you can define more than one field to search to make it easier on your admin editors to find the data they need.
|
||||
|
||||
For example, let's say you have a Posts collection with `title`, `metaDescription`, and `tags` fields - and you want all
|
||||
three of those fields to be searchable in the List view. You can simply
|
||||
add `admin.listSearchableFields: ['title', 'metaDescription', 'tags']` - and the admin UI will automatically search on
|
||||
those three fields plus the ID field.
|
||||
For example, let's say you have a Posts collection with `title`, `metaDescription`, and `tags` fields - and you want all three of those fields to be searchable in the List view. You can simply add `admin.listSearchableFields: ['title', 'metaDescription', 'tags']` - and the admin UI will automatically search on those three fields plus the ID field.
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
@@ -167,6 +153,50 @@ those three fields plus the ID field.
|
||||
so your admin queries can remain performant.
|
||||
</Banner>
|
||||
|
||||
### Admin Hooks
|
||||
|
||||
In addition to collection hooks themselves, Payload provides for admin UI-specific hooks that you can leverage.
|
||||
|
||||
**`beforeDuplicate`**
|
||||
|
||||
The `beforeDuplicate` hook is an async function that accepts an object containing the data to duplicate, as well as the locale of the doc to duplicate. Within this hook, you can modify the data to be duplicated, which is useful in cases where you have unique fields that need to be incremented or similar, as well as if you want to automatically modify a document's `title`.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
import { BeforeDuplicate, CollectionConfig } from 'payload/types'
|
||||
// Your auto-generated Page type
|
||||
import { Page } from '../payload-types.ts'
|
||||
|
||||
const beforeDuplicate: BeforeDuplicate<Page> = ({ data }) => {
|
||||
return {
|
||||
...data,
|
||||
title: `${data.title} Copy`,
|
||||
uniqueField: data.uniqueField ? `${data.uniqueField}-copy` : '',
|
||||
}
|
||||
}
|
||||
|
||||
export const Page: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
admin: {
|
||||
hooks: {
|
||||
beforeDuplicate,
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'uniqueField',
|
||||
type: 'text',
|
||||
unique: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
|
||||
You can import collection types as follows:
|
||||
|
||||
@@ -59,20 +59,20 @@ export default Nav
|
||||
|
||||
#### Global config example
|
||||
|
||||
You can find a few [example Global configs](https://github.com/payloadcms/public-demo/tree/master/src/payload/globals) in the Public Demo source code on GitHub.
|
||||
You can find an [example Global config](https://github.com/payloadcms/public-demo/blob/master/src/globals/MainMenu.ts) in the Public Demo source code on GitHub.
|
||||
|
||||
### Admin options
|
||||
|
||||
You can customize the way that the Admin panel behaves on a Global-by-Global basis by defining the `admin` property on a Global's config.
|
||||
|
||||
| Option | Description |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `group` | Text used as a label for grouping collection and global links together in the navigation. |
|
||||
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this global from navigation and admin routing. |
|
||||
| `components` | Swap in your own React components to be used within this Global. [More](/docs/admin/components#globals) |
|
||||
| `preview` | Function to generate a preview URL within the Admin panel for this global that can point to your app. [More](#preview). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
|
||||
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. |
|
||||
| Option | Description |
|
||||
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `group` | Text used as a label for grouping collection and global links together in the navigation. |
|
||||
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this global from navigation and admin routing. |
|
||||
| `components` | Swap in your own React components to be used within this Global. [More](/docs/admin/components#globals) |
|
||||
| `preview` | Function to generate a preview URL within the Admin panel for this global that can point to your app. [More](#preview). |
|
||||
| `livePreview`| Enable real-time editing for instant visual feedback of your front-end application. [More](/docs/live-preview/overview). |
|
||||
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. |
|
||||
|
||||
### Preview
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ After a user logs in, they can change their language selection in the `/account`
|
||||
<strong>Note:</strong>
|
||||
<br />
|
||||
If there is a language that Payload does not yet support, we accept code
|
||||
[contributions](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md).
|
||||
[contributions](https://github.com/payloadcms/payload/blob/master/contributing.md).
|
||||
</Banner>
|
||||
|
||||
### Node Express
|
||||
|
||||
@@ -6,13 +6,11 @@ desc: Add and maintain as many locales as you need by adding Localization to you
|
||||
keywords: localization, internationalization, i18n, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
Payload features deep field-based localization support. Maintaining as many locales as you need is easy. All
|
||||
localization support is opt-in by default. To do so, follow the two steps below.
|
||||
Payload features deep field-based localization support. Maintaining as many locales as you need is easy. All localization support is opt-in by default. To do so, follow the two steps below.
|
||||
|
||||
### Enabling in the Payload config
|
||||
|
||||
Add the `localization` property to your Payload config to enable localization project-wide. You'll need to provide a
|
||||
list of all locales that you'd like to support as well as set a few other options.
|
||||
Add the `localization` property to your Payload config to enable localization project-wide. You'll need to provide a list of all locales that you'd like to support as well as set a few other options.
|
||||
|
||||
**Example Payload config set up for localization:**
|
||||
|
||||
@@ -59,97 +57,39 @@ export default buildConfig({
|
||||
})
|
||||
```
|
||||
|
||||
**Example Payload config set up for localization with full locales objects (
|
||||
including [internationalization](/docs/configuration/i18n) support):**
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
export default buildConfig({
|
||||
collections: [
|
||||
// collections go here
|
||||
],
|
||||
localization: {
|
||||
locales: [
|
||||
{
|
||||
label: {
|
||||
en: 'English', // English label
|
||||
nb: 'Engelsk', // Norwegian label
|
||||
},
|
||||
code: 'en',
|
||||
},
|
||||
{
|
||||
label: {
|
||||
en: 'Norwegian', // English label
|
||||
nb: 'Norsk', // Norwegian label
|
||||
},
|
||||
code: 'nb',
|
||||
},
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
fallback: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**Here is a brief explanation of each of the options available within the `localization` property:**
|
||||
|
||||
**`locales`**
|
||||
|
||||
Array-based list of all the languages that you would like to support. This can be an array containing strings for each
|
||||
language code you want your project to store and serve or objects with a `label`, a locale `code`, `rtl` (
|
||||
right-to-left), and `fallbackLocale` property. The locale codes do not need to be in any specific format. It's up to you
|
||||
to define how to represent your locales. Common patterns are to use two-letter ISO 639 language codes or four-letter
|
||||
language and country codes (ISO 3166‑1) such as `en-US`, `en-UK`, `es-MX`, etc.
|
||||
|
||||
### Locale Properties:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`code`** \* | Unique code to identify the language throughout the APIs for `locale` and `fallbackLocale` |
|
||||
| **`label`** | A string to use for the selector when choosing a language, or an object keyed on the i18n keys for different languages in use. |
|
||||
| **`rtl`** | A boolean that when true will make the admin UI display in Right-To-Left. |
|
||||
| **`fallbackLocale`** | The code for this language to fallback to when properties of a document are not present. |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
Array-based list of all locales that you would like to support. These can be strings of locale codes or objects with a `label`, a locale `code`, and the `rtl` (right-to-left) property. The locale codes do not need to be in any specific format. It's up to you to define how to represent your locales. Common patterns are to use two-letter ISO 639 language codes or four-letter language and country codes (ISO 3166‑1) such as `en-US`, `en-UK`, `es-MX`, etc.
|
||||
|
||||
**`defaultLocale`**
|
||||
|
||||
Required string that matches one of the locale codes from the array provided. By default, if no locale is specified,
|
||||
documents will be returned in this locale.
|
||||
Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale.
|
||||
|
||||
**`fallback`**
|
||||
|
||||
Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a
|
||||
localized value corresponding to the requested locale, then if this property is enabled, the document will automatically
|
||||
fall back to the fallback locale value. If this property is not enabled, the value will not be populated.
|
||||
Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated.
|
||||
|
||||
### Field by field localization
|
||||
|
||||
Payload localization works on a **field** level—not a document level. In addition to configuring the base Payload config
|
||||
to support localization, you need to specify each field that you would like to localize.
|
||||
Payload localization works on a **field** level—not a document level. In addition to configuring the base Payload config to support localization, you need to specify each field that you would like to localize.
|
||||
|
||||
**Here is an example of how to enable localization for a field:**
|
||||
|
||||
```js
|
||||
{
|
||||
name: 'title',
|
||||
type
|
||||
:
|
||||
'text',
|
||||
// highlight-start
|
||||
localized
|
||||
:
|
||||
true,
|
||||
type: 'text',
|
||||
// highlight-start
|
||||
localized: true,
|
||||
// highlight-end
|
||||
}
|
||||
```
|
||||
|
||||
With the above configuration, the `title` field will now be saved in the database as an object of all locales instead of
|
||||
a single string.
|
||||
With the above configuration, the `title` field will now be saved in the database as an object of all locales instead of a single string.
|
||||
|
||||
All field types with a `name` property support the `localized` property—even the more complex field types like `array`s
|
||||
and `block`s.
|
||||
All field types with a `name` property support the `localized` property—even the more complex field types like `array`s and `block`s.
|
||||
|
||||
<Banner>
|
||||
<strong>Note:</strong>
|
||||
@@ -171,8 +111,7 @@ and `block`s.
|
||||
|
||||
### Retrieving localized docs
|
||||
|
||||
When retrieving documents, you can specify which locale you'd like to receive as well as which fallback locale should be
|
||||
used.
|
||||
When retrieving documents, you can specify which locale you'd like to receive as well as which fallback locale should be used.
|
||||
|
||||
##### REST API
|
||||
|
||||
@@ -184,8 +123,7 @@ Specify your desired locale by providing the `locale` query parameter directly i
|
||||
|
||||
**`?fallback-locale=`**
|
||||
|
||||
Specify fallback locale to be used by providing the `fallback-locale` query parameter. This can be provided as either a
|
||||
valid locale as provided to your base Payload config, or `'null'`, `'false'`, or `'none'` to disable falling back.
|
||||
Specify fallback locale to be used by providing the `fallback-locale` query parameter. This can be provided as either a valid locale as provided to your base Payload config, or `'null'`, `'false'`, or `'none'` to disable falling back.
|
||||
|
||||
**Example:**
|
||||
|
||||
@@ -197,9 +135,7 @@ fetch('https://localhost:3000/api/pages?locale=es&fallback-locale=none');
|
||||
|
||||
In the GraphQL API, you can specify `locale` and `fallbackLocale` args to all relevant queries and mutations.
|
||||
|
||||
The `locale` arg will only accept valid locales, but locales will be formatted automatically as valid GraphQL enum
|
||||
values (dashes or special characters will be converted to underscores, spaces will be removed, etc.). If you are curious
|
||||
to see how locales are auto-formatted, you can use the [GraphQL playground](/docs/graphql/overview#graphql-playground).
|
||||
The `locale` arg will only accept valid locales, but locales will be formatted automatically as valid GraphQL enum values (dashes or special characters will be converted to underscores, spaces will be removed, etc.). If you are curious to see how locales are auto-formatted, you can use the [GraphQL playground](/docs/graphql/overview#graphql-playground).
|
||||
|
||||
The `fallbackLocale` arg will accept valid locales as well as `none` to disable falling back.
|
||||
|
||||
@@ -223,9 +159,7 @@ query {
|
||||
|
||||
##### Local API
|
||||
|
||||
You can specify `locale` as well as `fallbackLocale` within the Local API as well as properties on the `options`
|
||||
argument. The `locale` property will accept any valid locale, and the `fallbackLocale` property will accept any valid
|
||||
locale as well as `'null'`, `'false'`, `false`, and `'none'`.
|
||||
You can specify `locale` as well as `fallbackLocale` within the Local API as well as properties on the `options` argument. The `locale` property will accept any valid locale, and the `fallbackLocale` property will accept any valid locale as well as `'null'`, `'false'`, `false`, and `'none'`.
|
||||
|
||||
**Example:**
|
||||
|
||||
|
||||
@@ -19,53 +19,48 @@ Payload is a _config-based_, code-first CMS and application framework. The Paylo
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `admin` \* | Base Payload admin configuration. Specify bundler\*, custom components, control metadata, set the Admin user collection, and [more](/docs/admin/overview#admin-options). Required. |
|
||||
| `editor` \* | Rich Text Editor which will be used by richText fields. Required. |
|
||||
| `db` \* | Database Adapter which will be used by Payload. Read more [here](/docs/database/overview). Required. |
|
||||
| `serverURL` | A string used to define the absolute URL of your app including the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port |
|
||||
| `collections` | An array of all Collections that Payload will manage. To read more about how to define your collection configs, [click here](/docs/configuration/collections). |
|
||||
| `globals` | An array of all Globals that Payload will manage. For more on Globals and their configs, [click here](/docs/configuration/globals). |
|
||||
| `cors` | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. |
|
||||
| `localization` | Opt-in and control how Payload handles the translation of your content into multiple locales. [More](/docs/configuration/localization) |
|
||||
| `graphQL` | Manage GraphQL-specific functionality here. Define your own queries and mutations, manage query complexity limits, and [more](/docs/graphql/overview#graphql-options). |
|
||||
| `cookiePrefix` | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| `csrf` | A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. [More](/docs/authentication/overview#csrf-protection) |
|
||||
| `defaultDepth` | If a user does not specify `depth` while requesting a resource, this depth will be used. [More](/docs/getting-started/concepts#depth) |
|
||||
| `maxDepth` | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. |
|
||||
| `indexSortableFields` | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
| `upload` | Base Payload upload configuration. [More](/docs/upload/overview#payload-wide-upload-options). |
|
||||
| `routes` | Control the routing structure that Payload binds itself to. Specify `admin`, `api`, `graphQL`, and `graphQLPlayground`. |
|
||||
| `email` | Base email settings to allow Payload to generate email such as Forgot Password requests and other requirements. [More](/docs/email/overview#configuration) |
|
||||
| `express` | Express-specific middleware options such as compression and JSON parsing. [More](/docs/configuration/express) |
|
||||
| `debug` | Enable to expose more detailed error information. |
|
||||
| `telemetry` | Disable Payload telemetry by passing `false`. [More](/docs/configuration/overview#telemetry) |
|
||||
| `rateLimit` | Control IP-based rate limiting for all Payload resources. Used to prevent DDoS attacks and [more](/docs/production/preventing-abuse#rate-limiting-requests). |
|
||||
| `hooks` | Tap into Payload-wide hooks. [More](/docs/hooks/overview) |
|
||||
| `plugins` | An array of Payload plugins. [More](/docs/plugins/overview) |
|
||||
| `endpoints` | An array of custom API endpoints added to the Payload router. [More](/docs/rest-api/overview#custom-endpoints) |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
_\* An asterisk denotes that a property is required._
|
||||
| Option | Description |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `serverURL` | A string used to define the absolute URL of your app including the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port |
|
||||
| `collections` | An array of all Collections that Payload will manage. To read more about how to define your collection configs, [click here](/docs/configuration/collections). |
|
||||
| `cors` | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. |
|
||||
| `globals` | An array of all Globals that Payload will manage. For more on Globals and their configs, [click here](/docs/configuration/globals). |
|
||||
| `admin` | Base Payload admin configuration. Specify custom components, control metadata, set the Admin user collection, and [more](/docs/admin/overview#admin-options). |
|
||||
| `editor` | Default richText editor which will be used by richText fields. |
|
||||
| `localization` | Opt-in and control how Payload handles the translation of your content into multiple locales. [More](/docs/configuration/localization) |
|
||||
| `graphQL` | Manage GraphQL-specific functionality here. Define your own queries and mutations, manage query complexity limits, and [more](/docs/graphql/overview#graphql-options). |
|
||||
| `cookiePrefix` | A string that will be prefixed to all cookies that Payload sets. |
|
||||
| `csrf` | A whitelist array of URLs to allow Payload cookies to be accepted from as a form of CSRF protection. [More](/docs/authentication/overview#csrf-protection) |
|
||||
| `defaultDepth` | If a user does not specify `depth` while requesting a resource, this depth will be used. [More](/docs/getting-started/concepts#depth) |
|
||||
| `maxDepth` | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. |
|
||||
| `indexSortableFields` | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
| `upload` | Base Payload upload configuration. [More](/docs/upload/overview#payload-wide-upload-options). |
|
||||
| `routes` | Control the routing structure that Payload binds itself to. Specify `admin`, `api`, `graphQL`, and `graphQLPlayground`. |
|
||||
| `email` | Base email settings to allow Payload to generate email such as Forgot Password requests and other requirements. [More](/docs/email/overview#configuration) |
|
||||
| `express` | Express-specific middleware options such as compression and JSON parsing. [More](/docs/configuration/express) |
|
||||
| `debug` | Enable to expose more detailed error information. |
|
||||
| `telemetry` | Disable Payload telemetry by passing `false`. [More](/docs/configuration/overview#telemetry) |
|
||||
| `rateLimit` | Control IP-based rate limiting for all Payload resources. Used to prevent DDoS attacks and [more](/docs/production/preventing-abuse#rate-limiting-requests). |
|
||||
| `hooks` | Tap into Payload-wide hooks. [More](/docs/hooks/overview) |
|
||||
| `plugins` | An array of Payload plugins. [More](/docs/plugins/overview) |
|
||||
| `endpoints` | An array of custom API endpoints added to the Payload router. [More](/docs/rest-api/overview#custom-endpoints) |
|
||||
| `custom` | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
#### Simple example
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres' // beta
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
|
||||
import { viteBundler } from '@payloadcms/bundler-vite'
|
||||
import { webpackBundler } from '@payloadcms/bundler-webpack'
|
||||
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical' // beta
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { slateEditor } from '@payloadcms/richtext-slate'
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
bundler: webpackBundler(), // or viteBundler()
|
||||
},
|
||||
bundler: webpackBundler() // or viteBundler(),
|
||||
db: mongooseAdapter({}) // or postgresAdapter({}),
|
||||
editor: lexicalEditor({}) // or slateEditor({})
|
||||
collections: [
|
||||
@@ -108,7 +103,7 @@ export default buildConfig({
|
||||
|
||||
#### Full example config
|
||||
|
||||
You can see a full [example config](https://github.com/payloadcms/public-demo/blob/master/src/payload/payload.config.ts) in the Public Demo source code on GitHub.
|
||||
You can see a full [example config](https://github.com/payloadcms/public-demo/blob/master/src/payload.config.ts) in the Public Demo source code on GitHub.
|
||||
|
||||
### Using environment variables in your config
|
||||
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
---
|
||||
title: Migrations
|
||||
label: Migrations
|
||||
order: 20
|
||||
keywords: database, migrations, ddl, sql, mongodb, postgres, documentation, Content Management System, cms, headless, typescript, node, react, express
|
||||
desc: Payload features first-party database migrations all done in TypeScript.
|
||||
---
|
||||
|
||||
Payload exposes a full suite of migration controls available for your use. Migration commands are accessible via
|
||||
the `npm run payload` command in your project directory.
|
||||
|
||||
Ensure you have an npm script called "payload" in your `package.json` file.
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Banner>
|
||||
Note that you need to run Payload migrations through the package manager that you are using,
|
||||
because Payload should not be globally installed on your system.
|
||||
</Banner>
|
||||
|
||||
### Migration file contents
|
||||
|
||||
Payload stores all created migrations in a folder that you can specify. By default, migrations are stored
|
||||
in `./src/migrations`.
|
||||
|
||||
A migration file has two exports - an `up` function, which is called when a migration is executed, and a `down` function
|
||||
that will be called if for some reason the migration fails to complete successfully. The `up` function should contain
|
||||
all changes that you attempt to make within the migration, and the `down` should ideally revert any changes you make.
|
||||
|
||||
For an added level of safety, migrations should leverage Payload [transactions](/docs/database/transactions). Migration
|
||||
functions should make use of the `req` by adding it to the arguments of your payload local API calls such
|
||||
as `payload.create` and database adapter methods like `payload.db.create`.
|
||||
|
||||
Here is an example migration file:
|
||||
|
||||
```ts
|
||||
import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/your-db-adapter'
|
||||
|
||||
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
|
||||
// Perform changes to your database here.
|
||||
// You have access to `payload` as an argument, and
|
||||
// everything is done in TypeScript.
|
||||
}
|
||||
|
||||
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
|
||||
// Do whatever you need to revert changes if the `up` function fails
|
||||
}
|
||||
```
|
||||
|
||||
### Migrations Directory
|
||||
|
||||
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be
|
||||
stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your
|
||||
migrations directory by searching in common locations ie. `./src/migrations`, `./dist/migrations`, `./migrations`, etc.
|
||||
|
||||
All database adapters should implement similar migration patterns, but there will be small differences based on the
|
||||
adapter and its specific needs. Below is a list of all migration commands that should be supported by your database
|
||||
adapter.
|
||||
|
||||
## Commands
|
||||
|
||||
### Migrate
|
||||
|
||||
The `migrate` command will run any migrations that have not yet been run.
|
||||
|
||||
```text
|
||||
npm run payload migrate
|
||||
```
|
||||
|
||||
### Create
|
||||
|
||||
Create a new migration file in the migrations directory. You can optionally name the migration that will be created. By
|
||||
default, migrations will be named using a timestamp.
|
||||
|
||||
```text
|
||||
npm run payload migrate:create optional-name-here
|
||||
```
|
||||
|
||||
### Status
|
||||
|
||||
The `migrate:status` command will check the status of migrations and output a table of which migrations have been run,
|
||||
and which migrations have not yet run.
|
||||
|
||||
`payload migrate:status`
|
||||
|
||||
```text
|
||||
npm run payload migrate:status
|
||||
```
|
||||
|
||||
### Down
|
||||
|
||||
Roll back the last batch of migrations.
|
||||
|
||||
```text
|
||||
npm run payload migrate:down
|
||||
```
|
||||
|
||||
### Refresh
|
||||
|
||||
Roll back all migrations that have been run, and run them again.
|
||||
|
||||
```text
|
||||
npm run payload migrate:refresh
|
||||
```
|
||||
|
||||
### Reset
|
||||
|
||||
Roll back all migrations.
|
||||
|
||||
```text
|
||||
npm run payload migrate:reset
|
||||
```
|
||||
|
||||
### Fresh
|
||||
|
||||
Drops all entities from the database and re-runs all migrations from scratch.
|
||||
|
||||
```text
|
||||
npm run payload migrate:fresh
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user