Compare commits
33 Commits
feat/impro
...
chore/list
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
275f9aa026 | ||
|
|
7277f17f14 | ||
|
|
7a73265bd6 | ||
|
|
ec593b453e | ||
|
|
a63a3d0518 | ||
|
|
57143b37d0 | ||
|
|
3ad56cd86f | ||
|
|
05e6f3326b | ||
|
|
8b6ba625b8 | ||
|
|
2b76a0484c | ||
|
|
66318697dd | ||
|
|
8940726601 | ||
|
|
ae32c555ac | ||
|
|
8ed410456c | ||
|
|
824f9a7f4d | ||
|
|
f25acb801c | ||
|
|
5f58daffd0 | ||
|
|
e413e1df1c | ||
|
|
bdbb99972c | ||
|
|
e29ac523d3 | ||
|
|
d8cfdc7bcb | ||
|
|
694c76d51a | ||
|
|
09721d4c20 | ||
|
|
834fdde088 | ||
|
|
45913e41f1 | ||
|
|
42da87b6e9 | ||
|
|
2a1ddf1e89 | ||
|
|
8af8befbd4 | ||
|
|
2118c6c47f | ||
|
|
a07fd9eba3 | ||
|
|
ea9abfdef3 | ||
|
|
b671fd5a6d | ||
|
|
ae0736b738 |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -8,14 +8,14 @@
|
||||
/packages/email-*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/storage-*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/create-payload-app/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
|
||||
|
||||
### Templates ###
|
||||
/templates/_data/ @denolfe @jmikrut @DanRibbens
|
||||
/templates/_template/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Build Files ###
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr @GermanJablo
|
||||
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Root ###
|
||||
|
||||
2
.github/workflows/lock-issues.yml
vendored
2
.github/workflows/lock-issues.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: dessant/lock-threads@v5
|
||||
with:
|
||||
process-only: 'issues'
|
||||
issue-inactive-days: '1'
|
||||
issue-inactive-days: '7'
|
||||
exclude-any-issue-labels: 'status: awaiting-reply'
|
||||
log-output: true
|
||||
issue-comment: >
|
||||
|
||||
@@ -654,6 +654,26 @@ const ExampleCollection = {
|
||||
]}
|
||||
/>
|
||||
|
||||
## useDocumentForm
|
||||
|
||||
The `useDocumentForm` hook works the same way as the [useForm](#useform) hook, but it always gives you access to the top-level `Form` of a document. This is useful if you need to access the document's `Form` context from within a child `Form`.
|
||||
|
||||
An example where this could happen would be custom components within lexical blocks, as lexical blocks initialize their own child `Form`.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useDocumentForm } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
const { fields: parentDocumentFields } = useDocumentForm()
|
||||
|
||||
return (
|
||||
<p>The document's Form has ${Object.keys(parentDocumentFields).length} fields</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## useCollapsible
|
||||
|
||||
The `useCollapsible` hook allows you to control parent collapsibles:
|
||||
|
||||
@@ -158,6 +158,7 @@ The following options are available:
|
||||
| **`beforeListTable`** | An array of components to inject _before_ the built-in List View's table |
|
||||
| **`afterList`** | An array of components to inject _after_ the built-in List View |
|
||||
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table |
|
||||
| **`listControlsMenu`** | An array of components to render as buttons within a menu next to the List Controls (after the Columns and Filters options) |
|
||||
| **`Description`** | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. |
|
||||
| **`edit.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
|
||||
| **`edit.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
|
||||
|
||||
@@ -112,7 +112,7 @@ The following arguments are provided to the `url` function:
|
||||
If your application requires a fully qualified URL, such as within deploying to Vercel Preview Deployments, you can use the `req` property to build this URL:
|
||||
|
||||
```ts
|
||||
url: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlight-line
|
||||
url: ({ data, req }) => `${req.protocol}//${req.host}/${data.slug}` // highlight-line
|
||||
```
|
||||
|
||||
### Breakpoints
|
||||
|
||||
@@ -10,11 +10,17 @@ To spin up this example locally, follow these steps:
|
||||
|
||||
- `npx create-payload-app --example multi-tenant`
|
||||
|
||||
2. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
2. `cp .env.example .env` to copy the example environment variables
|
||||
|
||||
3. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
3. `open http://localhost:3000` to access the home page
|
||||
4. `open http://localhost:3000/admin` to access the admin panel
|
||||
- Login with email `demo@payloadcms.com` and password `demo`
|
||||
4. `open http://localhost:3000` to access the home page
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
|
||||
### Default users
|
||||
|
||||
The seed script seeds 3 tenants.
|
||||
Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -28,7 +34,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
- #### Users
|
||||
|
||||
The `users` collection is auth-enabled and encompass both app-wide and tenant-scoped users based on the value of their `roles` and `tenants` fields. Users with the role `super-admin` can manage your entire application, while users with the _tenant role_ of `admin` have limited access to the platform and can manage only the tenant(s) they are assigned to, see [Tenants](#tenants) for more details.
|
||||
The `users` collection is auth-enabled and encompasses both app-wide and tenant-scoped users based on the value of their `roles` and `tenants` fields. Users with the role `super-admin` can manage your entire application, while users with the _tenant role_ of `admin` have limited access to the platform and can manage only the tenant(s) they are assigned to, see [Tenants](#tenants) for more details.
|
||||
|
||||
For additional help with authentication, see the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/cms#readme) or the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs.
|
||||
|
||||
@@ -40,13 +46,13 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
**Domain-based Tenant Setting**:
|
||||
|
||||
This example also supports domain-based tenant selection, where tenants can be associated with a specific domain. If a tenant is associated with a domain (e.g., `gold.localhost.com:3000`), when a user logs in from that domain, they will be automatically scoped to the matching tenant. This is accomplished through an optional `afterLogin` hook that sets a `payload-tenant` cookie based on the domain.
|
||||
This example also supports domain-based tenant selection, where tenants can be associated with a specific domain. If a tenant is associated with a domain (e.g., `gold.test:3000`), when a user logs in from that domain, they will be automatically scoped to the matching tenant. This is accomplished through an optional `afterLogin` hook that sets a `payload-tenant` cookie based on the domain.
|
||||
|
||||
The seed script seeds 3 tenants, for the domain portion of the example to function properly you will need to add the following entries to your systems `/etc/hosts` file:
|
||||
For the domain portion of the example to function properly, you will need to add the following entries to your system's `/etc/hosts` file:
|
||||
|
||||
- gold.localhost.com:3000
|
||||
- silver.localhost.com:3000
|
||||
- bronze.localhost.com:3000
|
||||
```
|
||||
127.0.0.1 gold.test silver.test bronze.test
|
||||
```
|
||||
|
||||
- #### Pages
|
||||
|
||||
@@ -54,7 +60,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
## Access control
|
||||
|
||||
Basic role-based access control is setup to determine what users can and cannot do based on their roles, which are:
|
||||
Basic role-based access control is set up to determine what users can and cannot do based on their roles, which are:
|
||||
|
||||
- `super-admin`: They can access the Payload admin panel to manage your multi-tenant application. They can see all tenants and make all operations.
|
||||
- `user`: They can only access the Payload admin panel if they are a tenant-admin, in which case they have a limited access to operations based on their tenant (see below).
|
||||
|
||||
@@ -10,10 +10,10 @@ export default async ({ params: paramsPromise }: { params: Promise<{ slug: strin
|
||||
<p>When you visit a tenant by domain, the domain is used to determine the tenant.</p>
|
||||
<p>
|
||||
For example, visiting{' '}
|
||||
<a href="http://gold.localhost.com:3000/tenant-domains/login">
|
||||
http://gold.localhost.com:3000/tenant-domains/login
|
||||
<a href="http://gold.test:3000/tenant-domains/login">
|
||||
http://gold.test:3000/tenant-domains/login
|
||||
</a>{' '}
|
||||
will show the tenant with the domain "gold.localhost.com".
|
||||
will show the tenant with the domain "gold.test".
|
||||
</p>
|
||||
|
||||
<h2>Slugs</h2>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Access } from 'payload'
|
||||
/**
|
||||
* Tenant admins and super admins can will be allowed access
|
||||
*/
|
||||
export const superAdminOrTeanantAdminAccess: Access = ({ req }) => {
|
||||
export const superAdminOrTenantAdminAccess: Access = ({ req }) => {
|
||||
if (!req.user) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { ensureUniqueSlug } from './hooks/ensureUniqueSlug'
|
||||
import { superAdminOrTeanantAdminAccess } from '@/collections/Pages/access/superAdminOrTenantAdmin'
|
||||
import { superAdminOrTenantAdminAccess } from '@/collections/Pages/access/superAdminOrTenantAdmin'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
access: {
|
||||
create: superAdminOrTeanantAdminAccess,
|
||||
delete: superAdminOrTeanantAdminAccess,
|
||||
create: superAdminOrTenantAdminAccess,
|
||||
delete: superAdminOrTenantAdminAccess,
|
||||
read: () => true,
|
||||
update: superAdminOrTeanantAdminAccess,
|
||||
update: superAdminOrTenantAdminAccess,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
|
||||
|
||||
export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 1',
|
||||
slug: 'gold',
|
||||
domain: 'gold.test',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant2 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 2',
|
||||
slug: 'silver',
|
||||
domain: 'silver.test',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant3 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 3',
|
||||
slug: 'bronze',
|
||||
domain: 'bronze.test',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
@@ -10,47 +37,16 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
},
|
||||
})
|
||||
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 1',
|
||||
slug: 'gold',
|
||||
domain: 'gold.localhost.com',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant2 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 2',
|
||||
slug: 'silver',
|
||||
domain: 'silver.localhost.com',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant3 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 3',
|
||||
slug: 'bronze',
|
||||
domain: 'bronze.localhost.com',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant1@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant1.id,
|
||||
},
|
||||
// {
|
||||
// roles: ['tenant-admin'],
|
||||
// tenant: tenant2.id,
|
||||
// },
|
||||
],
|
||||
username: 'tenant1',
|
||||
},
|
||||
@@ -60,7 +56,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant2@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -75,7 +71,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant3@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -90,7 +86,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'multi-admin@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -105,7 +101,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
tenant: tenant3.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant3',
|
||||
username: 'multi-admin',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -54,6 +54,7 @@ const generateEnvContent = (
|
||||
.filter((line) => line.includes('=') && !line.startsWith('#'))
|
||||
.forEach((line) => {
|
||||
const [key, value] = line.split('=')
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
envVars[key] = value
|
||||
})
|
||||
|
||||
|
||||
@@ -224,12 +224,12 @@ function insertBeforeAndAfter(content: string, loc: Loc): string {
|
||||
}
|
||||
|
||||
// insert ) after end
|
||||
lines[end.line - 1] = insert(lines[end.line - 1], end.column, ')')
|
||||
lines[end.line - 1] = insert(lines[end.line - 1]!, end.column, ')')
|
||||
// insert withPayload before start
|
||||
if (start.line === end.line) {
|
||||
lines[end.line - 1] = insert(lines[end.line - 1], start.column, 'withPayload(')
|
||||
lines[end.line - 1] = insert(lines[end.line - 1]!, start.column, 'withPayload(')
|
||||
} else {
|
||||
lines[start.line - 1] = insert(lines[start.line - 1], start.column, 'withPayload(')
|
||||
lines[start.line - 1] = insert(lines[start.line - 1]!, start.column, 'withPayload(')
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -16,7 +16,9 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
|
||||
|
||||
const result = await upsertRow<T>({
|
||||
data.createdAt = new Date().toISOString()
|
||||
|
||||
const result = await upsertRow<{ globalType: string } & T>({
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
@@ -26,5 +28,7 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
tableName,
|
||||
})
|
||||
|
||||
result.globalType = slug
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ export const transformArray = ({
|
||||
data: arrayRow,
|
||||
fieldPrefix: '',
|
||||
fields: field.flattenedFields,
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: arrayTableName,
|
||||
|
||||
@@ -101,6 +101,7 @@ export const transformBlocks = ({
|
||||
data: blockRow,
|
||||
fieldPrefix: '',
|
||||
fields: matchedBlock.flattenedFields,
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: blockTableName,
|
||||
|
||||
@@ -42,6 +42,10 @@ type Args = {
|
||||
fieldPrefix: string
|
||||
fields: FlattenedField[]
|
||||
forcedLocale?: string
|
||||
/**
|
||||
* Tracks whether the current traversion context is from array or block.
|
||||
*/
|
||||
insideArrayOrBlock?: boolean
|
||||
locales: {
|
||||
[locale: string]: Record<string, unknown>
|
||||
}
|
||||
@@ -77,6 +81,7 @@ export const traverseFields = ({
|
||||
fieldPrefix,
|
||||
fields,
|
||||
forcedLocale,
|
||||
insideArrayOrBlock = false,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -230,6 +235,7 @@ export const traverseFields = ({
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.flattenedFields,
|
||||
forcedLocale: localeKey,
|
||||
insideArrayOrBlock,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -258,6 +264,7 @@ export const traverseFields = ({
|
||||
existingLocales,
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.flattenedFields,
|
||||
insideArrayOrBlock,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -420,7 +427,7 @@ export const traverseFields = ({
|
||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||
if (Array.isArray(localeData)) {
|
||||
const newRows = transformSelects({
|
||||
id: data._uuid || data.id,
|
||||
id: insideArrayOrBlock ? data._uuid || data.id : undefined,
|
||||
data: localeData,
|
||||
locale: localeKey,
|
||||
})
|
||||
@@ -431,7 +438,7 @@ export const traverseFields = ({
|
||||
}
|
||||
} else if (Array.isArray(data[field.name])) {
|
||||
const newRows = transformSelects({
|
||||
id: data._uuid || data.id,
|
||||
id: insideArrayOrBlock ? data._uuid || data.id : undefined,
|
||||
data: data[field.name],
|
||||
locale: withinArrayOrBlockLocale,
|
||||
})
|
||||
@@ -472,8 +479,9 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
valuesToTransform.forEach(({ localeKey, ref, value }) => {
|
||||
let formattedValue = value
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
let formattedValue = value
|
||||
if (value && field.type === 'point' && adapter.name !== 'sqlite') {
|
||||
formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})`
|
||||
}
|
||||
@@ -483,12 +491,16 @@ export const traverseFields = ({
|
||||
formattedValue = new Date(value).toISOString()
|
||||
} else if (value instanceof Date) {
|
||||
formattedValue = value.toISOString()
|
||||
} else if (fieldName === 'updatedAt') {
|
||||
// let the db handle this
|
||||
formattedValue = new Date().toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date' && fieldName === 'updatedAt') {
|
||||
// let the db handle this
|
||||
formattedValue = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (typeof formattedValue !== 'undefined') {
|
||||
if (localeKey) {
|
||||
ref[localeKey][fieldName] = formattedValue
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
|
||||
const existingGlobal = await db.query[tableName].findFirst({})
|
||||
|
||||
const result = await upsertRow<T>({
|
||||
const result = await upsertRow<{ globalType: string } & T>({
|
||||
...(existingGlobal ? { id: existingGlobal.id, operation: 'update' } : { operation: 'create' }),
|
||||
adapter: this,
|
||||
data,
|
||||
@@ -28,5 +28,7 @@ export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
tableName,
|
||||
})
|
||||
|
||||
result.globalType = slug
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"eslint-plugin-jest-dom": "5.4.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-hooks": "5.0.0",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-a4b2d0d5-20250203",
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"globals": "15.12.0",
|
||||
"typescript": "5.7.3",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"eslint-plugin-jest-dom": "5.4.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-hooks": "5.0.0",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-a4b2d0d5-20250203",
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"globals": "15.12.0",
|
||||
"typescript": "5.7.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -91,6 +91,20 @@ export const RootLayout = async ({
|
||||
importMap,
|
||||
})
|
||||
|
||||
if (
|
||||
clientConfig.localization &&
|
||||
config.localization &&
|
||||
typeof config.localization.filterAvailableLocales === 'function'
|
||||
) {
|
||||
clientConfig.localization.locales = (
|
||||
await config.localization.filterAvailableLocales({
|
||||
locales: config.localization.locales,
|
||||
req,
|
||||
})
|
||||
).map(({ toString, ...rest }) => rest)
|
||||
clientConfig.localization.localeCodes = config.localization.locales.map(({ code }) => code)
|
||||
}
|
||||
|
||||
const locale = await getRequestLocale({
|
||||
req,
|
||||
})
|
||||
|
||||
@@ -91,6 +91,7 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
text(value, {
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'onChange',
|
||||
preferences: { fields: {} },
|
||||
@@ -120,6 +121,7 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
email(value, {
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'onChange',
|
||||
preferences: { fields: {} },
|
||||
|
||||
@@ -33,6 +33,15 @@ export const renderListViewSlots = ({
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.listControlsMenu) {
|
||||
result.ListControlsMenu = RenderServerComponent({
|
||||
clientProps,
|
||||
Component: collectionConfig.admin.components.listControlsMenu,
|
||||
importMap: payload.importMap,
|
||||
serverProps,
|
||||
})
|
||||
}
|
||||
|
||||
if (collectionConfig.admin.components?.afterListTable) {
|
||||
result.AfterListTable = RenderServerComponent({
|
||||
clientProps,
|
||||
|
||||
@@ -113,7 +113,7 @@ export const buildVersionFields = ({
|
||||
versionField.fieldByLocale = {}
|
||||
|
||||
for (const locale of selectedLocales) {
|
||||
versionField.fieldByLocale[locale] = buildVersionField({
|
||||
const localizedVersionField = buildVersionField({
|
||||
clientField: clientField as ClientField,
|
||||
clientSchemaMap,
|
||||
comparisonValue: comparisonValue?.[locale],
|
||||
@@ -133,12 +133,12 @@ export const buildVersionFields = ({
|
||||
selectedLocales,
|
||||
versionValue: versionValue?.[locale],
|
||||
})
|
||||
if (!versionField.fieldByLocale[locale]) {
|
||||
continue
|
||||
if (localizedVersionField) {
|
||||
versionField.fieldByLocale[locale] = localizedVersionField
|
||||
}
|
||||
}
|
||||
} else {
|
||||
versionField.field = buildVersionField({
|
||||
const baseVersionField = buildVersionField({
|
||||
clientField: clientField as ClientField,
|
||||
clientSchemaMap,
|
||||
comparisonValue,
|
||||
@@ -158,8 +158,8 @@ export const buildVersionFields = ({
|
||||
versionValue,
|
||||
})
|
||||
|
||||
if (!versionField.field) {
|
||||
continue
|
||||
if (baseVersionField) {
|
||||
versionField.field = baseVersionField
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -68,9 +68,16 @@ export type BuildFormStateArgs = {
|
||||
data?: Data
|
||||
docPermissions: SanitizedDocumentPermissions | undefined
|
||||
docPreferences: DocumentPreferences
|
||||
/**
|
||||
* In case `formState` is not the top-level, document form state, this can be passed to
|
||||
* provide the top-level form state.
|
||||
*/
|
||||
documentFormState?: FormState
|
||||
fallbackLocale?: false | TypedLocale
|
||||
formState?: FormState
|
||||
id?: number | string
|
||||
initialBlockData?: Data
|
||||
initialBlockFormState?: FormState
|
||||
/*
|
||||
If not i18n was passed, the language can be passed to init i18n
|
||||
*/
|
||||
|
||||
@@ -34,6 +34,7 @@ export const generatePasswordSaltHash = async ({
|
||||
const validationResult = password(passwordToSet, {
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'submit',
|
||||
preferences: { fields: {} },
|
||||
|
||||
@@ -30,6 +30,7 @@ export function iterateCollections({
|
||||
})
|
||||
|
||||
addToImportMap(collection.admin?.components?.afterList)
|
||||
addToImportMap(collection.admin?.components?.listControlsMenu)
|
||||
addToImportMap(collection.admin?.components?.afterListTable)
|
||||
addToImportMap(collection.admin?.components?.beforeList)
|
||||
addToImportMap(collection.admin?.components?.beforeListTable)
|
||||
|
||||
@@ -310,6 +310,7 @@ export type CollectionAdminOptions = {
|
||||
*/
|
||||
Upload?: CustomUpload
|
||||
}
|
||||
listControlsMenu?: CustomComponent[]
|
||||
views?: {
|
||||
/**
|
||||
* Set to a React component to replace the entire Edit View, including all nested routes.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
DefaultTranslationKeys,
|
||||
DefaultTranslationsObject,
|
||||
I18n,
|
||||
I18nClient,
|
||||
I18nOptions,
|
||||
TFunction,
|
||||
@@ -469,6 +470,14 @@ export type BaseLocalizationConfig = {
|
||||
* @default true
|
||||
*/
|
||||
fallback?: boolean
|
||||
/**
|
||||
* Define a function to filter the locales made available in Payload admin UI
|
||||
* based on user.
|
||||
*/
|
||||
filterAvailableLocales?: (args: {
|
||||
locales: Locale[]
|
||||
req: PayloadRequest
|
||||
}) => Locale[] | Promise<Locale[]>
|
||||
}
|
||||
|
||||
export type LocalizationConfigWithNoLabels = Prettify<
|
||||
@@ -1122,7 +1131,16 @@ export type Config = {
|
||||
* Allows you to modify the base JSON schema that is generated during generate:types. This JSON schema will be used
|
||||
* to generate the TypeScript interfaces.
|
||||
*/
|
||||
schema?: Array<(args: { jsonSchema: JSONSchema4 }) => JSONSchema4>
|
||||
schema?: Array<
|
||||
(args: {
|
||||
collectionIDFieldTypes: {
|
||||
[key: string]: 'number' | 'string'
|
||||
}
|
||||
config: SanitizedConfig
|
||||
i18n: I18n
|
||||
jsonSchema: JSONSchema4
|
||||
}) => JSONSchema4
|
||||
>
|
||||
}
|
||||
/**
|
||||
* Customize the handling of incoming file uploads for collections that have uploads enabled.
|
||||
|
||||
@@ -133,7 +133,13 @@ import type {
|
||||
TextareaFieldValidation,
|
||||
} from '../../index.js'
|
||||
import type { DocumentPreferences } from '../../preferences/types.js'
|
||||
import type { DefaultValue, Operation, PayloadRequest, Where } from '../../types/index.js'
|
||||
import type {
|
||||
DefaultValue,
|
||||
JsonObject,
|
||||
Operation,
|
||||
PayloadRequest,
|
||||
Where,
|
||||
} from '../../types/index.js'
|
||||
import type {
|
||||
NumberFieldManyValidation,
|
||||
NumberFieldSingleValidation,
|
||||
@@ -148,6 +154,10 @@ import type {
|
||||
} from '../validations.js'
|
||||
|
||||
export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSiblingData = any> = {
|
||||
/**
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
blockData: JsonObject | undefined
|
||||
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
@@ -212,7 +222,11 @@ export type FieldHook<TData extends TypeWithID = any, TValue = any, TSiblingData
|
||||
|
||||
export type FieldAccess<TData extends TypeWithID = any, TSiblingData = any> = (args: {
|
||||
/**
|
||||
* The incoming data used to `create` or `update` the document with. `data` is undefined during the `read` operation.
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
blockData?: JsonObject | undefined
|
||||
/**
|
||||
* The incoming, top-level document data used to `create` or `update` the document with.
|
||||
*/
|
||||
data?: Partial<TData>
|
||||
/**
|
||||
@@ -231,13 +245,33 @@ export type FieldAccess<TData extends TypeWithID = any, TSiblingData = any> = (a
|
||||
siblingData?: Partial<TSiblingData>
|
||||
}) => boolean | Promise<boolean>
|
||||
|
||||
//TODO: In 4.0, we should replace the three parameters of the condition function with a single, named parameter object
|
||||
export type Condition<TData extends TypeWithID = any, TSiblingData = any> = (
|
||||
/**
|
||||
* The top-level document data
|
||||
*/
|
||||
data: Partial<TData>,
|
||||
/**
|
||||
* Immediately adjacent data to this field. For example, if this is a `group` field, then `siblingData` will be the other fields within the group.
|
||||
*/
|
||||
siblingData: Partial<TSiblingData>,
|
||||
{ user }: { user: PayloadRequest['user'] },
|
||||
{
|
||||
blockData,
|
||||
user,
|
||||
}: {
|
||||
/**
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
blockData: Partial<TData>
|
||||
user: PayloadRequest['user']
|
||||
},
|
||||
) => boolean
|
||||
|
||||
export type FilterOptionsProps<TData = any> = {
|
||||
/**
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
blockData: TData
|
||||
/**
|
||||
* An object containing the full collection or global document currently being edited.
|
||||
*/
|
||||
@@ -348,6 +382,11 @@ export type LabelsClient = {
|
||||
}
|
||||
|
||||
export type BaseValidateOptions<TData, TSiblingData, TValue> = {
|
||||
/**
|
||||
/**
|
||||
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
|
||||
*/
|
||||
blockData: Partial<TData>
|
||||
collectionSlug?: string
|
||||
data: Partial<TData>
|
||||
event?: 'onChange' | 'submit'
|
||||
|
||||
@@ -11,6 +11,10 @@ import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
@@ -33,6 +37,7 @@ type Args = {
|
||||
// - Execute field hooks
|
||||
|
||||
export const promise = async ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -69,6 +74,7 @@ export const promise = async ({
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -104,6 +110,7 @@ export const promise = async ({
|
||||
rows.forEach((row, rowIndex) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -142,6 +149,7 @@ export const promise = async ({
|
||||
if (block) {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
blockData: siblingData?.[field.name]?.[rowIndex],
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -171,6 +179,7 @@ export const promise = async ({
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -193,6 +202,7 @@ export const promise = async ({
|
||||
|
||||
case 'group': {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -269,6 +279,7 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -291,6 +302,7 @@ export const promise = async ({
|
||||
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -7,6 +7,10 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { promise } from './promise.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
@@ -25,6 +29,7 @@ type Args = {
|
||||
}
|
||||
|
||||
export const traverseFields = async ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -46,6 +51,7 @@ export const traverseFields = async ({
|
||||
fields.forEach((field, fieldIndex) => {
|
||||
promises.push(
|
||||
promise({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -19,6 +19,10 @@ import { relationshipPopulationPromise } from './relationshipPopulationPromise.j
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
currentDepth: number
|
||||
@@ -60,6 +64,7 @@ type Args = {
|
||||
// - Populate relationships
|
||||
|
||||
export const promise = async ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -236,6 +241,7 @@ export const promise = async ({
|
||||
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
|
||||
(async () => {
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -266,6 +272,7 @@ export const promise = async ({
|
||||
await Promise.all(hookPromises)
|
||||
} else {
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -301,6 +308,7 @@ export const promise = async ({
|
||||
? true
|
||||
: await field.access.read({
|
||||
id: doc.id as number | string,
|
||||
blockData,
|
||||
data: doc,
|
||||
doc,
|
||||
req,
|
||||
@@ -364,6 +372,7 @@ export const promise = async ({
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, rowIndex) => {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -397,6 +406,7 @@ export const promise = async ({
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, rowIndex) => {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -476,6 +486,7 @@ export const promise = async ({
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -515,6 +526,7 @@ export const promise = async ({
|
||||
|
||||
if (block) {
|
||||
traverseFields({
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -554,6 +566,7 @@ export const promise = async ({
|
||||
case 'collapsible':
|
||||
case 'row': {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -595,6 +608,7 @@ export const promise = async ({
|
||||
const groupSelect = select?.[field.name]
|
||||
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -747,6 +761,7 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -780,6 +795,7 @@ export const promise = async ({
|
||||
|
||||
case 'tabs': {
|
||||
traverseFields({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
|
||||
@@ -13,6 +13,10 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { promise } from './promise.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
currentDepth: number
|
||||
@@ -45,6 +49,7 @@ type Args = {
|
||||
}
|
||||
|
||||
export const traverseFields = ({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
@@ -75,6 +80,7 @@ export const traverseFields = ({
|
||||
fields.forEach((field, fieldIndex) => {
|
||||
fieldPromises.push(
|
||||
promise({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
currentDepth,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ValidationFieldError } from '../../../errors/index.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, Operation, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
import type { Field, TabAsField, Validate } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
|
||||
@@ -16,6 +16,10 @@ import { getExistingRowDoc } from './getExistingRowDoc.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
@@ -48,6 +52,7 @@ type Args = {
|
||||
|
||||
export const promise = async ({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -77,7 +82,7 @@ export const promise = async ({
|
||||
})
|
||||
|
||||
const passesCondition = field.admin?.condition
|
||||
? Boolean(field.admin.condition(data, siblingData, { user: req.user }))
|
||||
? Boolean(field.admin.condition(data, siblingData, { blockData, user: req.user }))
|
||||
: true
|
||||
let skipValidationFromHere = skipValidation || !passesCondition
|
||||
const { localization } = req.payload.config
|
||||
@@ -102,6 +107,7 @@ export const promise = async ({
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -139,22 +145,27 @@ export const promise = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const validationResult = await field.validate(
|
||||
valueToValidate as never,
|
||||
{
|
||||
...field,
|
||||
id,
|
||||
collectionSlug: collection?.slug,
|
||||
data: deepMergeWithSourceArrays(doc, data),
|
||||
event: 'submit',
|
||||
jsonError,
|
||||
operation,
|
||||
preferences: { fields: {} },
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData),
|
||||
} as any,
|
||||
)
|
||||
const validateFn: Validate<object, object, object, object> = field.validate as Validate<
|
||||
object,
|
||||
object,
|
||||
object,
|
||||
object
|
||||
>
|
||||
const validationResult = await validateFn(valueToValidate as never, {
|
||||
...field,
|
||||
id,
|
||||
blockData,
|
||||
collectionSlug: collection?.slug,
|
||||
data: deepMergeWithSourceArrays(doc, data),
|
||||
event: 'submit',
|
||||
// @ts-expect-error
|
||||
jsonError,
|
||||
operation,
|
||||
preferences: { fields: {} },
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData),
|
||||
})
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
const label = getTranslatedLabel(field?.label || field?.name, req.i18n)
|
||||
@@ -217,6 +228,7 @@ export const promise = async ({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -268,6 +280,7 @@ export const promise = async ({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -301,6 +314,7 @@ export const promise = async ({
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -339,6 +353,7 @@ export const promise = async ({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -455,6 +470,7 @@ export const promise = async ({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -481,6 +497,7 @@ export const promise = async ({
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -8,6 +8,10 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { promise } from './promise.js'
|
||||
|
||||
type Args = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: JsonObject
|
||||
@@ -51,6 +55,7 @@ type Args = {
|
||||
*/
|
||||
export const traverseFields = async ({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -76,6 +81,7 @@ export const traverseFields = async ({
|
||||
promises.push(
|
||||
promise({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -9,6 +9,10 @@ import { runBeforeDuplicateHooks } from './runHook.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args<T> = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
doc: T
|
||||
@@ -25,6 +29,7 @@ type Args<T> = {
|
||||
|
||||
export const promise = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -63,6 +68,7 @@ export const promise = async <T>({
|
||||
const localizedValues = await localizedValuesPromise
|
||||
|
||||
const beforeDuplicateArgs: FieldHookArgs = {
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data: doc,
|
||||
@@ -96,6 +102,7 @@ export const promise = async <T>({
|
||||
siblingDoc[field.name] = localeData
|
||||
} else {
|
||||
const beforeDuplicateArgs: FieldHookArgs = {
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data: doc,
|
||||
@@ -143,6 +150,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -177,6 +185,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -199,6 +208,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -234,6 +244,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -270,6 +281,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -300,6 +312,7 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -324,6 +337,7 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -347,6 +361,7 @@ export const promise = async <T>({
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -367,6 +382,7 @@ export const promise = async <T>({
|
||||
case 'tab': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -386,6 +402,7 @@ export const promise = async <T>({
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
|
||||
@@ -6,6 +6,10 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { promise } from './promise.js'
|
||||
|
||||
type Args<T> = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
doc: T
|
||||
@@ -21,6 +25,7 @@ type Args<T> = {
|
||||
|
||||
export const traverseFields = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
@@ -38,6 +43,7 @@ export const traverseFields = async <T>({
|
||||
promises.push(
|
||||
promise({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
|
||||
@@ -14,6 +14,10 @@ import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args<T> = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: T
|
||||
@@ -47,6 +51,7 @@ type Args<T> = {
|
||||
|
||||
export const promise = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -270,6 +275,7 @@ export const promise = async <T>({
|
||||
await priorHook
|
||||
|
||||
const hookedValue = await currentHook({
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -298,7 +304,7 @@ export const promise = async <T>({
|
||||
if (field.access && field.access[operation]) {
|
||||
const result = overrideAccess
|
||||
? true
|
||||
: await field.access[operation]({ id, data, doc, req, siblingData })
|
||||
: await field.access[operation]({ id, blockData, data, doc, req, siblingData })
|
||||
|
||||
if (!result) {
|
||||
delete siblingData[field.name]
|
||||
@@ -335,6 +341,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -375,6 +382,7 @@ export const promise = async <T>({
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
blockData: row,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -404,6 +412,7 @@ export const promise = async <T>({
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -437,6 +446,7 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -522,6 +532,7 @@ export const promise = async <T>({
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -544,6 +555,7 @@ export const promise = async <T>({
|
||||
case 'tabs': {
|
||||
await traverseFields({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -7,6 +7,10 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { promise } from './promise.js'
|
||||
|
||||
type Args<T> = {
|
||||
/**
|
||||
* Data of the nearest parent block. If no parent block exists, this will be the `undefined`
|
||||
*/
|
||||
blockData?: JsonObject
|
||||
collection: null | SanitizedCollectionConfig
|
||||
context: RequestContext
|
||||
data: T
|
||||
@@ -32,6 +36,7 @@ type Args<T> = {
|
||||
|
||||
export const traverseFields = async <T>({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
@@ -53,6 +58,7 @@ export const traverseFields = async <T>({
|
||||
promises.push(
|
||||
promise({
|
||||
id,
|
||||
blockData,
|
||||
collection,
|
||||
context,
|
||||
data,
|
||||
|
||||
@@ -510,7 +510,7 @@ const validateFilterOptions: Validate<
|
||||
RelationshipField | UploadField
|
||||
> = async (
|
||||
value,
|
||||
{ id, data, filterOptions, relationTo, req, req: { payload, t, user }, siblingData },
|
||||
{ id, blockData, data, filterOptions, relationTo, req, req: { payload, t, user }, siblingData },
|
||||
) => {
|
||||
if (typeof filterOptions !== 'undefined' && value) {
|
||||
const options: {
|
||||
@@ -527,6 +527,7 @@ const validateFilterOptions: Validate<
|
||||
typeof filterOptions === 'function'
|
||||
? await filterOptions({
|
||||
id,
|
||||
blockData,
|
||||
data,
|
||||
relationTo: collection,
|
||||
req,
|
||||
|
||||
@@ -244,6 +244,11 @@ export const updateOperation = async <
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (!shouldSaveDraft) {
|
||||
// Ensure global has createdAt
|
||||
if (!result.createdAt) {
|
||||
result.createdAt = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (globalExists) {
|
||||
result = await payload.db.updateGlobal({
|
||||
slug,
|
||||
|
||||
@@ -217,7 +217,9 @@ function entityOrFieldToJsDocs({
|
||||
description = entity?.admin?.description?.[i18n.language]
|
||||
}
|
||||
} else if (typeof entity?.admin?.description === 'function' && i18n) {
|
||||
description = entity?.admin?.description(i18n)
|
||||
// do not evaluate description functions for generating JSDocs. The output of
|
||||
// those can differ depending on where and when they are called, creating
|
||||
// inconsistencies in the generated JSDocs.
|
||||
}
|
||||
}
|
||||
return description
|
||||
@@ -1102,7 +1104,7 @@ export function configToJSONSchema(
|
||||
|
||||
if (config?.typescript?.schema?.length) {
|
||||
for (const schema of config.typescript.schema) {
|
||||
jsonSchema = schema({ jsonSchema })
|
||||
jsonSchema = schema({ collectionIDFieldTypes, config, i18n, jsonSchema })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export const routeError = async ({
|
||||
|
||||
// Internal server errors can contain anything, including potentially sensitive data.
|
||||
// Therefore, error details will be hidden from the response unless `config.debug` is `true`
|
||||
if (!config.debug && status === httpStatus.INTERNAL_SERVER_ERROR) {
|
||||
if (!config.debug && !err.isPublic && status === httpStatus.INTERNAL_SERVER_ERROR) {
|
||||
response = formatErrors(new APIError('Something went wrong.'))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-multi-tenant",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "Multi Tenant plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -24,15 +24,21 @@ export const TenantField = (args: Props) => {
|
||||
const hasSetValueRef = React.useRef(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hasSetValueRef.current && value) {
|
||||
if (!hasSetValueRef.current) {
|
||||
// set value on load
|
||||
setTenant({ id: value, refresh: unique })
|
||||
if (value && value !== selectedTenantID) {
|
||||
setTenant({ id: value, refresh: unique })
|
||||
} else {
|
||||
// in the document view, the tenant field should always have a value
|
||||
const defaultValue =
|
||||
!selectedTenantID || selectedTenantID === SELECT_ALL
|
||||
? options[0]?.value
|
||||
: selectedTenantID
|
||||
setTenant({ id: defaultValue, refresh: unique })
|
||||
}
|
||||
hasSetValueRef.current = true
|
||||
} else if (selectedTenantID && selectedTenantID === SELECT_ALL && options?.[0]?.value) {
|
||||
// in the document view, the tenant field should always have a value
|
||||
setTenant({ id: options[0].value, refresh: unique })
|
||||
} else if ((!value || value !== selectedTenantID) && selectedTenantID) {
|
||||
// Update the field value when the tenant is changed
|
||||
} else if ((!value || value !== selectedTenantID) && selectedTenantID !== SELECT_ALL) {
|
||||
// Update the field on the document value when the tenant is changed
|
||||
setValue(selectedTenantID)
|
||||
}
|
||||
}, [value, selectedTenantID, setTenant, setValue, options, unique])
|
||||
|
||||
@@ -42,7 +42,7 @@ export const TenantSelector = ({ viewType }: { viewType?: ViewTypes }) => {
|
||||
selectedTenantID
|
||||
? selectedTenantID === SELECT_ALL
|
||||
? undefined
|
||||
: String(selectedTenantID)
|
||||
: (selectedTenantID as string)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
6
packages/plugin-multi-tenant/src/defaults.ts
Normal file
6
packages/plugin-multi-tenant/src/defaults.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const defaults = {
|
||||
tenantCollectionSlug: 'tenants',
|
||||
tenantFieldName: 'tenant',
|
||||
tenantsArrayFieldName: 'tenants',
|
||||
tenantsArrayTenantFieldName: 'tenant',
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type RelationshipField } from 'payload'
|
||||
import { APIError } from 'payload'
|
||||
|
||||
import { defaults } from '../../defaults.js'
|
||||
import { getCollectionIDType } from '../../utilities/getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from '../../utilities/getTenantFromCookie.js'
|
||||
|
||||
@@ -12,10 +13,10 @@ type Args = {
|
||||
unique: boolean
|
||||
}
|
||||
export const tenantField = ({
|
||||
name,
|
||||
name = defaults.tenantFieldName,
|
||||
access = undefined,
|
||||
debug,
|
||||
tenantsCollectionSlug,
|
||||
tenantsCollectionSlug = defaults.tenantCollectionSlug,
|
||||
unique,
|
||||
}: Args): RelationshipField => ({
|
||||
name,
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
import type { ArrayField, RelationshipField } from 'payload'
|
||||
|
||||
export const tenantsArrayField = (args: {
|
||||
import { defaults } from '../../defaults.js'
|
||||
|
||||
type Args = {
|
||||
arrayFieldAccess?: ArrayField['access']
|
||||
rowFields?: ArrayField['fields']
|
||||
tenantFieldAccess?: RelationshipField['access']
|
||||
tenantsArrayFieldName: ArrayField['name']
|
||||
tenantsArrayTenantFieldName: RelationshipField['name']
|
||||
tenantsCollectionSlug: string
|
||||
}): ArrayField => ({
|
||||
name: args.tenantsArrayFieldName,
|
||||
}
|
||||
export const tenantsArrayField = ({
|
||||
arrayFieldAccess,
|
||||
rowFields,
|
||||
tenantFieldAccess,
|
||||
tenantsArrayFieldName = defaults.tenantsArrayFieldName,
|
||||
tenantsArrayTenantFieldName = defaults.tenantsArrayFieldName,
|
||||
tenantsCollectionSlug = defaults.tenantCollectionSlug,
|
||||
}: Args): ArrayField => ({
|
||||
name: tenantsArrayFieldName,
|
||||
type: 'array',
|
||||
access: args?.arrayFieldAccess,
|
||||
access: arrayFieldAccess,
|
||||
fields: [
|
||||
{
|
||||
name: args.tenantsArrayTenantFieldName,
|
||||
name: tenantsArrayTenantFieldName,
|
||||
type: 'relationship',
|
||||
access: args.tenantFieldAccess,
|
||||
access: tenantFieldAccess,
|
||||
index: true,
|
||||
relationTo: args.tenantsCollectionSlug,
|
||||
relationTo: tenantsCollectionSlug,
|
||||
required: true,
|
||||
saveToJWT: true,
|
||||
},
|
||||
...(args?.rowFields || []),
|
||||
...(rowFields || []),
|
||||
],
|
||||
saveToJWT: true,
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CollectionConfig, Config } from 'payload'
|
||||
|
||||
import type { MultiTenantPluginConfig } from './types.js'
|
||||
|
||||
import { defaults } from './defaults.js'
|
||||
import { tenantField } from './fields/tenantField/index.js'
|
||||
import { tenantsArrayField } from './fields/tenantsArrayField/index.js'
|
||||
import { addTenantCleanup } from './hooks/afterTenantDelete.js'
|
||||
@@ -9,13 +10,6 @@ import { addCollectionAccess } from './utilities/addCollectionAccess.js'
|
||||
import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'
|
||||
import { withTenantListFilter } from './utilities/withTenantListFilter.js'
|
||||
|
||||
const defaults = {
|
||||
tenantCollectionSlug: 'tenants',
|
||||
tenantFieldName: 'tenant',
|
||||
tenantsArrayFieldName: 'tenants',
|
||||
tenantsArrayTenantFieldName: 'tenant',
|
||||
}
|
||||
|
||||
export const multiTenantPlugin =
|
||||
<ConfigType>(pluginConfig: MultiTenantPluginConfig<ConfigType>) =>
|
||||
(incomingConfig: Config): Config => {
|
||||
|
||||
@@ -33,7 +33,7 @@ export const TenantSelectionProvider = async ({
|
||||
})
|
||||
tenantOptions = docs.map((doc) => ({
|
||||
label: String(doc[useAsTitle]),
|
||||
value: String(doc.id),
|
||||
value: doc.id,
|
||||
}))
|
||||
} catch (_) {
|
||||
// user likely does not have access
|
||||
@@ -42,15 +42,17 @@ export const TenantSelectionProvider = async ({
|
||||
const cookies = await getCookies()
|
||||
let tenantCookie = cookies.get('payload-tenant')?.value
|
||||
let initialValue = undefined
|
||||
const isValidTenantCookie =
|
||||
(tenantOptions.length > 1 && tenantCookie === SELECT_ALL) ||
|
||||
tenantOptions.some((option) => option.value === tenantCookie)
|
||||
|
||||
if (isValidTenantCookie) {
|
||||
initialValue = tenantCookie
|
||||
if (tenantOptions.length > 1 && tenantCookie === SELECT_ALL) {
|
||||
initialValue = SELECT_ALL
|
||||
} else {
|
||||
tenantCookie = undefined
|
||||
initialValue = tenantOptions.length > 1 ? SELECT_ALL : tenantOptions[0]?.value
|
||||
const matchingOption = tenantOptions.find((option) => String(option.value) === tenantCookie)
|
||||
if (matchingOption) {
|
||||
initialValue = matchingOption.value
|
||||
} else {
|
||||
tenantCookie = undefined
|
||||
initialValue = tenantOptions.length > 1 ? SELECT_ALL : tenantOptions[0]?.value
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parseCookies } from 'payload'
|
||||
import { isNumber } from 'payload/shared'
|
||||
|
||||
/**
|
||||
* A function that takes request headers and an idType and returns the current tenant ID from the cookie
|
||||
@@ -13,5 +14,9 @@ export function getTenantFromCookie(
|
||||
): null | number | string {
|
||||
const cookies = parseCookies(headers)
|
||||
const selectedTenant = cookies.get('payload-tenant') || null
|
||||
return selectedTenant ? (idType === 'number' ? parseFloat(selectedTenant) : selectedTenant) : null
|
||||
return selectedTenant
|
||||
? idType === 'number' && isNumber(selectedTenant)
|
||||
? parseFloat(selectedTenant)
|
||||
: selectedTenant
|
||||
: null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -42,10 +42,25 @@ export type SearchPluginConfig = {
|
||||
defaultPriorities?: {
|
||||
[collection: string]: ((doc: any) => number | Promise<number>) | number
|
||||
}
|
||||
/**
|
||||
* Controls whether drafts are deleted from the search index
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
deleteDrafts?: boolean
|
||||
localize?: boolean
|
||||
/**
|
||||
* We use batching when re-indexing large collections. You can control the amount of items per batch, lower numbers should help with memory.
|
||||
*
|
||||
* @default 50
|
||||
*/
|
||||
reindexBatchSize?: number
|
||||
searchOverrides?: { fields?: FieldsOverride } & Partial<Omit<CollectionConfig, 'fields'>>
|
||||
/**
|
||||
* Controls whether drafts are synced to the search index
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
syncDrafts?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -142,15 +142,42 @@ export const syncDocAsSearchIndex = async ({
|
||||
}
|
||||
}
|
||||
if (deleteDrafts && status === 'draft') {
|
||||
// do not include draft docs in search results, so delete the record
|
||||
try {
|
||||
await payload.delete({
|
||||
id: searchDocID,
|
||||
collection: searchSlug,
|
||||
req,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
payload.logger.error({ err, msg: `Error deleting ${searchSlug} document.` })
|
||||
// Check to see if there's a published version of the doc
|
||||
// We don't want to remove the search doc if there is a published version but a new draft has been created
|
||||
const {
|
||||
docs: [docWithPublish],
|
||||
} = await payload.find({
|
||||
collection,
|
||||
draft: false,
|
||||
locale: syncLocale,
|
||||
req,
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (!docWithPublish) {
|
||||
// do not include draft docs in search results, so delete the record
|
||||
try {
|
||||
await payload.delete({
|
||||
id: searchDocID,
|
||||
collection: searchSlug,
|
||||
req,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
payload.logger.error({ err, msg: `Error deleting ${searchSlug} document.` })
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (doSync) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-sentry",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "Sentry plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.20.0",
|
||||
"version": "3.21.0",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const ListJSXConverter: JSXConverters<SerializedListItemNode | Serialized
|
||||
className={`list-item-checkbox${node.checked ? ' list-item-checkbox-checked' : ' list-item-checkbox-unchecked'}${hasSubLists ? ' nestedListItem' : ''}`}
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
|
||||
role="checkbox"
|
||||
style={{ listStyleType: 'none' }}
|
||||
tabIndex={-1}
|
||||
value={node?.value}
|
||||
>
|
||||
@@ -45,7 +46,11 @@ export const ListJSXConverter: JSXConverters<SerializedListItemNode | Serialized
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<li className={hasSubLists ? 'nestedListItem' : ''} value={node?.value}>
|
||||
<li
|
||||
className={`${hasSubLists ? 'nestedListItem' : ''}`}
|
||||
style={hasSubLists ? { listStyleType: 'none' } : undefined}
|
||||
value={node?.value}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.payload-richtext .nestedListItem,
|
||||
.payload-richtext .list-check {
|
||||
list-style-type: none;
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import type { JSXConverters } from './converter/types.js'
|
||||
|
||||
import { defaultJSXConverters } from './converter/defaultConverters.js'
|
||||
import { convertLexicalToJSX } from './converter/index.js'
|
||||
import './index.css'
|
||||
|
||||
export type JSXConvertersFunction<
|
||||
T extends { [key: string]: any; type?: string } =
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Pill,
|
||||
RenderFields,
|
||||
SectionTitle,
|
||||
useDocumentForm,
|
||||
useDocumentInfo,
|
||||
useEditDepth,
|
||||
useFormSubmitted,
|
||||
@@ -23,6 +24,7 @@ import { deepCopyObjectSimpleWithoutReactComponents, reduceFieldsToValues } from
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
|
||||
const baseClass = 'lexical-block'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { $getNodeByKey } from 'lexical'
|
||||
@@ -33,9 +35,9 @@ import type { BlockFields } from '../../server/nodes/BlocksNode.js'
|
||||
|
||||
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js'
|
||||
import { useLexicalDrawer } from '../../../../utilities/fieldsDrawer/useLexicalDrawer.js'
|
||||
import './index.scss'
|
||||
import { $isBlockNode } from '../nodes/BlocksNode.js'
|
||||
import { BlockContent } from './BlockContent.js'
|
||||
import './index.scss'
|
||||
import { removeEmptyArrayValues } from './removeEmptyArrayValues.js'
|
||||
|
||||
type Props = {
|
||||
@@ -64,6 +66,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
},
|
||||
uuid: uuidFromContext,
|
||||
} = useEditorConfigContext()
|
||||
|
||||
const { fields: parentDocumentFields } = useDocumentForm()
|
||||
const onChangeAbortControllerRef = useRef(new AbortController())
|
||||
const editDepth = useEditDepth()
|
||||
const [errorCount, setErrorCount] = React.useState(0)
|
||||
@@ -127,7 +131,9 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
data: formData,
|
||||
docPermissions: { fields: true },
|
||||
docPreferences: await getDocPreferences(),
|
||||
documentFormState: deepCopyObjectSimpleWithoutReactComponents(parentDocumentFields),
|
||||
globalSlug,
|
||||
initialBlockData: formData,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
schemaPath: schemaFieldsPath,
|
||||
@@ -164,6 +170,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
getDocPreferences,
|
||||
parentDocumentFields,
|
||||
])
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = React.useState<boolean>(
|
||||
@@ -174,7 +181,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
|
||||
const clientSchemaMap = featureClientSchemaMap['blocks']
|
||||
|
||||
const blocksField: BlocksFieldClient | undefined = clientSchemaMap[
|
||||
const blocksField: BlocksFieldClient | undefined = clientSchemaMap?.[
|
||||
componentMapRenderedBlockPath
|
||||
]?.[0] as BlocksFieldClient
|
||||
|
||||
@@ -196,8 +203,10 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
fields: true,
|
||||
},
|
||||
docPreferences: await getDocPreferences(),
|
||||
documentFormState: deepCopyObjectSimpleWithoutReactComponents(parentDocumentFields),
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
initialBlockFormState: prevFormState,
|
||||
operation: 'update',
|
||||
renderAllFields: submit ? true : false,
|
||||
schemaPath: schemaFieldsPath,
|
||||
@@ -208,7 +217,9 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
return prevFormState
|
||||
}
|
||||
|
||||
newFormState.blockName = prevFormState.blockName
|
||||
if (prevFormState.blockName) {
|
||||
newFormState.blockName = prevFormState.blockName
|
||||
}
|
||||
|
||||
const newFormStateData: BlockFields = reduceFieldsToValues(
|
||||
removeEmptyArrayValues({
|
||||
@@ -252,6 +263,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
globalSlug,
|
||||
schemaFieldsPath,
|
||||
formData.blockType,
|
||||
parentDocumentFields,
|
||||
editor,
|
||||
nodeKey,
|
||||
],
|
||||
@@ -436,6 +448,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
],
|
||||
)
|
||||
|
||||
const clientBlockFields = clientBlock?.fields ?? []
|
||||
|
||||
const BlockDrawer = useMemo(
|
||||
() => () => (
|
||||
<EditDepthProvider>
|
||||
@@ -449,7 +463,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
{initialState ? (
|
||||
<>
|
||||
<RenderFields
|
||||
fields={clientBlock?.fields}
|
||||
fields={clientBlockFields}
|
||||
forceRender
|
||||
parentIndexPath=""
|
||||
parentPath="" // See Blocks feature path for details as for why this is empty
|
||||
@@ -488,7 +502,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
return await onChange({ formState, submit: true })
|
||||
},
|
||||
]}
|
||||
fields={clientBlock?.fields}
|
||||
fields={clientBlockFields}
|
||||
initialState={initialState}
|
||||
onChange={[onChange]}
|
||||
onSubmit={(formState, newData) => {
|
||||
@@ -512,7 +526,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
CustomBlock={CustomBlock}
|
||||
EditButton={EditButton}
|
||||
errorCount={errorCount}
|
||||
formSchema={clientBlock?.fields}
|
||||
formSchema={clientBlockFields}
|
||||
initialState={initialState}
|
||||
nodeKey={nodeKey}
|
||||
RemoveButton={RemoveButton}
|
||||
@@ -523,6 +537,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
BlockCollapsible,
|
||||
BlockDrawer,
|
||||
CustomBlock,
|
||||
clientBlockFields,
|
||||
RemoveButton,
|
||||
EditButton,
|
||||
editor,
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { FormState } from 'payload'
|
||||
export function removeEmptyArrayValues({ fields }: { fields: FormState }): FormState {
|
||||
for (const key in fields) {
|
||||
const field = fields[key]
|
||||
if (Array.isArray(field.rows) && 'value' in field) {
|
||||
if (Array.isArray(field?.rows) && 'value' in field) {
|
||||
field.disableFormData = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
FormSubmit,
|
||||
RenderFields,
|
||||
ShimmerEffect,
|
||||
useDocumentForm,
|
||||
useDocumentInfo,
|
||||
useEditDepth,
|
||||
useServerFunctions,
|
||||
@@ -26,6 +27,7 @@ import { $getNodeByKey } from 'lexical'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
import { deepCopyObjectSimpleWithoutReactComponents } from 'payload/shared'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type { InlineBlockFields } from '../../server/nodes/InlineBlocksNode.js'
|
||||
@@ -77,6 +79,8 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
setCreatedInlineBlock,
|
||||
uuid: uuidFromContext,
|
||||
} = useEditorConfigContext()
|
||||
const { fields: parentDocumentFields } = useDocumentForm()
|
||||
|
||||
const { getFormState } = useServerFunctions()
|
||||
const editDepth = useEditDepth()
|
||||
const firstTimeDrawer = useRef(false)
|
||||
@@ -112,29 +116,25 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
|
||||
const clientSchemaMap = featureClientSchemaMap['blocks']
|
||||
|
||||
const blocksField: BlocksFieldClient = clientSchemaMap[
|
||||
const blocksField: BlocksFieldClient = clientSchemaMap?.[
|
||||
componentMapRenderedBlockPath
|
||||
]?.[0] as BlocksFieldClient
|
||||
|
||||
const clientBlock = blocksField?.blocks?.[0]
|
||||
|
||||
const clientBlockFields = clientBlock?.fields ?? []
|
||||
|
||||
// Open drawer on "mount"
|
||||
useEffect(() => {
|
||||
if (!firstTimeDrawer.current && createdInlineBlock?.getKey() === nodeKey) {
|
||||
// > 2 because they always have "id" and "blockName" fields
|
||||
if (clientBlock?.fields?.length > 2) {
|
||||
if (clientBlockFields.length > 2) {
|
||||
toggleDrawer()
|
||||
}
|
||||
setCreatedInlineBlock?.(undefined)
|
||||
firstTimeDrawer.current = true
|
||||
}
|
||||
}, [
|
||||
clientBlock?.fields?.length,
|
||||
createdInlineBlock,
|
||||
nodeKey,
|
||||
setCreatedInlineBlock,
|
||||
toggleDrawer,
|
||||
])
|
||||
}, [clientBlockFields.length, createdInlineBlock, nodeKey, setCreatedInlineBlock, toggleDrawer])
|
||||
|
||||
const removeInlineBlock = useCallback(() => {
|
||||
editor.update(() => {
|
||||
@@ -165,7 +165,10 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
data: formData,
|
||||
docPermissions: { fields: true },
|
||||
docPreferences: await getDocPreferences(),
|
||||
documentFormState: deepCopyObjectSimpleWithoutReactComponents(parentDocumentFields),
|
||||
globalSlug,
|
||||
initialBlockData: formData,
|
||||
initialBlockFormState: formData,
|
||||
operation: 'update',
|
||||
renderAllFields: true,
|
||||
schemaPath: schemaFieldsPath,
|
||||
@@ -195,6 +198,7 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
collectionSlug,
|
||||
globalSlug,
|
||||
getDocPreferences,
|
||||
parentDocumentFields,
|
||||
])
|
||||
|
||||
/**
|
||||
@@ -214,8 +218,10 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
fields: true,
|
||||
},
|
||||
docPreferences: await getDocPreferences(),
|
||||
documentFormState: deepCopyObjectSimpleWithoutReactComponents(parentDocumentFields),
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
initialBlockFormState: prevFormState,
|
||||
operation: 'update',
|
||||
renderAllFields: submit ? true : false,
|
||||
schemaPath: schemaFieldsPath,
|
||||
@@ -233,7 +239,15 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
|
||||
|
||||
return state
|
||||
},
|
||||
[getFormState, id, collectionSlug, getDocPreferences, globalSlug, schemaFieldsPath],
|
||||
[
|
||||
getFormState,
|
||||
id,
|
||||
collectionSlug,
|
||||
getDocPreferences,
|
||||
parentDocumentFields,
|
||||
globalSlug,
|
||||
schemaFieldsPath,
|
||||
],
|
||||
)
|
||||
// cleanup effect
|
||||
useEffect(() => {
|
||||
|
||||
@@ -25,13 +25,17 @@ export const BlocksFeatureClient = createClientFeature(
|
||||
const schemaMapRenderedInlineBlockPathPrefix = `${schemaPath}.lexical_internal_feature.blocks.lexical_inline_blocks`
|
||||
const clientSchema = featureClientSchemaMap['blocks']
|
||||
|
||||
if (!clientSchema) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const blocksFields: BlocksFieldClient[] = Object.entries(clientSchema)
|
||||
.filter(
|
||||
([key]) =>
|
||||
key.startsWith(schemaMapRenderedBlockPathPrefix + '.') &&
|
||||
!key.replace(schemaMapRenderedBlockPathPrefix + '.', '').includes('.'),
|
||||
)
|
||||
.map(([key, value]) => value[0] as BlocksFieldClient)
|
||||
.map(([, value]) => value[0] as BlocksFieldClient)
|
||||
|
||||
const inlineBlocksFields: BlocksFieldClient[] = Object.entries(clientSchema)
|
||||
.filter(
|
||||
@@ -39,15 +43,19 @@ export const BlocksFeatureClient = createClientFeature(
|
||||
key.startsWith(schemaMapRenderedInlineBlockPathPrefix + '.') &&
|
||||
!key.replace(schemaMapRenderedInlineBlockPathPrefix + '.', '').includes('.'),
|
||||
)
|
||||
.map(([key, value]) => value[0] as BlocksFieldClient)
|
||||
.map(([, value]) => value[0] as BlocksFieldClient)
|
||||
|
||||
const clientBlocks: ClientBlock[] = blocksFields.map((field) => {
|
||||
return field.blocks[0]
|
||||
})
|
||||
const clientBlocks: ClientBlock[] = blocksFields
|
||||
.map((field) => {
|
||||
return field.blocks[0]
|
||||
})
|
||||
.filter((block) => block !== undefined)
|
||||
|
||||
const clientInlineBlocks: ClientBlock[] = inlineBlocksFields.map((field) => {
|
||||
return field.blocks[0]
|
||||
})
|
||||
const clientInlineBlocks: ClientBlock[] = inlineBlocksFields
|
||||
.map((field) => {
|
||||
return field.blocks[0]
|
||||
})
|
||||
.filter((block) => block !== undefined)
|
||||
|
||||
return {
|
||||
nodes: [BlockNode, InlineBlockNode],
|
||||
|
||||
@@ -92,7 +92,7 @@ export const getBlockMarkdownTransformers = ({
|
||||
|
||||
const childrenString = linesInBetween.join('\n').trim()
|
||||
|
||||
const propsString: null | string = openMatch?.length > 2 ? openMatch[2]?.trim() : null
|
||||
const propsString = openMatch[2]?.trim()
|
||||
|
||||
const markdownToLexical = getMarkdownToLexical(allNodes, allTransformers)
|
||||
|
||||
|
||||
@@ -38,17 +38,17 @@ export function linesFromStartToContentAndPropsString({
|
||||
let isSelfClosing = false
|
||||
let isWithinCodeBlockAmount = 0
|
||||
|
||||
const beforeStartLine = linesCopy[0].slice(0, startMatch.index)
|
||||
const beforeStartLine = linesCopy[0]!.slice(0, startMatch.index)
|
||||
let endlineLastCharIndex = 0
|
||||
|
||||
let endLineIndex = startLineIndex
|
||||
|
||||
mainLoop: for (let lineIndex = 0; lineIndex < linesCopy.length; lineIndex++) {
|
||||
const line = trimChildren ? linesCopy[lineIndex].trim() : linesCopy[lineIndex]
|
||||
mainLoop: for (const [lineIndex, lineCopy] of linesCopy.entries()) {
|
||||
const line = trimChildren ? lineCopy.trim() : lineCopy
|
||||
let amountOfBeginningSpacesRemoved = 0
|
||||
if (trimChildren) {
|
||||
for (let i = 0; i < linesCopy[lineIndex].length; i++) {
|
||||
if (linesCopy[lineIndex][i] === ' ') {
|
||||
for (let i = 0; i < lineCopy.length; i++) {
|
||||
if (lineCopy[i] === ' ') {
|
||||
amountOfBeginningSpacesRemoved++
|
||||
} else {
|
||||
break
|
||||
@@ -159,7 +159,7 @@ export function linesFromStartToContentAndPropsString({
|
||||
}
|
||||
}
|
||||
|
||||
const afterEndLine = linesCopy[endLineIndex].trim().slice(endlineLastCharIndex)
|
||||
const afterEndLine = linesCopy[endLineIndex]!.trim().slice(endlineLastCharIndex)
|
||||
|
||||
return {
|
||||
afterEndLine,
|
||||
|
||||
@@ -361,18 +361,19 @@ function getMarkdownTransformerForBlock(
|
||||
if (beforeStartLine?.length) {
|
||||
prevNodes = markdownToLexical({ markdown: beforeStartLine })?.root?.children ?? []
|
||||
|
||||
if (prevNodes?.length) {
|
||||
rootNode.append($parseSerializedNode(prevNodes[0]))
|
||||
const firstPrevNode = prevNodes?.[0]
|
||||
if (firstPrevNode) {
|
||||
rootNode.append($parseSerializedNode(firstPrevNode))
|
||||
}
|
||||
}
|
||||
|
||||
rootNode.append(node)
|
||||
|
||||
if (afterEndLine?.length) {
|
||||
nextNodes = markdownToLexical({ markdown: afterEndLine })?.root?.children ?? []
|
||||
nextNodes = markdownToLexical({ markdown: afterEndLine })?.root?.children
|
||||
const lastChild = rootNode.getChildren()[rootNode.getChildren().length - 1]
|
||||
|
||||
const children = ($parseSerializedNode(nextNodes[0]) as ElementNode)?.getChildren()
|
||||
const children = ($parseSerializedNode(nextNodes[0]!) as ElementNode)?.getChildren()
|
||||
if (children?.length) {
|
||||
for (const child of children) {
|
||||
;(lastChild as ElementNode).append(child)
|
||||
@@ -408,7 +409,7 @@ function getMarkdownTransformerForBlock(
|
||||
childrenString = linesInBetween.join('\n').trim()
|
||||
}
|
||||
|
||||
const propsString: null | string = openMatch?.length > 1 ? openMatch[1]?.trim() : null
|
||||
const propsString = openMatch[1]?.trim()
|
||||
|
||||
const markdownToLexical = getMarkdownToLexical(allNodes, allTransformers)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const blockValidationHOC = (
|
||||
const blockFieldData = node.fields ?? ({} as BlockFields)
|
||||
|
||||
const {
|
||||
options: { id, collectionSlug, operation, preferences, req },
|
||||
options: { id, collectionSlug, data, operation, preferences, req },
|
||||
} = validation
|
||||
|
||||
// find block
|
||||
@@ -32,8 +32,10 @@ export const blockValidationHOC = (
|
||||
id,
|
||||
collectionSlug,
|
||||
data: blockFieldData,
|
||||
documentData: data,
|
||||
fields: block.fields,
|
||||
fieldSchemaMap: undefined,
|
||||
initialBlockData: blockFieldData,
|
||||
operation: operation === 'create' || operation === 'update' ? operation : 'update',
|
||||
permissions: {},
|
||||
preferences,
|
||||
@@ -42,12 +44,15 @@ export const blockValidationHOC = (
|
||||
schemaPath: '',
|
||||
})
|
||||
|
||||
let errorPaths: string[] = []
|
||||
const errorPathsSet = new Set<string>()
|
||||
for (const fieldKey in result) {
|
||||
if (result[fieldKey].errorPaths) {
|
||||
errorPaths = errorPaths.concat(result[fieldKey].errorPaths)
|
||||
if (result[fieldKey].errorPaths?.length) {
|
||||
for (const errorPath of result[fieldKey].errorPaths) {
|
||||
errorPathsSet.add(errorPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
const errorPaths = Array.from(errorPathsSet)
|
||||
|
||||
if (errorPaths.length) {
|
||||
return 'The following fields are invalid: ' + errorPaths.join(', ')
|
||||
|
||||
@@ -210,13 +210,15 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element {
|
||||
}
|
||||
|
||||
const getCellColumnIndex = (tableCellNode: TableCellNode, tableMap: TableMapType) => {
|
||||
for (let row = 0; row < tableMap.length; row++) {
|
||||
for (let column = 0; column < tableMap[row].length; column++) {
|
||||
if (tableMap[row][column].cell === tableCellNode) {
|
||||
return column
|
||||
let columnIndex: number | undefined
|
||||
tableMap.forEach((row) => {
|
||||
row.forEach((cell, columnIndexInner) => {
|
||||
if (cell.cell === tableCellNode) {
|
||||
columnIndex = columnIndexInner
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return columnIndex
|
||||
}
|
||||
|
||||
const updateColumnWidth = useCallback(
|
||||
|
||||
@@ -65,8 +65,12 @@ export const TableMarkdownTransformer: (props: {
|
||||
},
|
||||
regExp: TABLE_ROW_REG_EXP,
|
||||
replace: (parentNode, _1, match) => {
|
||||
const match0 = match[0]
|
||||
if (!match0) {
|
||||
return
|
||||
}
|
||||
// Header row
|
||||
if (TABLE_ROW_DIVIDER_REG_EXP.test(match[0])) {
|
||||
if (TABLE_ROW_DIVIDER_REG_EXP.test(match0)) {
|
||||
const table = parentNode.getPreviousSibling()
|
||||
if (!table || !$isTableNode(table)) {
|
||||
return
|
||||
@@ -91,7 +95,7 @@ export const TableMarkdownTransformer: (props: {
|
||||
return
|
||||
}
|
||||
|
||||
const matchCells = mapToTableCells(match[0], allTransformers)
|
||||
const matchCells = mapToTableCells(match0, allTransformers)
|
||||
|
||||
if (matchCells == null) {
|
||||
return
|
||||
@@ -136,7 +140,7 @@ export const TableMarkdownTransformer: (props: {
|
||||
table.append(tableRow)
|
||||
|
||||
for (let i = 0; i < maxCells; i++) {
|
||||
tableRow.append(i < cells.length ? cells[i] : $createTableCell('', allTransformers))
|
||||
tableRow.append(i < cells.length ? cells[i]! : $createTableCell('', allTransformers))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export const MarkdownTransformer: (enabledHeadingSizes: HeadingTagType[]) => Ele
|
||||
},
|
||||
regExp,
|
||||
replace: createBlockNode((match) => {
|
||||
const tag = ('h' + match[1].length) as HeadingTagType
|
||||
const tag = ('h' + match[1]?.length) as HeadingTagType
|
||||
return $createHeadingNode(tag)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
|
||||
linkParent = getSelectedNode(selection).getParent()
|
||||
} else {
|
||||
if (selectedNodes.length) {
|
||||
linkParent = selectedNodes[0].getParent()
|
||||
linkParent = selectedNodes[0]?.getParent() ?? null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -295,9 +295,9 @@ export function $toggleLink(payload: ({ fields: LinkFields } & LinkPayload) | nu
|
||||
if ($isLinkNode(parent)) {
|
||||
const children = parent.getChildren()
|
||||
|
||||
for (let i = 0; i < children.length; i += 1) {
|
||||
parent.insertBefore(children[i])
|
||||
}
|
||||
children.forEach((child) => {
|
||||
parent.insertBefore(child)
|
||||
})
|
||||
|
||||
parent.remove()
|
||||
}
|
||||
@@ -307,7 +307,7 @@ export function $toggleLink(payload: ({ fields: LinkFields } & LinkPayload) | nu
|
||||
}
|
||||
// Add or merge LinkNodes
|
||||
if (nodes?.length === 1) {
|
||||
const firstNode = nodes[0]
|
||||
const firstNode = nodes[0]!
|
||||
// if the first node is a LinkNode or if its
|
||||
// parent is a LinkNode, we update the URL, target and rel.
|
||||
const linkNode: LinkNode | null = $isLinkNode(firstNode)
|
||||
@@ -375,10 +375,7 @@ export function $toggleLink(payload: ({ fields: LinkFields } & LinkPayload) | nu
|
||||
}
|
||||
if (linkNode !== null) {
|
||||
const children = node.getChildren()
|
||||
|
||||
for (let i = 0; i < children.length; i += 1) {
|
||||
linkNode.append(children[i])
|
||||
}
|
||||
linkNode.append(...children)
|
||||
}
|
||||
|
||||
node.remove()
|
||||
|
||||
@@ -13,7 +13,7 @@ export const linkValidation = (
|
||||
return async ({
|
||||
node,
|
||||
validation: {
|
||||
options: { id, collectionSlug, operation, preferences, req },
|
||||
options: { id, collectionSlug, data, operation, preferences, req },
|
||||
},
|
||||
}) => {
|
||||
/**
|
||||
@@ -24,8 +24,10 @@ export const linkValidation = (
|
||||
id,
|
||||
collectionSlug,
|
||||
data: node.fields,
|
||||
documentData: data,
|
||||
fields: sanitizedFieldsWithoutText, // Sanitized in feature.server.ts
|
||||
fieldSchemaMap: undefined,
|
||||
initialBlockData: node.fields,
|
||||
operation: operation === 'create' || operation === 'update' ? operation : 'update',
|
||||
permissions: {},
|
||||
preferences,
|
||||
@@ -34,12 +36,15 @@ export const linkValidation = (
|
||||
schemaPath: '',
|
||||
})
|
||||
|
||||
let errorPaths: string[] = []
|
||||
const errorPathsSet = new Set<string>()
|
||||
for (const fieldKey in result) {
|
||||
if (result[fieldKey].errorPaths) {
|
||||
errorPaths = errorPaths.concat(result[fieldKey].errorPaths)
|
||||
if (result[fieldKey].errorPaths?.length) {
|
||||
for (const errorPath of result[fieldKey].errorPaths) {
|
||||
errorPathsSet.add(errorPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
const errorPaths = Array.from(errorPathsSet)
|
||||
|
||||
if (errorPaths.length) {
|
||||
return 'The following fields are invalid: ' + errorPaths.join(', ')
|
||||
|
||||
@@ -34,7 +34,7 @@ export const listReplace = (listType: ListType): ElementTransformer['replace'] =
|
||||
}
|
||||
listItem.append(...children)
|
||||
listItem.select(0, 0)
|
||||
const indent = Math.floor(match[1].length / LIST_INDENT_SIZE)
|
||||
const indent = Math.floor(match[1]!.length / LIST_INDENT_SIZE)
|
||||
if (indent) {
|
||||
listItem.setIndent(indent)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export const uploadValidation = (
|
||||
validation: {
|
||||
options: {
|
||||
id,
|
||||
data,
|
||||
operation,
|
||||
preferences,
|
||||
req,
|
||||
@@ -45,9 +46,12 @@ export const uploadValidation = (
|
||||
const result = await fieldSchemasToFormState({
|
||||
id,
|
||||
collectionSlug: node.relationTo,
|
||||
|
||||
data: node?.fields ?? {},
|
||||
documentData: data,
|
||||
fields: collection.fields,
|
||||
fieldSchemaMap: undefined,
|
||||
initialBlockData: node?.fields ?? {},
|
||||
operation: operation === 'create' || operation === 'update' ? operation : 'update',
|
||||
permissions: {},
|
||||
preferences,
|
||||
@@ -56,12 +60,15 @@ export const uploadValidation = (
|
||||
schemaPath: '',
|
||||
})
|
||||
|
||||
let errorPaths: string[] = []
|
||||
const errorPathsSet = new Set<string>()
|
||||
for (const fieldKey in result) {
|
||||
if (result[fieldKey].errorPaths) {
|
||||
errorPaths = errorPaths.concat(result[fieldKey].errorPaths)
|
||||
if (result[fieldKey].errorPaths?.length) {
|
||||
for (const errorPath of result[fieldKey].errorPaths) {
|
||||
errorPathsSet.add(errorPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
const errorPaths = Array.from(errorPathsSet)
|
||||
|
||||
if (errorPaths.length) {
|
||||
return 'The following fields are invalid: ' + errorPaths.join(', ')
|
||||
|
||||
@@ -56,6 +56,7 @@ export const RscEntryLexicalField: React.FC<
|
||||
id: args.id,
|
||||
clientFieldSchemaMap: args.clientFieldSchemaMap,
|
||||
collectionSlug: args.collectionSlug,
|
||||
documentData: args.data,
|
||||
field,
|
||||
fieldSchemaMap: args.fieldSchemaMap,
|
||||
lexicalFieldSchemaPath: schemaPath,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user