Compare commits

..

13 Commits

Author SHA1 Message Date
Elliot DeNolf
ffa20aa7d0 chore(release): v3.0.0-beta.29 [skip ci] 2024-05-09 17:19:54 -04:00
Alessio Gravili
f7a2cf96b9 chore: properly working generated types within tests (#6288) 2024-05-09 17:12:51 -04:00
Alessio Gravili
cfeac79b99 feat!: fix non-functional custom RSC component handling, separate label and description props, fix non-functional label function handling (#6264)
Breaking Changes:

- Globals config: `admin.description` no longer accepts a custom component. You will have to move it to `admin.components.elements.Description`
- Collections config: `admin.description` no longer accepts a custom component. You will have to move it to `admin.components.edit.Description`
- All Fields: `field.admin.description` no longer accepts a custom component. You will have to move it to `field.admin.components.Description`
- Collapsible Field: `field.label` no longer accepts a custom component. You will have to move it to `field.admin.components.RowLabel`
- Array Field: `field.admin.components.RowLabel` no longer accepts strings or records
- If you are using our exported field components in your own app, their `labelProps` property has been stripped down and no longer contains the `label` and `required` prop. Those can now only be configured at the top-level
2024-05-09 17:12:01 -04:00
Elliot DeNolf
821bed0ea6 ci: all green (#6289) 2024-05-09 16:33:05 -04:00
Jacob Fletcher
9e9111666b chore(examples/live-preview): migrates to 3.0 (#6268) 2024-05-09 15:32:46 -04:00
David Velasco
5065322d31 fix(plugin-form-builder): resolve labelValue from LabelFunction (#5817) 2024-05-09 16:23:44 -03:00
Paul
ad4796cdb2 fix(plugin-form-builder): export types correctly (#6287) 2024-05-09 14:42:14 -03:00
Alessio Gravili
43b7ba82da chore: fix dev:generate-types not working (#6284) 2024-05-09 10:37:11 -04:00
Alessio Gravili
3785c79ac9 fix(templates): yarn install broken for new template installs (#6283) 2024-05-09 10:17:09 -04:00
Jarrod Flesch
4384e9eb0e chore: fixes cannot destructure property 'schema' issue (#6282) 2024-05-09 10:16:30 -04:00
Alessio Gravili
9364f8da2e fix(templates): blank-3.0: pin next version, as it was breaking new installs (#6281) 2024-05-09 10:05:38 -04:00
Elliot DeNolf
a4ef359660 chore: examples linting (#6269) 2024-05-08 14:58:57 -04:00
Elliot DeNolf
848c05f247 chore(deps): sync pnpm-lock.yaml 2024-05-08 14:37:48 -04:00
291 changed files with 22703 additions and 18816 deletions

View File

@@ -93,10 +93,10 @@ jobs:
pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm install
- name: List changed files
run: git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF}...origin/${GITHUB_HEAD_REF}
- name: Lint staged
run: npx lint-staged --diff="origin/${GITHUB_BASE_REF}...origin/${GITHUB_HEAD_REF}"
run: |
git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF}...${GITHUB_SHA}
npx lint-staged --diff="origin/${GITHUB_BASE_REF}...${GITHUB_SHA}"
build:
needs: changes
@@ -490,3 +490,18 @@ jobs:
yarn install
yarn build
yarn generate:types
all-green:
name: All Green
if: always()
runs-on: ubuntu-latest
needs:
- lint
- build
- tests-unit
- tests-int
- tests-e2e
steps:
- if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
run: exit 1

View File

@@ -12,3 +12,4 @@
tsconfig.json
packages/payload/*.js
packages/payload/*.d.ts
payload-types.ts

View File

@@ -7,7 +7,7 @@ keywords: live preview, frontend, react, next.js, vue, nuxt.js, svelte, hook, us
---
<Banner type="info">
If your front-end application is supports Server Components like the [Next.js App Router](https://nextjs.org/docs/app), etc., we suggest setting up [server-side Live Preview](./server).
If your front-end application supports Server Components like the [Next.js App Router](https://nextjs.org/docs/app), etc., we suggest setting up [server-side Live Preview](./server).
</Banner>
While using Live Preview, the Admin panel emits a new `window.postMessage` event every time your document has changed. Your front-end application can listen for these events and re-render accordingly.

View File

@@ -16,7 +16,7 @@ Server-side Live Preview works by making a roundtrip to the server every time yo
It is recommended that you enable [Autosave](../versions/autosave) alongside Live Preview to make the experience feel more responsive.
</Banner>
If your front-end application is built with [React](#react), you can use the `RefreshRouteOnChange` function that Payload provides and give it your own router refresh function. In the future, all other major frameworks like Vue and Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own router refresh component](#building-your-own-router-refresh-component) for more information.
If your front-end application is built with [React](#react), you can use the `RefreshRouteOnChange` function that Payload provides. In the future, all other major frameworks like Vue and Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own router refresh component](#building-your-own-router-refresh-component) for more information.
### React
@@ -39,6 +39,7 @@ import config from '../payload.config'
export default async function Page() {
const payload = await getPayloadHMR({ config })
const page = await payload.find({
collection: 'pages',
draft: true
@@ -47,7 +48,7 @@ export default async function Page() {
return (
<Fragment>
<RefreshRouteOnSave />
<h1>Hello, world!</h1>
<h1>{page.title}</h1>
</Fragment>
)
}
@@ -166,7 +167,7 @@ If you are noticing that updates feel less snappy than client-side Live Preview
versions: {
drafts: {
autosave: {
interval: 10,
interval: 375,
},
},
},

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: "typescript",
semi: false,
singleQuote: true,
trailingComma: "all",
arrowParens: "avoid",
};

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: "typescript",
semi: false,
singleQuote: true,
trailingComma: "all",
arrowParens: "avoid",
};

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: "typescript",
semi: false,
singleQuote: true,
trailingComma: "all",
arrowParens: "avoid",
};

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,6 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"semi": false
}

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -4,18 +4,21 @@ This is a [Next.js](https://nextjs.org) app using the [App Router](https://nextj
> This example uses the App Router, the latest API of Next.js. If your app is using the legacy [Pages Router](https://nextjs.org/docs/pages), check out the official [Pages Router Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-pages).
**IMPORTANT—This application runs on a different server as Payload and establishes a connection from another domain or port over HTTP.** For an integrated setup that runs on a single server and uses the [Local API](https://payloadcms.com/docs/local-api/overview#local-api), check out [how to serve Payload alongside Next.js](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). To learn more about this, check out [how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
## Getting Started
### Payload
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for authentication.
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for live preview.
### Next.js
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server
4. `pnpm dev`, `yarn dev`, or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload) for full details.

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -4,18 +4,21 @@ This is a [Next.js](https://nextjs.org) app using the [Pages Router](https://nex
> This example uses the Pages Router, the legacy API of Next.js. If your app is using the latest [App Router](https://nextjs.org/docs/app), check out the official [App Router Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/next-app).
**IMPORTANT—This application runs on a different server as Payload and establishes a connection from another domain or port over HTTP.** For an integrated setup that runs on a single server and uses the [Local API](https://payloadcms.com/docs/local-api/overview#local-api), check out [how to serve Payload alongside Next.js](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). To learn more about this, check out [how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
## Getting Started
### Payload
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for authentication.
First you'll need a running Payload app. There is one made explicitly for this example and [can be found here](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload). If you have not done so already, clone it down and follow the setup instructions there. This will provide all the necessary APIs that your Next.js app requires for live preview.
### Next.js
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server
4. `pnpm dev`, `yarn dev`, or `npm run dev` to start the server
5. `open http://localhost:3001` to see the result
Once running you will find a couple seeded pages on your local environment with some basic instructions. You can also start editing the pages by modifying the documents within Payload. See the [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload) for full details.

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
DATABASE_URI=mongodb://127.0.0.1/payload-example-live-preview
PAYLOAD_SECRET=ENTER-STRING-HERE
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3001
PAYLOAD_PUBLIC_SEED=true
PAYLOAD_DROP_DATABASE=true
PAYLOAD_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_SERVER_URL=http://localhost:3000

View File

@@ -0,0 +1,12 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp
playwright.config.ts
jest.config.js

View File

@@ -1,4 +1,15 @@
module.exports = {
extends: ['plugin:@next/next/core-web-vitals', '@payloadcms'],
ignorePatterns: ['**/payload-types.ts'],
overrides: [
{
extends: ['plugin:@typescript-eslint/disable-type-checked'],
files: ['*.js', '*.cjs', '*.json', '*.md', '*.yml', '*.yaml'],
},
],
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname,
},
root: true,
extends: ['@payloadcms'],
}

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -2,21 +2,31 @@
The [Payload Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview/payload) demonstrates how to implement [Live Preview](https://payloadcms.com/docs/live-preview) in [Payload](https://github.com/payloadcms/payload). With Live Preview you can render your front-end application directly within the Admin panel. As you type, your changes take effect in real-time. No need to save a draft or publish your changes.
There are various fully working front-ends made explicitly for this example, including:
**IMPORTANT—This example includes a fully integrated Next.js App Router front-end that runs on the same server as Payload.** If you are working on an application running on an entirely separate server, there are various fully working, separately running front-ends made explicitly for this example, including:
- [Next.js App Router](../next-app)
- [Next.js Pages Router](../next-pages)
Follow the instructions in each respective README to get started. If you are setting up Live Preview for another front-end, please consider contributing to this repo with your own example!
Those applications run directly alongside this one. Follow the instructions in each respective README to get started. If you are setting up authentication for another front-end, please consider contributing to this repo with your own example!
To learn more about this, [check out how Payload can be used in its various headless capacities](https://payloadcms.com/blog/the-ultimate-guide-to-using-nextjs-with-payload).
## Quick Start
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:3000/admin` to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
1. `cp .env.example .env` to copy the example environment variables
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
- Press `y` when prompted to seed the database
1. `open http://localhost:3000` to access the home page
1. `open http://localhost:3000/admin` to access the admin panel
- Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
@@ -64,9 +74,82 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
While using Live Preview, the Admin panel emits a new `window.postMessage` event every time a change is made to the document. Your front-end application can listen for these events and re-render accordingly.
### React
There are two ways to use Live Preview in your own application depending on whether your front-end framework supports server components:
If your front-end application is built with React or Next.js, use the [`useLivePreview`](#react) React hook that Payload provides.
- [Server-side Live Preview (suggested)](#server)
- [Client-side Live Preview](#client)
<Banner type="info">
We suggest using server-side Live Preview if your framework supports it, it is both simpler to setup and more performant to run than the client-side alternative.
</Banner>
### Server
> Server-side Live Preview is only for front-end frameworks that support the concept of Server Components, i.e. [React Server Components](https://react.dev/reference/rsc/server-components). If your front-end application is built with a client-side framework like the [Next.js Pages Router](https://nextjs.org/docs/pages), [React Router](https://reactrouter.com), [Vue 3](https://vuejs.org), etc., see [client-side Live Preview](#client).
Server-side Live Preview works by making a roundtrip to the server every time your document is saved, i.e. draft save, autosave, or publish. While using Live Preview, the Admin panel emits a new `window.postMessage` event which your front-end application can use to invoke this process. In Next.js, this means simply calling `router.refresh()` which will hydrate the HTML using new data straight from the [Local API](../local-api/overview).
If your server-side front-end application is built with [React](#react), you can use the `RefreshRouteOnChange` function that Payload provides. In the future, all other major frameworks like Vue and Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own router refresh component](https://payloadcms.com/docs/live-preview/server#building-your-own-router-refresh-component) for more information.
#### React
If your front-end application is built with server-side [React](https://react.dev), i.e. [Next.js App Router](https://nextjs.org/docs/app), you can use the `RefreshRouteOnSave` component that Payload provides and thread it your framework's refresh function.
First, install the `@payloadcms/live-preview-react` package:
```bash
npm install @payloadcms/live-preview-react
```
Then, render `RefreshRouteOnSave` anywhere in your `page.tsx`. Here's an example:
`page.tsx`:
```tsx
import { RefreshRouteOnSave } from './RefreshRouteOnSave.tsx'
import { getPayloadHMR } from '@payloadcms/next'
import config from '../payload.config'
export default async function Page() {
const payload = await getPayloadHMR({ config })
const page = await payload.find({
collection: 'pages',
draft: true,
})
return (
<Fragment>
<RefreshRouteOnSave />
<h1>{page.title}</h1>
</Fragment>
)
}
```
`RefreshRouteOnSave.tsx`:
```tsx
'use client'
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
import { useRouter } from 'next/navigation.js'
import React from 'react'
export const RefreshRouteOnSave: React.FC = () => {
const router = useRouter()
return <PayloadLivePreview refresh={router.refresh} serverURL={process.env.PAYLOAD_SERVER_URL} />
}
```
For more details on how to setup server-side Live Preview, see the [server-side Live Preview](https://payloadcms.com/docs/live-preview/server) docs.
### Client
> If your front-end application is supports Server Components like the [Next.js App Router](https://nextjs.org/docs/app), etc., we suggest setting up [server-side Live Preview](#server).
#### React
If your front-end application is built with client-side React such as Next.js Pages Router, React Router, etc., use the [`useLivePreview`](#react) React hook that Payload provides.
First, install the `@payloadcms/live-preview-react` package:
@@ -77,8 +160,8 @@ npm install @payloadcms/live-preview-react
Then, use the `useLivePreview` hook in your React component:
```tsx
'use client';
import { useLivePreview } from '@payloadcms/live-preview-react';
'use client'
import { useLivePreview } from '@payloadcms/live-preview-react'
import { Page as PageType } from '@/payload-types'
// Fetch the page in a server component, pass it to the client component, then thread it through the hook
@@ -95,13 +178,11 @@ export const PageClient: React.FC<{
depth: 2, // Ensure that the depth matches the request for `initialPage`
})
return (
<h1>{data.title}</h1>
)
return <h1>{data.title}</h1>
}
```
### JavaScript
#### JavaScript
In the future, all other major frameworks like Vue, Svelte, etc will be officially supported. If you are using any of these framework today, you can still integrate with Live Preview yourself using the tooling that Payload provides.
@@ -114,7 +195,7 @@ npm install @payloadcms/live-preview
Then, build your own hook:
```tsx
import { subscribe, unsubscribe } from '@payloadcms/live-preview';
import { subscribe, unsubscribe } from '@payloadcms/live-preview'
// Build your own hook to subscribe to the live preview events
// This function will handle everything for you like
@@ -125,6 +206,8 @@ import { subscribe, unsubscribe } from '@payloadcms/live-preview';
See [building your own Live Preview hook](https://payloadcms.com/docs/live-preview/frontend#building-your-own-hook) for more details.
For more details on how to setup client-side Live Preview, see the [client-side Live Preview](https://payloadcms.com/docs/live-preview/client) docs.
## Development
To spin up this example locally, follow the [Quick Start](#quick-start).

View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -0,0 +1,8 @@
import { withPayload } from '@payloadcms/next/withPayload'
/** @type {import('next').NextConfig} */
const nextConfig = {
// Your Next.js config here
}
export default withPayload(nextConfig)

View File

@@ -1,6 +0,0 @@
{
"$schema": "https://json.schemastore.org/nodemon.json",
"ext": "ts",
"exec": "ts-node src/server.ts -- -I",
"stdin": false
}

View File

@@ -1,49 +1,44 @@
{
"name": "payload-example-live-preview",
"description": "Payload Live Preview example.",
"version": "1.0.0",
"main": "dist/server.js",
"description": "Payload Live Preview example.",
"license": "MIT",
"main": "dist/server.js",
"scripts": {
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"seed": "rm -rf media && cross-env PAYLOAD_PUBLIC_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts ts-node src/server.ts",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
"build": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts NODE_OPTIONS=--no-deprecation next build",
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts && pnpm seed && cross-env NODE_OPTIONS=--no-deprecation next dev",
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"lint": "eslint src",
"lint:fix": "eslint --fix --ext .ts,.tsx src"
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"lint:fix": "eslint --fix --ext .ts,.tsx src",
"payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload",
"seed": "npm run payload migrate:fresh",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start"
},
"dependencies": {
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "latest",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"payload": "latest"
"@payloadcms/db-mongodb": "3.0.0-beta.28",
"@payloadcms/live-preview-react": "3.0.0-beta.28",
"@payloadcms/next": "3.0.0-beta.28",
"@payloadcms/richtext-slate": "3.0.0-beta.28",
"@payloadcms/ui": "3.0.0-beta.28",
"cross-env": "^7.0.3",
"next": "14.3.0-canary.7",
"payload": "3.0.0-beta.28",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.3"
},
"devDependencies": {
"@payloadcms/eslint-config": "^0.0.2",
"@types/express": "^4.17.9",
"@types/node": "18.11.3",
"@types/react": "18.0.21",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"eslint": "^8.19.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"nodemon": "^2.0.6",
"prettier": "^2.7.1",
"ts-node": "^9.1.1",
"typescript": "^4.8.4"
"@next/eslint-plugin-next": "^13.1.6",
"@payloadcms/eslint-config": "^1.1.1",
"@swc/core": "^1.4.14",
"@swc/types": "^0.1.6",
"@types/node": "^20.11.25",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"tsx": "^4.7.1",
"typescript": "5.4.4"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
'use client'
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
import { useRouter } from 'next/navigation.js'
import React from 'react'
export const RefreshRouteOnSave: React.FC = () => {
const router = useRouter()
return (
<PayloadLivePreview
refresh={() => router.refresh()}
serverURL={process.env.NEXT_PUBLIC_SERVER_URL || ''}
/>
)
}

View File

@@ -0,0 +1,3 @@
.page {
margin-top: calc(var(--base) * 2);
}

View File

@@ -0,0 +1,70 @@
/* eslint-disable no-restricted-exports */
import { getPayloadHMR } from '@payloadcms/next/utilities'
import { notFound } from 'next/navigation'
import React from 'react'
import { Fragment } from 'react'
import type { Page as PageType } from '../../../payload-types'
import config from '../../../payload.config'
import { Gutter } from '../_components/Gutter'
import RichText from '../_components/RichText'
import { RefreshRouteOnSave } from './RefreshRouteOnSave'
import classes from './index.module.scss'
interface PageParams {
params: { slug: string }
}
export default async function Page({ params: { slug = 'home' } }: PageParams) {
const payload = await getPayloadHMR({ config })
const pageRes = await payload.find({
collection: 'pages',
draft: true,
limit: 1,
where: {
slug: {
equals: slug,
},
},
})
const data = pageRes?.docs?.[0] as PageType | null
if (data === null) {
return notFound()
}
return (
<Fragment>
<RefreshRouteOnSave />
<main className={classes.page}>
<Gutter>
<RichText content={data?.richText} />
</Gutter>
</main>
</Fragment>
)
}
export async function generateStaticParams() {
const payload = await getPayloadHMR({ config })
const pagesRes = await payload.find({
collection: 'pages',
depth: 0,
draft: true,
limit: 100,
})
const pages = pagesRes?.docs
return pages.map(({ slug }) =>
slug !== 'home'
? {
slug,
}
: {},
) // eslint-disable-line function-paren-newline
}

View File

@@ -0,0 +1,55 @@
.button {
border: none;
cursor: pointer;
display: inline-flex;
justify-content: center;
background-color: transparent;
}
.content {
display: flex;
align-items: center;
justify-content: space-around;
svg {
margin-right: calc(var(--base) / 2);
width: var(--base);
height: var(--base);
}
}
.label {
text-align: center;
display: flex;
align-items: center;
}
.button {
text-decoration: none;
display: inline-flex;
padding: 12px 24px;
}
.primary--white {
background-color: black;
color: white;
}
.primary--black {
background-color: white;
color: black;
}
.secondary--white {
background-color: white;
box-shadow: inset 0 0 0 1px black;
}
.secondary--black {
background-color: black;
box-shadow: inset 0 0 0 1px white;
}
.appearance--default {
padding: 0;
}

View File

@@ -0,0 +1,73 @@
import type { ElementType } from 'react';
import Link from 'next/link'
import React from 'react'
import classes from './index.module.scss'
export type Props = {
appearance?: 'default' | 'primary' | 'secondary'
className?: string
disabled?: boolean
el?: 'a' | 'button' | 'link'
href?: string
label?: string
newTab?: boolean
onClick?: () => void
type?: 'button' | 'submit'
}
export const Button: React.FC<Props> = ({
type = 'button',
appearance,
className: classNameFromProps,
disabled,
el: elFromProps = 'link',
href,
label,
newTab,
onClick,
}) => {
let el = elFromProps
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
const className = [
classes.button,
classNameFromProps,
classes[`appearance--${appearance}`],
classes.button,
]
.filter(Boolean)
.join(' ')
const content = (
<div className={classes.content}>
{/* <Chevron /> */}
<span className={classes.label}>{label}</span>
</div>
)
if (onClick || type === 'submit') el = 'button'
if (el === 'link') {
return (
<Link className={className} href={href || ''} {...newTabProps} onClick={onClick}>
{content}
</Link>
)
}
const Element: ElementType = el
return (
<Element
className={className}
href={href}
type={type}
{...newTabProps}
disabled={disabled}
onClick={onClick}
>
{content}
</Element>
)
}

View File

@@ -0,0 +1,67 @@
import Link from 'next/link'
import React from 'react'
import type { Page } from '../../../payload-types'
import { Button } from '../Button'
export type CMSLinkType = {
appearance?: 'default' | 'primary' | 'secondary'
children?: React.ReactNode
className?: string
label?: string
newTab?: boolean
reference?: {
relationTo: 'pages'
value: Page | string
}
type?: 'custom' | 'reference'
url?: string
}
export const CMSLink: React.FC<CMSLinkType> = ({
type,
appearance,
children,
className,
label,
newTab,
reference,
url,
}) => {
const href =
type === 'reference' && typeof reference?.value === 'object' && reference.value.slug
? `/${reference.value.slug === 'home' ? '' : reference.value.slug}`
: url
if (!appearance) {
const newTabProps = newTab ? { rel: 'noopener noreferrer', target: '_blank' } : {}
if (type === 'custom') {
return (
<a href={url} {...newTabProps} className={className}>
{label && label}
{children && children}
</a>
)
}
if (href) {
return (
<Link href={href} {...newTabProps} className={className} prefetch={false}>
{label && label}
{children && children}
</Link>
)
}
}
const buttonProps = {
appearance,
href,
label,
newTab,
}
return <Button className={className} {...buttonProps} el="link" />
}

View File

@@ -0,0 +1,13 @@
.gutter {
max-width: var(--max-width);
width: 100%;
margin: auto;
}
.gutterLeft {
padding-left: var(--gutter-h);
}
.gutterRight {
padding-right: var(--gutter-h);
}

View File

@@ -0,0 +1,35 @@
import type { Ref } from 'react';
import React, { forwardRef } from 'react'
import classes from './index.module.scss'
type Props = {
children: React.ReactNode
className?: string
left?: boolean
ref?: Ref<HTMLDivElement>
right?: boolean
}
export const Gutter: React.FC<Props> = forwardRef<HTMLDivElement, Props>((props, ref) => {
const { children, className, left = true, right = true } = props
return (
<div
className={[
classes.gutter,
left && classes.gutterLeft,
right && classes.gutterRight,
className,
]
.filter(Boolean)
.join(' ')}
ref={ref}
>
{children}
</div>
)
})
Gutter.displayName = 'Gutter'

View File

@@ -0,0 +1,32 @@
.header {
padding: var(--base) 0;
}
.wrap {
display: flex;
justify-content: space-between;
gap: calc(var(--base) / 2);
flex-wrap: wrap;
}
.logo {
flex-shrink: 0;
}
.nav {
display: flex;
align-items: center;
gap: var(--base);
white-space: nowrap;
overflow: hidden;
flex-wrap: wrap;
a {
display: block;
text-decoration: none;
}
@media (max-width: 1000px) {
gap: 0 calc(var(--base) / 2);
}
}

View File

@@ -0,0 +1,50 @@
import { getPayloadHMR } from '@payloadcms/next/utilities'
import Image from 'next/image'
import Link from 'next/link'
import React from 'react'
import config from '../../../../payload.config'
import { CMSLink } from '../CMSLink'
import { Gutter } from '../Gutter'
import classes from './index.module.scss'
export const Header = async () => {
const payload = await getPayloadHMR({ config })
const mainMenu = await payload.findGlobal({
slug: 'main-menu',
depth: 0,
})
const { navItems } = mainMenu
const hasNavItems = navItems && Array.isArray(navItems) && navItems.length > 0
return (
<header className={classes.header}>
<Gutter className={classes.wrap}>
<Link className={classes.logo} href="/">
<picture>
<source
media="(prefers-color-scheme: dark)"
srcSet="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-light.svg"
/>
<Image
alt="Payload Logo"
height={30}
src="https://raw.githubusercontent.com/payloadcms/payload/master/src/admin/assets/images/payload-logo-dark.svg"
width={150}
/>
</picture>
</Link>
{hasNavItems && (
<nav className={classes.nav}>
{navItems.map(({ link }, i) => {
return <CMSLink key={i} {...link} />
})}
</nav>
)}
</Gutter>
</header>
)
}

View File

@@ -0,0 +1,9 @@
.richText {
:first-child {
margin-top: 0;
}
a {
text-decoration: underline;
}
}

View File

@@ -0,0 +1,18 @@
import React from 'react'
import classes from './index.module.scss'
import serialize from './serialize'
const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => {
if (!content) {
return null
}
return (
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
{serialize(content)}
</div>
)
}
export default RichText

View File

@@ -0,0 +1,92 @@
import escapeHTML from 'escape-html'
import React, { Fragment } from 'react'
import { Text } from 'slate'
// eslint-disable-next-line no-use-before-define
type Children = Leaf[]
type Leaf = {
[key: string]: unknown
children: Children
type: string
url?: string
value?: {
alt: string
url: string
}
}
const serialize = (children: Children): React.ReactNode[] =>
children.map((node, i) => {
if (Text.isText(node)) {
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
if (node.bold) {
text = <strong key={i}>{text}</strong>
}
if (node.code) {
text = <code key={i}>{text}</code>
}
if (node.italic) {
text = <em key={i}>{text}</em>
}
if (node.underline) {
text = (
<span key={i} style={{ textDecoration: 'underline' }}>
{text}
</span>
)
}
if (node.strikethrough) {
text = (
<span key={i} style={{ textDecoration: 'line-through' }}>
{text}
</span>
)
}
return <Fragment key={i}>{text}</Fragment>
}
if (!node) {
return null
}
switch (node.type) {
case 'h1':
return <h1 key={i}>{serialize(node.children)}</h1>
case 'h2':
return <h2 key={i}>{serialize(node.children)}</h2>
case 'h3':
return <h3 key={i}>{serialize(node.children)}</h3>
case 'h4':
return <h4 key={i}>{serialize(node.children)}</h4>
case 'h5':
return <h5 key={i}>{serialize(node.children)}</h5>
case 'h6':
return <h6 key={i}>{serialize(node.children)}</h6>
case 'blockquote':
return <blockquote key={i}>{serialize(node.children)}</blockquote>
case 'ul':
return <ul key={i}>{serialize(node.children)}</ul>
case 'ol':
return <ol key={i}>{serialize(node.children)}</ol>
case 'li':
return <li key={i}>{serialize(node.children)}</li>
case 'link':
return (
<a href={escapeHTML(node.url)} key={i}>
{serialize(node.children)}
</a>
)
default:
return <p key={i}>{serialize(node.children)}</p>
}
})
export default serialize

View File

@@ -0,0 +1,107 @@
$breakpoint: 1000px;
:root {
--max-width: 1600px;
--foreground-rgb: 0, 0, 0;
--background-rgb: 255, 255, 255;
--block-spacing: 2rem;
--gutter-h: 4rem;
--base: 1rem;
@media (max-width: $breakpoint) {
--block-spacing: 1rem;
--gutter-h: 2rem;
--base: 0.75rem;
}
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-rgb: 7, 7, 7;
}
}
* {
box-sizing: border-box;
}
html {
font-size: 20px;
line-height: 1.5;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
@media (max-width: $breakpoint) {
font-size: 16px;
}
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
margin: 0;
color: rgb(var(--foreground-rgb));
background: rgb(var(--background-rgb));
}
img {
height: auto;
max-width: 100%;
display: block;
}
h1 {
font-size: 4.5rem;
line-height: 1.2;
margin: 0 0 2.5rem 0;
@media (max-width: $breakpoint) {
font-size: 3rem;
margin: 0 0 1.5rem 0;
}
}
h2 {
font-size: 3.5rem;
line-height: 1.2;
margin: 0 0 2.5rem 0;
}
h3 {
font-size: 2.5rem;
line-height: 1.2;
margin: 0 0 2rem 0;
}
h4 {
font-size: 1.5rem;
line-height: 1.2;
margin: 0 0 1rem 0;
}
h5 {
font-size: 1.25rem;
line-height: 1.2;
margin: 0 0 1rem 0;
}
h6 {
font-size: 1rem;
line-height: 1.2;
margin: 0 0 1rem 0;
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}

View File

@@ -0,0 +1,23 @@
/* eslint-disable no-restricted-exports */
import React from 'react'
import { Header } from './_components/Header'
import './app.scss'
export const metadata = {
description: 'Generated by create next app',
title: 'Create Next App',
}
export default function RootLayout(props: { children: React.ReactNode }) {
const { children } = props
return (
<html lang="en">
<body>
<Header />
{children}
</body>
</html>
)
}

View File

@@ -0,0 +1,3 @@
import Page from './[slug]/page'
export default Page

View File

@@ -0,0 +1,22 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
type Args = {
params: {
segments: string[]
}
searchParams: {
[key: string]: string | string[]
}
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
export default NotFound

View File

@@ -0,0 +1,22 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
type Args = {
params: {
segments: string[]
}
searchParams: {
[key: string]: string | string[]
}
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
export default Page

View File

@@ -0,0 +1,10 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -0,0 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -0,0 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)

View File

@@ -0,0 +1,16 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import configPromise from '@payload-config'
import '@payloadcms/next/css'
import { RootLayout } from '@payloadcms/next/layouts'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import React from 'react'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
export default Layout

View File

@@ -8,7 +8,7 @@ const format = (val: string): string =>
const formatSlug =
(fallback: string): FieldHook =>
({ operation, value, originalDoc, data }) => {
({ data, operation, originalDoc, value }) => {
if (typeof value === 'string') {
return format(value)
}

View File

@@ -6,19 +6,21 @@ import formatSlug from './hooks/formatSlug'
export const Pages: CollectionConfig = {
slug: 'pages',
access: {
create: loggedIn,
delete: loggedIn,
read: () => true,
update: loggedIn,
},
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'slug', 'updatedAt'],
livePreview: {
url: ({ data }) =>
`${process.env.PAYLOAD_PUBLIC_SITE_URL}${data.slug !== 'home' ? `/${data.slug}` : ''}`,
url: ({ data }) => {
const isHomePage = data.slug === 'home'
return `${process.env.PAYLOAD_PUBLIC_SITE_URL}${!isHomePage ? `/${data.slug}` : ''}`
},
},
},
access: {
read: () => true,
create: loggedIn,
update: loggedIn,
delete: loggedIn,
useAsTitle: 'title',
},
fields: [
{
@@ -28,16 +30,23 @@ export const Pages: CollectionConfig = {
},
{
name: 'slug',
label: 'Slug',
type: 'text',
index: true,
admin: {
position: 'sidebar',
},
hooks: {
beforeValidate: [formatSlug('title')],
},
index: true,
label: 'Slug',
},
richText(),
],
versions: {
drafts: {
autosave: {
interval: 375,
},
},
},
}

View File

@@ -2,9 +2,9 @@ import type { CollectionConfig } from 'payload/types'
export const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [],
}

View File

@@ -3,6 +3,10 @@ import type { Field } from 'payload/types'
import deepMerge from '../utilities/deepMerge'
export const appearanceOptions = {
default: {
label: 'Default',
value: 'default',
},
primary: {
label: 'Primary Button',
value: 'primary',
@@ -11,13 +15,9 @@ export const appearanceOptions = {
label: 'Secondary Button',
value: 'secondary',
},
default: {
label: 'Default',
value: 'default',
},
}
export type LinkAppearances = 'primary' | 'secondary' | 'default'
export type LinkAppearances = 'default' | 'primary' | 'secondary'
type LinkType = (options?: {
appearances?: LinkAppearances[] | false
@@ -39,6 +39,11 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
{
name: 'type',
type: 'radio',
admin: {
layout: 'horizontal',
width: '50%',
},
defaultValue: 'reference',
options: [
{
label: 'Internal link',
@@ -49,22 +54,17 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
value: 'custom',
},
],
defaultValue: 'reference',
admin: {
layout: 'horizontal',
width: '50%',
},
},
{
name: 'newTab',
label: 'Open in new tab',
type: 'checkbox',
admin: {
width: '50%',
style: {
alignSelf: 'flex-end',
},
width: '50%',
},
label: 'Open in new tab',
},
],
},
@@ -74,23 +74,23 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
const linkTypes: Field[] = [
{
name: 'reference',
label: 'Document to link to',
type: 'relationship',
relationTo: ['pages'],
required: true,
maxDepth: 1,
admin: {
condition: (_, siblingData) => siblingData?.type === 'reference',
},
label: 'Document to link to',
maxDepth: 1,
relationTo: ['pages'],
required: true,
},
{
name: 'url',
label: 'Custom URL',
type: 'text',
required: true,
admin: {
condition: (_, siblingData) => siblingData?.type === 'custom',
},
label: 'Custom URL',
required: true,
},
]
@@ -104,12 +104,12 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
...linkTypes,
{
name: 'label',
label: 'Label',
type: 'text',
required: true,
admin: {
width: '50%',
},
label: 'Label',
required: true,
},
],
})
@@ -131,11 +131,11 @@ const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } =
linkResult.fields.push({
name: 'appearance',
type: 'select',
defaultValue: 'default',
options: appearanceOptionsToUse,
admin: {
description: 'Choose how the link should be rendered.',
},
defaultValue: 'default',
options: appearanceOptionsToUse,
})
}

View File

@@ -1,7 +1,8 @@
import { slateEditor } from '@payloadcms/richtext-slate'
import type { RichTextElement, RichTextLeaf } from '@payloadcms/richtext-slate/dist/types'
import type { RichTextField } from 'payload/types'
import { slateEditor } from '@payloadcms/richtext-slate'
import deepMerge from '../../utilities/deepMerge'
import link from '../link'
import elements from './elements'
@@ -26,27 +27,28 @@ const richText: RichText = (
{
name: 'richText',
type: 'richText',
required: true,
editor: slateEditor({
admin: {
elements: [...elements, ...(additions.elements || [])],
leaves: [...leaves, ...(additions.leaves || [])],
upload: {
collections: {
media: {
fields: [
{
type: 'richText',
name: 'caption',
label: 'Caption',
type: 'richText',
editor: slateEditor({
admin: {
elements: [...elements],
leaves: [...leaves],
},
}),
label: 'Caption',
},
{
type: 'radio',
name: 'alignment',
type: 'radio',
label: 'Alignment',
options: [
{
@@ -81,10 +83,9 @@ const richText: RichText = (
},
},
},
elements: [...elements, ...(additions.elements || [])],
leaves: [...leaves, ...(additions.leaves || [])],
},
}),
required: true,
},
overrides,
)

View File

@@ -11,12 +11,12 @@ export const MainMenu: GlobalConfig = {
{
name: 'navItems',
type: 'array',
maxRows: 6,
fields: [
link({
appearances: false,
}),
],
maxRows: 6,
},
],
}

View File

@@ -0,0 +1,130 @@
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
import type { Page } from '../payload-types'
export const home: Partial<Page> = {
slug: 'home',
richText: [
{
type: 'h1',
children: [
{
text: 'Payload Live Preview Example',
},
],
},
{
children: [
{ text: 'This is a ' },
{
type: 'link',
children: [{ text: 'Next.js' }],
linkType: 'custom',
newTab: true,
url: 'https://nextjs.org',
},
{ text: " app made explicitly for Payload's " },
{
type: 'link',
children: [{ text: 'Live Preview Example' }],
linkType: 'custom',
newTab: true,
url: 'https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload',
},
{ text: '. With ' },
{
type: 'link',
children: [{ text: 'Live Preview' }],
newTab: true,
url: 'https://payloadcms.com/docs/live-preview',
},
{
text: ' you can edit this page in the admin panel and see the changes reflected here in real time.',
},
],
},
],
title: 'Home',
}
export const examplePage: Partial<Page> = {
slug: 'example-page',
richText: [
{
type: 'h1',
children: [
{
text: 'Example Page',
},
],
},
{
children: [
{
text: 'This is an example page. You can edit this page in the Admin panel and see the changes reflected here in real time.',
},
],
},
],
title: 'Example Page',
}
export async function up({ payload }: MigrateUpArgs): Promise<void> {
await payload.create({
collection: 'users',
data: {
email: 'demo@payloadcms.com',
password: 'demo',
},
})
const { id: examplePageID } = await payload.create({
collection: 'pages',
data: examplePage as any, // eslint-disable-line
})
const homepageJSON = JSON.parse(JSON.stringify(home))
const { id: homePageID } = await payload.create({
collection: 'pages',
data: homepageJSON,
})
await payload.updateGlobal({
slug: 'main-menu',
data: {
navItems: [
{
link: {
type: 'reference',
label: 'Home',
reference: {
relationTo: 'pages',
value: homePageID,
},
url: '',
},
},
{
link: {
type: 'reference',
label: 'Example Page',
reference: {
relationTo: 'pages',
value: examplePageID,
},
url: '',
},
},
{
link: {
type: 'custom',
label: 'Dashboard',
reference: undefined,
url: 'http://localhost:3000/admin',
},
},
],
},
})
}

View File

@@ -1,6 +1,6 @@
import { webpackBundler } from '@payloadcms/bundler-webpack'
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { slateEditor } from '@payloadcms/richtext-slate'
import { fileURLToPath } from 'node:url'
import path from 'path'
import { buildConfig } from 'payload/config'
@@ -9,31 +9,34 @@ import { Users } from './collections/Users'
import BeforeLogin from './components/BeforeLogin'
import { MainMenu } from './globals/MainMenu'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfig({
collections: [Pages, Users],
admin: {
bundler: webpackBundler(), // bundler-config
components: {
beforeLogin: [BeforeLogin],
},
livePreview: {
breakpoints: [
{
label: 'Mobile',
name: 'mobile',
width: 375,
height: 667,
label: 'Mobile',
width: 375,
},
],
},
},
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
globals: [MainMenu],
editor: slateEditor({}),
collections: [Pages, Users],
db: mongooseAdapter({
url: process.env.DATABASE_URI,
url: process.env.DATABASE_URI || '',
}),
editor: slateEditor({}),
globals: [MainMenu],
secret: process.env.PAYLOAD_SECRET || '',
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts'),
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})

View File

@@ -1,46 +0,0 @@
import type { Page } from '../payload-types'
export const home: Partial<Page> = {
title: 'Home',
slug: 'home',
richText: [
{
type: 'h1',
children: [
{
text: 'Payload Live Preview Example',
},
],
},
{
children: [
{ text: 'This is a ' },
{
type: 'link',
linkType: 'custom',
url: 'https://nextjs.org',
newTab: true,
children: [{ text: 'Next.js' }],
},
{ text: " app made explicitly for Payload's " },
{
type: 'link',
linkType: 'custom',
newTab: true,
url: 'https://github.com/payloadcms/payload/tree/master/examples/live-preview/payload',
children: [{ text: 'Live Preview Example' }],
},
{ text: '. With ' },
{
type: 'link',
newTab: true,
url: 'https://payloadcms.com/docs/live-preview',
children: [{ text: 'Live Preview' }],
},
{
text: ' you can edit this page in the admin panel and see the changes reflected here in real time.',
},
],
},
],
}

View File

@@ -1,64 +0,0 @@
import type { Payload } from 'payload'
import { home } from './home'
import { examplePage } from './page'
export const seed = async (payload: Payload): Promise<void> => {
await payload.create({
collection: 'users',
data: {
email: 'demo@payloadcms.com',
password: 'demo',
},
})
const { id: examplePageID } = await payload.create({
collection: 'pages',
data: examplePage as any, // eslint-disable-line
})
const homepageJSON = JSON.parse(JSON.stringify(home))
const { id: homePageID } = await payload.create({
collection: 'pages',
data: homepageJSON,
})
await payload.updateGlobal({
slug: 'main-menu',
data: {
navItems: [
{
link: {
type: 'reference',
reference: {
relationTo: 'pages',
value: homePageID,
},
label: 'Home',
url: '',
},
},
{
link: {
type: 'reference',
reference: {
relationTo: 'pages',
value: examplePageID,
},
label: 'Example Page',
url: '',
},
},
{
link: {
type: 'custom',
reference: null,
label: 'Dashboard',
url: 'http://localhost:3000/admin',
},
},
],
},
})
}

View File

@@ -1,23 +0,0 @@
import type { Page } from '../payload-types'
export const examplePage: Partial<Page> = {
title: 'Example Page',
slug: 'example-page',
richText: [
{
type: 'h1',
children: [
{
text: 'Example Page',
},
],
},
{
children: [
{
text: 'This is an example page. You can edit this page in the Admin panel and see the changes reflected here in real time.',
},
],
},
],
}

View File

@@ -1,36 +0,0 @@
import dotenv from 'dotenv'
import path from 'path'
dotenv.config({
path: path.resolve(__dirname, '../.env'),
})
import express from 'express'
import payload from 'payload'
import { seed } from './seed'
const app = express()
app.get('/', (_, res) => {
res.redirect('/admin')
})
const start = async (): Promise<void> => {
await payload.init({
secret: process.env.PAYLOAD_SECRET,
express: app,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
},
})
if (process.env.PAYLOAD_PUBLIC_SEED === 'true') {
payload.logger.info('---- SEEDING DATABASE ----')
await seed(payload)
}
app.listen(3000)
}
start()

View File

@@ -1,33 +1,28 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"baseUrl": ".",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react",
"sourceMap": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"node_modules/*": ["./node_modules/*"]
},
"@/*": ["./src/*"],
"@payload-config": ["src/payload.config.ts"]
}
},
"include": [
"src"
],
"exclude": [
"node_modules",
"dist",
"build",
],
"ts-node": {
"transpileOnly": true
}
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: "typescript",
semi: false,
singleQuote: true,
trailingComma: "all",
arrowParens: "avoid",
};

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,8 +0,0 @@
module.exports = {
printWidth: 100,
parser: 'typescript',
semi: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
}

View File

@@ -1,6 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"semi": false
}

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"description": "The official live preview React SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -4,4 +4,5 @@ export { isLivePreviewEvent } from './isLivePreviewEvent.js'
export { mergeData } from './mergeData.js'
export { ready } from './ready.js'
export { subscribe } from './subscribe.js'
export { traverseRichText } from './traverseRichText.js'
export { unsubscribe } from './unsubscribe.js'

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -63,11 +63,10 @@ export const getGraphql = async (config: Promise<SanitizedConfig> | SanitizedCon
}
if (!cached.promise) {
// eslint-disable-next-line no-async-promise-executor
cached.promise = new Promise(async (resolve) => {
const resolvedConfig = await config
const resolvedConfig = await config
cached.promise = new Promise((resolve) => {
const schema = configToSchema(resolvedConfig)
resolve(schema)
resolve(cached.graphql || schema)
})
}

View File

@@ -1,5 +1,6 @@
import type { AdminViewProps } from 'payload/types'
import { WithServerSideProps } from '@payloadcms/ui/elements/WithServerSideProps'
import { Logo } from '@payloadcms/ui/graphics/Logo'
import { redirect } from 'next/navigation.js'
import React, { Fragment } from 'react'
@@ -16,6 +17,7 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, searchPara
const {
payload: { config },
payload,
user,
} = req
@@ -25,6 +27,18 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, searchPara
routes: { admin },
} = config
const BeforeLogins = Array.isArray(beforeLogin)
? beforeLogin.map((Component, i) => (
<WithServerSideProps Component={Component} key={i} payload={payload} />
))
: null
const AfterLogins = Array.isArray(afterLogin)
? afterLogin.map((Component, i) => (
<WithServerSideProps Component={Component} key={i} payload={payload} />
))
: null
if (user) {
redirect(admin)
}
@@ -36,9 +50,9 @@ export const LoginView: React.FC<AdminViewProps> = ({ initPageResult, searchPara
<div className={`${loginBaseClass}__brand`}>
<Logo config={config} />
</div>
{Array.isArray(beforeLogin) && beforeLogin.map((Component, i) => <Component key={i} />)}
{Array.isArray(BeforeLogins) && BeforeLogins.map((Component) => Component)}
{!collectionConfig?.auth?.disableLocalStrategy && <LoginForm searchParams={searchParams} />}
{Array.isArray(afterLogin) && afterLogin.map((Component, i) => <Component key={i} />)}
{Array.isArray(AfterLogins) && AfterLogins.map((Component) => Component)}
</Fragment>
)
}

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.0.0-beta.28",
"version": "3.0.0-beta.29",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",

View File

@@ -7,11 +7,7 @@ export type DescriptionFunction = LabelFunction
export type DescriptionComponent = CustomComponent<FieldDescriptionProps>
export type Description =
| DescriptionComponent
| DescriptionFunction
| Record<string, string>
| string
export type Description = DescriptionFunction | Record<string, string> | string
export type FieldDescriptionProps = {
CustomDescription?: React.ReactNode

View File

@@ -1,10 +1,10 @@
import type { LabelFunction } from '../../config/types.js'
export type LabelProps = {
CustomLabel?: React.ReactNode
as?: 'label' | 'span'
htmlFor?: string
label?: LabelFunction | Record<string, string> | false | string
label?: Record<string, string> | string
required?: boolean
unstyled?: boolean
}
export type SanitizedLabelProps = Omit<LabelProps, 'label' | 'required'>

View File

@@ -2,4 +2,4 @@ import type { CustomComponent } from '../../config/types.js'
export type RowLabelComponent = CustomComponent
export type RowLabel = Record<string, string> | RowLabelComponent | string
export type RowLabel = Record<string, string> | string

View File

@@ -22,7 +22,7 @@ export type {
FieldDescriptionProps,
} from './forms/FieldDescription.js'
export type { Data, FilterOptionsResult, FormField, FormState, Row } from './forms/Form.js'
export type { LabelProps } from './forms/Label.js'
export type { LabelProps, SanitizedLabelProps } from './forms/Label.js'
export type { RowLabel, RowLabelComponent } from './forms/RowLabel.js'
export type {

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import fs from 'fs'
import { compile } from 'json-schema-to-typescript'
@@ -7,7 +6,7 @@ import type { SanitizedConfig } from '../config/types.js'
import { configToJSONSchema } from '../utilities/configToJSONSchema.js'
import Logger from '../utilities/logger.js'
export function generateTypes(config: SanitizedConfig): void {
export async function generateTypes(config: SanitizedConfig): Promise<void> {
const logger = Logger()
const outputFile = process.env.PAYLOAD_TS_OUTPUT_PATH || config.typescript.outputFile
@@ -16,8 +15,9 @@ export function generateTypes(config: SanitizedConfig): void {
const jsonSchema = configToJSONSchema(config, config.db.defaultIDType)
const declare = `declare module 'payload' {\n export interface GeneratedTypes extends Config {}\n}`
const declareWithTSIgnoreError = `declare module 'payload' {\n // @ts-ignore \n export interface GeneratedTypes extends Config {}\n}`
compile(jsonSchema, 'Config', {
let compiled = await compile(jsonSchema, 'Config', {
bannerComment:
'/* tslint:disable */\n/* eslint-disable */\n/**\n* This file was automatically generated by Payload.\n* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,\n* and re-run `payload generate:types` to regenerate this file.\n*/',
style: {
@@ -27,11 +27,15 @@ export function generateTypes(config: SanitizedConfig): void {
// If a field defines an interfaceName, it should be included in the generated types
// even if it's not used by another type. Reason: the user might want to use it in their own code.
unreachableDefinitions: true,
}).then((compiled) => {
if (config.typescript.declare !== false) {
})
if (config.typescript.declare !== false) {
if (config.typescript.declare?.ignoreTSError) {
compiled += `\n\n${declareWithTSIgnoreError}`
} else {
compiled += `\n\n${declare}`
}
fs.writeFileSync(outputFile, compiled)
logger.info(`Types written to ${outputFile}`)
})
}
fs.writeFileSync(outputFile, compiled)
logger.info(`Types written to ${outputFile}`)
}

View File

@@ -30,6 +30,7 @@ const collectionSchema = joi.object().keys({
BeforeList: joi.array().items(componentSchema),
BeforeListTable: joi.array().items(componentSchema),
edit: joi.object({
Description: componentSchema,
PreviewButton: componentSchema,
PublishButton: componentSchema,
SaveButton: componentSchema,
@@ -59,7 +60,9 @@ const collectionSchema = joi.object().keys({
}),
custom: joi.object().pattern(joi.string(), joi.any()),
defaultColumns: joi.array().items(joi.string()),
description: joi.alternatives().try(joi.string(), componentSchema),
description: joi
.alternatives()
.try(joi.func(), joi.object().pattern(joi.string(), [joi.string()]), joi.string()),
enableRichTextLink: joi.boolean(),
enableRichTextRelationship: joi.boolean(),
group: joi.alternatives().try(joi.string(), joi.object().pattern(joi.string(), [joi.string()])),

View File

@@ -14,6 +14,7 @@ import type {
EditConfig,
Endpoint,
EntityDescription,
EntityDescriptionComponent,
GeneratePreviewURL,
LabelFunction,
LivePreviewConfig,
@@ -212,6 +213,8 @@ export type CollectionAdminOptions = {
* Components within the edit view
*/
edit?: {
Description?: EntityDescriptionComponent
/**
* Replaces the "Preview" button
*/

View File

@@ -178,7 +178,7 @@ export default joi.object({
sharp: joi.any(),
telemetry: joi.boolean(),
typescript: joi.object({
declare: joi.boolean(),
declare: joi.alternatives().try(joi.boolean(), joi.object({ ignoreTSError: joi.boolean() })),
outputFile: joi.string(),
}),
upload: joi.object(),

View File

@@ -629,7 +629,18 @@ export type Config = {
/** Control how typescript interfaces are generated from your collections. */
typescript?: {
/** Disable declare block in generated types file */
declare?: false
declare?:
| {
/**
* @internal internal use only to allow for multiple declarations within a monorepo and suppress the "Duplicate identifier GeneratedTypes" error
*
* Adds a @ts-ignore flag above the GeneratedTypes interface declaration
*
* @default false
*/
ignoreTSError?: boolean
}
| false
/** Filename to write the generated types to */
outputFile?: string
}
@@ -694,12 +705,8 @@ export type EditConfig =
export type EntityDescriptionComponent = CustomComponent
export type EntityDescriptionFunction = () => string
export type EntityDescriptionFunction = ({ t }: { t: TFunction }) => string
export type EntityDescription =
| EntityDescriptionComponent
| EntityDescriptionFunction
| Record<string, string>
| string
export type EntityDescription = EntityDescriptionFunction | Record<string, string> | string
export type { EmailAdapter, SendEmailOptions }

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