Compare commits
40 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bf27b117a | ||
|
|
806c22e6bd | ||
|
|
39d7b717a9 | ||
|
|
9d1997e6a0 | ||
|
|
c65f5027d6 | ||
|
|
dc496e4387 | ||
|
|
3d86bf1974 | ||
|
|
96e7c95ebc | ||
|
|
fca4ee995e | ||
|
|
352ed0ebef | ||
|
|
bcf9b17321 | ||
|
|
a19263245f | ||
|
|
78e55d61be | ||
|
|
cea272e189 | ||
|
|
8b13dc64d1 | ||
|
|
5fc9f76406 | ||
|
|
6c0f99082b | ||
|
|
90b7b20699 | ||
|
|
9cb84c48b9 | ||
|
|
390f88867f | ||
|
|
b33b5f43f4 | ||
|
|
56aded8507 | ||
|
|
78dd6a2d5b | ||
|
|
a063b81460 | ||
|
|
18d9314f22 | ||
|
|
8d120373a7 | ||
|
|
f88cef5470 | ||
|
|
5dfcffa281 | ||
|
|
fa3d250053 | ||
|
|
4b2a9f75d0 | ||
|
|
e225783d76 | ||
|
|
0d552fd523 | ||
|
|
69ada97df5 | ||
|
|
81e7355ee0 | ||
|
|
ce8b95f6bb | ||
|
|
c1b0d93c93 | ||
|
|
6227276d2c | ||
|
|
ee62ed6ebb | ||
|
|
0283039257 | ||
|
|
b546c7b655 |
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -9,7 +9,7 @@ inputs:
|
||||
pnpm-version:
|
||||
description: 'The pnpm version to use'
|
||||
required: true
|
||||
default: 8.15.7
|
||||
default: 9.7.0
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
|
||||
45
.github/workflows/main.yml
vendored
45
.github/workflows/main.yml
vendored
@@ -18,7 +18,7 @@ concurrency:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 18.20.2
|
||||
PNPM_VERSION: 8.15.7
|
||||
PNPM_VERSION: 9.7.0
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
@@ -427,8 +427,8 @@ jobs:
|
||||
- name: Pack and build app
|
||||
run: |
|
||||
set -ex
|
||||
pnpm run script:pack --dest templates/blank-3.0
|
||||
cd templates/blank-3.0
|
||||
pnpm run script:pack --dest templates/blank
|
||||
cd templates/blank
|
||||
cp .env.example .env
|
||||
ls -la
|
||||
pnpm add ./*.tgz --ignore-workspace
|
||||
@@ -552,42 +552,13 @@ jobs:
|
||||
|
||||
publish-canary:
|
||||
name: Publish Canary
|
||||
if: github.ref == 'refs/heads/beta'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- all-green
|
||||
|
||||
steps:
|
||||
# https://github.com/actions/virtual-environments/issues/1187
|
||||
- name: tune linux network
|
||||
run: sudo ethtool -K eth0 tx off rx off
|
||||
|
||||
- name: Setup Node@${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Restore build
|
||||
uses: actions/cache@v4
|
||||
timeout-minutes: 10
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}
|
||||
|
||||
- name: Load npm token
|
||||
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Canary release script
|
||||
# dry run hard-coded to true for testing and no npm token provided
|
||||
run: pnpm tsx ./scripts/publish-canary.ts
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
# debug github.ref output
|
||||
- run: |
|
||||
echo github.ref: ${{ github.ref }}
|
||||
echo isBeta: ${{ github.ref == 'refs/heads/beta' }}
|
||||
echo isMain: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
2
.github/workflows/release-canary.yml
vendored
2
.github/workflows/release-canary.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 18.20.2
|
||||
PNPM_VERSION: 8.15.7
|
||||
PNPM_VERSION: 9.7.0
|
||||
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
|
||||
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
|
||||
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,6 +5,7 @@ dist
|
||||
!/.idea/runConfigurations
|
||||
!/.idea/payload.iml
|
||||
|
||||
|
||||
test-results
|
||||
.devcontainer
|
||||
.localstack
|
||||
@@ -300,3 +301,8 @@ $RECYCLE.BIN/
|
||||
|
||||
/build
|
||||
.swc
|
||||
app/(payload)/admin/importMap.js
|
||||
test/live-preview/app/(payload)/admin/importMap.js
|
||||
/test/live-preview/app/(payload)/admin/importMap.js
|
||||
test/admin-root/app/(payload)/admin/importMap.js
|
||||
/test/admin-root/app/(payload)/admin/importMap.js
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pnpm run lint-staged --quiet
|
||||
|
||||
1
.idea/payload.iml
generated
1
.idea/payload.iml
generated
@@ -26,6 +26,7 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/next/.swc" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/next/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/next/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/payload/fields" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud-storage/.turbo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud-storage/dist" />
|
||||
|
||||
13
.idea/runConfigurations/Run_Dev_Fields.xml
generated
13
.idea/runConfigurations/Run_Dev_Fields.xml
generated
@@ -1,8 +1,13 @@
|
||||
<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>
|
||||
<configuration default="false" name="Run Dev Fields" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="dev" />
|
||||
</scripts>
|
||||
<arguments value="fields" />
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
13
.idea/runConfigurations/Run_Dev__community.xml
generated
13
.idea/runConfigurations/Run_Dev__community.xml
generated
@@ -1,8 +1,13 @@
|
||||
<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>
|
||||
<configuration default="false" name="Run Dev _community" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="dev" />
|
||||
</scripts>
|
||||
<arguments value="_community" />
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
13
.idea/runConfigurations/Run_Dev_admin.xml
generated
Normal file
13
.idea/runConfigurations/Run_Dev_admin.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Dev admin" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="dev" />
|
||||
</scripts>
|
||||
<arguments value="admin" />
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1,7 +1,7 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="true" type="JavaScriptTestRunnerJest">
|
||||
<node-interpreter value="project" />
|
||||
<node-options value="--experimental-vm-modules --no-deprecation" />
|
||||
<node-options value="--no-deprecation" />
|
||||
<envs />
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2" />
|
||||
|
||||
3
.npmrc
3
.npmrc
@@ -1,2 +1,3 @@
|
||||
symlink=true
|
||||
node-linker=isolated # due to a typescript bug, isolated mode requires @types/express-serve-static-core, terser and monaco-editor to be installed https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189 along with two other changes in the code which I've marked with (tsbugisolatedmode) in the code
|
||||
node-linker=isolated
|
||||
hoist-workspace-packages=false # the default in pnpm v9 is true, but that can break our runtime dependency checks
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -42,8 +42,8 @@
|
||||
}
|
||||
},
|
||||
"files.insertFinalNewline": true,
|
||||
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" node 'node_modules/jest/bin/jest.js'",
|
||||
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
|
||||
"jestrunner.debugOptions": {
|
||||
"runtimeArgs": ["--experimental-vm-modules", "--no-deprecation"]
|
||||
"runtimeArgs": ["--no-deprecation"]
|
||||
}
|
||||
}
|
||||
|
||||
14
app/(app)/layout.tsx
Normal file
14
app/(app)/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Generated by Next.js',
|
||||
title: 'Next.js',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
11
app/(app)/test/page.tsx
Normal file
11
app/(app)/test/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayloadHMR } from '@payloadcms/next/utilities'
|
||||
|
||||
export const Page = async ({ params, searchParams }) => {
|
||||
const payload = await getPayloadHMR({
|
||||
config: configPromise,
|
||||
})
|
||||
return <div>test ${payload?.config?.collections?.length}</div>
|
||||
}
|
||||
|
||||
export default Page
|
||||
@@ -5,6 +5,8 @@ import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
@@ -17,6 +19,7 @@ type Args = {
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
|
||||
@@ -5,6 +5,8 @@ import config from '@payload-config'
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
@@ -17,6 +19,7 @@ type Args = {
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default Page
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import configPromise from '@payload-config'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
|
||||
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import React from 'react'
|
||||
@@ -11,6 +14,10 @@ type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={configPromise} importMap={importMap}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
|
||||
@@ -51,7 +51,6 @@ To override Collection Components, use the `admin.components` property in your [
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
import { CustomSaveButton } from './CustomSaveButton'
|
||||
|
||||
export const MyCollection: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
|
||||
@@ -24,6 +24,178 @@ There are four main types of Custom Components in Payload:
|
||||
|
||||
To swap in your own Custom Component, consult the list of available components. Determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-components) accordingly.
|
||||
|
||||
|
||||
## Defining Custom Components in the Payload Config
|
||||
|
||||
In the Payload Config, you can define custom React Components to enhance the admin interface. However, these components should not be imported directly into the server-only Payload Config to avoid including client-side code. Instead, you specify the path to the component. Here’s how you can do it:
|
||||
|
||||
|
||||
src/components/Logout.tsx
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<button>Click me!</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
payload.config.ts:
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
components: {
|
||||
logout: {
|
||||
Button: '/src/components/Logout#MyComponent'
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In the path `/src/components/Logout#MyComponent`, `/src/components/Logout` is the file path, and `MyComponent` is the named export. If the component is the default export, the export name can be omitted. Path and export name are separated by a `#`.
|
||||
|
||||
### Configuring the Base Directory
|
||||
|
||||
Component paths, by default, are relative to your working directory - this is usually where your Next.js config lies. To simplify component paths, you have the option to configure the *base directory* using the `admin.baseDir.baseDir` property:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname, 'src'),
|
||||
},
|
||||
components: {
|
||||
logout: {
|
||||
Button: '/components/Logout#MyComponent'
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
In this example, we set the base directory to the `src` directory - thus we can omit the `/src/` part of our component path string.
|
||||
|
||||
### Passing Props
|
||||
|
||||
Each React Component in the Payload Config is typed as `PayloadComponent`. This usually is a string, but can also be an object containing the following properties:
|
||||
|
||||
| Property | Description |
|
||||
|---------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `clientProps` | Props to be passed to the React Component if it's a Client Component |
|
||||
| `exportName` | Instead of declaring named exports using `#` in the component path, you can also omit them from `path` and pass them in here. |
|
||||
| `path` | Path to the React Component. Named exports can be appended to the end of the path, separated by a `#` |
|
||||
| `serverProps` | Props to be passed to the React Component if it's a Server Component |
|
||||
|
||||
To pass in props from the config, you can use the `clientProps` and/or `serverProps` properties. This alleviates the need to use an HOC (Higher-Order-Component) to declare a React Component with props passed in.
|
||||
|
||||
Here is an example:
|
||||
|
||||
src/components/Logout.tsx
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
export const MyComponent = ({ text }: { text: string }) => {
|
||||
return (
|
||||
<button>Click me! {text}</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
payload.config.ts:
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
admin: { // highlight-line
|
||||
components: {
|
||||
logout: {
|
||||
Button: {
|
||||
path: '/src/components/Logout',
|
||||
clientProps: {
|
||||
text: 'Some Text.'
|
||||
},
|
||||
exportName: 'MyComponent'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Import Maps
|
||||
|
||||
It's essential to understand how `PayloadComponent` paths function behind the scenes. Directly importing React Components into your Payload Config using import statements can introduce client-only modules like CSS into your server-only config. This could error when attempting to load the Payload Config in server-only environments and unnecessarily increase the size of the Payload Config, which should remain streamlined and efficient for server use.
|
||||
|
||||
Instead, we utilize component paths to reference React Components. This method enhances the Payload Config with actual React Component imports on the client side, without affecting server-side usage. A script is deployed to scan the Payload Config, collecting all component paths and creating an `importMap.js`. This file, located in app/(payload)/admin/importMap.js, must be statically imported by your Next.js root page and layout. The script imports all the React Components from the specified paths into a Map, associating them with their respective paths (the ones you defined).
|
||||
|
||||
When constructing the `ClientConfig`, Payload uses the component paths as keys to fetch the corresponding React Component imports from the Import Map. It then substitutes the `PayloadComponent` with a `MappedComponent`. A `MappedComponent` includes the React Component and additional metadata, such as whether it's a server or a client component and which props it should receive. These components are then rendered through the `<RenderComponent />` component within the Payload Admin Panel.
|
||||
|
||||
Import maps are regenerated whenever you modify any element related to component paths. This regeneration occurs at startup and whenever Hot Module Replacement (HMR) runs. If the import maps fail to regenerate during HMR, you can restart your application and execute the `payload generate:importmap` command to manually create a new import map.
|
||||
|
||||
### Component paths in external packages
|
||||
|
||||
Component paths are resolved relative to your project's base directory, which is either your current working directory or the directory specified in `config.admin.baseDir`. When using custom components from external packages, you can't use relative paths. Instead, use an import path that's accessible as if you were writing an import statement in your project's base directory.
|
||||
|
||||
For example, to export a field with a custom component from an external package named `my-external-package`:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
export const MyCustomField: Field = {
|
||||
type: 'text',
|
||||
name: 'MyField',
|
||||
admin: {
|
||||
components: {
|
||||
Field: 'my-external-package/client#MyFieldComponent'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Despite `MyFieldComponent` living in `src/components/MyFieldComponent.tsx` in `my-external-package`, this will not be accessible from the consuming project. Instead, we recommend exporting all custom components from one file in the external package. For example, you can define a `src/client.ts file in `my-external-package`:
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
export { MyFieldComponent } from './components/MyFieldComponent'
|
||||
```
|
||||
|
||||
Then, update the package.json of `my-external-package:
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"exports": {
|
||||
"./client": {
|
||||
"import": "./dist/client.js",
|
||||
"types": "./dist/client.d.ts",
|
||||
"default": "./dist/client.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This setup allows you to specify the component path as `my-external-package/client#MyFieldComponent` as seen above. The import map will generate:
|
||||
|
||||
```ts
|
||||
import { MyFieldComponent } from 'my-external-package/client'
|
||||
```
|
||||
|
||||
which is a valid way to access MyFieldComponent that can be resolved by the consuming project.
|
||||
|
||||
## Root Components
|
||||
|
||||
Root Components are those that effect the [Admin Panel](./overview) generally, such as the logo or the main nav.
|
||||
@@ -33,8 +205,6 @@ To override Root Components, use the `admin.components` property in your [Payloa
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
import { MyCustomLogo } from './MyCustomLogo'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
@@ -81,13 +251,11 @@ To add a Custom Provider, use the `admin.components.providers` property in your
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
import { MyProvider } from './MyProvider'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
providers: [MyProvider], // highlight-line
|
||||
providers: ['/path/to/MyProvider'], // highlight-line
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -207,7 +375,7 @@ import React from 'react'
|
||||
import { useConfig } from '@payloadcms/ui'
|
||||
|
||||
export const MyClientComponent: React.FC = () => {
|
||||
const { serverURL } = useConfig() // highlight-line
|
||||
const { config: { serverURL } } = useConfig() // highlight-line
|
||||
|
||||
return (
|
||||
<Link href={serverURL}>
|
||||
@@ -221,6 +389,22 @@ export const MyClientComponent: React.FC = () => {
|
||||
See [Using Hooks](#using-hooks) for more details.
|
||||
</Banner>
|
||||
|
||||
All [Field Components](./fields) automatically receive their respective Client Field Config through a common [`field`](./fields#the-field-prop) prop:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { TextFieldProps } from 'payload'
|
||||
|
||||
export const MyClientFieldComponent: TextFieldProps = ({ field: { name } }) => {
|
||||
return (
|
||||
<p>
|
||||
{`This field's name is ${name}`}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Using Hooks
|
||||
|
||||
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](./hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts:
|
||||
|
||||
@@ -117,7 +117,7 @@ export const CollectionConfig: CollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
Field: MyFieldComponent, // highlight-line
|
||||
Field: '/path/to/MyFieldComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -135,32 +135,12 @@ All Field Components receive the following props:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`AfterInput`** | The rendered result of the `admin.components.afterInput` property. [More details](#afterinput-and-beforeinput). |
|
||||
| **`BeforeInput`** | The rendered result of the `admin.components.beforeInput` property. [More details](#afterinput-and-beforeinput). |
|
||||
| **`CustomDescription`** | The rendered result of the `admin.components.Description` property. [More details](#the-description-component). |
|
||||
| **`CustomError`** | The rendered result of the `admin.components.Error` property. [More details](#the-error-component). |
|
||||
| **`CustomLabel`** | The rendered result of the `admin.components.Label` property. [More details](#the-label-component).
|
||||
| **`path`** | The static path of the field at render time. [More details](./hooks#usefieldprops). |
|
||||
| **`disabled`** | The `admin.disabled` property defined in the [Field Config](../fields/overview). |
|
||||
| **`required`** | The `admin.required` property defined in the [Field Config](../fields/overview). |
|
||||
| **`className`** | The `admin.className` property defined in the [Field Config](../fields/overview). |
|
||||
| **`style`** | The `admin.style` property defined in the [Field Config](../fields/overview). |
|
||||
| **`custom`** | The `admin.custom` property defined in the [Field Config](../fields/overview).
|
||||
| **`placeholder`** | The `admin.placeholder` property defined in the [Field Config](../fields/overview). |
|
||||
| **`descriptionProps`** | An object that contains the props for the `FieldDescription` component. |
|
||||
| **`labelProps`** | An object that contains the props needed for the `FieldLabel` component. |
|
||||
| **`errorProps`** | An object that contains the props for the `FieldError` component. |
|
||||
| **`docPreferences`** | An object that contains the preferences for the document. |
|
||||
| **`label`** | The label value provided in the field, it can be used with i18n. |
|
||||
| **`docPreferences`** | An object that contains the [Preferences](./preferences) for the document.
|
||||
| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
|
||||
| **`locale`** | The locale of the field. [More details](../configuration/localization). |
|
||||
| **`localized`** | A boolean value that represents if the field is localized or not. [More details](../fields/localized). |
|
||||
| **`readOnly`** | A boolean value that represents if the field is read-only or not. |
|
||||
| **`rtl`** | A boolean value that represents if the field should be rendered right-to-left or not. [More details](../configuration/i18n). |
|
||||
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
|
||||
| **`validate`** | A function that can be used to validate the field. |
|
||||
| **`hasMany`** | If a [`relationship`](../fields/relationship) field, the `hasMany` property defined in the [Field Config](../fields/overview). |
|
||||
| **`maxLength`** | If a [`text`](../fields/text) field, the `maxLength` property defined in the [Field Config](../fields/overview). |
|
||||
| **`minLength`** | If a [`text`](../fields/text) field, the `minLength` property defined in the [Field Config](../fields/overview). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
@@ -193,6 +173,105 @@ export const CustomTextField: React.FC = () => {
|
||||
For a complete list of all available React hooks, see the [Payload React Hooks](./hooks) documentation. For additional help, see [Building Custom Components](./components#building-custom-components).
|
||||
</Banner>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Field Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview). The convention is to append `Props` to the type of field, i.e. `TextFieldProps`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
ArrayFieldProps,
|
||||
BlocksFieldProps,
|
||||
CheckboxFieldProps,
|
||||
CodeFieldProps,
|
||||
CollapsibleFieldProps,
|
||||
DateFieldProps,
|
||||
EmailFieldProps,
|
||||
GroupFieldProps,
|
||||
HiddenFieldProps,
|
||||
JSONFieldProps,
|
||||
NumberFieldProps,
|
||||
PointFieldProps,
|
||||
RadioFieldProps,
|
||||
RelationshipFieldProps,
|
||||
RichTextFieldProps,
|
||||
RowFieldProps,
|
||||
SelectFieldProps,
|
||||
TabsFieldProps,
|
||||
TextFieldProps,
|
||||
TextareaFieldProps,
|
||||
UploadFieldProps
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
### The `field` Prop
|
||||
|
||||
All Field Components are passed a client-friendly version of their Field Config through a common `field` prop. Since the raw Field Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types), Payload sanitized it into a [Client Config](./components#accessing-the-payload-config) that is safe to pass into Client Components.
|
||||
|
||||
The exact shape of this prop is unique to the specific [Field Type](../fields/overview) being rendered, minus all non-serializable properties. Any [Custom Components](../components) are also resolved into a "mapped component" that is safe to pass.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import type { TextFieldProps } from 'payload'
|
||||
|
||||
export const MyClientFieldComponent: React.FC<TextFieldProps> = ({ field: { name } }) => {
|
||||
return (
|
||||
<p>
|
||||
{`This field's name is ${name}`}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The following additional properties are also provided to the `field` prop:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`_isPresentational`** | A boolean indicating that the field is purely visual and does not directly affect data or change data shape, i.e. the [UI Field](../fields/ui). |
|
||||
| **`_path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray[0].myField`. |
|
||||
| **`_schemaPath`** | A string representing the direct, static path to the [Field Config](../fields/overview), i.e. `myGroup.myArray.myField` |
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Note:</strong>
|
||||
These properties are underscored to denote that they are not part of the original Field Config, and instead are attached during client sanitization to make fields easier to work with on the front-end.
|
||||
</Banner>
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Field Components, you can import the client field props to ensure type safety in your component. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview). The convention is to append `Client` to the type of field, i.e. `TextFieldClient`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
ArrayFieldClient,
|
||||
BlocksFieldClient,
|
||||
CheckboxFieldClient,
|
||||
CodeFieldClient,
|
||||
CollapsibleFieldClient,
|
||||
DateFieldClient,
|
||||
EmailFieldClient,
|
||||
GroupFieldClient,
|
||||
HiddenFieldClient,
|
||||
JSONFieldClient,
|
||||
NumberFieldClient,
|
||||
PointFieldClient,
|
||||
RadioFieldClient,
|
||||
RelationshipFieldClient,
|
||||
RichTextFieldClient,
|
||||
RowFieldClient,
|
||||
SelectFieldClient,
|
||||
TabsFieldClient,
|
||||
TextFieldClient,
|
||||
TextareaFieldClient,
|
||||
UploadFieldClient
|
||||
} from 'payload'
|
||||
```
|
||||
|
||||
When working on the client, you will never have access to objects of type `Field`. This is reserved for the server-side. Instead, you can use `ClientField` which is a union type of all the client fields:
|
||||
|
||||
```tsx
|
||||
import type { ClientField } from 'payload'
|
||||
```
|
||||
|
||||
### The Cell Component
|
||||
|
||||
The Cell Component is rendered in the table of the List View. It represents the value of the field when displayed in a table cell.
|
||||
@@ -207,7 +286,7 @@ export const myField: Field = {
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: MyCustomCell, // highlight-line
|
||||
Cell: '/path/to/MyCustomCellComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -219,20 +298,9 @@ All Cell Components receive the following props:
|
||||
|
||||
| Property | Description |
|
||||
| ---------------- | ----------------------------------------------------------------- |
|
||||
| **`name`** | The name of the field. |
|
||||
| **`className`** | The `admin.className` property defined in the [Field Config](../fields/overview). |
|
||||
| **`fieldType`** | The `type` property defined in the [Field Config](../fields/overview). |
|
||||
| **`schemaPath`** | The path to the field in the schema. Similar to `path`, but without dynamic indices. |
|
||||
| **`isFieldAffectingData`** | A boolean value that represents if the field is affecting the data or not. |
|
||||
| **`label`** | The label value provided in the field, it can be used with i18n. |
|
||||
| **`labels`** | An object that contains the labels for the field. |
|
||||
| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
|
||||
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
|
||||
| **`onClick`** | A function that is called when the cell is clicked. |
|
||||
| **`dateDisplayFormat`** | If a [`date`](../fields/date) field, the `admin.dateDisplayFormat` property defined in the [Field Config](../fields/overview). |
|
||||
| **`options`** | If a [`select`](../fields/select) field, this is an array of options defined in the [Field Config](../fields/overview). [More details](../fields/select). |
|
||||
| **`relationTo`** | If a [`relationship`](../fields/relationship). or [`upload`](../fields/upload) field, this is the collection(s) the field is related to. |
|
||||
| **`richTextComponentMap`** | If a [`richText`](../fields/rich-text) field, this is an object that maps the rich text components. [More details](../fields/rich-text). |
|
||||
| **`blocks`** | If a [`blocks`](../fields/blocks) field, this is an array of labels and slugs representing the blocks defined in the [Field Config](../fields/overview). [More details](../fields/blocks). |
|
||||
|
||||
<Banner type="info">
|
||||
<strong>Tip:</strong>
|
||||
@@ -258,7 +326,7 @@ export const myField: Field = {
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Label: MyCustomLabel, // highlight-line
|
||||
Label: '/path/to/MyCustomLabelComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -270,7 +338,7 @@ Custom Label Components receive all [Field Component](#the-field-component) prop
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`schemaPath`** | The path to the field in the schema. Similar to `path`, but without dynamic indices. |
|
||||
| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
@@ -279,7 +347,7 @@ Custom Label Components receive all [Field Component](#the-field-component) prop
|
||||
|
||||
#### TypeScript
|
||||
|
||||
When building Custom Error Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview). The convention is to append `ErrorComponent` to the type of field, i.e. `TextFieldErrorComponent`.
|
||||
When building Custom Label Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Label Component, one for every [Field Type](../fields/overview). The convention is to append `LabelComponent` to the type of field, i.e. `TextFieldLabelComponent`.
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
@@ -321,7 +389,7 @@ export const myField: Field = {
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Error: MyCustomError, // highlight-line
|
||||
Error: '/path/to/MyCustomErrorComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -333,7 +401,7 @@ Custom Error Components receive all [Field Component](#the-field-component) prop
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | ------------------------------------------------------------- |
|
||||
| **`path`*** | The static path of the field at render time. [More details](./hooks#usefieldprops). |
|
||||
| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
@@ -443,7 +511,6 @@ To easily add a Description Component to a field, use the `admin.components.Desc
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
import { MyCustomDescription } from './MyCustomDescription'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
@@ -454,7 +521,7 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
type: 'text',
|
||||
admin: {
|
||||
components: {
|
||||
Description: MyCustomDescription, // highlight-line
|
||||
Description: '/path/to/MyCustomDescriptionComponent', // highlight-line
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,7 +535,7 @@ Custom Description Components receive all [Field Component](#the-field-component
|
||||
|
||||
| Property | Description |
|
||||
| -------------- | ---------------------------------------------------------------- |
|
||||
| **`description`** | The `description` property defined in the [Field Config](../fields/overview). |
|
||||
| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Reminder:</strong>
|
||||
@@ -524,8 +591,8 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
// highlight-start
|
||||
beforeInput: [MyCustomComponent],
|
||||
afterInput: [MyOtherCustomComponent],
|
||||
beforeInput: ['/path/to/MyCustomComponent'],
|
||||
afterInput: ['/path/to/MyOtherCustomComponent'],
|
||||
// highlight-end
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ To override Global Components, use the `admin.components` property in your [Glob
|
||||
|
||||
```ts
|
||||
import type { SanitizedGlobalConfig } from 'payload'
|
||||
import { CustomSaveButton } from './CustomSaveButton'
|
||||
|
||||
export const MyGlobal: SanitizedGlobalConfig = {
|
||||
// ...
|
||||
|
||||
@@ -52,7 +52,7 @@ The `useField` hook accepts the following arguments:
|
||||
The `useField` hook returns the following object:
|
||||
|
||||
```ts
|
||||
type FieldResult<T> = {
|
||||
type FieldType<T> = {
|
||||
errorMessage?: string
|
||||
errorPaths?: string[]
|
||||
filterOptions?: FilterOptionsResult
|
||||
@@ -65,7 +65,7 @@ type FieldResult<T> = {
|
||||
readOnly?: boolean
|
||||
rows?: Row[]
|
||||
schemaPath: string
|
||||
setValue: (val: unknown, disableModifyingForm?: boolean) => voi
|
||||
setValue: (val: unknown, disableModifyingForm?: boolean) => void
|
||||
showError: boolean
|
||||
valid?: boolean
|
||||
value: T
|
||||
@@ -74,9 +74,9 @@ type FieldResult<T> = {
|
||||
|
||||
## useFieldProps
|
||||
|
||||
All [Custom Field Components](./fields#the-field-component) are rendered on the server, and as such, only have access to static props at render time. But, some fields can be dynamic, such as when nested in an [`array`](../fields/array) or [`blocks`](../fields/block) field. For example, items can be added, re-ordered, or deleted on-the-fly.
|
||||
[Custom Field Components](./fields#the-field-component) can be rendered on the server. When using a server component as a custom field component, you can access dynamic props from within any client component rendered by your custom server component. This is done using the `useFieldProps` hook. This is important because some fields can be dynamic, such as when nested in an [`array`](../fields/array) or [`blocks`](../fields/block) field. For example, items can be added, re-ordered, or deleted on-the-fly.
|
||||
|
||||
For this reason, dynamic props like `path` are managed in their own React context, which can be accessed using the `useFieldProps` hook:
|
||||
You can use the `useFieldProps` hooks to access dynamic props like `path`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -463,7 +463,7 @@ export const CustomArrayManager = () => {
|
||||
name: "customArrayManager",
|
||||
admin: {
|
||||
components: {
|
||||
Field: CustomArrayManager,
|
||||
Field: '/path/to/CustomArrayManagerField',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -560,7 +560,7 @@ export const CustomArrayManager = () => {
|
||||
name: "customArrayManager",
|
||||
admin: {
|
||||
components: {
|
||||
Field: CustomArrayManager,
|
||||
Field: '/path/to/CustomArrayManagerField',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -670,7 +670,7 @@ export const CustomArrayManager = () => {
|
||||
name: "customArrayManager",
|
||||
admin: {
|
||||
components: {
|
||||
Field: CustomArrayManager,
|
||||
Field: '/path/to/CustomArrayManagerField',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -818,7 +818,7 @@ import { useConfig } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const config = useConfig()
|
||||
const { config } = useConfig()
|
||||
// highlight-end
|
||||
|
||||
return <span>{config.serverURL}</span>
|
||||
|
||||
@@ -167,12 +167,12 @@ const config = buildConfig({
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Default route | Description |
|
||||
| ------------------ | ----------------------- | ------------------------------------- |
|
||||
| `admin` | `/admin` | The Admin Panel itself. |
|
||||
| `api` | `/api` | The [REST API](../rest-api/overview) base path. |
|
||||
| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
|
||||
| `graphQLPlayground`| `/graphql-playground` | The GraphQL Playground. |
|
||||
| Option | Default route | Description |
|
||||
|---------------------|-----------------------|---------------------------------------------------|
|
||||
| `admin` | `/admin` | The Admin Panel itself. |
|
||||
| `api` | `/api` | The [REST API](../rest-api/overview) base path. |
|
||||
| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
|
||||
| `graphQLPlayground` | `/graphql-playground` | The GraphQL Playground. |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
|
||||
@@ -31,7 +31,9 @@ const config = buildConfig({
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Dashboard: MyCustomDashboardView, // highlight-line
|
||||
dashboard: {
|
||||
Component: '/path/to/MyCustomDashboardView#MyCustomDashboardViewComponent', // highlight-line
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -44,8 +46,8 @@ The following options are available:
|
||||
|
||||
| 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](./overview). |
|
||||
| **`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](./overview). |
|
||||
|
||||
For more granular control, pass a configuration object instead. Payload exposes the following properties for each view:
|
||||
|
||||
@@ -72,9 +74,9 @@ const config = buildConfig({
|
||||
components: {
|
||||
views: {
|
||||
// highlight-start
|
||||
MyCustomView: {
|
||||
myCustomView: {
|
||||
// highlight-end
|
||||
Component: MyCustomView,
|
||||
Component: '/path/to/MyCustomView#MyCustomViewComponent',
|
||||
path: '/my-custom-view',
|
||||
},
|
||||
},
|
||||
@@ -108,7 +110,9 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: MyCustomEditView, // highlight-line
|
||||
edit: {
|
||||
Component: '/path/to/MyCustomEditView', // highlight-line
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -126,8 +130,8 @@ The following options are available:
|
||||
|
||||
| Property | Description |
|
||||
| ---------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Edit`** | The Edit View is used to edit a single document for any given Collection. [More details](#document-views). |
|
||||
| **`List`** | The List View is used to show a list of documents for any given Collection. |
|
||||
| **`edit`** | The Edit View is used to edit a single document for any given Collection. [More details](#document-views). |
|
||||
| **`list`** | The List View is used to show a list of documents for any given Collection. |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
@@ -148,7 +152,7 @@ export const MyGlobalConfig: SanitizedGlobalConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: MyCustomEditView, // highlight-line
|
||||
edit: '/path/to/MyCustomEditView', // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -166,7 +170,7 @@ The following options are available:
|
||||
|
||||
| Property | Description |
|
||||
| ---------- | ------------------------------------------------------------------- |
|
||||
| **`Edit`** | The Edit View is used to edit a single document for any given Global. [More details](#document-views). |
|
||||
| **`edit`** | The Edit View is used to edit a single document for any given Global. [More details](#document-views). |
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Note:</strong>
|
||||
@@ -187,9 +191,9 @@ export const MyCollectionOrGlobalConfig: SanitizedCollectionConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
API: {
|
||||
Component: MyCustomAPIView, // highlight-line
|
||||
edit: {
|
||||
api: {
|
||||
Component: '/path/to/MyCustomAPIViewComponent', // highlight-line
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -209,15 +213,15 @@ The following options are available:
|
||||
|
||||
| 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). |
|
||||
| **`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). |
|
||||
|
||||
### Document Tabs
|
||||
|
||||
Each Document View can be given a new tab in the Edit View, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Edit View, use the `Component.Tab` key:
|
||||
Each Document View can be given a new tab in the Edit View, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Edit View, use the `tab` key:
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
@@ -227,17 +231,19 @@ export const MyCollection: SanitizedCollectionConfig = {
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: {
|
||||
MyCustomTab: {
|
||||
Component: MyCustomTab,
|
||||
edit: {
|
||||
myCustomTab: {
|
||||
Component: '/path/to/MyCustomTab',
|
||||
path: '/my-custom-tab',
|
||||
Tab: MyCustomTab // highlight-line
|
||||
tab: {
|
||||
Component: '/path/to/MyCustomTabComponent' // highlight-line
|
||||
}
|
||||
},
|
||||
AnotherCustomView: {
|
||||
Component: AnotherCustomView,
|
||||
anotherCustomTab: {
|
||||
Component: '/path/to/AnotherCustomView',
|
||||
path: '/another-custom-view',
|
||||
// highlight-start
|
||||
Tab: {
|
||||
tab: {
|
||||
label: 'Another Custom View',
|
||||
href: '/another-custom-view',
|
||||
}
|
||||
@@ -261,14 +267,15 @@ Custom Views are just [Custom Components](./components) rendered at the page-lev
|
||||
|
||||
```ts
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
import { MyCustomView } from './MyCustomView'
|
||||
|
||||
export const MyCollectionConfig: SanitizedCollectionConfig = {
|
||||
// ...
|
||||
admin: {
|
||||
components: {
|
||||
views: {
|
||||
Edit: MyCustomView, // highlight-line
|
||||
edit: {
|
||||
Component: '/path/to/MyCustomView' // highlight-line
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -102,5 +102,5 @@ You can import types from Payload to help make writing your Collection configs e
|
||||
The `CollectionConfig` type represents a raw Collection Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedCollectionConfig` type represents a Collection Config after it has been fully sanitized. Generally, this is only used internally by Payload.
|
||||
|
||||
```ts
|
||||
import { CollectionConfig, SanitizedCollectionConfig } from 'payload'
|
||||
import type { CollectionConfig, SanitizedCollectionConfig } from 'payload'
|
||||
```
|
||||
|
||||
@@ -106,5 +106,5 @@ You can import types from Payload to help make writing your Global configs easie
|
||||
The `GlobalConfig` type represents a raw Global Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedGlobalConfig` type represents a Global Config after it has been fully sanitized. Generally, this is only used internally by Payload.
|
||||
|
||||
```ts
|
||||
import { GlobalConfig, SanitizedGlobalConfig } from 'payload'
|
||||
import type { GlobalConfig, SanitizedGlobalConfig } from 'payload'
|
||||
```
|
||||
|
||||
@@ -246,5 +246,11 @@ You can import types from Payload to help make writing your config easier and ty
|
||||
The `Config` type represents a raw Payload Config in its full form. Only the bare minimum properties are marked as required. The `SanitizedConfig` type represents a Payload Config after it has been fully sanitized. Generally, this is only used internally by Payload.
|
||||
|
||||
```ts
|
||||
import { Config, SanitizedConfig } from 'payload'
|
||||
import type { Config, SanitizedConfig } from 'payload'
|
||||
```
|
||||
|
||||
## Server vs. Client
|
||||
|
||||
The Payload Config only lives on the server and is not allowed to contain any client-side code. That way, you can load up the Payload Config in any server environment or standalone script, without having to use Bundlers or Node.js loaders to handle importing client-only modules (e.g. scss files or React Components) without any errors.
|
||||
|
||||
Behind the curtains, the Next.js-based Admin Panel generates a ClientConfig, which strips away any server-only code and enriches the config with React Components.
|
||||
|
||||
@@ -211,3 +211,32 @@ In the example above, we've specified a `ci` script which we can use as our "bui
|
||||
This will require that your build pipeline can connect to your database, and it will simply run the `payload migrate` command prior to starting the build process. By calling `payload migrate`, Payload will automatically execute any migrations in your `/migrations` folder that have not yet been executed against your production database, in the order that they were created.
|
||||
|
||||
If it fails, the deployment will be rejected. But now, with your build script set up to run your migrations, you will be all set! Next time you deploy, your CI will execute the required migrations for you, and your database will be caught up with the shape that your Payload Config requires.
|
||||
|
||||
## Running migrations in production
|
||||
|
||||
In certain cases, you might want to run migrations at runtime when the server starts. Running them during build time may be impossible due to not having access to your database connection while building or similar reasoning.
|
||||
|
||||
If you're using a long-running server or container where your Node server starts up one time and then stays initialized, you might prefer to run migrations on server startup instead of within your CI.
|
||||
|
||||
In order to run migrations at runtime, on initialization, you can pass your migrations to your database adapter under the `prodMigrations` key as follows:
|
||||
|
||||
```ts
|
||||
// Import your migrations from the `index.ts` file
|
||||
// that Payload generates for you
|
||||
import { migrations } from './migrations'
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// your config here
|
||||
db: postgresAdapter({
|
||||
// your adapter config here
|
||||
prodMigrations: migrations
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Passing your migrations as shown above will tell Payload, in production only, to execute any migrations that need to be run prior to completing the initialization of Payload. This is ideal for long-running services where Payload will only be initialized at startup.
|
||||
|
||||
<Banner type="warning">
|
||||
Warning - if Payload is instructed to run migrations in production, this may slow down serverless cold starts on platforms such as Vercel. Generally, this option should only be used for long-running servers / containers.
|
||||
</Banner>
|
||||
|
||||
@@ -12,7 +12,7 @@ Currently, Payload officially supports the following Database Adapters:
|
||||
|
||||
- [MongoDB](/docs/database/mongodb) with [Mongoose](https://mongoosejs.com/)
|
||||
- [Postgres](/docs/database/postgres) with [Drizzle](https://drizzle.team/)
|
||||
- Coming soon: SQLite and MySQL using Drizzle.
|
||||
- [SQLite](/docs/database/sqlite) with [Drizzle](https://drizzle.team/)
|
||||
|
||||
To configure a Database Adapter, use the `db` property in your [Payload Config](../configuration/overview):
|
||||
|
||||
@@ -59,7 +59,7 @@ You should prefer MongoDB if:
|
||||
|
||||
Many projects might call for more rigid database architecture where the shape of your data is strongly enforced at the database level. For example, if you know the shape of your data and it's relatively "flat", and you don't anticipate it to change often, your workload might suit relational databases like Postgres very well.
|
||||
|
||||
You should prefer a relational DB like Postgres if:
|
||||
You should prefer a relational DB like Postgres or SQLite if:
|
||||
|
||||
- You are comfortable with [Migrations](./migrations)
|
||||
- You require enforced data consistency at the database level
|
||||
|
||||
@@ -54,8 +54,8 @@ export const ExampleCollection: CollectionConfig = {
|
||||
type: 'ui', // required
|
||||
admin: {
|
||||
components: {
|
||||
Field: MyCustomUIField,
|
||||
Cell: MyCustomUICell,
|
||||
Field: '/path/to/MyCustomUIField',
|
||||
Cell: '/path/to/MyCustomUICell',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ To install a Database Adapter, you can run **one** of the following commands:
|
||||
|
||||
#### 2. Copy Payload files into your Next.js app folder
|
||||
|
||||
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/beta/templates/blank-3.0/src/app/(payload)) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
|
||||
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/beta/templates/blank/src/app/(payload)) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
|
||||
|
||||
```plaintext
|
||||
app/
|
||||
|
||||
@@ -20,7 +20,7 @@ IMPORTANT: This will overwrite all slate data. We recommend doing the following
|
||||
3. Add the SlateToLexicalFeature (as seen below) first, and test it out by loading up the Admin Panel, to see if the migrator works as expected. You might have to build some custom converters for some fields first in order to convert custom Slate nodes. The SlateToLexicalFeature is where the converters are stored. Only fields with this feature added will be migrated.
|
||||
|
||||
```ts
|
||||
import { migrateSlateToLexical } from '@payloadcms/richtext-lexical'
|
||||
import { migrateSlateToLexical } from '@payloadcms/richtext-lexical/migrate'
|
||||
|
||||
await migrateSlateToLexical({ payload })
|
||||
```
|
||||
@@ -34,7 +34,8 @@ Simply add the `SlateToLexicalFeature` to your editor:
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { SlateToLexicalFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { SlateToLexicalFeature } from '@payloadcms/richtext-lexical/migrate'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
@@ -64,8 +65,8 @@ The easy way to solve this: Edit the richText field and save the document! This
|
||||
If you have custom Slate nodes, create a custom converter for them. Here's the Upload converter as an example:
|
||||
|
||||
```ts
|
||||
import type { SerializedUploadNode } from '../uploadNode.'
|
||||
import type { SlateNodeConverter } from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedUploadNode } from '../uploadNode'
|
||||
import type { SlateNodeConverter } from '@payloadcms/richtext-lexical/migrate'
|
||||
|
||||
export const SlateUploadConverter: SlateNodeConverter = {
|
||||
converter({ slateNode }) {
|
||||
@@ -95,9 +96,9 @@ When using the `SlateToLexicalFeature`, you can add your custom converters to th
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import {
|
||||
SlateToLexicalFeature,
|
||||
lexicalEditor,
|
||||
defaultSlateConverters,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
title: Using Payload outside Next.js
|
||||
label: Outside Next.js
|
||||
order: 20
|
||||
desc: Payload can be used outside of Next.js within standalone scripts or in other frameworks like Remix, Sveltekit, Nuxt, and similar.
|
||||
desc: Payload can be used outside of Next.js within standalone scripts or in other frameworks like Remix, SvelteKit, Nuxt, and similar.
|
||||
keywords: local api, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
Payload can be used completely outside of Next.js which is helpful in cases like running scripts, using Payload in a separate backend service, or using Payload's Local API to fetch your data directly from your database in other frontend frameworks like Sveltekit, Remix, Nuxt, and similar.
|
||||
Payload can be used completely outside of Next.js which is helpful in cases like running scripts, using Payload in a separate backend service, or using Payload's Local API to fetch your data directly from your database in other frontend frameworks like SvelteKit, Remix, Nuxt, and similar.
|
||||
|
||||
<Banner>
|
||||
<strong>Note:</strong>
|
||||
@@ -16,38 +16,16 @@ Payload can be used completely outside of Next.js which is helpful in cases like
|
||||
|
||||
## Importing the Payload Config outside of Next.js
|
||||
|
||||
Your Payload Config likely has imports which need to be handled properly, such as CSS imports and similar. If you were to try and import your config without any Node support for SCSS / CSS files, you'll see errors that arise accordingly.
|
||||
Payload provides a convenient way to run standalone scripts, which can be useful for tasks like seeding your database or performing one-off operations.
|
||||
|
||||
This is especially relevant if you are importing your Payload Config outside of a bundler context, such as in standalone Node scripts.
|
||||
|
||||
For these cases, you can use Payload's `importConfig` function to handle importing your config safely. It will handle everything you need to be able to load and use your Payload Config, without any client-side files present.
|
||||
|
||||
Here's an example of a seed script that creates a few documents for local development / testing purposes, using Payload's `importConfig` function to safely import Payload, and the `getPayload` function to retrieve an initialized copy of Payload.
|
||||
In standalone scripts, can simply import the Payload Config and use it right away. If you need an initialized copy of Payload, you can then use the `getPayload` function. This can be useful for tasks like seeding your database or performing other one-off operations.
|
||||
|
||||
```ts
|
||||
// We are importing `getPayload` because we don't need HMR
|
||||
// for a standalone script. For usage of Payload inside Next.js,
|
||||
// you should always use `import { getPayloadHMR } from '@payloadcms/next/utilities'` instead.
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
// This is a helper function that will make sure we can safely load the Payload Config
|
||||
// and all of its client-side files, such as CSS, SCSS, etc.
|
||||
import { importConfig } from 'payload/node'
|
||||
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
// In ESM, you can create the "dirname" variable
|
||||
// like this. We'll use this with `dotenv` to load our `.env` file, if necessary.
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
// If you don't need to load your .env file,
|
||||
// then you can skip this part!
|
||||
dotenv.config({
|
||||
path: path.resolve(dirname, '../.env'),
|
||||
})
|
||||
import config from '@payload-config'
|
||||
|
||||
const seed = async () => {
|
||||
// Get a local copy of Payload by passing your config
|
||||
@@ -71,6 +49,26 @@ const seed = async () => {
|
||||
}
|
||||
|
||||
// Call the function here to run your seed script
|
||||
seed()
|
||||
|
||||
await seed()
|
||||
```
|
||||
|
||||
You can then execute the script using `payload run`. Example: if you placed this standalone script in `src/seed.ts`, you would execute it like this:
|
||||
|
||||
```sh
|
||||
payload run src/seed.ts
|
||||
```
|
||||
|
||||
The `payload run` command does two things for you:
|
||||
|
||||
1. It loads the environment variables the same way Next.js loads them, eliminating the need for additional dependencies like `dotenv`. The usage of `dotenv` is not recommended, as Next.js loads environment variables differently. By using `payload run`, you ensure consistent environment variable handling across your Payload and Next.js setup.
|
||||
2. It initializes swc, allowing direct execution of TypeScript files without requiring tools like tsx or ts-node.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you encounter import-related errors, try running the script in TSX mode:
|
||||
|
||||
```sh
|
||||
payload run src/seed.ts --use-tsx
|
||||
```
|
||||
|
||||
Note: Install tsx in your project first. Be aware that TSX mode is slower than the default swc mode, so only use it if necessary.
|
||||
|
||||
@@ -62,19 +62,16 @@ If you are accessing Payload via function arguments or `req.payload`, HMR is aut
|
||||
|
||||
**Option 2 - outside of Next.js**
|
||||
|
||||
If you are using Payload outside of Next.js, for example in standalone scripts or in other frameworks, you can import Payload with no HMR functionality.
|
||||
If you are using Payload outside of Next.js, for example in standalone scripts or in other frameworks, you can import Payload with no HMR functionality. Instead of using `getPayloadHMR`, you can use `getPayload`.
|
||||
|
||||
```ts
|
||||
import { getPayload } from 'payload'
|
||||
import { importConfig } from 'payload/node'
|
||||
import config from '@payload-config'
|
||||
|
||||
const config = await importConfig('./payload.config.ts')
|
||||
const payload = await getPayload({ config })
|
||||
```
|
||||
|
||||
Both options function in exactly the same way outside of one having HMR support and the other not. However, when you import your Payload Config, you need to make sure that you can import it safely.
|
||||
|
||||
For more information about using Payload outside of Next.js, [click here](/docs/beta/local-api/outside-nextjs).
|
||||
Both options function in exactly the same way outside of one having HMR support and the other not. For more information about using Payload outside of Next.js, [click here](/docs/beta/local-api/outside-nextjs).
|
||||
|
||||
## Local options available
|
||||
|
||||
|
||||
@@ -159,7 +159,6 @@ import {
|
||||
useAllFormFields,
|
||||
useAuth,
|
||||
useClientFunctions,
|
||||
useComponentMap,
|
||||
useConfig,
|
||||
useDebounce,
|
||||
useDebouncedCallback,
|
||||
@@ -212,7 +211,6 @@ import {
|
||||
ActionsProvider,
|
||||
AuthProvider,
|
||||
ClientFunctionProvider,
|
||||
ComponentMapProvider,
|
||||
ConfigProvider,
|
||||
DocumentEventsProvider,
|
||||
DocumentInfoProvider,
|
||||
@@ -299,14 +297,10 @@ import {
|
||||
fieldBaseClass,
|
||||
|
||||
// TS Types
|
||||
ActionMap,
|
||||
CollectionComponentMap,
|
||||
ColumnPreferences,
|
||||
ConfigComponentMapBase,
|
||||
DocumentInfoContext,
|
||||
DocumentInfoProps,
|
||||
FieldType,
|
||||
FieldComponentProps,
|
||||
FormProps,
|
||||
RowLabelProps,
|
||||
SelectFieldProps,
|
||||
@@ -323,7 +317,6 @@ import {
|
||||
AppHeader,
|
||||
BlocksDrawer,
|
||||
Column,
|
||||
ComponentMap,
|
||||
DefaultBlockImage,
|
||||
DeleteMany,
|
||||
DocumentControls,
|
||||
@@ -338,7 +331,6 @@ import {
|
||||
FormLoadingOverlayToggle,
|
||||
FormSubmit,
|
||||
GenerateConfirmation,
|
||||
GlobalComponentMap,
|
||||
HydrateClientUser,
|
||||
ListControls,
|
||||
ListSelection,
|
||||
@@ -349,7 +341,8 @@ import {
|
||||
PublishMany,
|
||||
ReactSelect,
|
||||
ReactSelectOption,
|
||||
ReducedBlock,
|
||||
ClientField,
|
||||
ClientBlock,
|
||||
RenderFields,
|
||||
SectionTitle,
|
||||
Select,
|
||||
@@ -716,7 +709,7 @@ import type { FormState } from 'payload'
|
||||
|
||||
This is because the configs themselves are not serializable and so they cannot be thread through to the client, i.e. the `DocumentInfoContext`. Instead, various properties of the config are passed instead, like `collectionSlug` and `globalSlug`. You can use these to access a client-side config, if needed, through the `useConfig` hook (see next bullet).
|
||||
|
||||
17. The `useConfig` hook now returns a `ClientConfig` and not a `SanizitedConfig`.
|
||||
17. The `useConfig` hook now returns a `ClientConfig` and not a `SanitizedConfig`.
|
||||
|
||||
This is because the config itself is not serializable and so it is not able to be thread through to the client, i.e. the `ConfigContext`.
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ The `beforeEmail` property is a [beforeChange](<[beforeChange](https://payloadcm
|
||||
// payload.config.ts
|
||||
formBuilder({
|
||||
// ...
|
||||
beforeEmail: (emailsToSend) => {
|
||||
beforeEmail: (emailsToSend, beforeChangeParams) => {
|
||||
// modify the emails in any way before they are sent
|
||||
return emails.map((email) => ({
|
||||
...email,
|
||||
@@ -119,6 +119,23 @@ formBuilder({
|
||||
})
|
||||
```
|
||||
|
||||
For full types with `beforeChangeParams`, you can import the types from the plugin:
|
||||
|
||||
```ts
|
||||
import type { BeforeEmail } from '@payloadcms/plugin-form-builder'
|
||||
// Your generated FormSubmission type
|
||||
import type {FormSubmission} from '@payload-types'
|
||||
|
||||
// Pass it through and 'data' or 'originalDoc' will now be typed
|
||||
const beforeEmail: BeforeEmail<FormSubmission> = (emailsToSend, beforeChangeParams) => {
|
||||
// modify the emails in any way before they are sent
|
||||
return emails.map((email) => ({
|
||||
...email,
|
||||
html: email.html, // transform the html in any way you'd like (maybe wrap it in an html template?)
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### `formOverrides`
|
||||
|
||||
Override anything on the `forms` collection by sending a [Payload Collection Config](https://payloadcms.com/docs/configuration/collections) to the `formOverrides` property.
|
||||
|
||||
@@ -66,6 +66,8 @@ export default config
|
||||
| ------------- | ---------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `collections` | `string[]` | An array of collection slugs to populate in the `to` field of each redirect. |
|
||||
| `overrides` | `object` | A partial collection config that allows you to override anything on the `redirects` collection. |
|
||||
| `redirectTypes` | `string[]` | Provide an array of redirects if you want to provide options for the type of redirects to be supported. |
|
||||
| `redirectTypeFieldOverride` | `Field` | A partial Field config that allows you to override the Redirect Type field if enabled above. |
|
||||
|
||||
Note that the fields in overrides take a function that receives the default fields and returns an array of fields. This allows you to add fields to the collection.
|
||||
|
||||
@@ -83,6 +85,10 @@ redirectsPlugin({
|
||||
]
|
||||
},
|
||||
},
|
||||
redirectTypes: ['301', '302'],
|
||||
redirectTypeFieldOverride: {
|
||||
label: 'Redirect Type (Overridden)',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ _An asterisk denotes that an option is required._
|
||||
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
|
||||
| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. |
|
||||
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
|
||||
| **`filenameCompoundIndex`** | Field slugs to use for a compount index instead of the default filename index.
|
||||
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
|
||||
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
|
||||
| **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. |
|
||||
@@ -169,6 +170,22 @@ When an uploaded image is smaller than the defined image size, we have 3 options
|
||||
image size. Use the `withoutEnlargement` prop to change this.
|
||||
</Banner>
|
||||
|
||||
#### Custom file name per size
|
||||
|
||||
Each image size supports a `generateImageName` function that can be used to generate a custom file name for the resized image.
|
||||
This function receives the original file name, the resize name, the extension, height and width as arguments.
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'thumbnail',
|
||||
width: 400,
|
||||
height: 300,
|
||||
generateImageName: ({ height, sizeName, extension, width }) => {
|
||||
return `custom-${sizeName}-${height}-${width}.${extension}`
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Crop and Focal Point Selector
|
||||
|
||||
This feature is only available for image file types.
|
||||
|
||||
@@ -28,6 +28,7 @@ export const rootParserOptions = {
|
||||
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
|
||||
EXPERIMENTAL_useProjectService: {
|
||||
allowDefaultProjectForFiles: ['./src/*.ts', './src/*.tsx'],
|
||||
maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 100,
|
||||
},
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('Users', () => {
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit --detectOpenHandles"
|
||||
"test": "jest --forceExit --detectOpenHandles"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -29,6 +29,6 @@
|
||||
"build:server": "tsc",
|
||||
"build": "yarn build:server && yarn build:payload",
|
||||
"serve": "PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit --detectOpenHandles"
|
||||
"test": "jest --forceExit --detectOpenHandles"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
const esModules = [
|
||||
// file-type and all dependencies: https://github.com/sindresorhus/file-type
|
||||
'file-type',
|
||||
'strtok3',
|
||||
'readable-web-to-node-stream',
|
||||
'token-types',
|
||||
'peek-readable',
|
||||
'find-up',
|
||||
'locate-path',
|
||||
'p-locate',
|
||||
'p-limit',
|
||||
'yocto-queue',
|
||||
'unicorn-magic',
|
||||
'path-exists',
|
||||
'qs-esm',
|
||||
].join('|')
|
||||
|
||||
/** @type {import('jest').Config} */
|
||||
const baseJestConfig = {
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
setupFiles: ['<rootDir>/test/jest.setup.env.js'],
|
||||
setupFilesAfterEnv: ['<rootDir>/test/jest.setup.js'],
|
||||
transformIgnorePatterns: [
|
||||
`/node_modules/(?!.pnpm)(?!(${esModules})/)`,
|
||||
`/node_modules/.pnpm/(?!(${esModules.replace(/\//g, '\\+')})@)`,
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|scss)$': '<rootDir>/test/helpers/mocks/emptyModule.js',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import bundleAnalyzer from '@next/bundle-analyzer'
|
||||
|
||||
import withPayload from './packages/next/src/withPayload.js'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(__filename)
|
||||
|
||||
const withBundleAnalyzer = bundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
@@ -15,6 +20,10 @@ export default withBundleAnalyzer(
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
|
||||
55
package.json
55
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -20,6 +20,7 @@
|
||||
"build:email-nodemailer": "turbo build --filter email-nodemailer",
|
||||
"build:email-resend": "turbo build --filter email-resend",
|
||||
"build:eslint-config": "turbo build --filter eslint-config",
|
||||
"build:essentials:force": "pnpm clean:build && turbo build --filter=\"payload...\" --filter=\"@payloadcms/ui\" --filter=\"@payloadcms/next\" --filter=\"@payloadcms/db-mongodb\" --filter=\"@payloadcms/db-postgres\" --filter=\"@payloadcms/richtext-lexical\" --filter=\"@payloadcms/translations\" --filter=\"@payloadcms/plugin-cloud\" --filter=\"@payloadcms/graphql\" --no-cache --force",
|
||||
"build:force": "pnpm run build:core:force",
|
||||
"build:graphql": "turbo build --filter graphql",
|
||||
"build:live-preview": "turbo build --filter live-preview",
|
||||
@@ -52,10 +53,12 @@
|
||||
"clean:all": "node ./scripts/delete-recursively.js '@node_modules' 'media/*' '**/dist/' '**/.cache/*' '**/.next/*' '**/.turbo/*' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
|
||||
"clean:build": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
|
||||
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next/*",
|
||||
"dev": "cross-env NODE_OPTIONS=--no-deprecation node ./test/dev.js",
|
||||
"dev:generate-graphql-schema": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateGraphQLSchema.ts",
|
||||
"dev:generate-types": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateTypes.ts",
|
||||
"dev:postgres": "cross-env NODE_OPTIONS=--no-deprecation PAYLOAD_DATABASE=postgres node ./test/dev.js",
|
||||
"dev": "pnpm runts ./test/dev.ts",
|
||||
"runts": "node --no-deprecation --import @swc-node/register/esm-register",
|
||||
"dev:generate-graphql-schema": "pnpm runts ./test/generateGraphQLSchema.ts",
|
||||
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
|
||||
"dev:generate-types": "pnpm runts ./test/generateTypes.ts",
|
||||
"dev:postgres": "pnpm runts ./test/dev.ts",
|
||||
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
|
||||
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
|
||||
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
|
||||
@@ -67,20 +70,20 @@
|
||||
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
|
||||
"prepare": "husky",
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release:alpha": "tsx ./scripts/release.ts --bump prerelease --tag alpha",
|
||||
"release:beta": "tsx ./scripts/release.ts --bump prerelease --tag beta",
|
||||
"script:gen-templates": "tsx ./scripts/generate-template-variations.ts",
|
||||
"script:list-published": "tsx scripts/lib/getPackageRegistryVersions.ts",
|
||||
"script:pack": "tsx scripts/pack-all-to-dest.ts",
|
||||
"release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
|
||||
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
|
||||
"script:gen-templates": "pnpm runts ./scripts/generate-template-variations.ts",
|
||||
"script:list-published": "pnpm runts scripts/lib/getPackageRegistryVersions.ts",
|
||||
"script:pack": "pnpm runts scripts/pack-all-to-dest.ts",
|
||||
"pretest": "pnpm build",
|
||||
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
|
||||
"test:components": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" jest --config=jest.components.config.js",
|
||||
"test:e2e": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 tsx ./test/runE2E.ts",
|
||||
"test:components": "cross-env NODE_OPTIONS=\" --no-deprecation\" jest --config=jest.components.config.js",
|
||||
"test:e2e": "pnpm runts ./test/runE2E.ts",
|
||||
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
|
||||
"test:int": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:postgres": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:unit": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
|
||||
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
|
||||
"translateNewKeys": "pnpm --filter payload run translateNewKeys"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -100,8 +103,9 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
"@payloadcms/live-preview-react": "workspace:*",
|
||||
"@playwright/test": "1.43.0",
|
||||
"@swc/cli": "0.3.12",
|
||||
"@playwright/test": "1.46.0",
|
||||
"@swc-node/register": "1.10.9",
|
||||
"@swc/cli": "0.4.0",
|
||||
"@swc/jest": "0.2.36",
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"@types/jest": "29.5.12",
|
||||
@@ -134,21 +138,21 @@
|
||||
"next": "15.0.0-canary.104",
|
||||
"open": "^10.1.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"playwright": "1.43.0",
|
||||
"playwright-core": "1.43.0",
|
||||
"playwright": "1.46.0",
|
||||
"playwright-core": "1.46.0",
|
||||
"prettier": "3.3.2",
|
||||
"prompts": "2.4.2",
|
||||
"react": "^19.0.0-rc-06d0b89e-20240801",
|
||||
"react-dom": "^19.0.0-rc-06d0b89e-20240801",
|
||||
"react": "19.0.0-rc-06d0b89e-20240801",
|
||||
"react-dom": "19.0.0-rc-06d0b89e-20240801",
|
||||
"rimraf": "3.0.2",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "0.32.6",
|
||||
"shelljs": "0.8.5",
|
||||
"slash": "3.0.0",
|
||||
"sort-package-json": "^2.10.0",
|
||||
"swc-plugin-transform-remove-imports": "1.14.0",
|
||||
"swc-plugin-transform-remove-imports": "1.15.0",
|
||||
"tempy": "1.0.1",
|
||||
"tsx": "4.16.2",
|
||||
"tsx": "4.17.0",
|
||||
"turbo": "^1.13.3",
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
@@ -158,7 +162,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.20.2 || >=20.9.0",
|
||||
"pnpm": "^8.15.7"
|
||||
"pnpm": "^9.7.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"allowedDeprecatedVersions": {
|
||||
@@ -177,9 +181,6 @@
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom",
|
||||
"typescript": "$typescript"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"playwright@1.43.0": "patches/playwright@1.43.0.patch"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -22,7 +22,7 @@ const customJestConfig = {
|
||||
},
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['<rootDir>/**/*spec.ts'],
|
||||
testTimeout: 90000,
|
||||
testTimeout: 160000,
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': ['@swc/jest'],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -42,7 +42,7 @@
|
||||
"build": "pnpm pack-template-files && pnpm typecheck && pnpm build:swc",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"pack-template-files": "tsx src/scripts/pack-template-files.ts",
|
||||
"pack-template-files": "node --no-deprecation --import @swc-node/register/esm-register src/scripts/pack-template-files.ts",
|
||||
"prepublishOnly": "pnpm clean && pnpm build",
|
||||
"test": "jest",
|
||||
"typecheck": "tsc"
|
||||
@@ -50,7 +50,7 @@
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@sindresorhus/slugify": "^1.1.0",
|
||||
"@swc/core": "^1.6.13",
|
||||
"@swc/core": "1.7.10",
|
||||
"arg": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"comment-json": "^4.2.3",
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('createProject', () => {
|
||||
const args = {
|
||||
_: ['project-name'],
|
||||
'--db': 'mongodb',
|
||||
'--local-template': 'blank-3.0',
|
||||
'--local-template': 'blank',
|
||||
'--no-deps': true,
|
||||
} as CliArgs
|
||||
const packageManager = 'yarn'
|
||||
@@ -64,8 +64,8 @@ describe('createProject', () => {
|
||||
const templates = getValidTemplates()
|
||||
|
||||
it.each([
|
||||
['blank-3.0', 'mongodb'],
|
||||
['blank-3.0', 'postgres'],
|
||||
['blank', 'mongodb'],
|
||||
['blank', 'postgres'],
|
||||
|
||||
// TODO: Re-enable these once 3.0 is stable and templates updated
|
||||
// ['website', 'mongodb'],
|
||||
|
||||
@@ -182,7 +182,7 @@ async function installAndConfigurePayload(
|
||||
const templateFilesPath =
|
||||
dirname.endsWith('dist') || useDistFiles
|
||||
? path.resolve(dirname, '../..', 'dist/template')
|
||||
: path.resolve(dirname, '../../../../templates/blank-3.0')
|
||||
: path.resolve(dirname, '../../../../templates/blank')
|
||||
|
||||
logDebug(`Using template files from: ${templateFilesPath}`)
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export function getValidTemplates(): ProjectTemplate[] {
|
||||
name: 'blank',
|
||||
type: 'starter',
|
||||
description: 'Blank 3.0 Template',
|
||||
url: 'https://github.com/payloadcms/payload/templates/blank-3.0#beta',
|
||||
url: 'https://github.com/payloadcms/payload/templates/blank#beta',
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function updatePayloadInProject(
|
||||
|
||||
const templateFilesPath =
|
||||
process.env.JEST_WORKER_ID !== undefined
|
||||
? path.resolve(dirname, '../../../../templates/blank-3.0')
|
||||
? path.resolve(dirname, '../../../../templates/blank')
|
||||
: path.resolve(dirname, '../..', 'dist/template')
|
||||
|
||||
const templateSrcDir = path.resolve(templateFilesPath, 'src/app/(payload)')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ExportDefaultExpression, ModuleItem } from '@swc/core'
|
||||
|
||||
import swc from '@swc/core'
|
||||
import { parse } from '@swc/core'
|
||||
import chalk from 'chalk'
|
||||
import { Syntax, parseModule } from 'esprima-next'
|
||||
import fs from 'fs'
|
||||
@@ -281,10 +281,10 @@ async function compileTypeScriptFileToAST(
|
||||
* https://github.com/swc-project/swc/issues/1366
|
||||
*/
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
parseOffset = (await swc.parse('')).span.end
|
||||
parseOffset = (await parse('')).span.end
|
||||
}
|
||||
|
||||
const module = await swc.parse(fileContent, {
|
||||
const module = await parse(fileContent, {
|
||||
syntax: 'typescript',
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const dirname = path.dirname(filename)
|
||||
main()
|
||||
|
||||
/**
|
||||
* Copy the necessary template files from `templates/blank-3.0` to `dist/template`
|
||||
* Copy the necessary template files from `templates/blank` to `dist/template`
|
||||
*
|
||||
* Eventually, this should be replaced with using tar.x to stream from the git repo
|
||||
*/
|
||||
@@ -17,7 +17,7 @@ main()
|
||||
async function main() {
|
||||
const root = path.resolve(dirname, '../../../../')
|
||||
const outputPath = path.resolve(dirname, '../../dist/template')
|
||||
const sourceTemplatePath = path.resolve(root, 'templates/blank-3.0')
|
||||
const sourceTemplatePath = path.resolve(root, 'templates/blank')
|
||||
|
||||
if (!fs.existsSync(sourceTemplatePath)) {
|
||||
throw new Error(`Source path does not exist: ${sourceTemplatePath}`)
|
||||
@@ -27,7 +27,7 @@ async function main() {
|
||||
fs.mkdirSync(outputPath, { recursive: true })
|
||||
}
|
||||
|
||||
// Copy the src directory from `templates/blank-3.0` to `dist`
|
||||
// Copy the src directory from `templates/blank` to `dist`
|
||||
const srcPath = path.resolve(sourceTemplatePath, 'src')
|
||||
const distSrcPath = path.resolve(outputPath, 'src')
|
||||
// Copy entire file structure from src to dist
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -21,6 +21,18 @@ const buildCollectionSchema = (
|
||||
},
|
||||
})
|
||||
|
||||
if (Array.isArray(collection.upload.filenameCompoundIndex)) {
|
||||
const indexDefinition: Record<string, 1> = collection.upload.filenameCompoundIndex.reduce(
|
||||
(acc, index) => {
|
||||
acc[index] = 1
|
||||
return acc
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
schema.index(indexDefinition, { unique: true })
|
||||
}
|
||||
|
||||
if (config.indexSortableFields && collection.timestamps !== false) {
|
||||
schema.index({ updatedAt: 1 })
|
||||
schema.index({ createdAt: 1 })
|
||||
|
||||
@@ -67,31 +67,6 @@ export type FieldGenerator<TSchema, TField> = {
|
||||
schema: TSchema
|
||||
}
|
||||
|
||||
/**
|
||||
* Field config types that need representation in the database
|
||||
*/
|
||||
type FieldType =
|
||||
| 'array'
|
||||
| 'blocks'
|
||||
| 'checkbox'
|
||||
| 'code'
|
||||
| 'collapsible'
|
||||
| 'date'
|
||||
| 'email'
|
||||
| 'group'
|
||||
| 'json'
|
||||
| 'number'
|
||||
| 'point'
|
||||
| 'radio'
|
||||
| 'relationship'
|
||||
| 'richText'
|
||||
| 'row'
|
||||
| 'select'
|
||||
| 'tabs'
|
||||
| 'text'
|
||||
| 'textarea'
|
||||
| 'upload'
|
||||
|
||||
export type FieldGeneratorFunction<TSchema, TField extends Field> = (
|
||||
args: FieldGenerator<TSchema, TField>,
|
||||
) => void
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -42,7 +42,7 @@
|
||||
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||
"prepack": "pnpm clean && pnpm turbo build",
|
||||
"prepublishOnly": "pnpm clean && pnpm turbo build",
|
||||
"renamePredefinedMigrations": "tsx ./scripts/renamePredefinedMigrations.ts"
|
||||
"renamePredefinedMigrations": "node --no-deprecation --import @swc-node/register/esm-register ./scripts/renamePredefinedMigrations.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/drizzle": "workspace:*",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { MigrationTemplateArgs } from 'payload'
|
||||
|
||||
export const indent = (text: string) =>
|
||||
text
|
||||
.split('\n')
|
||||
.map((line) => ` ${line}`)
|
||||
.join('\n')
|
||||
|
||||
export const getMigrationTemplate = ({
|
||||
downSQL,
|
||||
imports,
|
||||
@@ -7,10 +13,10 @@ export const getMigrationTemplate = ({
|
||||
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
${imports ? `${imports}\n` : ''}
|
||||
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
|
||||
${upSQL}
|
||||
${indent(upSQL)}
|
||||
}
|
||||
|
||||
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
|
||||
${downSQL}
|
||||
${indent(downSQL)}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Init, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { createTableName } from '@payloadcms/drizzle'
|
||||
import { uniqueIndex } from 'drizzle-orm/pg-core'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { BaseExtraConfig } from './schema/build.js'
|
||||
import type { PostgresAdapter } from './types.js'
|
||||
|
||||
import { buildTable } from './schema/build.js'
|
||||
@@ -34,8 +36,22 @@ export const init: Init = function init(this: PostgresAdapter) {
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {}
|
||||
|
||||
if (collection.upload.filenameCompoundIndex) {
|
||||
const indexName = `${tableName}_filename_compound_idx`
|
||||
|
||||
baseExtraConfig.filename_compound_index = (cols) => {
|
||||
const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => {
|
||||
return cols[f]
|
||||
})
|
||||
return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
baseExtraConfig,
|
||||
disableNotNull: !!collection?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
fields: collection.fields,
|
||||
|
||||
@@ -38,6 +38,10 @@ export type RelationMap = Map<string, { localized: boolean; target: string; type
|
||||
type Args = {
|
||||
adapter: PostgresAdapter
|
||||
baseColumns?: Record<string, PgColumnBuilder>
|
||||
/**
|
||||
* After table is created, run these functions to add extra config to the table
|
||||
* ie. indexes, multiple columns, etc
|
||||
*/
|
||||
baseExtraConfig?: BaseExtraConfig
|
||||
buildNumbers?: boolean
|
||||
buildRelationships?: boolean
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Payload Postgres Adapter
|
||||
# Payload SQLite Adapter
|
||||
|
||||
Official SQLite adapter for [Payload](https://payloadcms.com).
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { MigrationTemplateArgs } from 'payload'
|
||||
|
||||
export const indent = (text: string) =>
|
||||
text
|
||||
.split('\n')
|
||||
.map((line) => ` ${line}`)
|
||||
.join('\n')
|
||||
|
||||
export const getMigrationTemplate = ({
|
||||
downSQL,
|
||||
imports,
|
||||
@@ -7,10 +13,10 @@ export const getMigrationTemplate = ({
|
||||
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-sqlite'
|
||||
${imports ? `${imports}\n` : ''}
|
||||
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
|
||||
${upSQL}
|
||||
${indent(upSQL)}
|
||||
}
|
||||
|
||||
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
|
||||
${downSQL}
|
||||
${indent(downSQL)}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
|
||||
import type { Init, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { createTableName } from '@payloadcms/drizzle'
|
||||
import { uniqueIndex } from 'drizzle-orm/sqlite-core'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { BaseExtraConfig } from './schema/build.js'
|
||||
import type { SQLiteAdapter } from './types.js'
|
||||
|
||||
import { buildTable } from './schema/build.js'
|
||||
@@ -37,6 +38,19 @@ export const init: Init = function init(this: SQLiteAdapter) {
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
|
||||
const baseExtraConfig: BaseExtraConfig = {}
|
||||
|
||||
if (collection.upload.filenameCompoundIndex) {
|
||||
const indexName = `${tableName}_filename_compound_idx`
|
||||
|
||||
baseExtraConfig.filename_compound_index = (cols) => {
|
||||
const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => {
|
||||
return cols[f]
|
||||
})
|
||||
return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
buildTable({
|
||||
adapter: this,
|
||||
disableNotNull: !!collection?.versions?.drafts,
|
||||
|
||||
@@ -40,6 +40,10 @@ export type RelationMap = Map<string, { localized: boolean; target: string; type
|
||||
type Args = {
|
||||
adapter: SQLiteAdapter
|
||||
baseColumns?: Record<string, SQLiteColumnBuilder>
|
||||
/**
|
||||
* After table is created, run these functions to add extra config to the table
|
||||
* ie. indexes, multiple columns, etc
|
||||
*/
|
||||
baseExtraConfig?: BaseExtraConfig
|
||||
buildNumbers?: boolean
|
||||
buildRelationships?: boolean
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -74,16 +74,12 @@ export const migrate: DrizzleAdapter['migrate'] = async function migrate(
|
||||
}
|
||||
|
||||
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
|
||||
const db = payload.db as DrizzleAdapter
|
||||
const { generateDrizzleJson } = db.requireDrizzleKit()
|
||||
const start = Date.now()
|
||||
const req = { payload } as PayloadRequest
|
||||
const adapter = payload.db as DrizzleAdapter
|
||||
|
||||
payload.logger.info({ msg: `Migrating: ${migration.name}` })
|
||||
|
||||
const drizzleJSON = await generateDrizzleJson({ schema: adapter.schema })
|
||||
|
||||
try {
|
||||
await initTransaction(req)
|
||||
const db = adapter?.sessions[await req.transactionID]?.db || adapter.drizzle
|
||||
@@ -94,7 +90,6 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
|
||||
data: {
|
||||
name: migration.name,
|
||||
batch,
|
||||
schema: drizzleJSON,
|
||||
},
|
||||
req,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { bin } from './dist/bin/index.js'
|
||||
import { register } from 'node:module'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
bin()
|
||||
// Allow disabling SWC for debugging
|
||||
if (process.env.DISABLE_SWC !== 'true') {
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
const url = pathToFileURL(dirname).toString() + '/'
|
||||
|
||||
register('@swc-node/register/esm', url)
|
||||
}
|
||||
|
||||
const start = async () => {
|
||||
const { bin } = await import('./dist/bin/index.js')
|
||||
await bin()
|
||||
}
|
||||
|
||||
void start()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -43,6 +43,7 @@
|
||||
"dependencies": {
|
||||
"graphql-scalars": "1.22.2",
|
||||
"pluralize": "8.0.0",
|
||||
"@swc-node/register": "1.10.9",
|
||||
"ts-essentials": "7.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* eslint-disable no-console */
|
||||
import minimist from 'minimist'
|
||||
import { findConfig, importConfig, loadEnv } from 'payload/node'
|
||||
import { findConfig, loadEnv } from 'payload/node'
|
||||
|
||||
import { generateSchema } from './generateSchema.js'
|
||||
|
||||
export const bin = async () => {
|
||||
loadEnv()
|
||||
const configPath = findConfig()
|
||||
const config = await importConfig(configPath)
|
||||
const config = await (await import(configPath)).default
|
||||
|
||||
const args = minimist(process.argv.slice(2))
|
||||
const script = (typeof args._[0] === 'string' ? args._[0] : '').toLowerCase()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -71,7 +71,9 @@ export const mergeData = async <T>(args: {
|
||||
try {
|
||||
res = await requestHandler({
|
||||
apiPath: apiRoute || '/api',
|
||||
endpoint: `${collection}?depth=${depth}&where[id][in]=${Array.from(ids).join(',')}`,
|
||||
endpoint: encodeURI(
|
||||
`${collection}?depth=${depth}&where[id][in]=${Array.from(ids).join(',')}`,
|
||||
),
|
||||
serverURL,
|
||||
}).then((res) => res.json())
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.76",
|
||||
"version": "3.0.0-beta.80",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -90,7 +90,7 @@
|
||||
"esbuild": "0.23.0",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
"payload": "workspace:*",
|
||||
"swc-plugin-transform-remove-imports": "1.14.0"
|
||||
"swc-plugin-transform-remove-imports": "1.15.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^16.8.1",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { DocumentTabConfig, DocumentTabProps } from 'payload'
|
||||
|
||||
import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { DocumentTabLink } from './TabLink.js'
|
||||
@@ -7,22 +8,25 @@ import './index.scss'
|
||||
|
||||
export const baseClass = 'doc-tab'
|
||||
|
||||
export const DocumentTab: React.FC<DocumentTabConfig & DocumentTabProps> = (props) => {
|
||||
export const DocumentTab: React.FC<
|
||||
{ readonly Pill_Component?: React.FC } & DocumentTabConfig & DocumentTabProps
|
||||
> = (props) => {
|
||||
const {
|
||||
Pill,
|
||||
Pill_Component,
|
||||
apiURL,
|
||||
collectionConfig,
|
||||
condition,
|
||||
config,
|
||||
globalConfig,
|
||||
href: tabHref,
|
||||
i18n,
|
||||
isActive: tabIsActive,
|
||||
label,
|
||||
newTab,
|
||||
payload,
|
||||
permissions,
|
||||
} = props
|
||||
|
||||
const { config } = payload
|
||||
const { routes } = config
|
||||
|
||||
let href = typeof tabHref === 'string' ? tabHref : ''
|
||||
@@ -55,6 +59,17 @@ export const DocumentTab: React.FC<DocumentTabConfig & DocumentTabProps> = (prop
|
||||
})
|
||||
: label
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedPin = createMappedComponent(Pill, undefined, Pill_Component, 'Pill')
|
||||
|
||||
return (
|
||||
<DocumentTabLink
|
||||
adminRoute={routes.admin}
|
||||
@@ -67,10 +82,10 @@ export const DocumentTab: React.FC<DocumentTabConfig & DocumentTabProps> = (prop
|
||||
>
|
||||
<span className={`${baseClass}__label`}>
|
||||
{labelToRender}
|
||||
{Pill && (
|
||||
{mappedPin && (
|
||||
<Fragment>
|
||||
|
||||
<Pill />
|
||||
<RenderComponent mappedComponent={mappedPin} />
|
||||
</Fragment>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -12,9 +12,9 @@ export const getCustomViews = (args: {
|
||||
|
||||
if (collectionConfig) {
|
||||
const collectionViewsConfig =
|
||||
typeof collectionConfig?.admin?.components?.views?.Edit === 'object' &&
|
||||
typeof collectionConfig?.admin?.components?.views?.Edit !== 'function'
|
||||
? collectionConfig?.admin?.components?.views?.Edit
|
||||
typeof collectionConfig?.admin?.components?.views?.edit === 'object' &&
|
||||
typeof collectionConfig?.admin?.components?.views?.edit !== 'function'
|
||||
? collectionConfig?.admin?.components?.views?.edit
|
||||
: undefined
|
||||
|
||||
customViews = Object.entries(collectionViewsConfig || {}).reduce((prev, [key, view]) => {
|
||||
@@ -28,9 +28,9 @@ export const getCustomViews = (args: {
|
||||
|
||||
if (globalConfig) {
|
||||
const globalViewsConfig =
|
||||
typeof globalConfig?.admin?.components?.views?.Edit === 'object' &&
|
||||
typeof globalConfig?.admin?.components?.views?.Edit !== 'function'
|
||||
? globalConfig?.admin?.components?.views?.Edit
|
||||
typeof globalConfig?.admin?.components?.views?.edit === 'object' &&
|
||||
typeof globalConfig?.admin?.components?.views?.edit !== 'function'
|
||||
? globalConfig?.admin?.components?.views?.edit
|
||||
: undefined
|
||||
|
||||
customViews = Object.entries(globalViewsConfig || {}).reduce((prev, [key, view]) => {
|
||||
|
||||
@@ -9,9 +9,9 @@ export const getViewConfig = (args: {
|
||||
|
||||
if (collectionConfig) {
|
||||
const collectionConfigViewsConfig =
|
||||
typeof collectionConfig?.admin?.components?.views?.Edit === 'object' &&
|
||||
typeof collectionConfig?.admin?.components?.views?.Edit !== 'function'
|
||||
? collectionConfig?.admin?.components?.views?.Edit
|
||||
typeof collectionConfig?.admin?.components?.views?.edit === 'object' &&
|
||||
typeof collectionConfig?.admin?.components?.views?.edit !== 'function'
|
||||
? collectionConfig?.admin?.components?.views?.edit
|
||||
: undefined
|
||||
|
||||
return collectionConfigViewsConfig?.[name]
|
||||
@@ -19,9 +19,9 @@ export const getViewConfig = (args: {
|
||||
|
||||
if (globalConfig) {
|
||||
const globalConfigViewsConfig =
|
||||
typeof globalConfig?.admin?.components?.views?.Edit === 'object' &&
|
||||
typeof globalConfig?.admin?.components?.views?.Edit !== 'function'
|
||||
? globalConfig?.admin?.components?.views?.Edit
|
||||
typeof globalConfig?.admin?.components?.views?.edit === 'object' &&
|
||||
typeof globalConfig?.admin?.components?.views?.edit !== 'function'
|
||||
? globalConfig?.admin?.components?.views?.edit
|
||||
: undefined
|
||||
|
||||
return globalConfigViewsConfig?.[name]
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
|
||||
&__tabs {
|
||||
padding: 0;
|
||||
margin-left: var(--gutter-h);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type {
|
||||
Payload,
|
||||
Permissions,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
|
||||
import { isPlainObject } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
@@ -20,12 +21,13 @@ const baseClass = 'doc-tabs'
|
||||
|
||||
export const DocumentTabs: React.FC<{
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
config: SanitizedConfig
|
||||
globalConfig: SanitizedGlobalConfig
|
||||
i18n: I18n
|
||||
payload: Payload
|
||||
permissions: Permissions
|
||||
}> = (props) => {
|
||||
const { collectionConfig, config, globalConfig, permissions } = props
|
||||
const { collectionConfig, globalConfig, i18n, payload, permissions } = props
|
||||
const { config } = payload
|
||||
|
||||
const customViews = getCustomViews({ collectionConfig, globalConfig })
|
||||
|
||||
@@ -46,10 +48,9 @@ export const DocumentTabs: React.FC<{
|
||||
})
|
||||
?.map(([name, tab], index) => {
|
||||
const viewConfig = getViewConfig({ name, collectionConfig, globalConfig })
|
||||
const tabFromConfig = viewConfig && 'Tab' in viewConfig ? viewConfig.Tab : undefined
|
||||
const tabConfig = typeof tabFromConfig === 'object' ? tabFromConfig : undefined
|
||||
const tabFromConfig = viewConfig && 'tab' in viewConfig ? viewConfig.tab : undefined
|
||||
|
||||
const { condition } = tabConfig || {}
|
||||
const { condition } = tabFromConfig || {}
|
||||
|
||||
const meetsCondition =
|
||||
!condition ||
|
||||
@@ -72,17 +73,39 @@ export const DocumentTabs: React.FC<{
|
||||
return null
|
||||
})}
|
||||
{customViews?.map((CustomView, index) => {
|
||||
if ('Tab' in CustomView) {
|
||||
const { Tab, path } = CustomView
|
||||
if ('tab' in CustomView) {
|
||||
const { path, tab } = CustomView
|
||||
|
||||
if (typeof Tab === 'object' && !isPlainObject(Tab)) {
|
||||
throw new Error(
|
||||
`Custom 'Tab' Component for path: "${path}" must be a React Server Component. To use client-side functionality, render your Client Component within a Server Component and pass it only props that are serializable. More info: https://react.dev/reference/react/use-server#serializable-parameters-and-return-values`,
|
||||
if (tab.Component) {
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
payload,
|
||||
permissions,
|
||||
...props,
|
||||
key: `tab-custom-${index}`,
|
||||
path,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedTab = createMappedComponent(
|
||||
tab.Component,
|
||||
undefined,
|
||||
undefined,
|
||||
'tab.Component',
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof Tab === 'function') {
|
||||
return <Tab path={path} {...props} key={`tab-custom-${index}`} />
|
||||
return (
|
||||
<RenderComponent
|
||||
clientProps={{
|
||||
key: `tab-custom-${index}`,
|
||||
path,
|
||||
}}
|
||||
key={`tab-custom-${index}`}
|
||||
mappedComponent={mappedTab}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -90,7 +113,7 @@ export const DocumentTabs: React.FC<{
|
||||
key={`tab-custom-${index}`}
|
||||
{...{
|
||||
...props,
|
||||
...Tab,
|
||||
...tab,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import type { DocumentTabConfig } from 'payload'
|
||||
import type React from 'react'
|
||||
|
||||
import { VersionsPill } from './VersionsPill/index.js'
|
||||
|
||||
export const documentViewKeys = [
|
||||
'API',
|
||||
'Default',
|
||||
'LivePreview',
|
||||
'References',
|
||||
'Relationships',
|
||||
'Version',
|
||||
'Versions',
|
||||
'api',
|
||||
'default',
|
||||
'livePreview',
|
||||
'references',
|
||||
'relationships',
|
||||
'version',
|
||||
'versions',
|
||||
]
|
||||
|
||||
export type DocumentViewKey = (typeof documentViewKeys)[number]
|
||||
@@ -17,10 +18,11 @@ export type DocumentViewKey = (typeof documentViewKeys)[number]
|
||||
export const tabs: Record<
|
||||
DocumentViewKey,
|
||||
{
|
||||
Pill_Component?: React.FC
|
||||
order?: number // TODO: expose this to the globalConfig config
|
||||
} & DocumentTabConfig
|
||||
> = {
|
||||
API: {
|
||||
api: {
|
||||
condition: ({ collectionConfig, globalConfig }) =>
|
||||
(collectionConfig && !collectionConfig?.admin?.hideAPIURL) ||
|
||||
(globalConfig && !globalConfig?.admin?.hideAPIURL),
|
||||
@@ -28,14 +30,14 @@ export const tabs: Record<
|
||||
label: 'API',
|
||||
order: 1000,
|
||||
},
|
||||
Default: {
|
||||
default: {
|
||||
href: '',
|
||||
// isActive: ({ href, location }) =>
|
||||
// location.pathname === href || location.pathname === `${href}/create`,
|
||||
label: ({ t }) => t('general:edit'),
|
||||
order: 0,
|
||||
},
|
||||
LivePreview: {
|
||||
livePreview: {
|
||||
condition: ({ collectionConfig, config, globalConfig }) => {
|
||||
if (collectionConfig) {
|
||||
return Boolean(
|
||||
@@ -57,17 +59,17 @@ export const tabs: Record<
|
||||
label: ({ t }) => t('general:livePreview'),
|
||||
order: 100,
|
||||
},
|
||||
References: {
|
||||
references: {
|
||||
condition: () => false,
|
||||
},
|
||||
Relationships: {
|
||||
relationships: {
|
||||
condition: () => false,
|
||||
},
|
||||
Version: {
|
||||
version: {
|
||||
condition: () => false,
|
||||
},
|
||||
Versions: {
|
||||
Pill: VersionsPill,
|
||||
versions: {
|
||||
Pill_Component: VersionsPill,
|
||||
condition: ({ collectionConfig, globalConfig, permissions }) =>
|
||||
Boolean(
|
||||
(collectionConfig?.versions &&
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
|
||||
&__title {
|
||||
width: 100%;
|
||||
padding-left: var(--gutter-h);
|
||||
padding-right: var(--gutter-h);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type {
|
||||
Payload,
|
||||
Permissions,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
@@ -16,14 +16,14 @@ const baseClass = `doc-header`
|
||||
|
||||
export const DocumentHeader: React.FC<{
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
config: SanitizedConfig
|
||||
customHeader?: React.ReactNode
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
hideTabs?: boolean
|
||||
i18n: I18n
|
||||
payload: Payload
|
||||
permissions: Permissions
|
||||
}> = (props) => {
|
||||
const { collectionConfig, config, customHeader, globalConfig, hideTabs, i18n, permissions } =
|
||||
const { collectionConfig, customHeader, globalConfig, hideTabs, i18n, payload, permissions } =
|
||||
props
|
||||
|
||||
return (
|
||||
@@ -35,9 +35,9 @@ export const DocumentHeader: React.FC<{
|
||||
{!hideTabs && (
|
||||
<DocumentTabs
|
||||
collectionConfig={collectionConfig}
|
||||
config={config}
|
||||
globalConfig={globalConfig}
|
||||
i18n={i18n}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { email, username } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
loginWithUsername?: LoginWithUsernameOptions | false
|
||||
readonly loginWithUsername?: LoginWithUsernameOptions | false
|
||||
}
|
||||
function EmailFieldComponent(props: Props) {
|
||||
const { loginWithUsername } = props
|
||||
@@ -21,10 +21,11 @@ function EmailFieldComponent(props: Props) {
|
||||
return (
|
||||
<EmailField
|
||||
autoComplete="off"
|
||||
label={t('general:email')}
|
||||
name="email"
|
||||
path="email"
|
||||
required={requireEmail}
|
||||
field={{
|
||||
name: 'email',
|
||||
label: t('general:email'),
|
||||
required: requireEmail,
|
||||
}}
|
||||
validate={email}
|
||||
/>
|
||||
)
|
||||
@@ -43,10 +44,11 @@ function UsernameFieldComponent(props: Props) {
|
||||
if (showUsernameField) {
|
||||
return (
|
||||
<TextField
|
||||
label={t('authentication:username')}
|
||||
name="username"
|
||||
path="username"
|
||||
required={requireUsername}
|
||||
field={{
|
||||
name: 'username',
|
||||
label: t('authentication:username'),
|
||||
required: requireUsername,
|
||||
}}
|
||||
validate={username}
|
||||
/>
|
||||
)
|
||||
@@ -70,25 +72,34 @@ export function RenderEmailAndUsernameFields(props: RenderEmailAndUsernameFields
|
||||
return (
|
||||
<RenderFields
|
||||
className={className}
|
||||
fieldMap={[
|
||||
fields={[
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text',
|
||||
CustomField: <EmailFieldComponent loginWithUsername={loginWithUsername} />,
|
||||
cellComponentProps: null,
|
||||
fieldComponentProps: { type: 'email', autoComplete: 'off', readOnly },
|
||||
fieldIsPresentational: false,
|
||||
isFieldAffectingData: true,
|
||||
admin: {
|
||||
autoComplete: 'off',
|
||||
components: {
|
||||
Field: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: <EmailFieldComponent loginWithUsername={loginWithUsername} />,
|
||||
},
|
||||
},
|
||||
},
|
||||
localized: false,
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
CustomField: <UsernameFieldComponent loginWithUsername={loginWithUsername} />,
|
||||
cellComponentProps: null,
|
||||
fieldComponentProps: { type: 'text', readOnly },
|
||||
fieldIsPresentational: false,
|
||||
isFieldAffectingData: true,
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
type: 'client',
|
||||
Component: null,
|
||||
RenderedComponent: <UsernameFieldComponent loginWithUsername={loginWithUsername} />,
|
||||
},
|
||||
},
|
||||
},
|
||||
localized: false,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--base);
|
||||
padding: base(2);
|
||||
}
|
||||
|
||||
&__content {
|
||||
|
||||
@@ -50,6 +50,7 @@ const Component: React.FC<{
|
||||
}
|
||||
|
||||
export const LeaveWithoutSaving: React.FC = () => {
|
||||
const { closeModal } = useModal()
|
||||
const modified = useFormModified()
|
||||
const { user } = useAuth()
|
||||
const [show, setShow] = React.useState(false)
|
||||
@@ -61,7 +62,11 @@ export const LeaveWithoutSaving: React.FC = () => {
|
||||
setShow(true)
|
||||
}, [])
|
||||
|
||||
usePreventLeave({ hasAccepted, onPrevent, prevent })
|
||||
const handleAccept = useCallback(() => {
|
||||
closeModal(modalSlug)
|
||||
}, [closeModal])
|
||||
|
||||
usePreventLeave({ hasAccepted, onAccept: handleAccept, onPrevent, prevent })
|
||||
|
||||
return (
|
||||
<Component
|
||||
|
||||
@@ -57,12 +57,14 @@ export const useBeforeUnload = (enabled: (() => boolean) | boolean = true, messa
|
||||
export const usePreventLeave = ({
|
||||
hasAccepted = false,
|
||||
message = 'Are you sure want to leave this page?',
|
||||
onAccept,
|
||||
onPrevent,
|
||||
prevent = true,
|
||||
}: {
|
||||
hasAccepted: boolean
|
||||
// if no `onPrevent` is provided, the message will be displayed in a confirm dialog
|
||||
message?: string
|
||||
onAccept?: () => void
|
||||
// to use a custom confirmation dialog, provide a function that returns a boolean
|
||||
onPrevent?: () => void
|
||||
prevent: boolean
|
||||
@@ -142,7 +144,8 @@ export const usePreventLeave = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAccepted && cancelledURL.current) {
|
||||
if (onAccept) onAccept()
|
||||
router.push(cancelledURL.current)
|
||||
}
|
||||
}, [hasAccepted, router])
|
||||
}, [hasAccepted, onAccept, router])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ServerProps } from 'payload'
|
||||
|
||||
import { PayloadLogo, RenderCustomComponent } from '@payloadcms/ui/shared'
|
||||
import { PayloadLogo, RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
export const Logo: React.FC<ServerProps> = (props) => {
|
||||
@@ -16,19 +16,20 @@ export const Logo: React.FC<ServerProps> = (props) => {
|
||||
} = {},
|
||||
} = payload.config
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={CustomLogo}
|
||||
DefaultComponent={PayloadLogo}
|
||||
serverOnlyProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const mappedCustomLogo = createMappedComponent(CustomLogo, undefined, PayloadLogo, 'CustomLogo')
|
||||
|
||||
return <RenderComponent mappedComponent={mappedCustomLogo} />
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
width: var(--nav-width);
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
opacity: 0;
|
||||
transition: opacity var(--nav-trans-time) ease-in-out;
|
||||
|
||||
&--nav-animate {
|
||||
transition: opacity var(--nav-trans-time) ease-in-out;
|
||||
}
|
||||
|
||||
&--nav-open {
|
||||
opacity: 1;
|
||||
|
||||
@@ -10,10 +10,19 @@ export const NavWrapper: React.FC<{
|
||||
}> = (props) => {
|
||||
const { baseClass, children } = props
|
||||
|
||||
const { navOpen, navRef } = useNav()
|
||||
const { hydrated, navOpen, navRef, shouldAnimate } = useNav()
|
||||
|
||||
return (
|
||||
<aside className={[baseClass, navOpen && `${baseClass}--nav-open`].filter(Boolean).join(' ')}>
|
||||
<aside
|
||||
className={[
|
||||
baseClass,
|
||||
navOpen && `${baseClass}--nav-open`,
|
||||
shouldAnimate && `${baseClass}--nav-animate`,
|
||||
hydrated && `${baseClass}--nav-hydrated`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={`${baseClass}__scroll`} ref={navRef}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -25,9 +25,11 @@ export const DefaultNavClient: React.FC = () => {
|
||||
const pathname = usePathname()
|
||||
|
||||
const {
|
||||
collections,
|
||||
globals,
|
||||
routes: { admin: adminRoute },
|
||||
config: {
|
||||
collections,
|
||||
globals,
|
||||
routes: { admin: adminRoute },
|
||||
},
|
||||
} = useConfig()
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
@@ -98,11 +100,7 @@ export const DefaultNavClient: React.FC = () => {
|
||||
key={i}
|
||||
tabIndex={!navOpen ? -1 : undefined}
|
||||
>
|
||||
{activeCollection && (
|
||||
<span className={`${baseClass}__link-icon`}>
|
||||
<ChevronIcon direction="right" />
|
||||
</span>
|
||||
)}
|
||||
{activeCollection && <div className={`${baseClass}__link-indicator`} />}
|
||||
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
|
||||
</LinkElement>
|
||||
)
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
width: var(--nav-width);
|
||||
border-right: 1px solid var(--theme-elevation-100);
|
||||
opacity: 0;
|
||||
transition: opacity var(--nav-trans-time) ease-in-out;
|
||||
overflow: hidden;
|
||||
|
||||
&--nav-animate {
|
||||
transition: opacity var(--nav-trans-time) ease-in-out;
|
||||
}
|
||||
|
||||
&--nav-open {
|
||||
opacity: 1;
|
||||
@@ -18,7 +22,7 @@
|
||||
&__header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
height: var(--app-header-height);
|
||||
}
|
||||
|
||||
@@ -26,6 +30,7 @@
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__mobile-close {
|
||||
@@ -33,7 +38,7 @@
|
||||
background: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
padding: calc(var(--base) * 0.75) var(--gutter-h);
|
||||
padding: base(0.8) 0;
|
||||
}
|
||||
|
||||
&__scroll {
|
||||
@@ -85,7 +90,9 @@
|
||||
nav {
|
||||
a {
|
||||
position: relative;
|
||||
padding: base(0.125) base(1.5) base(0.125) 0;
|
||||
padding-block: base(0.125);
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: base(1.5);
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -112,10 +119,16 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__link-icon {
|
||||
margin-right: calc(var(--base) * 0.25);
|
||||
top: -1px;
|
||||
position: relative;
|
||||
&__link-indicator {
|
||||
position: absolute;
|
||||
display: block;
|
||||
// top: 0;
|
||||
inset-inline-start: base(-1);
|
||||
width: 2px;
|
||||
height: 16px;
|
||||
border-start-end-radius: 2px;
|
||||
border-end-end-radius: 2px;
|
||||
background: var(--theme-text);
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ServerProps } from 'payload'
|
||||
|
||||
import { Logout } from '@payloadcms/ui'
|
||||
import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { NavHamburger } from './NavHamburger/index.js'
|
||||
@@ -9,8 +10,6 @@ import './index.scss'
|
||||
|
||||
const baseClass = 'nav'
|
||||
|
||||
import { WithServerSideProps } from '@payloadcms/ui/shared'
|
||||
|
||||
import { DefaultNavClient } from './index.client.js'
|
||||
|
||||
export type NavProps = ServerProps
|
||||
@@ -28,48 +27,38 @@ export const DefaultNav: React.FC<NavProps> = (props) => {
|
||||
},
|
||||
} = payload.config
|
||||
|
||||
const BeforeNavLinks = Array.isArray(beforeNavLinks)
|
||||
? beforeNavLinks.map((Component, i) => (
|
||||
<WithServerSideProps
|
||||
Component={Component}
|
||||
key={i}
|
||||
serverOnlyProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
: null
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: payload.importMap,
|
||||
serverProps: {
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
const AfterNavLinks = Array.isArray(afterNavLinks)
|
||||
? afterNavLinks.map((Component, i) => (
|
||||
<WithServerSideProps
|
||||
Component={Component}
|
||||
key={i}
|
||||
serverOnlyProps={{
|
||||
i18n,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
: null
|
||||
const mappedBeforeNavLinks = createMappedComponent(
|
||||
beforeNavLinks,
|
||||
undefined,
|
||||
undefined,
|
||||
'beforeNavLinks',
|
||||
)
|
||||
const mappedAfterNavLinks = createMappedComponent(
|
||||
afterNavLinks,
|
||||
undefined,
|
||||
undefined,
|
||||
'afterNavLinks',
|
||||
)
|
||||
|
||||
return (
|
||||
<NavWrapper baseClass={baseClass}>
|
||||
<nav className={`${baseClass}__wrap`}>
|
||||
{Array.isArray(BeforeNavLinks) && BeforeNavLinks.map((Component) => Component)}
|
||||
<RenderComponent mappedComponent={mappedBeforeNavLinks} />
|
||||
<DefaultNavClient />
|
||||
{Array.isArray(AfterNavLinks) && AfterNavLinks.map((Component) => Component)}
|
||||
<RenderComponent mappedComponent={mappedAfterNavLinks} />
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Logout />
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
|
||||
import type { PayloadRequest, SanitizedConfig } from 'payload'
|
||||
import type { ImportMap, PayloadRequest, SanitizedConfig } from 'payload'
|
||||
|
||||
import { initI18n, rtlLanguages } from '@payloadcms/translations'
|
||||
import { RootProvider } from '@payloadcms/ui'
|
||||
import '@payloadcms/ui/scss/app.scss'
|
||||
import { buildComponentMap } from '@payloadcms/ui/utilities/buildComponentMap'
|
||||
import { createClientConfig } from '@payloadcms/ui/utilities/createClientConfig'
|
||||
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
|
||||
import { createClientConfig, createLocalReq, parseCookies } from 'payload'
|
||||
import * as qs from 'qs-esm'
|
||||
import { createLocalReq, parseCookies } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
|
||||
@@ -24,9 +23,11 @@ export const metadata = {
|
||||
export const RootLayout = async ({
|
||||
children,
|
||||
config: configPromise,
|
||||
importMap,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
config: Promise<SanitizedConfig>
|
||||
readonly children: React.ReactNode
|
||||
readonly config: Promise<SanitizedConfig>
|
||||
readonly importMap: ImportMap
|
||||
}) => {
|
||||
const config = await configPromise
|
||||
|
||||
@@ -67,7 +68,15 @@ export const RootLayout = async ({
|
||||
)
|
||||
const { permissions, user } = await payload.auth({ headers, req })
|
||||
|
||||
const clientConfig = await createClientConfig({ config, t: i18n.t })
|
||||
const { clientConfig, render } = await createClientConfig({
|
||||
DefaultEditView,
|
||||
DefaultListView,
|
||||
children,
|
||||
config,
|
||||
i18n,
|
||||
importMap,
|
||||
payload,
|
||||
})
|
||||
|
||||
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
|
||||
? 'RTL'
|
||||
@@ -97,22 +106,47 @@ export const RootLayout = async ({
|
||||
})
|
||||
}
|
||||
|
||||
const { componentMap, wrappedChildren } = buildComponentMap({
|
||||
DefaultEditView,
|
||||
DefaultListView,
|
||||
children,
|
||||
i18n,
|
||||
payload,
|
||||
})
|
||||
const navPreferences = user
|
||||
? (
|
||||
await payload.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
req,
|
||||
user,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: 'nav',
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: user.collection,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
)?.docs?.[0]
|
||||
: null
|
||||
|
||||
const isNavOpen = (navPreferences?.value as any)?.open ?? true
|
||||
|
||||
return (
|
||||
<html data-theme={theme} dir={dir} lang={languageCode}>
|
||||
<body>
|
||||
<RootProvider
|
||||
componentMap={componentMap}
|
||||
config={clientConfig}
|
||||
dateFNSKey={i18n.dateFNSKey}
|
||||
fallbackLang={clientConfig.i18n.fallbackLanguage}
|
||||
isNavOpen={isNavOpen}
|
||||
languageCode={languageCode}
|
||||
languageOptions={languageOptions}
|
||||
permissions={permissions}
|
||||
@@ -121,7 +155,7 @@ export const RootLayout = async ({
|
||||
translations={i18n.translations}
|
||||
user={user}
|
||||
>
|
||||
{wrappedChildren}
|
||||
{render}
|
||||
</RootProvider>
|
||||
<div id="portal" />
|
||||
</body>
|
||||
|
||||
@@ -20,6 +20,8 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
|
||||
status: httpStatus.OK,
|
||||
})
|
||||
} catch (err) {
|
||||
req.payload.logger.error({ err, msg: `There was an error building form state` })
|
||||
|
||||
if (err.message === 'Could not find field schema for given path') {
|
||||
return Response.json(
|
||||
{
|
||||
@@ -39,8 +41,6 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
|
||||
})
|
||||
}
|
||||
|
||||
req.payload.logger.error({ err, msg: `There was an error building form state` })
|
||||
|
||||
return routeError({
|
||||
config: req.payload.config,
|
||||
err,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { MappedComponent } from 'payload'
|
||||
|
||||
import { RenderComponent } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
export const OGImage: React.FC<{
|
||||
Icon: React.ComponentType<any>
|
||||
Icon: MappedComponent
|
||||
description?: string
|
||||
fontFamily?: string
|
||||
leader?: string
|
||||
@@ -82,7 +85,12 @@ export const OGImage: React.FC<{
|
||||
width: '38px',
|
||||
}}
|
||||
>
|
||||
<Icon fill="white" />
|
||||
<RenderComponent
|
||||
clientProps={{
|
||||
fill: 'white',
|
||||
}}
|
||||
mappedComponent={Icon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
import { PayloadIcon } from '@payloadcms/ui/shared'
|
||||
import { PayloadIcon, getCreateMappedComponent } from '@payloadcms/ui/shared'
|
||||
import fs from 'fs/promises'
|
||||
import { ImageResponse } from 'next/og.js'
|
||||
import { NextResponse } from 'next/server.js'
|
||||
@@ -32,7 +32,18 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
|
||||
const hasLeader = searchParams.has('leader')
|
||||
const leader = hasLeader ? searchParams.get('leader')?.slice(0, 100).replace('-', ' ') : ''
|
||||
const description = searchParams.has('description') ? searchParams.get('description') : ''
|
||||
const Icon = config.admin?.components?.graphics?.Icon || PayloadIcon
|
||||
|
||||
const createMappedComponent = getCreateMappedComponent({
|
||||
importMap: req.payload.importMap,
|
||||
serverProps: {},
|
||||
})
|
||||
|
||||
const mappedIcon = createMappedComponent(
|
||||
config.admin?.components?.graphics?.Icon,
|
||||
undefined,
|
||||
PayloadIcon,
|
||||
'config.admin.components.graphics.Icon',
|
||||
)
|
||||
|
||||
let fontData
|
||||
|
||||
@@ -50,7 +61,7 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<OGImage
|
||||
Icon={Icon}
|
||||
Icon={mappedIcon}
|
||||
description={description}
|
||||
fontFamily={fontFamily}
|
||||
leader={leader}
|
||||
|
||||
@@ -38,19 +38,19 @@
|
||||
|
||||
--gutter-h: #{base(3)};
|
||||
--spacing-view-bottom: var(--gutter-h);
|
||||
--app-header-height: calc(var(--base) * 3);
|
||||
--doc-controls-height: calc(var(--base) * 3);
|
||||
--doc-controls-height: calc(var(--base) * 2.8);
|
||||
--app-header-height: calc(var(--base) * 2.8);
|
||||
--nav-width: 275px;
|
||||
--nav-trans-time: 150ms;
|
||||
|
||||
@include mid-break {
|
||||
--gutter-h: #{base(2)};
|
||||
--app-header-height: calc(var(--base) * 2);
|
||||
--doc-controls-height: calc(var(--base) * 2.5);
|
||||
--app-header-height: calc(var(--base) * 2.4);
|
||||
--doc-controls-height: calc(var(--base) * 2.4);
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
--gutter-h: #{base(0.5)};
|
||||
--gutter-h: #{base(0.8)};
|
||||
--spacing-view-bottom: calc(var(--base) * 2);
|
||||
--nav-width: 100vw;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
font-size: $baseline-body-size;
|
||||
line-height: $baseline-px;
|
||||
font-weight: normal;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
%code {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user