Compare commits
186 Commits
v3.38.0
...
fix/findVa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58f841cee4 | ||
|
|
f0a157f939 | ||
|
|
19d6b1aac8 | ||
|
|
9b382b6adf | ||
|
|
bf5d31960b | ||
|
|
3ad29d251f | ||
|
|
fc83823e5d | ||
|
|
2a41d3fbb1 | ||
|
|
07e9444c09 | ||
|
|
c772a3207c | ||
|
|
c701dd41a9 | ||
|
|
5dd13f2873 | ||
|
|
878be913fd | ||
|
|
4dfb2d24bb | ||
|
|
230128b92e | ||
|
|
23f42040ab | ||
|
|
8596ac5694 | ||
|
|
0524f198a6 | ||
|
|
5f2e846350 | ||
|
|
d3265b9931 | ||
|
|
33261b36bf | ||
|
|
73d4201df8 | ||
|
|
324daff553 | ||
|
|
50d3da5824 | ||
|
|
41aac41df4 | ||
|
|
6a5b95af7c | ||
|
|
22b1858ee8 | ||
|
|
2e7bfcbd63 | ||
|
|
3ee9a32a38 | ||
|
|
c2d38b4109 | ||
|
|
5d9c537145 | ||
|
|
904b6a6dbe | ||
|
|
cc6de7ef42 | ||
|
|
a3ef4fbfac | ||
|
|
e9ff611879 | ||
|
|
5825d0cfc7 | ||
|
|
103b476c82 | ||
|
|
a3279b319e | ||
|
|
bbb0ab784c | ||
|
|
32eac5b0c2 | ||
|
|
86098c9140 | ||
|
|
2ab8e2e194 | ||
|
|
1235a183ff | ||
|
|
81d333f4b0 | ||
|
|
4fe3423e54 | ||
|
|
7fd2cdf04c | ||
|
|
e8c2b15e2b | ||
|
|
3127d6ad6d | ||
|
|
72ab319d37 | ||
|
|
8d507996c8 | ||
|
|
aa82763cb8 | ||
|
|
e7e6a7dd97 | ||
|
|
2a929cf385 | ||
|
|
e14c670a51 | ||
|
|
21f5d3473c | ||
|
|
38029cdd6e | ||
|
|
14252696ce | ||
|
|
5855f3a475 | ||
|
|
529bfe149e | ||
|
|
36597110e9 | ||
|
|
18f2f899c5 | ||
|
|
4f2b237858 | ||
|
|
d90afba70d | ||
|
|
decd512daa | ||
|
|
d4899b84cc | ||
|
|
6fb2beb983 | ||
|
|
4166621966 | ||
|
|
b2bf95b17b | ||
|
|
10e29dd5e2 | ||
|
|
e395a0aa66 | ||
|
|
4c4ae1295e | ||
|
|
cead312d4b | ||
|
|
adb805dadb | ||
|
|
e5f0ca3d45 | ||
|
|
219fd01717 | ||
|
|
1f6efe9a46 | ||
|
|
fe2b7693cc | ||
|
|
30d4a098b1 | ||
|
|
aa7918fe6e | ||
|
|
f23f87243c | ||
|
|
41b75882a1 | ||
|
|
88769c8244 | ||
|
|
bd6ee317c1 | ||
|
|
cfd3c34aba | ||
|
|
270137e92e | ||
|
|
cab817d2aa | ||
|
|
db622e2b79 | ||
|
|
c52204317a | ||
|
|
a85af1a6d3 | ||
|
|
bccadd5101 | ||
|
|
2fa723743e | ||
|
|
2904de778d | ||
|
|
d37dfb1376 | ||
|
|
b203f617af | ||
|
|
ee46f27881 | ||
|
|
2426784726 | ||
|
|
95b78a5951 | ||
|
|
d1c1ad2a1d | ||
|
|
2980bdb799 | ||
|
|
fe6923d0a7 | ||
|
|
7eec63ae69 | ||
|
|
77a7bc5e9c | ||
|
|
780becc88a | ||
|
|
a2d394ec82 | ||
|
|
b1c8c96e97 | ||
|
|
6b6b596489 | ||
|
|
a89bc1479f | ||
|
|
bd9f3b5bd2 | ||
|
|
2584ff42eb | ||
|
|
cc1f5fb70c | ||
|
|
4f7f378b84 | ||
|
|
b279fa7bde | ||
|
|
93230a5915 | ||
|
|
9f31daf8e7 | ||
|
|
ded7164dca | ||
|
|
cc4526844a | ||
|
|
362f25f593 | ||
|
|
7567d2358a | ||
|
|
b418c3cade | ||
|
|
db0d07d9a7 | ||
|
|
b56e2faad2 | ||
|
|
56982f9811 | ||
|
|
9e1258811a | ||
|
|
1d2accfcbb | ||
|
|
4a8bea2dde | ||
|
|
0149e42276 | ||
|
|
36d9900774 | ||
|
|
f4d624a0c5 | ||
|
|
57bcfcc8be | ||
|
|
d73ddbde0c | ||
|
|
189dd64799 | ||
|
|
572ce2955a | ||
|
|
87ca312a54 | ||
|
|
2b2fa67031 | ||
|
|
2f45749634 | ||
|
|
0534dd9506 | ||
|
|
aee1ea1346 | ||
|
|
82ba8cf8f4 | ||
|
|
1dfff2b0b0 | ||
|
|
6b695355c3 | ||
|
|
29c32d3141 | ||
|
|
f6afbed5d2 | ||
|
|
5dcf96ca10 | ||
|
|
91c22bd88c | ||
|
|
0d8b5677d9 | ||
|
|
e55c89c4b7 | ||
|
|
f762d683c6 | ||
|
|
953c538af0 | ||
|
|
65d0272950 | ||
|
|
c570d6178c | ||
|
|
c935420937 | ||
|
|
72dd527a15 | ||
|
|
1d6e0941e7 | ||
|
|
91f7deb278 | ||
|
|
d3986cfaf0 | ||
|
|
4a9de40098 | ||
|
|
f4679a4088 | ||
|
|
cb8ee7d2b0 | ||
|
|
06ef8da836 | ||
|
|
91dc98978d | ||
|
|
07ff1ee7be | ||
|
|
72d393d24c | ||
|
|
95d4324af3 | ||
|
|
a617d3166c | ||
|
|
6770e7a1b3 | ||
|
|
f5e535dacf | ||
|
|
3d9c25e278 | ||
|
|
3bdb127ad4 | ||
|
|
de4be1eb78 | ||
|
|
d26529282f | ||
|
|
2694603353 | ||
|
|
9630de1bac | ||
|
|
91e867ecb1 | ||
|
|
25e196bdd7 | ||
|
|
b631eebdb5 | ||
|
|
a66f134cf6 | ||
|
|
b3aec9a23f | ||
|
|
31911d87c1 | ||
|
|
2d37ac41a2 | ||
|
|
b68af8ba31 | ||
|
|
4c775d1ced | ||
|
|
4706019a22 | ||
|
|
9c33a48192 | ||
|
|
8ec6784645 | ||
|
|
8d9bf835a0 | ||
|
|
2eef7ee388 |
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -62,12 +62,6 @@ jobs:
|
||||
echo "templates: ${{ steps.filter.outputs.templates }}"
|
||||
|
||||
lint:
|
||||
# Follows same github's ci skip: [skip lint], [lint skip], [no lint]
|
||||
if: >
|
||||
github.event_name == 'pull_request' &&
|
||||
!contains(github.event.pull_request.title, '[skip lint]') &&
|
||||
!contains(github.event.pull_request.title, '[lint skip]') &&
|
||||
!contains(github.event.pull_request.title, '[no lint]')
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -81,10 +75,8 @@ jobs:
|
||||
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Lint staged
|
||||
run: |
|
||||
git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF}...${GITHUB_SHA}
|
||||
npx lint-staged --diff="origin/${GITHUB_BASE_REF}...${GITHUB_SHA}"
|
||||
- name: Lint
|
||||
run: pnpm lint -- --quiet
|
||||
|
||||
build:
|
||||
needs: changes
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@ package-lock.json
|
||||
dist
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
/.idea/runConfigurations/_template*
|
||||
!/.idea/payload.iml
|
||||
|
||||
# Custom actions
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="true" type="JavaScriptTestRunnerJest">
|
||||
<node-interpreter value="project" />
|
||||
<node-options value="--no-deprecation" />
|
||||
<envs />
|
||||
<scope-kind value="ALL" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -118,6 +118,13 @@
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts folder-view",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"name": "Run Dev Folder View",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "pnpm tsx --no-deprecation test/dev.ts localization",
|
||||
"cwd": "${workspaceFolder}",
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -7,9 +7,6 @@
|
||||
},
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"eslint.rules.customizations": [
|
||||
// Defaultt all ESLint errors to 'warn' to differentate from TypeScript's 'error' level
|
||||
{ "rule": "*", "severity": "warn" },
|
||||
|
||||
// Silence some warnings that will get auto-fixed
|
||||
{ "rule": "perfectionist/*", "severity": "off", "fixable": true },
|
||||
{ "rule": "curly", "severity": "off", "fixable": true },
|
||||
|
||||
@@ -132,6 +132,7 @@ The following options are available:
|
||||
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this Collection. |
|
||||
| `enableRichTextLink` | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| `enableRichTextRelationship` | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](../folders/overview). |
|
||||
| `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
|
||||
| `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
|
||||
@@ -84,6 +84,7 @@ The following options are available:
|
||||
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
|
||||
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
|
||||
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
|
||||
| `folders` | An optional object to configure global folder settings. [More details](../folders/overview). |
|
||||
| `queryPresets` | An object that to configure Collection Query Presets. [More details](../query-presets/overview). |
|
||||
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
|
||||
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
|
||||
|
||||
@@ -100,7 +100,7 @@ Here are the available Presentational Fields:
|
||||
|
||||
### Virtual Fields
|
||||
|
||||
Virtual fields are used to display data that is not stored in the database. They are useful for displaying computed values that populate within the APi response through hooks, etc.
|
||||
Virtual fields are used to display data that is not stored in the database. They are useful for displaying computed values that populate within the API response through hooks, etc.
|
||||
|
||||
Here are the available Virtual Fields:
|
||||
|
||||
|
||||
100
docs/folders/overview.mdx
Normal file
100
docs/folders/overview.mdx
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Folders
|
||||
label: Folders
|
||||
order: 10
|
||||
desc: Folders allow you to group documents across collections, and are a great way to organize your content.
|
||||
keywords: folders, folder, content organization
|
||||
---
|
||||
|
||||
Folders allow you to group documents across collections, and are a great way to organize your content. Folders are built on top of relationship fields, when you enable folders on a collection, Payload adds a hidden relationship field `folders`, that relates to a folder — or no folder. Folders also have the `folder` field, allowing folders to be nested within other folders.
|
||||
|
||||
The configuration for folders is done in two places, the collection config and the Payload config. The collection config is where you enable folders, and the Payload config is where you configure the global folder settings.
|
||||
|
||||
## Folder Configuration
|
||||
|
||||
On the payload config, you can configure the following settings under the `folders` property:
|
||||
|
||||
```ts
|
||||
// Type definition
|
||||
|
||||
type RootFoldersConfiguration = {
|
||||
/**
|
||||
* An array of functions to be ran when the folder collection is initialized
|
||||
* This allows plugins to modify the collection configuration
|
||||
*/
|
||||
collectionOverrides?: (({
|
||||
collection,
|
||||
}: {
|
||||
collection: CollectionConfig
|
||||
}) => CollectionConfig | Promise<CollectionConfig>)[]
|
||||
/**
|
||||
* Ability to view hidden fields and collections related to folders
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean
|
||||
/**
|
||||
* The Folder field name
|
||||
*
|
||||
* @default "folder"
|
||||
*/
|
||||
fieldName?: string
|
||||
/**
|
||||
* Slug for the folder collection
|
||||
*
|
||||
* @default "payload-folders"
|
||||
*/
|
||||
slug?: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// Example usage
|
||||
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
folders: {
|
||||
// highlight-start
|
||||
debug: true, // optional
|
||||
collectionOverrides: [
|
||||
async ({ collection }) => {
|
||||
return collection
|
||||
},
|
||||
], // optional
|
||||
fieldName: 'folder', // optional
|
||||
slug: 'payload-folders', // optional
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Collection Configuration
|
||||
|
||||
To enable folders on a collection, you need to set the `admin.folders` property to `true` on the collection config. This will add a hidden relationship field to the collection that relates to a folder — or no folder.
|
||||
|
||||
```ts
|
||||
// Type definition
|
||||
|
||||
type CollectionFoldersConfiguration = boolean
|
||||
```
|
||||
|
||||
```ts
|
||||
// Example usage
|
||||
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug: 'pages',
|
||||
// highlight-start
|
||||
admin: {
|
||||
folders: true, // defaults to false
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
@@ -81,7 +81,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/main/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:
|
||||
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/main/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/
|
||||
|
||||
@@ -85,6 +85,7 @@ formBuilderPlugin({
|
||||
checkbox: true,
|
||||
number: true,
|
||||
message: true,
|
||||
date: false,
|
||||
payment: false,
|
||||
},
|
||||
})
|
||||
@@ -349,6 +350,18 @@ Maps to a `checkbox` input on your front-end. Used to collect a boolean value.
|
||||
| `width` | string | The width of the field on the front-end. |
|
||||
| `required` | checkbox | Whether or not the field is required when submitted. |
|
||||
|
||||
### Date
|
||||
|
||||
Maps to a `date` input on your front-end. Used to collect a date value.
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------- | -------- | ---------------------------------------------------- |
|
||||
| `name` | string | The name of the field. |
|
||||
| `label` | string | The label of the field. |
|
||||
| `defaultValue` | date | The default value of the field. |
|
||||
| `width` | string | The width of the field on the front-end. |
|
||||
| `required` | checkbox | Whether or not the field is required when submitted. |
|
||||
|
||||
### Number
|
||||
|
||||
Maps to a `number` input on your front-end. Used to collect a number.
|
||||
@@ -421,6 +434,42 @@ formBuilderPlugin({
|
||||
})
|
||||
```
|
||||
|
||||
### Customizing the date field default value
|
||||
|
||||
You can custommise the default value of the date field and any other aspects of the date block in this way.
|
||||
Note that the end submission source will be responsible for the timezone of the date. Payload only stores the date in UTC format.
|
||||
|
||||
```ts
|
||||
import { fields as formFields } from '@payloadcms/plugin-form-builder'
|
||||
|
||||
// payload.config.ts
|
||||
formBuilderPlugin({
|
||||
fields: {
|
||||
// date: true, // just enable it without any customizations
|
||||
date: {
|
||||
...formFields.date,
|
||||
fields: [
|
||||
...(formFields.date && 'fields' in formFields.date
|
||||
? formFields.date.fields.map((field) => {
|
||||
if ('name' in field && field.name === 'defaultValue') {
|
||||
return {
|
||||
...field,
|
||||
timezone: true, // optionally enable timezone
|
||||
admin: {
|
||||
...field.admin,
|
||||
description: 'This is a date field',
|
||||
},
|
||||
}
|
||||
}
|
||||
return field
|
||||
})
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Email
|
||||
|
||||
This plugin relies on the [email configuration](../email/overview) defined in your Payload configuration. It will read from your config and attempt to send your emails using the credentials provided.
|
||||
|
||||
@@ -6,14 +6,14 @@ desc: Converting between lexical richtext and HTML
|
||||
keywords: lexical, richtext, html
|
||||
---
|
||||
|
||||
## Converting Rich Text to HTML
|
||||
## Rich Text to HTML
|
||||
|
||||
There are two main approaches to convert your Lexical-based rich text to HTML:
|
||||
|
||||
1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand.
|
||||
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API and may not work well with live preview.
|
||||
|
||||
### Generating HTML on-demand (Recommended)
|
||||
### On-demand
|
||||
|
||||
To convert JSON to HTML on-demand, use the `convertLexicalToHTML` function from `@payloadcms/richtext-lexical/html`. Here's an example of how to use it in a React component in your frontend:
|
||||
|
||||
@@ -32,61 +32,81 @@ export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
}
|
||||
```
|
||||
|
||||
### Converting Lexical Blocks
|
||||
#### Dynamic Population (Advanced)
|
||||
|
||||
If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example:
|
||||
By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
|
||||
import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedInlineBlockNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import {
|
||||
convertLexicalToHTML,
|
||||
type HTMLConvertersFunction,
|
||||
} from '@payloadcms/richtext-lexical/html'
|
||||
import React from 'react'
|
||||
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<MyTextBlock>
|
||||
| SerializedInlineBlockNode<MyInlineBlock>
|
||||
|
||||
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({
|
||||
defaultConverters,
|
||||
}) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
// Each key should match your block's slug
|
||||
myTextBlock: ({ node, providedCSSString }) =>
|
||||
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
|
||||
},
|
||||
inlineBlocks: {
|
||||
// Each key should match your inline block's slug
|
||||
myInlineBlock: ({ node, providedStyleTag }) =>
|
||||
`<span${providedStyleTag}>${node.fields.text}</span$>`,
|
||||
},
|
||||
})
|
||||
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
|
||||
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
const html = convertLexicalToHTML({
|
||||
converters: htmlConverters,
|
||||
data,
|
||||
})
|
||||
const [html, setHTML] = useState<null | string>(null)
|
||||
useEffect(() => {
|
||||
async function convert() {
|
||||
const html = await convertLexicalToHTMLAsync({
|
||||
data,
|
||||
populate: getRestPopulateFn({
|
||||
apiURL: `http://localhost:3000/api`,
|
||||
}),
|
||||
})
|
||||
setHTML(html)
|
||||
}
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
void convert()
|
||||
}, [data])
|
||||
|
||||
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
### Outputting HTML from the Collection
|
||||
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function:
|
||||
|
||||
To automatically generate HTML from the saved richText field in your Collection, use the `lexicalHTMLField()` helper. This approach converts the JSON to HTML using an `afterRead` hook. For instance:
|
||||
```tsx
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
|
||||
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../config.js'
|
||||
|
||||
export const MyRSCComponent = async ({
|
||||
data,
|
||||
}: {
|
||||
data: SerializedEditorState
|
||||
}) => {
|
||||
const payload = await getPayload({
|
||||
config,
|
||||
})
|
||||
|
||||
const html = await convertLexicalToHTMLAsync({
|
||||
data,
|
||||
populate: await getPayloadPopulateFn({
|
||||
currentDepth: 0,
|
||||
depth: 1,
|
||||
payload,
|
||||
}),
|
||||
})
|
||||
|
||||
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
### HTML field
|
||||
|
||||
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended for two reasons:
|
||||
|
||||
1. It creates a column with duplicate content in another format.
|
||||
2. In [client-side live preview](/docs/live-preview/client), it makes it not "live".
|
||||
|
||||
Consider using the [on-demand HTML converter above](/docs/rich-text/converting-html#on-demand-recommended) or the [JSX converter](/docs/rich-text/converting-jsx) unless you have a good reason.
|
||||
|
||||
```ts
|
||||
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
|
||||
@@ -154,74 +174,59 @@ const Pages: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
### Generating HTML in Your Frontend with Dynamic Population (Advanced)
|
||||
## Blocks to HTML
|
||||
|
||||
By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function:
|
||||
If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
|
||||
import type {
|
||||
DefaultNodeTypes,
|
||||
SerializedBlockNode,
|
||||
SerializedInlineBlockNode,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
|
||||
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
const [html, setHTML] = useState<null | string>(null)
|
||||
useEffect(() => {
|
||||
async function convert() {
|
||||
const html = await convertLexicalToHTMLAsync({
|
||||
data,
|
||||
populate: getRestPopulateFn({
|
||||
apiURL: `http://localhost:3000/api`,
|
||||
}),
|
||||
})
|
||||
setHTML(html)
|
||||
}
|
||||
|
||||
void convert()
|
||||
}, [data])
|
||||
|
||||
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function:
|
||||
|
||||
```tsx
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
|
||||
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
|
||||
import { getPayload } from 'payload'
|
||||
import {
|
||||
convertLexicalToHTML,
|
||||
type HTMLConvertersFunction,
|
||||
} from '@payloadcms/richtext-lexical/html'
|
||||
import React from 'react'
|
||||
|
||||
import config from '../../config.js'
|
||||
type NodeTypes =
|
||||
| DefaultNodeTypes
|
||||
| SerializedBlockNode<MyTextBlock>
|
||||
| SerializedInlineBlockNode<MyInlineBlock>
|
||||
|
||||
export const MyRSCComponent = async ({
|
||||
data,
|
||||
}: {
|
||||
data: SerializedEditorState
|
||||
}) => {
|
||||
const payload = await getPayload({
|
||||
config,
|
||||
})
|
||||
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({
|
||||
defaultConverters,
|
||||
}) => ({
|
||||
...defaultConverters,
|
||||
blocks: {
|
||||
// Each key should match your block's slug
|
||||
myTextBlock: ({ node, providedCSSString }) =>
|
||||
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
|
||||
},
|
||||
inlineBlocks: {
|
||||
// Each key should match your inline block's slug
|
||||
myInlineBlock: ({ node, providedStyleTag }) =>
|
||||
`<span${providedStyleTag}>${node.fields.text}</span$>`,
|
||||
},
|
||||
})
|
||||
|
||||
const html = await convertLexicalToHTMLAsync({
|
||||
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
const html = convertLexicalToHTML({
|
||||
converters: htmlConverters,
|
||||
data,
|
||||
populate: await getPayloadPopulateFn({
|
||||
currentDepth: 0,
|
||||
depth: 1,
|
||||
payload,
|
||||
}),
|
||||
})
|
||||
|
||||
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
}
|
||||
```
|
||||
|
||||
## Converting HTML to Richtext
|
||||
## HTML to Richtext
|
||||
|
||||
If you need to convert raw HTML into a Lexical editor state, use `convertHTMLToLexical` from `@payloadcms/richtext-lexical`, along with the [editorConfigFactory to retrieve the editor config](/docs/rich-text/converters#retrieving-the-editor-config):
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and JSX
|
||||
keywords: lexical, richtext, jsx
|
||||
---
|
||||
|
||||
## Converting Richtext to JSX
|
||||
## Richtext to JSX
|
||||
|
||||
To convert richtext to JSX, import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the richtext content to it:
|
||||
|
||||
@@ -28,7 +28,7 @@ The `RichText` component includes built-in converters for common Lexical nodes.
|
||||
populated data to work correctly.
|
||||
</Banner>
|
||||
|
||||
### Converting Internal Links
|
||||
### Internal Links
|
||||
|
||||
By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.
|
||||
|
||||
@@ -81,7 +81,7 @@ export const MyComponent: React.FC<{
|
||||
}
|
||||
```
|
||||
|
||||
### Converting Lexical Blocks
|
||||
### Lexical Blocks
|
||||
|
||||
If your rich text includes custom Blocks or Inline Blocks, you must supply custom converters that match each block's slug. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
|
||||
|
||||
@@ -133,7 +133,7 @@ export const MyComponent: React.FC<{
|
||||
}
|
||||
```
|
||||
|
||||
### Overriding Default JSX Converters
|
||||
### Overriding Converters
|
||||
|
||||
You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and Markdown / MDX
|
||||
keywords: lexical, richtext, markdown, md, mdx
|
||||
---
|
||||
|
||||
## Converting Richtext to Markdown
|
||||
## Richtext to Markdown
|
||||
|
||||
If you have access to the Payload Config and the [lexical editor config](/docs/rich-text/converters#retrieving-the-editor-config), you can convert the lexical editor state to Markdown with the following:
|
||||
|
||||
@@ -91,7 +91,7 @@ const Pages: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
## Converting Markdown to Richtext
|
||||
## Markdown to Richtext
|
||||
|
||||
If you have access to the Payload Config and the [lexical editor config](/docs/rich-text/converters#retrieving-the-editor-config), you can convert Markdown to the lexical editor state with the following:
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and plaintext
|
||||
keywords: lexical, richtext, plaintext, text
|
||||
---
|
||||
|
||||
## Converting Richtext to Plaintext
|
||||
## Richtext to Plaintext
|
||||
|
||||
Here's how you can convert richtext data to plaintext using `@payloadcms/richtext-lexical/plaintext`.
|
||||
|
||||
|
||||
@@ -142,32 +142,33 @@ import { CallToAction } from '../blocks/CallToAction'
|
||||
|
||||
Here's an overview of all the included features:
|
||||
|
||||
| Feature Name | Included by default | Description |
|
||||
| ------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`BoldFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
|
||||
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
|
||||
| **`ChecklistFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
|
||||
| Feature Name | Included by default | Description |
|
||||
| ----------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`BoldFeature`** | Yes | Handles the bold text format |
|
||||
| **`ItalicFeature`** | Yes | Handles the italic text format |
|
||||
| **`UnderlineFeature`** | Yes | Handles the underline text format |
|
||||
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
|
||||
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
|
||||
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
|
||||
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
|
||||
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
|
||||
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
|
||||
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
|
||||
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
|
||||
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
|
||||
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
|
||||
| **`ChecklistFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
|
||||
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
|
||||
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
|
||||
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
|
||||
| **`EXPERIMENTAL_TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. |
|
||||
|
||||
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!
|
||||
|
||||
|
||||
@@ -81,15 +81,6 @@ export const rootEslintConfig = [
|
||||
|
||||
export default [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/eslint-config/**/*.ts'],
|
||||
rules: {
|
||||
|
||||
@@ -74,9 +74,9 @@
|
||||
"docker:start": "docker compose -f test/docker-compose.yml up -d",
|
||||
"docker:stop": "docker compose -f test/docker-compose.yml down",
|
||||
"force:build": "pnpm run build:core:force",
|
||||
"lint": "turbo run lint --concurrency 1 --continue",
|
||||
"lint": "turbo run lint --log-order=grouped --continue",
|
||||
"lint-staged": "lint-staged",
|
||||
"lint:fix": "turbo run lint:fix --concurrency 1 --continue",
|
||||
"lint:fix": "turbo run lint:fix --log-order=grouped --continue",
|
||||
"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",
|
||||
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -1,19 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
ignores: ['bin/cli.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -22,7 +22,9 @@ const updateEnvExampleVariables = (
|
||||
|
||||
const [key] = line.split('=')
|
||||
|
||||
if (!key) {return}
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
|
||||
if (key === 'DATABASE_URI' || key === 'POSTGRES_URL' || key === 'MONGODB_URI') {
|
||||
const dbChoice = databaseType ? dbChoiceRecord[databaseType] : null
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -20,7 +20,6 @@ type SearchParam = {
|
||||
|
||||
const subQueryOptions = {
|
||||
lean: true,
|
||||
limit: 50,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +183,7 @@ export async function buildSearchParam({
|
||||
select[joinPath] = true
|
||||
}
|
||||
|
||||
const result = await SubModel.find(subQuery).lean().limit(50).select(select)
|
||||
const result = await SubModel.find(subQuery).lean().select(select)
|
||||
|
||||
const $in: unknown[] = []
|
||||
|
||||
|
||||
@@ -150,6 +150,18 @@ export const buildSortParam = ({
|
||||
sort = [sort]
|
||||
}
|
||||
|
||||
// In the case of Mongo, when sorting by a field that is not unique, the results are not guaranteed to be in the same order each time.
|
||||
// So we add a fallback sort to ensure that the results are always in the same order.
|
||||
let fallbackSort = '-id'
|
||||
|
||||
if (timestamps) {
|
||||
fallbackSort = '-createdAt'
|
||||
}
|
||||
|
||||
if (!(sort.includes(fallbackSort) || sort.includes(fallbackSort.replace('-', '')))) {
|
||||
sort.push(fallbackSort)
|
||||
}
|
||||
|
||||
const sorting = sort.reduce<Record<string, string>>((acc, item) => {
|
||||
let sortProperty: string
|
||||
let sortDirection: SortDirection
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -4,7 +4,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const count: Count = async function count(
|
||||
|
||||
@@ -5,7 +5,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
||||
|
||||
@@ -5,7 +5,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const countVersions: CountVersions = async function countVersions(
|
||||
|
||||
@@ -23,10 +23,10 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
data,
|
||||
db,
|
||||
fields: globalConfig.flattenedFields,
|
||||
ignoreResult: returning === false,
|
||||
operation: 'create',
|
||||
req,
|
||||
tableName,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
|
||||
@@ -17,11 +17,11 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
globalSlug,
|
||||
publishedLocale,
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
returning,
|
||||
}: CreateGlobalVersionArgs,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
@@ -42,11 +42,11 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
},
|
||||
db,
|
||||
fields: buildVersionGlobalFields(this.payload.config, global, true),
|
||||
ignoreResult: returning === false ? 'idOnly' : false,
|
||||
operation: 'create',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
ignoreResult: returning === false ? 'idOnly' : false,
|
||||
})
|
||||
|
||||
const table = this.tables[tableName]
|
||||
|
||||
@@ -18,11 +18,11 @@ export async function createVersion<T extends TypeWithID>(
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
versionData,
|
||||
returning,
|
||||
}: CreateVersionArgs<T>,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
|
||||
@@ -6,7 +6,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { transform } from './transform/read/index.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
@@ -4,7 +4,7 @@ import { inArray } from 'drizzle-orm'
|
||||
|
||||
import type { DrizzleAdapter } from '../types.js'
|
||||
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
import { buildQuery } from '../queries/buildQuery.js'
|
||||
import { selectDistinct } from '../queries/selectDistinct.js'
|
||||
import { transform } from '../transform/read/index.js'
|
||||
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
|
||||
|
||||
@@ -19,13 +19,17 @@ import toSnakeCase from 'to-snake-case'
|
||||
import type { BuildQueryJoinAliases, DrizzleAdapter } from '../types.js'
|
||||
import type { Result } from './buildFindManyArgs.js'
|
||||
|
||||
import buildQuery from '../queries/buildQuery.js'
|
||||
import { buildQuery } from '../queries/buildQuery.js'
|
||||
import { getTableAlias } from '../queries/getTableAlias.js'
|
||||
import { operatorMap } from '../queries/operatorMap.js'
|
||||
import { getArrayRelationName } from '../utilities/getArrayRelationName.js'
|
||||
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
|
||||
import { jsonAggBuildObject } from '../utilities/json.js'
|
||||
import { rawConstraint } from '../utilities/rawConstraint.js'
|
||||
import {
|
||||
InternalBlockTableNameIndex,
|
||||
resolveBlockTableName,
|
||||
} from '../utilities/validateExistingBlockIsIdentical.js'
|
||||
|
||||
const flattenAllWherePaths = (where: Where, paths: string[]) => {
|
||||
for (const k in where) {
|
||||
@@ -250,7 +254,7 @@ export const traverseFields = ({
|
||||
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
const blockKey = `_blocks_${block.slug}`
|
||||
const blockKey = `_blocks_${block.slug}${!block[InternalBlockTableNameIndex] ? '' : `_${block[InternalBlockTableNameIndex]}`}`
|
||||
|
||||
let blockSelect: boolean | SelectType | undefined
|
||||
|
||||
@@ -290,8 +294,9 @@ export const traverseFields = ({
|
||||
with: {},
|
||||
}
|
||||
|
||||
const tableName = adapter.tableNameMap.get(
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
const tableName = resolveBlockTableName(
|
||||
block,
|
||||
adapter.tableNameMap.get(`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`),
|
||||
)
|
||||
|
||||
if (typeof blockSelect === 'object') {
|
||||
|
||||
@@ -23,7 +23,7 @@ export { migrateFresh } from './migrateFresh.js'
|
||||
export { migrateRefresh } from './migrateRefresh.js'
|
||||
export { migrateReset } from './migrateReset.js'
|
||||
export { migrateStatus } from './migrateStatus.js'
|
||||
export { default as buildQuery } from './queries/buildQuery.js'
|
||||
export { buildQuery } from './queries/buildQuery.js'
|
||||
export { operatorMap } from './queries/operatorMap.js'
|
||||
export type { Operators } from './queries/operatorMap.js'
|
||||
export { parseParams } from './queries/parseParams.js'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FlattenedBlock, FlattenedField } from 'payload'
|
||||
import type { FlattenedField } from 'payload'
|
||||
|
||||
type Args = {
|
||||
doc: Record<string, unknown>
|
||||
@@ -54,7 +54,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
|
||||
const matchedBlock = field.blocks.find(
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined
|
||||
)
|
||||
|
||||
if (matchedBlock) {
|
||||
return traverseFields({
|
||||
@@ -75,7 +75,7 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
|
||||
const matchedBlock = field.blocks.find(
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined
|
||||
)
|
||||
|
||||
if (matchedBlock) {
|
||||
return traverseFields({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SQL, Table } from 'drizzle-orm'
|
||||
import type { FlattenedField, Sort } from 'payload'
|
||||
|
||||
import { asc, desc, or } from 'drizzle-orm'
|
||||
import { asc, desc } from 'drizzle-orm'
|
||||
|
||||
import type { DrizzleAdapter, GenericColumn } from '../types.js'
|
||||
import type { BuildQueryJoinAliases, BuildQueryResult } from './buildQuery.js'
|
||||
@@ -39,8 +39,9 @@ export const buildOrderBy = ({
|
||||
}: Args): BuildQueryResult['orderBy'] => {
|
||||
const orderBy: BuildQueryResult['orderBy'] = []
|
||||
|
||||
const createdAt = adapter.tables[tableName]?.createdAt
|
||||
|
||||
if (!sort) {
|
||||
const createdAt = adapter.tables[tableName]?.createdAt
|
||||
if (createdAt) {
|
||||
sort = '-createdAt'
|
||||
} else {
|
||||
@@ -52,6 +53,18 @@ export const buildOrderBy = ({
|
||||
sort = [sort]
|
||||
}
|
||||
|
||||
// In the case of Mongo, when sorting by a field that is not unique, the results are not guaranteed to be in the same order each time.
|
||||
// So we add a fallback sort to ensure that the results are always in the same order.
|
||||
let fallbackSort = '-id'
|
||||
|
||||
if (createdAt) {
|
||||
fallbackSort = '-createdAt'
|
||||
}
|
||||
|
||||
if (!(sort.includes(fallbackSort) || sort.includes(fallbackSort.replace('-', '')))) {
|
||||
sort.push(fallbackSort)
|
||||
}
|
||||
|
||||
for (const sortItem of sort) {
|
||||
let sortProperty: string
|
||||
let sortDirection: 'asc' | 'desc'
|
||||
@@ -92,7 +105,7 @@ export const buildOrderBy = ({
|
||||
|
||||
selectFields[sortTableColumnName] = sortTable[sortTableColumnName]
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (_) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ export type BuildQueryResult = {
|
||||
selectFields: Record<string, GenericColumn>
|
||||
where: SQL
|
||||
}
|
||||
const buildQuery = function buildQuery({
|
||||
|
||||
export const buildQuery = function buildQuery({
|
||||
adapter,
|
||||
aliasTable,
|
||||
fields,
|
||||
@@ -92,5 +93,3 @@ const buildQuery = function buildQuery({
|
||||
where,
|
||||
}
|
||||
}
|
||||
|
||||
export default buildQuery
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { DrizzleAdapter, GenericColumn } from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
|
||||
import { isPolymorphicRelationship } from '../utilities/isPolymorphicRelationship.js'
|
||||
import { resolveBlockTableName } from '../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { addJoinTable } from './addJoinTable.js'
|
||||
import { getTableAlias } from './getTableAlias.js'
|
||||
|
||||
@@ -193,8 +194,9 @@ export const getTableColumnFromPath = ({
|
||||
(block) => typeof block !== 'string' && block.slug === blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
newTableName = adapter.tableNameMap.get(
|
||||
`${tableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
newTableName = resolveBlockTableName(
|
||||
block,
|
||||
adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`),
|
||||
)
|
||||
|
||||
const { newAliasTable } = getTableAlias({ adapter, tableName: newTableName })
|
||||
@@ -220,7 +222,11 @@ export const getTableColumnFromPath = ({
|
||||
const hasBlockField = (field.blockReferences ?? field.blocks).some((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
|
||||
newTableName = adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`)
|
||||
newTableName = resolveBlockTableName(
|
||||
block,
|
||||
adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`),
|
||||
)
|
||||
|
||||
constraintPath = `${constraintPath}${field.name}.%.`
|
||||
|
||||
let result: TableColumn
|
||||
@@ -274,7 +280,7 @@ export const getTableColumnFromPath = ({
|
||||
tableName: newTableName,
|
||||
value,
|
||||
})
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
// this is fine, not every block will have the field
|
||||
}
|
||||
if (!result) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
notInArray,
|
||||
or,
|
||||
type SQL,
|
||||
type SQLWrapper,
|
||||
} from 'drizzle-orm'
|
||||
|
||||
type OperatorKeys =
|
||||
@@ -35,7 +34,7 @@ type OperatorKeys =
|
||||
| 'not_like'
|
||||
| 'or'
|
||||
|
||||
export type Operators = Record<OperatorKeys, (column: Column, value: SQLWrapper | unknown) => SQL>
|
||||
export type Operators = Record<OperatorKeys, (column: Column, value: unknown) => SQL>
|
||||
|
||||
export const operatorMap: Operators = {
|
||||
and,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { QueryPromise, SQL } from 'drizzle-orm'
|
||||
import type { PgSelect } from 'drizzle-orm/pg-core'
|
||||
import type { SQLiteColumn, SQLiteSelect } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import type {
|
||||
|
||||
@@ -32,6 +32,7 @@ type Args = {
|
||||
* ie. indexes, multiple columns, etc
|
||||
*/
|
||||
baseIndexes?: Record<string, RawIndex>
|
||||
blocksTableNameMap: Record<string, number>
|
||||
buildNumbers?: boolean
|
||||
buildRelationships?: boolean
|
||||
compoundIndexes?: SanitizedCompoundIndex[]
|
||||
@@ -70,6 +71,7 @@ export const buildTable = ({
|
||||
baseColumns = {},
|
||||
baseForeignKeys = {},
|
||||
baseIndexes = {},
|
||||
blocksTableNameMap,
|
||||
compoundIndexes,
|
||||
disableNotNull,
|
||||
disableRelsTableUnique = false,
|
||||
@@ -120,6 +122,7 @@ export const buildTable = ({
|
||||
hasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
blocksTableNameMap,
|
||||
columns,
|
||||
disableNotNull,
|
||||
disableRelsTableUnique,
|
||||
|
||||
@@ -56,6 +56,7 @@ export const buildRawSchema = ({
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
blocksTableNameMap: {},
|
||||
compoundIndexes: collection.sanitizedIndexes,
|
||||
disableNotNull: !!collection?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
@@ -75,6 +76,7 @@ export const buildRawSchema = ({
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
blocksTableNameMap: {},
|
||||
compoundIndexes: buildVersionCompoundIndexes({ indexes: collection.sanitizedIndexes }),
|
||||
disableNotNull: !!collection.versions?.drafts,
|
||||
disableUnique: true,
|
||||
@@ -96,6 +98,7 @@ export const buildRawSchema = ({
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
blocksTableNameMap: {},
|
||||
disableNotNull: !!global?.versions?.drafts,
|
||||
disableUnique: false,
|
||||
fields: global.flattenedFields,
|
||||
@@ -118,6 +121,7 @@ export const buildRawSchema = ({
|
||||
|
||||
buildTable({
|
||||
adapter,
|
||||
blocksTableNameMap: {},
|
||||
disableNotNull: !!global.versions?.drafts,
|
||||
disableUnique: true,
|
||||
fields: versionFields,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { CompoundIndex, FlattenedField } from 'payload'
|
||||
import type { FlattenedField } from 'payload'
|
||||
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import {
|
||||
array,
|
||||
fieldAffectsData,
|
||||
fieldIsVirtual,
|
||||
fieldShouldBeLocalized,
|
||||
@@ -25,13 +24,18 @@ import { createTableName } from '../createTableName.js'
|
||||
import { buildIndexName } from '../utilities/buildIndexName.js'
|
||||
import { getArrayRelationName } from '../utilities/getArrayRelationName.js'
|
||||
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
|
||||
import { validateExistingBlockIsIdentical } from '../utilities/validateExistingBlockIsIdentical.js'
|
||||
import {
|
||||
InternalBlockTableNameIndex,
|
||||
setInternalBlockIndex,
|
||||
validateExistingBlockIsIdentical,
|
||||
} from '../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { buildTable } from './build.js'
|
||||
import { idToUUID } from './idToUUID.js'
|
||||
import { withDefault } from './withDefault.js'
|
||||
|
||||
type Args = {
|
||||
adapter: DrizzleAdapter
|
||||
blocksTableNameMap: Record<string, number>
|
||||
columnPrefix?: string
|
||||
columns: Record<string, RawColumn>
|
||||
disableNotNull: boolean
|
||||
@@ -72,6 +76,7 @@ type Result = {
|
||||
|
||||
export const traverseFields = ({
|
||||
adapter,
|
||||
blocksTableNameMap,
|
||||
columnPrefix,
|
||||
columns,
|
||||
disableNotNull,
|
||||
@@ -250,6 +255,7 @@ export const traverseFields = ({
|
||||
baseColumns,
|
||||
baseForeignKeys,
|
||||
baseIndexes,
|
||||
blocksTableNameMap,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableRelsTableUnique: true,
|
||||
disableUnique,
|
||||
@@ -369,7 +375,7 @@ export const traverseFields = ({
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
|
||||
const blockTableName = createTableName({
|
||||
let blockTableName = createTableName({
|
||||
adapter,
|
||||
config: block,
|
||||
parentTableName: rootTableName,
|
||||
@@ -377,6 +383,27 @@ export const traverseFields = ({
|
||||
throwValidationError,
|
||||
versionsCustomName: versions,
|
||||
})
|
||||
|
||||
if (typeof blocksTableNameMap[blockTableName] === 'undefined') {
|
||||
blocksTableNameMap[blockTableName] = 1
|
||||
} else if (
|
||||
!validateExistingBlockIsIdentical({
|
||||
block,
|
||||
localized: field.localized,
|
||||
rootTableName,
|
||||
table: adapter.rawTables[blockTableName],
|
||||
tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`],
|
||||
})
|
||||
) {
|
||||
blocksTableNameMap[blockTableName]++
|
||||
setInternalBlockIndex(block, blocksTableNameMap[blockTableName])
|
||||
blockTableName = `${blockTableName}_${blocksTableNameMap[blockTableName]}`
|
||||
}
|
||||
let relationName = `_blocks_${block.slug}`
|
||||
if (typeof block[InternalBlockTableNameIndex] !== 'undefined') {
|
||||
relationName = `_blocks_${block.slug}_${block[InternalBlockTableNameIndex]}`
|
||||
}
|
||||
|
||||
if (!adapter.rawTables[blockTableName]) {
|
||||
const baseColumns: Record<string, RawColumn> = {
|
||||
_order: {
|
||||
@@ -456,6 +483,7 @@ export const traverseFields = ({
|
||||
baseColumns,
|
||||
baseForeignKeys,
|
||||
baseIndexes,
|
||||
blocksTableNameMap,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
disableRelsTableUnique: true,
|
||||
disableUnique,
|
||||
@@ -506,7 +534,7 @@ export const traverseFields = ({
|
||||
},
|
||||
],
|
||||
references: ['id'],
|
||||
relationName: `_blocks_${block.slug}`,
|
||||
relationName,
|
||||
to: rootTableName,
|
||||
},
|
||||
}
|
||||
@@ -554,18 +582,10 @@ export const traverseFields = ({
|
||||
})
|
||||
|
||||
adapter.rawRelations[blockTableName] = blockRelations
|
||||
} else if (process.env.NODE_ENV !== 'production' && !versions) {
|
||||
validateExistingBlockIsIdentical({
|
||||
block,
|
||||
localized: field.localized,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
rootTableName,
|
||||
table: adapter.rawTables[blockTableName],
|
||||
tableLocales: adapter.rawTables[`${blockTableName}${adapter.localesSuffix}`],
|
||||
})
|
||||
}
|
||||
|
||||
// blocks relationships are defined from the collection or globals table down to the block, bypassing any subBlocks
|
||||
rootRelationsToBuild.set(`_blocks_${block.slug}`, {
|
||||
rootRelationsToBuild.set(relationName, {
|
||||
type: 'many',
|
||||
// blocks are not localized on the parent table
|
||||
localized: false,
|
||||
@@ -629,6 +649,7 @@ export const traverseFields = ({
|
||||
hasManyTextField: groupHasManyTextField,
|
||||
} = traverseFields({
|
||||
adapter,
|
||||
blocksTableNameMap,
|
||||
columnPrefix: `${columnName}_`,
|
||||
columns,
|
||||
disableNotNull: disableNotNullFromHere,
|
||||
@@ -845,6 +866,7 @@ export const traverseFields = ({
|
||||
baseColumns,
|
||||
baseForeignKeys,
|
||||
baseIndexes,
|
||||
blocksTableNameMap,
|
||||
disableNotNull,
|
||||
disableUnique,
|
||||
fields: [],
|
||||
|
||||
@@ -12,7 +12,7 @@ export const commitTransaction: CommitTransaction = async function commitTransac
|
||||
|
||||
try {
|
||||
await this.sessions[id].resolve()
|
||||
} catch (err: unknown) {
|
||||
} catch (_) {
|
||||
await this.sessions[id].reject()
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export const transform = <T extends Record<string, unknown> | TypeWithID>({
|
||||
}
|
||||
|
||||
const blocks = createBlocksMap(data)
|
||||
|
||||
const deletions = []
|
||||
|
||||
const result = traverseFields<T>({
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
|
||||
|
||||
import { getArrayRelationName } from '../../utilities/getArrayRelationName.js'
|
||||
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { transformHasManyNumber } from './hasManyNumber.js'
|
||||
import { transformHasManyText } from './hasManyText.js'
|
||||
import { transformRelationship } from './relationship.js'
|
||||
@@ -248,8 +249,9 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
const tableName = adapter.tableNameMap.get(
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
const tableName = resolveBlockTableName(
|
||||
block,
|
||||
adapter.tableNameMap.get(`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`),
|
||||
)
|
||||
|
||||
if (block) {
|
||||
@@ -327,8 +329,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
delete row._index
|
||||
}
|
||||
|
||||
const tableName = adapter.tableNameMap.get(
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
const tableName = resolveBlockTableName(
|
||||
block,
|
||||
adapter.tableNameMap.get(
|
||||
`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
),
|
||||
)
|
||||
|
||||
acc.push(
|
||||
@@ -665,10 +670,6 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
withinArrayOrBlockLocale: locale || withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
if ('_order' in ref) {
|
||||
delete ref._order
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
|
||||
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
@@ -66,10 +67,6 @@ export const transformBlocks = ({
|
||||
}
|
||||
const blockType = toSnakeCase(blockRow.blockType)
|
||||
|
||||
if (!blocks[blockType]) {
|
||||
blocks[blockType] = []
|
||||
}
|
||||
|
||||
const newRow: BlockRowToInsert = {
|
||||
arrays: {},
|
||||
locales: {},
|
||||
@@ -86,7 +83,14 @@ export const transformBlocks = ({
|
||||
newRow.row._locale = withinArrayOrBlockLocale
|
||||
}
|
||||
|
||||
const blockTableName = adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`)
|
||||
const blockTableName = resolveBlockTableName(
|
||||
matchedBlock,
|
||||
adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`),
|
||||
)
|
||||
|
||||
if (!blocks[blockTableName]) {
|
||||
blocks[blockTableName] = []
|
||||
}
|
||||
|
||||
const hasUUID = adapter.tables[blockTableName]._uuid
|
||||
|
||||
@@ -124,6 +128,6 @@ export const transformBlocks = ({
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
blocks[blockType].push(newRow)
|
||||
blocks[blockTableName].push(newRow)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const transformRelationship = ({ baseRow, data, field, relationships }: A
|
||||
if (Array.isArray(field.relationTo) && valueIsValueWithRelation(relation)) {
|
||||
relationRow[`${relation.relationTo}ID`] = relation.value
|
||||
relationships.push(relationRow)
|
||||
} else {
|
||||
} else if (typeof field.relationTo === 'string') {
|
||||
relationRow[`${field.relationTo}ID`] = relation
|
||||
if (relation) {
|
||||
relationships.push(relationRow)
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { ArrayRowToInsert, BlockRowToInsert, RelationshipToDelete } from './types.js'
|
||||
|
||||
import { isArrayOfRows } from '../../utilities/isArrayOfRows.js'
|
||||
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
import { transformArray } from './array.js'
|
||||
import { transformBlocks } from './blocks.js'
|
||||
import { transformNumbers } from './numbers.js'
|
||||
@@ -175,7 +176,17 @@ export const traverseFields = ({
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
;(field.blockReferences ?? field.blocks).forEach((block) => {
|
||||
blocksToDelete.add(toSnakeCase(typeof block === 'string' ? block : block.slug))
|
||||
const matchedBlock =
|
||||
typeof block === 'string'
|
||||
? adapter.payload.config.blocks.find((each) => each.slug === block)
|
||||
: block
|
||||
|
||||
blocksToDelete.add(
|
||||
resolveBlockTableName(
|
||||
matchedBlock,
|
||||
adapter.tableNameMap.get(`${baseTableName}_blocks_${toSnakeCase(matchedBlock.slug)}`),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
if (isLocalized) {
|
||||
|
||||
@@ -28,7 +28,7 @@ export type RowToInsert = {
|
||||
[tableName: string]: ArrayRowToInsert[]
|
||||
}
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
[tableName: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
locales: {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
this: DrizzleAdapter,
|
||||
{ slug, data, req, select, returning }: UpdateGlobalArgs,
|
||||
{ slug, data, req, returning, select }: UpdateGlobalArgs,
|
||||
): Promise<T> {
|
||||
const db = await getTransaction(this, req)
|
||||
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
|
||||
@@ -23,10 +23,10 @@ export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
data,
|
||||
db,
|
||||
fields: globalConfig.flattenedFields,
|
||||
ignoreResult: returning === false,
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
@@ -21,10 +21,10 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
global,
|
||||
locale,
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
versionData,
|
||||
where: whereArg,
|
||||
returning,
|
||||
}: UpdateGlobalVersionArgs<T>,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
@@ -53,12 +53,12 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
data: versionData,
|
||||
db,
|
||||
fields,
|
||||
ignoreResult: returning === false,
|
||||
operation: 'update',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
where,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
@@ -5,7 +5,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
@@ -10,7 +10,7 @@ import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
@@ -21,10 +21,10 @@ export async function updateVersion<T extends TypeWithID>(
|
||||
collection,
|
||||
locale,
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
versionData,
|
||||
where: whereArg,
|
||||
returning,
|
||||
}: UpdateVersionArgs<T>,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
@@ -50,13 +50,13 @@ export async function updateVersion<T extends TypeWithID>(
|
||||
data: versionData,
|
||||
db,
|
||||
fields,
|
||||
ignoreResult: returning === false,
|
||||
joinQuery: false,
|
||||
operation: 'update',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
where,
|
||||
ignoreResult: returning === false,
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
|
||||
@@ -134,16 +134,16 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
|
||||
// If there are blocks, add parent to each, and then
|
||||
// store by table name and rows
|
||||
Object.keys(rowToInsert.blocks).forEach((blockName) => {
|
||||
rowToInsert.blocks[blockName].forEach((blockRow) => {
|
||||
Object.keys(rowToInsert.blocks).forEach((tableName) => {
|
||||
rowToInsert.blocks[tableName].forEach((blockRow) => {
|
||||
blockRow.row._parentID = insertedRow.id
|
||||
if (!blocksToInsert[blockName]) {
|
||||
blocksToInsert[blockName] = []
|
||||
if (!blocksToInsert[tableName]) {
|
||||
blocksToInsert[tableName] = []
|
||||
}
|
||||
if (blockRow.row.uuid) {
|
||||
delete blockRow.row.uuid
|
||||
}
|
||||
blocksToInsert[blockName].push(blockRow)
|
||||
blocksToInsert[tableName].push(blockRow)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -258,12 +258,11 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
const insertedBlockRows: Record<string, Record<string, unknown>[]> = {}
|
||||
|
||||
if (operation === 'update') {
|
||||
for (const blockName of rowToInsert.blocksToDelete) {
|
||||
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
|
||||
const blockTable = adapter.tables[blockTableName]
|
||||
for (const tableName of rowToInsert.blocksToDelete) {
|
||||
const blockTable = adapter.tables[tableName]
|
||||
await adapter.deleteWhere({
|
||||
db,
|
||||
tableName: blockTableName,
|
||||
tableName,
|
||||
where: eq(blockTable._parentID, insertedRow.id),
|
||||
})
|
||||
}
|
||||
@@ -272,15 +271,14 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
// When versions are enabled, this is used to track mapping between blocks/arrays ObjectID to their numeric generated representation, then we use it for nested to arrays/blocks select hasMany in versions.
|
||||
const arraysBlocksUUIDMap: Record<string, number | string> = {}
|
||||
|
||||
for (const [blockName, blockRows] of Object.entries(blocksToInsert)) {
|
||||
const blockTableName = adapter.tableNameMap.get(`${tableName}_blocks_${blockName}`)
|
||||
insertedBlockRows[blockName] = await adapter.insert({
|
||||
for (const [tableName, blockRows] of Object.entries(blocksToInsert)) {
|
||||
insertedBlockRows[tableName] = await adapter.insert({
|
||||
db,
|
||||
tableName: blockTableName,
|
||||
tableName,
|
||||
values: blockRows.map(({ row }) => row),
|
||||
})
|
||||
|
||||
insertedBlockRows[blockName].forEach((row, i) => {
|
||||
insertedBlockRows[tableName].forEach((row, i) => {
|
||||
blockRows[i].row = row
|
||||
if (
|
||||
typeof row._uuid === 'string' &&
|
||||
@@ -310,7 +308,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
if (blockLocaleRowsToInsert.length > 0) {
|
||||
await adapter.insert({
|
||||
db,
|
||||
tableName: `${blockTableName}${adapter.localesSuffix}`,
|
||||
tableName: `${tableName}${adapter.localesSuffix}`,
|
||||
values: blockLocaleRowsToInsert,
|
||||
})
|
||||
}
|
||||
@@ -319,7 +317,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
adapter,
|
||||
arrays: blockRows.map(({ arrays }) => arrays),
|
||||
db,
|
||||
parentRows: insertedBlockRows[blockName],
|
||||
parentRows: insertedBlockRows[tableName],
|
||||
uuidMap: arraysBlocksUUIDMap,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const insertArrays = async ({
|
||||
|
||||
// Add any sub arrays that need to be created
|
||||
// We will call this recursively below
|
||||
arrayRows.forEach((arrayRow, i) => {
|
||||
arrayRows.forEach((arrayRow) => {
|
||||
if (Object.keys(arrayRow.arrays).length > 0) {
|
||||
rowsByTable[tableName].arrays.push(arrayRow.arrays)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type BaseArgs = {
|
||||
* When true, skips reading the data back from the database and returns the input data
|
||||
* @default false
|
||||
*/
|
||||
ignoreResult?: boolean | 'idOnly'
|
||||
ignoreResult?: 'idOnly' | boolean
|
||||
joinQuery?: JoinQuery
|
||||
path?: string
|
||||
req?: Partial<PayloadRequest>
|
||||
|
||||
@@ -7,7 +7,11 @@ export const createBlocksMap = (data: Record<string, unknown>): BlocksMap => {
|
||||
|
||||
Object.entries(data).forEach(([key, rows]) => {
|
||||
if (key.startsWith('_blocks_') && Array.isArray(rows)) {
|
||||
const blockType = key.replace('_blocks_', '')
|
||||
let blockType = key.replace('_blocks_', '')
|
||||
const parsed = blockType.split('_')
|
||||
if (parsed.length === 2 && Number.isInteger(Number(parsed[1]))) {
|
||||
blockType = parsed[0]
|
||||
}
|
||||
|
||||
rows.forEach((row) => {
|
||||
if ('_path' in row) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Block, Field } from 'payload'
|
||||
import type { Block, Field, FlattenedBlock } from 'payload'
|
||||
|
||||
import { InvalidConfiguration } from 'payload'
|
||||
import {
|
||||
fieldAffectsData,
|
||||
fieldHasSubFields,
|
||||
@@ -83,14 +82,16 @@ const getFlattenedFieldNames = (args: {
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if all the fields in a block are identical to the existing table
|
||||
*/
|
||||
export const validateExistingBlockIsIdentical = ({
|
||||
block,
|
||||
localized,
|
||||
parentIsLocalized,
|
||||
rootTableName,
|
||||
table,
|
||||
tableLocales,
|
||||
}: Args): void => {
|
||||
}: Args): boolean => {
|
||||
const fieldNames = getFlattenedFieldNames({
|
||||
fields: block.fields,
|
||||
parentIsLocalized: parentIsLocalized || localized,
|
||||
@@ -110,18 +111,21 @@ export const validateExistingBlockIsIdentical = ({
|
||||
})
|
||||
|
||||
if (missingField) {
|
||||
throw new InvalidConfiguration(
|
||||
`The table ${rootTableName} has multiple blocks with slug ${
|
||||
block.slug
|
||||
}, but the schemas do not match. One block includes the field ${
|
||||
typeof missingField === 'string' ? missingField : missingField.name
|
||||
}, while the other block does not.`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
if (Boolean(localized) !== Boolean(table.columns._locale)) {
|
||||
throw new InvalidConfiguration(
|
||||
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One is localized, but another is not. Block schemas of the same name must match exactly.`,
|
||||
)
|
||||
}
|
||||
return Boolean(localized) === Boolean(table.columns._locale)
|
||||
}
|
||||
|
||||
export const InternalBlockTableNameIndex = Symbol('InternalBlockTableNameIndex')
|
||||
export const setInternalBlockIndex = (block: FlattenedBlock, index: number) => {
|
||||
block[InternalBlockTableNameIndex] = index
|
||||
}
|
||||
|
||||
export const resolveBlockTableName = (block: FlattenedBlock, originalTableName: string) => {
|
||||
if (!block[InternalBlockTableNameIndex]) {
|
||||
return originalTableName
|
||||
}
|
||||
|
||||
return `${originalTableName}_${block[InternalBlockTableNameIndex]}`
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -10,6 +10,7 @@ import globals from 'globals'
|
||||
import importX from 'eslint-plugin-import-x'
|
||||
import typescriptParser from '@typescript-eslint/parser'
|
||||
import { deepMerge } from './deepMerge.js'
|
||||
import reactCompiler from 'eslint-plugin-react-compiler'
|
||||
|
||||
const baseRules = {
|
||||
// This rule makes no sense when overriding class methods. This is used a lot in richtext-lexical.
|
||||
@@ -125,6 +126,52 @@ export const rootEslintConfig = [
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
projectService: {
|
||||
// This is necessary because `tsconfig.base.json` defines `"rootDir": "${configDir}/src"`,
|
||||
// And the following files aren't in src because they aren't transpiled.
|
||||
// This is typescript-eslint's way of adding files that aren't included in tsconfig.
|
||||
// See: https://typescript-eslint.io/troubleshooting/typed-linting/#i-get-errors-telling-me--was-not-found-by-the-project-service-consider-either-including-it-in-the-tsconfigjson-or-including-it-in-allowdefaultproject
|
||||
// The best practice is to have a tsconfig.json that covers ALL files and is used for
|
||||
// typechecking (with noEmit), and a `tsconfig.build.json` that is used for the build
|
||||
// (or alternatively, swc, tsup or tsdown). That's what we should ideally do, in which case
|
||||
// this hardcoded list wouldn't be necessary. Note that these files don't currently go
|
||||
// through ts, only through eslint.
|
||||
allowDefaultProject: [
|
||||
'../payload/bin.js',
|
||||
'../payload/bundle.js',
|
||||
'../next/babel.config.cjs',
|
||||
'../next/bundleScss.js',
|
||||
'../ui/babel.config.cjs',
|
||||
'../ui/bundle.js',
|
||||
'../graphql/bin.js',
|
||||
'../richtext-lexical/babel.config.cjs',
|
||||
'../richtext-lexical/bundle.js',
|
||||
'../richtext-lexical/scripts/translateNewKeys.ts',
|
||||
'../db-postgres/bundle.js',
|
||||
'../db-postgres/relationships-v2-v3.mjs',
|
||||
'../db-postgres/scripts/renamePredefinedMigrations.ts',
|
||||
'../db-sqlite/bundle.js',
|
||||
'../db-vercel-postgres/relationships-v2-v3.mjs',
|
||||
'../db-vercel-postgres/scripts/renamePredefinedMigrations.ts',
|
||||
'../plugin-cloud-storage/azure.d.ts',
|
||||
'../plugin-cloud-storage/azure.js',
|
||||
'../plugin-cloud-storage/gcs.d.ts',
|
||||
'../plugin-cloud-storage/gcs.js',
|
||||
'../plugin-cloud-storage/s3.d.ts',
|
||||
'../plugin-cloud-storage/s3.js',
|
||||
'../plugin-redirects/types.d.ts',
|
||||
'../plugin-redirects/types.js',
|
||||
'../translations/scripts/translateNewKeys/applyEslintFixes.ts',
|
||||
'../translations/scripts/translateNewKeys/findMissingKeys.ts',
|
||||
'../translations/scripts/translateNewKeys/generateTsObjectLiteral.ts',
|
||||
'../translations/scripts/translateNewKeys/index.ts',
|
||||
'../translations/scripts/translateNewKeys/run.ts',
|
||||
'../translations/scripts/translateNewKeys/sortKeys.ts',
|
||||
'../translations/scripts/translateNewKeys/translateText.ts',
|
||||
'../create-payload-app/bin/cli.js',
|
||||
],
|
||||
},
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
@@ -206,6 +253,10 @@ export const rootEslintConfig = [
|
||||
},
|
||||
files: ['*.config.ts', 'config.ts'],
|
||||
},
|
||||
{
|
||||
name: 'React Compiler',
|
||||
...reactCompiler.configs.recommended,
|
||||
},
|
||||
]
|
||||
|
||||
export default rootEslintConfig
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"eslint-plugin-jest-dom": "5.5.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-d331ba04-20250307",
|
||||
"eslint-plugin-regexp": "2.7.0",
|
||||
"globals": "16.0.0",
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -17,7 +17,7 @@ export const recursivelyBuildNestedPaths = ({ field, nestedFieldName2, parentNam
|
||||
if (field.type === 'tabs') {
|
||||
// if the tab has a name, treat it as a group
|
||||
// otherwise, treat it as a row
|
||||
return (field.tabs as Tab[]).reduce((tabSchema, tab: any) => {
|
||||
return field.tabs.reduce((tabSchema, tab: any) => {
|
||||
tabSchema.push(
|
||||
...recursivelyBuildNestedPaths({
|
||||
field: {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -1,18 +0,0 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
@@ -1,6 +1,4 @@
|
||||
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
|
||||
import reactCompiler from 'eslint-plugin-react-compiler'
|
||||
const { rules } = reactCompiler
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
@@ -8,28 +6,12 @@ const { rules } = reactCompiler
|
||||
export const index = [
|
||||
...rootEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
...rootParserOptions,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: '../../app/',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'react-compiler': {
|
||||
rules,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'react-compiler/react-compiler': 'error',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
|
||||
@@ -118,7 +118,6 @@
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
|
||||
"payload": "workspace:*",
|
||||
"swc-plugin-transform-remove-imports": "3.1.0"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { Link } from '@payloadcms/ui'
|
||||
import { Button } from '@payloadcms/ui'
|
||||
import { useParams, usePathname, useSearchParams } from 'next/navigation.js'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
import React from 'react'
|
||||
@@ -13,7 +13,6 @@ export const DocumentTabLink: React.FC<{
|
||||
children?: React.ReactNode
|
||||
href: string
|
||||
isActive?: boolean
|
||||
isCollection?: boolean
|
||||
newTab?: boolean
|
||||
}> = ({
|
||||
adminRoute,
|
||||
@@ -54,19 +53,17 @@ export const DocumentTabLink: React.FC<{
|
||||
isActiveFromProps
|
||||
|
||||
return (
|
||||
<li
|
||||
<Button
|
||||
aria-label={ariaLabel}
|
||||
buttonStyle="tab"
|
||||
className={[baseClass, isActive && `${baseClass}--active`].filter(Boolean).join(' ')}
|
||||
disabled={isActive}
|
||||
el={!isActive || href !== pathname ? 'link' : 'div'}
|
||||
newTab={newTab}
|
||||
size="medium"
|
||||
to={!isActive || href !== pathname ? hrefWithLocale : undefined}
|
||||
>
|
||||
<Link
|
||||
className={`${baseClass}__link`}
|
||||
href={!isActive || href !== pathname ? hrefWithLocale : ''}
|
||||
prefetch={false}
|
||||
{...(newTab && { rel: 'noopener noreferrer', target: '_blank' })}
|
||||
tabIndex={isActive ? -1 : 0}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
</li>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,74 +1,24 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
@layer payload-default {
|
||||
.doc-tab {
|
||||
@extend %h5;
|
||||
position: relative;
|
||||
|
||||
&__link {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
// Use a pseudo element for the accessability so that it doesn't take up DOM space
|
||||
// Also because the parent element has `overflow: hidden` which would clip an outline
|
||||
&:focus-visible::after {
|
||||
content: '';
|
||||
border: var(--accessibility-outline);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--style-radius-s);
|
||||
background-color: var(--theme-elevation-50);
|
||||
opacity: 0;
|
||||
}
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.doc-tab__count {
|
||||
.pill-version-count {
|
||||
background-color: var(--theme-elevation-150);
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
font-weight: 600;
|
||||
&::before {
|
||||
opacity: 1;
|
||||
background-color: var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
.doc-tab {
|
||||
&__count {
|
||||
background-color: var(--theme-elevation-250);
|
||||
}
|
||||
.pill-version-count {
|
||||
background-color: var(--theme-elevation-250);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.doc-tab {
|
||||
&__count {
|
||||
background-color: var(--theme-elevation-250);
|
||||
}
|
||||
.pill-version-count {
|
||||
background-color: var(--theme-elevation-250);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,16 +30,7 @@
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
line-height: base(1.2);
|
||||
padding: base(0.2) base(0.6);
|
||||
}
|
||||
|
||||
&__count {
|
||||
line-height: base(0.8);
|
||||
min-width: base(0.8);
|
||||
text-align: center;
|
||||
background-color: var(--theme-elevation-100);
|
||||
border-radius: var(--style-radius-s);
|
||||
line-height: calc(var(--base) * 1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ export const DocumentTab: React.FC<
|
||||
baseClass={baseClass}
|
||||
href={href}
|
||||
isActive={isActive}
|
||||
isCollection={!!collectionConfig && !globalConfig}
|
||||
newTab={newTab}
|
||||
>
|
||||
<span className={`${baseClass}__label`}>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@layer payload-default {
|
||||
.pill-version-count {
|
||||
line-height: calc(var(--base) * 0.8);
|
||||
min-width: calc(var(--base) * 0.8);
|
||||
text-align: center;
|
||||
background-color: var(--theme-elevation-100);
|
||||
border-radius: var(--style-radius-s);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
import { useDocumentInfo } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
import { baseClass } from '../../Tab/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'pill-version-count'
|
||||
|
||||
export const VersionsPill: React.FC = () => {
|
||||
const { versionCount } = useDocumentInfo()
|
||||
@@ -11,5 +13,5 @@ export const VersionsPill: React.FC = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
return <span className={`${baseClass}__count`}>{versionCount}</span>
|
||||
return <span className={baseClass}>{versionCount}</span>
|
||||
}
|
||||
|
||||
@@ -1,41 +1,36 @@
|
||||
import type { DefaultDocumentIDType, NavPreferences, Payload, User } from 'payload'
|
||||
import type { NavPreferences, PayloadRequest } from 'payload'
|
||||
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getNavPrefs = cache(
|
||||
async (
|
||||
payload: Payload,
|
||||
userID: DefaultDocumentIDType,
|
||||
userSlug: string,
|
||||
): Promise<NavPreferences> => {
|
||||
return userSlug
|
||||
? await payload
|
||||
.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: 'nav',
|
||||
},
|
||||
export const getNavPrefs = cache(async (req: PayloadRequest): Promise<NavPreferences> => {
|
||||
return req?.user?.collection
|
||||
? await req.payload
|
||||
.find({
|
||||
collection: 'payload-preferences',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: {
|
||||
equals: 'nav',
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: userSlug,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: req.user.collection,
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: userID,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: req?.user?.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
?.then((res) => res?.docs?.[0]?.value)
|
||||
: null
|
||||
},
|
||||
)
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
?.then((res) => res?.docs?.[0]?.value)
|
||||
: null
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { groupNavItems } from '@payloadcms/ui/shared'
|
||||
import type { NavPreferences } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { Link, NavGroup, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { BrowseByFolderButton, Link, NavGroup, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { EntityType } from '@payloadcms/ui/shared'
|
||||
import { usePathname } from 'next/navigation.js'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
@@ -20,14 +20,35 @@ export const DefaultNavClient: React.FC<{
|
||||
|
||||
const {
|
||||
config: {
|
||||
admin: {
|
||||
routes: { browseByFolder: foldersRoute },
|
||||
},
|
||||
collections,
|
||||
routes: { admin: adminRoute },
|
||||
},
|
||||
} = useConfig()
|
||||
|
||||
const [folderCollectionSlugs] = React.useState<string[]>(() => {
|
||||
return collections.reduce<string[]>((acc, collection) => {
|
||||
if (collection.admin.folders) {
|
||||
acc.push(collection.slug)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
})
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const folderURL = formatAdminURL({
|
||||
adminRoute,
|
||||
path: foldersRoute,
|
||||
})
|
||||
|
||||
const viewingRootFolderView = pathname.startsWith(folderURL)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{folderCollectionSlugs.length > 0 && <BrowseByFolderButton active={viewingRootFolderView} />}
|
||||
{groups.map(({ entities, label }, key) => {
|
||||
return (
|
||||
<NavGroup isOpen={navPreferences?.groups?.[label]?.open} key={key} label={label}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { EntityToGroup } from '@payloadcms/ui/shared'
|
||||
import type { ServerProps } from 'payload'
|
||||
import type { PayloadRequest, ServerProps } from 'payload'
|
||||
|
||||
import { Logout } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
@@ -15,7 +15,9 @@ const baseClass = 'nav'
|
||||
import { getNavPrefs } from './getNavPrefs.js'
|
||||
import { DefaultNavClient } from './index.client.js'
|
||||
|
||||
export type NavProps = ServerProps
|
||||
export type NavProps = {
|
||||
req?: PayloadRequest
|
||||
} & ServerProps
|
||||
|
||||
export const DefaultNav: React.FC<NavProps> = async (props) => {
|
||||
const {
|
||||
@@ -25,6 +27,7 @@ export const DefaultNav: React.FC<NavProps> = async (props) => {
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
req,
|
||||
searchParams,
|
||||
user,
|
||||
viewType,
|
||||
@@ -68,7 +71,7 @@ export const DefaultNav: React.FC<NavProps> = async (props) => {
|
||||
i18n,
|
||||
)
|
||||
|
||||
const navPreferences = await getNavPrefs(payload, user?.id, user?.collection)
|
||||
const navPreferences = await getNavPrefs(req)
|
||||
|
||||
const LogoutComponent = RenderServerComponent({
|
||||
clientProps: {
|
||||
|
||||
@@ -79,7 +79,7 @@ export const RootLayout = async ({
|
||||
})
|
||||
}
|
||||
|
||||
const navPrefs = await getNavPrefs(req.payload, req.user?.id, req.user?.collection)
|
||||
const navPrefs = await getNavPrefs(req)
|
||||
|
||||
const clientConfig = getClientConfig({
|
||||
config,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
CustomComponent,
|
||||
DocumentSubViewTypes,
|
||||
PayloadRequest,
|
||||
ServerProps,
|
||||
ViewTypes,
|
||||
VisibleEntities,
|
||||
@@ -32,6 +33,7 @@ export type DefaultTemplateProps = {
|
||||
docID?: number | string
|
||||
documentSubViewType?: DocumentSubViewTypes
|
||||
globalSlug?: string
|
||||
req?: PayloadRequest
|
||||
viewActions?: CustomComponent[]
|
||||
viewType?: ViewTypes
|
||||
visibleEntities: VisibleEntities
|
||||
@@ -49,6 +51,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
req,
|
||||
searchParams,
|
||||
user,
|
||||
viewActions,
|
||||
@@ -84,6 +87,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
req,
|
||||
searchParams,
|
||||
user,
|
||||
}),
|
||||
@@ -98,6 +102,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
|
||||
globalSlug,
|
||||
collectionSlug,
|
||||
docID,
|
||||
req,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
161
packages/next/src/views/BrowseByFolder/buildView.tsx
Normal file
161
packages/next/src/views/BrowseByFolder/buildView.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import type {
|
||||
AdminViewServerProps,
|
||||
BuildCollectionFolderViewResult,
|
||||
FolderListViewServerPropsOnly,
|
||||
ListQuery,
|
||||
} from 'payload'
|
||||
|
||||
import { DefaultBrowseByFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { redirect } from 'next/navigation.js'
|
||||
import { getFolderData } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import { getPreferences } from '../../utilities/getPreferences.js'
|
||||
|
||||
export type BuildFolderViewArgs = {
|
||||
customCellProps?: Record<string, any>
|
||||
disableBulkDelete?: boolean
|
||||
disableBulkEdit?: boolean
|
||||
enableRowSelections: boolean
|
||||
folderID?: number | string
|
||||
isInDrawer?: boolean
|
||||
overrideEntityVisibility?: boolean
|
||||
query: ListQuery
|
||||
} & AdminViewServerProps
|
||||
|
||||
export const buildBrowseByFolderView = async (
|
||||
args: BuildFolderViewArgs,
|
||||
): Promise<BuildCollectionFolderViewResult> => {
|
||||
const {
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
enableRowSelections,
|
||||
folderCollectionSlugs,
|
||||
folderID,
|
||||
initPageResult,
|
||||
isInDrawer,
|
||||
params,
|
||||
query: queryFromArgs,
|
||||
searchParams,
|
||||
} = args
|
||||
|
||||
const {
|
||||
locale: fullLocale,
|
||||
permissions,
|
||||
req: {
|
||||
i18n,
|
||||
payload,
|
||||
payload: { config },
|
||||
query: queryFromReq,
|
||||
user,
|
||||
},
|
||||
visibleEntities,
|
||||
} = initPageResult
|
||||
|
||||
const collections = folderCollectionSlugs.filter(
|
||||
(collectionSlug) =>
|
||||
permissions?.collections?.[collectionSlug]?.read &&
|
||||
visibleEntities.collections.includes(collectionSlug),
|
||||
)
|
||||
|
||||
if (!collections.length) {
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const query = queryFromArgs || queryFromReq
|
||||
const selectedCollectionSlugs: string[] =
|
||||
Array.isArray(query?.relationTo) && query.relationTo.length
|
||||
? query.relationTo
|
||||
: [...folderCollectionSlugs, config.folders.slug]
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
const { breadcrumbs, documents, subfolders } = await getFolderData({
|
||||
folderID,
|
||||
payload: initPageResult.req.payload,
|
||||
search: query?.search as string,
|
||||
user: initPageResult.req.user,
|
||||
})
|
||||
|
||||
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
|
||||
|
||||
if (
|
||||
!isInDrawer &&
|
||||
((resolvedFolderID && folderID && folderID !== resolvedFolderID) ||
|
||||
(folderID && !resolvedFolderID))
|
||||
) {
|
||||
return redirect(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: config.admin.routes.browseByFolder,
|
||||
serverURL: config.serverURL,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const browseByFolderPreferences = await getPreferences<{ viewPreference: string }>(
|
||||
'browse-by-folder',
|
||||
payload,
|
||||
user.id,
|
||||
user.collection,
|
||||
)
|
||||
|
||||
const serverProps: Omit<FolderListViewServerPropsOnly, 'collectionConfig' | 'listPreferences'> = {
|
||||
documents,
|
||||
i18n,
|
||||
locale: fullLocale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
subfolders,
|
||||
user,
|
||||
}
|
||||
|
||||
// const folderViewSlots = renderFolderViewSlots({
|
||||
// clientProps: {
|
||||
// },
|
||||
// description: staticDescription,
|
||||
// payload,
|
||||
// serverProps,
|
||||
// })
|
||||
|
||||
// documents cannot be created without a parent folder in this view
|
||||
const hasCreatePermissionCollectionSlugs = folderID
|
||||
? [config.folders.slug, ...folderCollectionSlugs]
|
||||
: [config.folders.slug]
|
||||
|
||||
return {
|
||||
View: (
|
||||
<FolderProvider
|
||||
breadcrumbs={breadcrumbs}
|
||||
documents={documents}
|
||||
filteredCollectionSlugs={selectedCollectionSlugs}
|
||||
folderCollectionSlugs={folderCollectionSlugs}
|
||||
folderID={folderID}
|
||||
subfolders={subfolders}
|
||||
>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
{RenderServerComponent({
|
||||
clientProps: {
|
||||
// ...folderViewSlots,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
enableRowSelections,
|
||||
hasCreatePermissionCollectionSlugs,
|
||||
selectedCollectionSlugs,
|
||||
viewPreference: browseByFolderPreferences?.value?.viewPreference,
|
||||
},
|
||||
// Component:config.folders?.components?.views?.list?.Component,
|
||||
Fallback: DefaultBrowseByFolderView,
|
||||
importMap: payload.importMap,
|
||||
serverProps,
|
||||
})}
|
||||
</FolderProvider>
|
||||
),
|
||||
}
|
||||
}
|
||||
20
packages/next/src/views/BrowseByFolder/index.tsx
Normal file
20
packages/next/src/views/BrowseByFolder/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type React from 'react'
|
||||
|
||||
import { notFound } from 'next/navigation.js'
|
||||
|
||||
import type { BuildFolderViewArgs } from './buildView.js'
|
||||
|
||||
import { buildBrowseByFolderView } from './buildView.js'
|
||||
|
||||
export const BrowseByFolder: React.FC<BuildFolderViewArgs> = async (args) => {
|
||||
try {
|
||||
const { View } = await buildBrowseByFolderView(args)
|
||||
return View
|
||||
} catch (error) {
|
||||
if (error.message === 'not-found') {
|
||||
notFound()
|
||||
} else {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
}
|
||||
23
packages/next/src/views/BrowseByFolder/metadata.ts
Normal file
23
packages/next/src/views/BrowseByFolder/metadata.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateBrowseByFolderMetadata = async (
|
||||
args: Parameters<GenerateViewMetadata>[0],
|
||||
): Promise<Metadata> => {
|
||||
const { config, i18n } = args
|
||||
|
||||
const title: string = i18n.t('folder:browseByFolder')
|
||||
const description: string = ''
|
||||
const keywords: string = ''
|
||||
|
||||
return generateMetadata({
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
serverURL: config.serverURL,
|
||||
title,
|
||||
})
|
||||
}
|
||||
207
packages/next/src/views/CollectionFolders/buildView.tsx
Normal file
207
packages/next/src/views/CollectionFolders/buildView.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import type {
|
||||
AdminViewServerProps,
|
||||
BuildCollectionFolderViewResult,
|
||||
FolderListViewServerPropsOnly,
|
||||
ListQuery,
|
||||
Where,
|
||||
} from 'payload'
|
||||
|
||||
import { DefaultCollectionFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared'
|
||||
import { redirect } from 'next/navigation.js'
|
||||
import { getFolderData, parseDocumentID } from 'payload'
|
||||
import React from 'react'
|
||||
|
||||
import { getPreferences } from '../../utilities/getPreferences.js'
|
||||
|
||||
// import { renderFolderViewSlots } from './renderFolderViewSlots.js'
|
||||
|
||||
export type BuildCollectionFolderViewStateArgs = {
|
||||
disableBulkDelete?: boolean
|
||||
disableBulkEdit?: boolean
|
||||
enableRowSelections: boolean
|
||||
folderID?: number | string
|
||||
isInDrawer?: boolean
|
||||
overrideEntityVisibility?: boolean
|
||||
query: ListQuery
|
||||
} & AdminViewServerProps
|
||||
|
||||
/**
|
||||
* Builds the entire view for collection-folder views on the server
|
||||
*/
|
||||
export const buildCollectionFolderView = async (
|
||||
args: BuildCollectionFolderViewStateArgs,
|
||||
): Promise<BuildCollectionFolderViewResult> => {
|
||||
const {
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
enableRowSelections,
|
||||
folderCollectionSlugs,
|
||||
folderID,
|
||||
initPageResult,
|
||||
isInDrawer,
|
||||
overrideEntityVisibility,
|
||||
params,
|
||||
query: queryFromArgs,
|
||||
searchParams,
|
||||
} = args
|
||||
|
||||
const {
|
||||
collectionConfig,
|
||||
collectionConfig: { slug: collectionSlug },
|
||||
locale: fullLocale,
|
||||
permissions,
|
||||
req: {
|
||||
i18n,
|
||||
payload,
|
||||
payload: { config },
|
||||
query: queryFromReq,
|
||||
user,
|
||||
},
|
||||
visibleEntities,
|
||||
} = initPageResult
|
||||
|
||||
if (!permissions?.collections?.[collectionSlug]?.read) {
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
if (collectionConfig) {
|
||||
const query = queryFromArgs || queryFromReq
|
||||
|
||||
const collectionFolderPreferences = await getPreferences<{ viewPreference: string }>(
|
||||
`${collectionSlug}-collection-folder`,
|
||||
payload,
|
||||
user.id,
|
||||
user.collection,
|
||||
)
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
if (
|
||||
(!visibleEntities.collections.includes(collectionSlug) && !overrideEntityVisibility) ||
|
||||
!folderCollectionSlugs.includes(collectionSlug)
|
||||
) {
|
||||
throw new Error('not-found')
|
||||
}
|
||||
|
||||
const whereConstraints = [
|
||||
mergeListSearchAndWhere({
|
||||
collectionConfig,
|
||||
search: typeof query?.search === 'string' ? query.search : undefined,
|
||||
where: (query?.where as Where) || undefined,
|
||||
}),
|
||||
]
|
||||
|
||||
if (folderID) {
|
||||
whereConstraints.push({
|
||||
[config.folders.fieldName]: {
|
||||
equals: parseDocumentID({ id: folderID, collectionSlug, payload }),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
whereConstraints.push({
|
||||
[config.folders.fieldName]: {
|
||||
exists: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const { breadcrumbs, documents, subfolders } = await getFolderData({
|
||||
collectionSlug,
|
||||
folderID,
|
||||
payload: initPageResult.req.payload,
|
||||
search: query?.search as string,
|
||||
user: initPageResult.req.user,
|
||||
})
|
||||
|
||||
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
|
||||
|
||||
if (
|
||||
!isInDrawer &&
|
||||
((resolvedFolderID && folderID && folderID !== resolvedFolderID) ||
|
||||
(folderID && !resolvedFolderID))
|
||||
) {
|
||||
return redirect(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/${config.folders.slug}`,
|
||||
serverURL: config.serverURL,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const newDocumentURL = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
})
|
||||
|
||||
const hasCreatePermission = permissions?.collections?.[collectionSlug]?.create
|
||||
|
||||
const serverProps: FolderListViewServerPropsOnly = {
|
||||
collectionConfig,
|
||||
documents,
|
||||
i18n,
|
||||
locale: fullLocale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
searchParams,
|
||||
subfolders,
|
||||
user,
|
||||
}
|
||||
|
||||
// We could support slots in the folder view in the future
|
||||
// const folderViewSlots = renderFolderViewSlots({
|
||||
// clientProps: {
|
||||
// collectionSlug,
|
||||
// hasCreatePermission,
|
||||
// newDocumentURL,
|
||||
// },
|
||||
// collectionConfig,
|
||||
// description: typeof collectionConfig.admin.description === 'function'
|
||||
// ? collectionConfig.admin.description({ t: i18n.t })
|
||||
// : collectionConfig.admin.description,
|
||||
// payload,
|
||||
// serverProps,
|
||||
// })
|
||||
|
||||
const search = query?.search as string
|
||||
|
||||
return {
|
||||
View: (
|
||||
<FolderProvider
|
||||
breadcrumbs={breadcrumbs}
|
||||
collectionSlug={collectionSlug}
|
||||
documents={documents}
|
||||
folderCollectionSlugs={folderCollectionSlugs}
|
||||
folderID={folderID}
|
||||
search={search}
|
||||
subfolders={subfolders}
|
||||
>
|
||||
<HydrateAuthProvider permissions={permissions} />
|
||||
{RenderServerComponent({
|
||||
clientProps: {
|
||||
// ...folderViewSlots,
|
||||
collectionSlug,
|
||||
disableBulkDelete,
|
||||
disableBulkEdit,
|
||||
enableRowSelections,
|
||||
hasCreatePermission,
|
||||
newDocumentURL,
|
||||
viewPreference: collectionFolderPreferences?.value?.viewPreference,
|
||||
},
|
||||
Component: collectionConfig?.admin?.components?.views?.list?.Component,
|
||||
Fallback: DefaultCollectionFolderView,
|
||||
importMap: payload.importMap,
|
||||
serverProps,
|
||||
})}
|
||||
</FolderProvider>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('not-found')
|
||||
}
|
||||
20
packages/next/src/views/CollectionFolders/index.tsx
Normal file
20
packages/next/src/views/CollectionFolders/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type React from 'react'
|
||||
|
||||
import { notFound } from 'next/navigation.js'
|
||||
|
||||
import type { BuildCollectionFolderViewStateArgs } from './buildView.js'
|
||||
|
||||
import { buildCollectionFolderView } from './buildView.js'
|
||||
|
||||
export const CollectionFolderView: React.FC<BuildCollectionFolderViewStateArgs> = async (args) => {
|
||||
try {
|
||||
const { View } = await buildCollectionFolderView(args)
|
||||
return View
|
||||
} catch (error) {
|
||||
if (error.message === 'not-found') {
|
||||
notFound()
|
||||
} else {
|
||||
console.error(error) // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
}
|
||||
35
packages/next/src/views/CollectionFolders/metadata.ts
Normal file
35
packages/next/src/views/CollectionFolders/metadata.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Metadata } from 'next'
|
||||
import type { SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
import type { GenerateViewMetadata } from '../Root/index.js'
|
||||
|
||||
import { generateMetadata } from '../../utilities/meta.js'
|
||||
|
||||
export const generateCollectionFolderMetadata = async (
|
||||
args: {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
} & Parameters<GenerateViewMetadata>[0],
|
||||
): Promise<Metadata> => {
|
||||
const { collectionConfig, config, i18n } = args
|
||||
|
||||
let title: string = ''
|
||||
const description: string = ''
|
||||
const keywords: string = ''
|
||||
|
||||
if (collectionConfig) {
|
||||
title = getTranslation(collectionConfig.labels.singular, i18n)
|
||||
}
|
||||
|
||||
title = `${title ? `${title} ` : title}${i18n.t('folder:folders')}`
|
||||
|
||||
return generateMetadata({
|
||||
...(config.admin.meta || {}),
|
||||
description,
|
||||
keywords,
|
||||
serverURL: config.serverURL,
|
||||
title,
|
||||
...(collectionConfig?.admin?.meta || {}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type {
|
||||
AfterFolderListClientProps,
|
||||
AfterFolderListTableClientProps,
|
||||
AfterFolderListTableServerPropsOnly,
|
||||
BeforeFolderListClientProps,
|
||||
BeforeFolderListServerPropsOnly,
|
||||
BeforeFolderListTableClientProps,
|
||||
BeforeFolderListTableServerPropsOnly,
|
||||
FolderListViewServerPropsOnly,
|
||||
FolderListViewSlots,
|
||||
ListViewSlotSharedClientProps,
|
||||
Payload,
|
||||
SanitizedCollectionConfig,
|
||||
StaticDescription,
|
||||
ViewDescriptionClientProps,
|
||||
ViewDescriptionServerPropsOnly,
|
||||
} from 'payload'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
|
||||
type Args = {
|
||||
clientProps: ListViewSlotSharedClientProps
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
description?: StaticDescription
|
||||
payload: Payload
|
||||
serverProps: FolderListViewServerPropsOnly
|
||||
}
|
||||
|
||||
export const renderFolderViewSlots = ({
|
||||
clientProps,
|
||||
collectionConfig,
|
||||
description,
|
||||
payload,
|
||||
serverProps,
|
||||
}: Args): FolderListViewSlots => {
|
||||
const result: FolderListViewSlots = {} as FolderListViewSlots
|
||||
|
||||
if (collectionConfig.admin.components?.afterList) {
|
||||
result.AfterFolderList = RenderServerComponent({
|
||||
clientProps: clientProps satisfies AfterFolderListClientProps,
|
||||
Component: collectionConfig.admin.components.afterList,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies AfterFolderListTableServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
const listMenuItems = collectionConfig.admin.components?.listMenuItems
|
||||
if (Array.isArray(listMenuItems)) {
|
||||
result.listMenuItems = [
|
||||
RenderServerComponent({
|
||||
clientProps,
|
||||
Component: listMenuItems,
|
||||
importMap: payload.importMap,
|
||||
serverProps,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.afterListTable) {
|
||||
result.AfterFolderListTable = RenderServerComponent({
|
||||
clientProps: clientProps satisfies AfterFolderListTableClientProps,
|
||||
Component: collectionConfig.admin.components.afterListTable,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies AfterFolderListTableServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.beforeList) {
|
||||
result.BeforeFolderList = RenderServerComponent({
|
||||
clientProps: clientProps satisfies BeforeFolderListClientProps,
|
||||
Component: collectionConfig.admin.components.beforeList,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies BeforeFolderListServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.beforeListTable) {
|
||||
result.BeforeFolderListTable = RenderServerComponent({
|
||||
clientProps: clientProps satisfies BeforeFolderListTableClientProps,
|
||||
Component: collectionConfig.admin.components.beforeListTable,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies BeforeFolderListTableServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.Description) {
|
||||
result.Description = RenderServerComponent({
|
||||
clientProps: {
|
||||
collectionSlug: collectionConfig.slug,
|
||||
description,
|
||||
} satisfies ViewDescriptionClientProps,
|
||||
Component: collectionConfig.admin.components.Description,
|
||||
importMap: payload.importMap,
|
||||
serverProps: serverProps satisfies ViewDescriptionServerPropsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -29,6 +29,7 @@ export const getDocumentData = async ({
|
||||
}: Args): Promise<null | Record<string, unknown> | TypeWithID> => {
|
||||
const id = sanitizeID(idArg)
|
||||
let resolvedData: Record<string, unknown> | TypeWithID = null
|
||||
const { transactionID, ...rest } = req
|
||||
|
||||
try {
|
||||
if (collectionSlug && id) {
|
||||
@@ -41,9 +42,7 @@ export const getDocumentData = async ({
|
||||
locale: locale?.code,
|
||||
overrideAccess: false,
|
||||
req: {
|
||||
query: req?.query,
|
||||
search: req?.search,
|
||||
searchParams: req?.searchParams,
|
||||
...rest,
|
||||
},
|
||||
user,
|
||||
})
|
||||
@@ -58,9 +57,7 @@ export const getDocumentData = async ({
|
||||
locale: locale?.code,
|
||||
overrideAccess: false,
|
||||
req: {
|
||||
query: req?.query,
|
||||
search: req?.search,
|
||||
searchParams: req?.searchParams,
|
||||
...rest,
|
||||
},
|
||||
user,
|
||||
})
|
||||
|
||||
@@ -32,9 +32,12 @@ import { renderDocumentSlots } from './renderDocumentSlots.js'
|
||||
|
||||
export const generateMetadata: GenerateEditViewMetadata = async (args) => getMetaBySegment(args)
|
||||
|
||||
// This function will be responsible for rendering an Edit Document view
|
||||
// it will be called on the server for Edit page views as well as
|
||||
// called on-demand from document drawers
|
||||
/**
|
||||
* This function is responsible for rendering
|
||||
* an Edit Document view on the server for both:
|
||||
* - default document edit views
|
||||
* - on-demand edit views within drawers
|
||||
*/
|
||||
export const renderDocument = async ({
|
||||
disableActions,
|
||||
documentSubViewType,
|
||||
|
||||
@@ -40,6 +40,12 @@ type RenderListViewArgs = {
|
||||
redirectAfterDuplicate?: boolean
|
||||
} & AdminViewServerProps
|
||||
|
||||
/**
|
||||
* This function is responsible for rendering
|
||||
* the list view on the server for both:
|
||||
* - default list view
|
||||
* - list view within drawers
|
||||
*/
|
||||
export const renderListView = async (
|
||||
args: RenderListViewArgs,
|
||||
): Promise<{
|
||||
|
||||
@@ -16,7 +16,9 @@ export const DeviceContainer: React.FC<{
|
||||
|
||||
// Keep an accurate measurement of the actual device size as it is truly rendered
|
||||
// This is helpful when `sizes` are non-number units like percentages, etc.
|
||||
// eslint-disable-next-line react-compiler/react-compiler -- TODO: fix
|
||||
const { size: measuredDeviceSize } = useResize(deviceFrameRef.current)
|
||||
// eslint-disable-next-line react-compiler/react-compiler -- TODO: fix
|
||||
const { size: outerFrameSize } = useResize(outerFrameRef.current)
|
||||
|
||||
let deviceIsLargerThanFrame: boolean = false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdminViewConfig, SanitizedConfig } from 'payload'
|
||||
|
||||
import type { ViewFromConfig } from './getViewFromConfig.js'
|
||||
import type { ViewFromConfig } from './getRouteData.js'
|
||||
|
||||
import { isPathMatchingRoute } from './isPathMatchingRoute.js'
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AdminViewServerProps,
|
||||
CollectionSlug,
|
||||
DocumentSubViewTypes,
|
||||
ImportMap,
|
||||
PayloadComponent,
|
||||
@@ -14,6 +15,8 @@ import { formatAdminURL } from 'payload/shared'
|
||||
import type { initPage } from '../../utilities/initPage/index.js'
|
||||
|
||||
import { Account } from '../Account/index.js'
|
||||
import { BrowseByFolder } from '../BrowseByFolder/index.js'
|
||||
import { CollectionFolderView } from '../CollectionFolders/index.js'
|
||||
import { CreateFirstUserView } from '../CreateFirstUser/index.js'
|
||||
import { Dashboard } from '../Dashboard/index.js'
|
||||
import { Document as DocumentView } from '../Document/index.js'
|
||||
@@ -31,6 +34,7 @@ import { isPathMatchingRoute } from './isPathMatchingRoute.js'
|
||||
|
||||
const baseClasses = {
|
||||
account: 'account',
|
||||
folders: 'folders',
|
||||
forgot: forgotPasswordBaseClass,
|
||||
login: loginBaseClass,
|
||||
reset: resetPasswordBaseClass,
|
||||
@@ -48,6 +52,7 @@ export type ViewFromConfig = {
|
||||
|
||||
const oneSegmentViews: OneSegmentViews = {
|
||||
account: Account,
|
||||
browseByFolder: BrowseByFolder,
|
||||
createFirstUser: CreateFirstUserView,
|
||||
forgot: ForgotPasswordView,
|
||||
inactivity: LogoutInactivity,
|
||||
@@ -56,7 +61,7 @@ const oneSegmentViews: OneSegmentViews = {
|
||||
unauthorized: UnauthorizedView,
|
||||
}
|
||||
|
||||
type GetViewFromConfigArgs = {
|
||||
type GetRouteDataArgs = {
|
||||
adminRoute: string
|
||||
config: SanitizedConfig
|
||||
currentRoute: string
|
||||
@@ -67,9 +72,11 @@ type GetViewFromConfigArgs = {
|
||||
segments: string[]
|
||||
}
|
||||
|
||||
type GetViewFromConfigResult = {
|
||||
type GetRouteDataResult = {
|
||||
DefaultView: ViewFromConfig
|
||||
documentSubViewType?: DocumentSubViewTypes
|
||||
folderCollectionSlugs: CollectionSlug[]
|
||||
folderID?: string
|
||||
initPageOptions: Parameters<typeof initPage>[0]
|
||||
serverProps: ServerPropsFromView
|
||||
templateClassName: string
|
||||
@@ -77,19 +84,20 @@ type GetViewFromConfigResult = {
|
||||
viewType?: ViewTypes
|
||||
}
|
||||
|
||||
export const getViewFromConfig = ({
|
||||
export const getRouteData = ({
|
||||
adminRoute,
|
||||
config,
|
||||
currentRoute,
|
||||
importMap,
|
||||
searchParams,
|
||||
segments,
|
||||
}: GetViewFromConfigArgs): GetViewFromConfigResult => {
|
||||
}: GetRouteDataArgs): GetRouteDataResult => {
|
||||
let ViewToRender: ViewFromConfig = null
|
||||
let templateClassName: string
|
||||
let templateType: 'default' | 'minimal' | undefined
|
||||
let documentSubViewType: DocumentSubViewTypes
|
||||
let viewType: ViewTypes
|
||||
let folderID: string
|
||||
|
||||
const initPageOptions: Parameters<typeof initPage>[0] = {
|
||||
config,
|
||||
@@ -105,6 +113,13 @@ export const getViewFromConfig = ({
|
||||
let matchedCollection: SanitizedConfig['collections'][number] = undefined
|
||||
let matchedGlobal: SanitizedConfig['globals'][number] = undefined
|
||||
|
||||
const folderCollectionSlugs = config.collections.reduce((acc, { slug, admin }) => {
|
||||
if (admin?.folders) {
|
||||
return [...acc, slug]
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
const serverProps: ServerPropsFromView = {
|
||||
viewActions: config?.admin?.components?.actions || [],
|
||||
}
|
||||
@@ -153,6 +168,7 @@ export const getViewFromConfig = ({
|
||||
if (oneSegmentViews[viewKey]) {
|
||||
// --> /account
|
||||
// --> /create-first-user
|
||||
// --> /browse-by-folder
|
||||
// --> /forgot
|
||||
// --> /login
|
||||
// --> /logout
|
||||
@@ -170,6 +186,11 @@ export const getViewFromConfig = ({
|
||||
templateType = 'default'
|
||||
viewType = 'account'
|
||||
}
|
||||
|
||||
if (folderCollectionSlugs.length && viewKey === 'browseByFolder') {
|
||||
templateType = 'default'
|
||||
viewType = 'folders'
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -182,9 +203,19 @@ export const getViewFromConfig = ({
|
||||
templateClassName = baseClasses[segmentTwo]
|
||||
templateType = 'minimal'
|
||||
viewType = 'reset'
|
||||
}
|
||||
|
||||
if (isCollection && matchedCollection) {
|
||||
} else if (
|
||||
folderCollectionSlugs.length &&
|
||||
`/${segmentOne}` === config.admin.routes.browseByFolder
|
||||
) {
|
||||
// --> /browse-by-folder/:folderID
|
||||
ViewToRender = {
|
||||
Component: oneSegmentViews.browseByFolder,
|
||||
}
|
||||
templateClassName = baseClasses.folders
|
||||
templateType = 'default'
|
||||
viewType = 'folders'
|
||||
folderID = segmentTwo
|
||||
} else if (isCollection && matchedCollection) {
|
||||
// --> /collections/:collectionSlug
|
||||
|
||||
ViewToRender = {
|
||||
@@ -229,31 +260,47 @@ export const getViewFromConfig = ({
|
||||
templateType = 'minimal'
|
||||
viewType = 'verify'
|
||||
} else if (isCollection && matchedCollection) {
|
||||
// Custom Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:versionID
|
||||
if (
|
||||
segmentThree === config.folders.slug &&
|
||||
folderCollectionSlugs.includes(matchedCollection.slug)
|
||||
) {
|
||||
// Collection Folder Views
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug/:folderID
|
||||
ViewToRender = {
|
||||
Component: CollectionFolderView,
|
||||
}
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
templateClassName = `collection-folders`
|
||||
templateType = 'default'
|
||||
viewType = 'collection-folders'
|
||||
folderID = segmentFour
|
||||
} else {
|
||||
// Collection Edit Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:versionID
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
}
|
||||
|
||||
templateClassName = `collection-default-edit`
|
||||
templateType = 'default'
|
||||
|
||||
const viewInfo = getDocumentViewInfo([segmentFour, segmentFive])
|
||||
viewType = viewInfo.viewType
|
||||
documentSubViewType = viewInfo.documentSubViewType
|
||||
|
||||
attachViewActions({
|
||||
collectionOrGlobal: matchedCollection,
|
||||
serverProps,
|
||||
viewKeyArg: documentSubViewType,
|
||||
})
|
||||
}
|
||||
|
||||
templateClassName = `collection-default-edit`
|
||||
templateType = 'default'
|
||||
|
||||
const viewInfo = getDocumentViewInfo([segmentFour, segmentFive])
|
||||
viewType = viewInfo.viewType
|
||||
documentSubViewType = viewInfo.documentSubViewType
|
||||
|
||||
attachViewActions({
|
||||
collectionOrGlobal: matchedCollection,
|
||||
serverProps,
|
||||
viewKeyArg: documentSubViewType,
|
||||
})
|
||||
} else if (isGlobal && matchedGlobal) {
|
||||
// Custom Views
|
||||
// Global Edit Views
|
||||
// --> /globals/:globalSlug/versions
|
||||
// --> /globals/:globalSlug/preview
|
||||
// --> /globals/:globalSlug/versions/:versionID
|
||||
@@ -288,6 +335,8 @@ export const getViewFromConfig = ({
|
||||
return {
|
||||
DefaultView: ViewToRender,
|
||||
documentSubViewType,
|
||||
folderCollectionSlugs,
|
||||
folderID,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
templateClassName,
|
||||
@@ -1,22 +1,23 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type {
|
||||
AdminViewClientProps,
|
||||
AdminViewServerPropsOnly,
|
||||
ImportMap,
|
||||
SanitizedConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import {
|
||||
type AdminViewClientProps,
|
||||
type AdminViewServerPropsOnly,
|
||||
type ImportMap,
|
||||
parseDocumentID,
|
||||
type SanitizedConfig,
|
||||
} from 'payload'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import { DefaultTemplate } from '../../templates/Default/index.js'
|
||||
import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
||||
import { initPage } from '../../utilities/initPage/index.js'
|
||||
import { getViewFromConfig } from './getViewFromConfig.js'
|
||||
import { getRouteData } from './getRouteData.js'
|
||||
|
||||
export type GenerateViewMetadata = (args: {
|
||||
config: SanitizedConfig
|
||||
@@ -64,12 +65,14 @@ export const RootPage = async ({
|
||||
const {
|
||||
DefaultView,
|
||||
documentSubViewType,
|
||||
folderCollectionSlugs,
|
||||
folderID: folderIDParam,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
templateClassName,
|
||||
templateType,
|
||||
viewType,
|
||||
} = getViewFromConfig({
|
||||
} = getRouteData({
|
||||
adminRoute,
|
||||
config,
|
||||
currentRoute,
|
||||
@@ -89,6 +92,10 @@ export const RootPage = async ({
|
||||
})
|
||||
?.then((doc) => !!doc))
|
||||
|
||||
/**
|
||||
* This function is responsible for handling the case where the view is not found.
|
||||
* The current route did not match any default views or custom route views.
|
||||
*/
|
||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
||||
if (initPageResult?.req?.user) {
|
||||
notFound()
|
||||
@@ -132,8 +139,20 @@ export const RootPage = async ({
|
||||
importMap,
|
||||
})
|
||||
|
||||
const payload = initPageResult?.req.payload
|
||||
const folderID = parseDocumentID({
|
||||
id: folderIDParam,
|
||||
collectionSlug: payload.config.folders.slug,
|
||||
payload,
|
||||
})
|
||||
|
||||
const RenderedView = RenderServerComponent({
|
||||
clientProps: { clientConfig, documentSubViewType, viewType } satisfies AdminViewClientProps,
|
||||
clientProps: {
|
||||
clientConfig,
|
||||
documentSubViewType,
|
||||
folderCollectionSlugs,
|
||||
viewType,
|
||||
} satisfies AdminViewClientProps,
|
||||
Component: DefaultView.payloadComponent,
|
||||
Fallback: DefaultView.Component,
|
||||
importMap,
|
||||
@@ -141,6 +160,7 @@ export const RootPage = async ({
|
||||
...serverProps,
|
||||
clientConfig,
|
||||
docID: initPageResult?.docID,
|
||||
folderID,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
importMap,
|
||||
initPageResult,
|
||||
@@ -151,8 +171,8 @@ export const RootPage = async ({
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{!templateType && <Fragment>{RenderedView}</Fragment>}
|
||||
<React.Fragment>
|
||||
{!templateType && <React.Fragment>{RenderedView}</React.Fragment>}
|
||||
{templateType === 'minimal' && (
|
||||
<MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
|
||||
)}
|
||||
@@ -167,6 +187,7 @@ export const RootPage = async ({
|
||||
params={params}
|
||||
payload={initPageResult?.req.payload}
|
||||
permissions={initPageResult?.permissions}
|
||||
req={initPageResult?.req}
|
||||
searchParams={searchParams}
|
||||
user={initPageResult?.req.user}
|
||||
viewActions={serverProps.viewActions}
|
||||
@@ -181,6 +202,6 @@ export const RootPage = async ({
|
||||
{RenderedView}
|
||||
</DefaultTemplate>
|
||||
)}
|
||||
</Fragment>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
|
||||
import { generateAccountViewMetadata } from '../Account/metadata.js'
|
||||
import { generateBrowseByFolderMetadata } from '../BrowseByFolder/metadata.js'
|
||||
import { generateCollectionFolderMetadata } from '../CollectionFolders/metadata.js'
|
||||
import { generateCreateFirstUserViewMetadata } from '../CreateFirstUser/metadata.js'
|
||||
import { generateDashboardViewMetadata } from '../Dashboard/metadata.js'
|
||||
import { generateDocumentViewMetadata } from '../Document/metadata.js'
|
||||
@@ -18,6 +20,7 @@ import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
|
||||
const oneSegmentMeta = {
|
||||
'create-first-user': generateCreateFirstUserViewMetadata,
|
||||
folders: generateBrowseByFolderMetadata,
|
||||
forgot: generateForgotPasswordViewMetadata,
|
||||
login: generateLoginViewMetadata,
|
||||
logout: generateUnauthorizedViewMetadata,
|
||||
@@ -40,12 +43,18 @@ export const generatePageMetadata = async ({
|
||||
params: paramsPromise,
|
||||
}: Args) => {
|
||||
const config = await configPromise
|
||||
|
||||
const params = await paramsPromise
|
||||
|
||||
const folderCollectionSlugs = config.collections.reduce((acc, { slug, admin }) => {
|
||||
if (admin?.folders) {
|
||||
return [...acc, slug]
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
const segments = Array.isArray(params.segments) ? params.segments : []
|
||||
|
||||
const currentRoute = `/${segments.join('/')}`
|
||||
const [segmentOne, segmentTwo] = segments
|
||||
const [segmentOne, segmentTwo, segmentThree] = segments
|
||||
|
||||
const isGlobal = segmentOne === 'globals'
|
||||
const isCollection = segmentOne === 'collections'
|
||||
@@ -72,7 +81,14 @@ export const generatePageMetadata = async ({
|
||||
break
|
||||
}
|
||||
case 1: {
|
||||
if (oneSegmentMeta[segmentOne] && segmentOne !== 'account') {
|
||||
if (folderCollectionSlugs.length && `/${segmentOne}` === config.admin.routes.browseByFolder) {
|
||||
// --> /:folderCollectionSlug
|
||||
meta = await oneSegmentMeta.folders({ config, i18n })
|
||||
} else if (segmentOne === 'account') {
|
||||
// --> /account
|
||||
meta = await generateAccountViewMetadata({ config, i18n })
|
||||
break
|
||||
} else if (oneSegmentMeta[segmentOne]) {
|
||||
// --> /create-first-user
|
||||
// --> /forgot
|
||||
// --> /login
|
||||
@@ -81,10 +97,6 @@ export const generatePageMetadata = async ({
|
||||
// --> /unauthorized
|
||||
meta = await oneSegmentMeta[segmentOne]({ config, i18n })
|
||||
break
|
||||
} else if (segmentOne === 'account') {
|
||||
// --> /account
|
||||
meta = await generateAccountViewMetadata({ config, i18n })
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -92,8 +104,13 @@ export const generatePageMetadata = async ({
|
||||
if (`/${segmentOne}` === config.admin.routes.reset) {
|
||||
// --> /reset/:token
|
||||
meta = await generateResetPasswordViewMetadata({ config, i18n })
|
||||
}
|
||||
if (isCollection) {
|
||||
} else if (
|
||||
folderCollectionSlugs.length &&
|
||||
`/${segmentOne}` === config.admin.routes.browseByFolder
|
||||
) {
|
||||
// --> /browse-by-folder/:folderID
|
||||
meta = await generateBrowseByFolderMetadata({ config, i18n })
|
||||
} else if (isCollection) {
|
||||
// --> /collections/:collectionSlug
|
||||
meta = await generateListViewMetadata({ collectionConfig, config, i18n })
|
||||
} else if (isGlobal) {
|
||||
@@ -112,15 +129,29 @@ export const generatePageMetadata = async ({
|
||||
// --> /:collectionSlug/verify/:token
|
||||
meta = await generateVerifyViewMetadata({ config, i18n })
|
||||
} else if (isCollection) {
|
||||
// Custom Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:version
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
meta = await generateDocumentViewMetadata({ collectionConfig, config, i18n, params })
|
||||
if (segmentThree === config.folders.slug) {
|
||||
if (folderCollectionSlugs.includes(collectionConfig.slug)) {
|
||||
// Collection Folder Views
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug/:id
|
||||
meta = await generateCollectionFolderMetadata({
|
||||
collectionConfig,
|
||||
config,
|
||||
i18n,
|
||||
params,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Collection Document Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/preview
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:version
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
meta = await generateDocumentViewMetadata({ collectionConfig, config, i18n, params })
|
||||
}
|
||||
} else if (isGlobal) {
|
||||
// Custom Views
|
||||
// Global Document Views
|
||||
// --> /globals/:globalSlug/versions
|
||||
// --> /globals/:globalSlug/versions/:version
|
||||
// --> /globals/:globalSlug/preview
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ArrayFieldClient, BlocksFieldClient, ClientConfig, ClientField } from 'payload'
|
||||
|
||||
import { fieldShouldBeLocalized } from 'payload/shared'
|
||||
import { fieldShouldBeLocalized, groupHasName } from 'payload/shared'
|
||||
|
||||
import { fieldHasChanges } from './fieldHasChanges.js'
|
||||
import { getFieldsForRowComparison } from './getFieldsForRowComparison.js'
|
||||
@@ -114,25 +114,37 @@ export function countChangedFields({
|
||||
|
||||
// Fields that have nested fields and nest their fields' data.
|
||||
case 'group': {
|
||||
if (locales && fieldShouldBeLocalized({ field, parentIsLocalized })) {
|
||||
locales.forEach((locale) => {
|
||||
if (groupHasName(field)) {
|
||||
if (locales && fieldShouldBeLocalized({ field, parentIsLocalized })) {
|
||||
locales.forEach((locale) => {
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[field.name]?.[locale],
|
||||
config,
|
||||
fields: field.fields,
|
||||
locales,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
version: version?.[field.name]?.[locale],
|
||||
})
|
||||
})
|
||||
} else {
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[field.name]?.[locale],
|
||||
comparison: comparison?.[field.name],
|
||||
config,
|
||||
fields: field.fields,
|
||||
locales,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
version: version?.[field.name]?.[locale],
|
||||
version: version?.[field.name],
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Unnamed group field: data is NOT nested under `field.name`
|
||||
count += countChangedFields({
|
||||
comparison: comparison?.[field.name],
|
||||
comparison,
|
||||
config,
|
||||
fields: field.fields,
|
||||
locales,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
version: version?.[field.name],
|
||||
version,
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user