Compare commits

..

7 Commits

Author SHA1 Message Date
Sasha
4283bebc37 chore: union with Client / VercelClient 2024-10-18 22:28:11 +03:00
Sasha
4082680099 fix(drizzle): expose db.drizzle.$client type 2024-10-17 23:35:05 +03:00
Sasha
9056b9fe9b fix(db-mongodb): virtual fields within row / collapsible / tabs (#8733)
Fixes https://github.com/payloadcms/payload/issues/8674
2024-10-17 16:23:45 -04:00
Paul
82f278931b fix(ui): padding on relationship fields when no options or loading text is displayed (#8767) 2024-10-17 19:46:28 +00:00
Germán Jabloñski
a7895560a6 fix(richtext-lexical): fix CLS on collapsed/expanded state of Lexical blocks (#8029)
## Description

The goal is to reduce CLS on collapsed/expanded state of Lexical blocks.
That state is stored as "preferences" and is different for each user.
As Payload has been working so far, if the state of a Lexical block was
"collapsed", it was rendered expanded, and when the correct state was
obtained from the server, it was collapsed producing a CLS with a poor
UX.

My original idea was to get the correct state on the first render.
Talking to @AlessioGr and @jmikrut, we saw that this can be a bit
difficult or challenging, since the feature on the server does not have
access to the Payload object, nor to the user who is making the request.

I was instructed to mimic the behavior of blocks not in Lexial
(`\ui\src\fields\Collapsible\index.tsx`). There the blocks are rendered
after the collapse/expand state is obtained in a useEffect.

In the following video, the case where the first block is collapsed is
shown, rendering everything with a "fast 4G" connection throttle.


https://github.com/user-attachments/assets/078e37c7-6540-4183-a266-bd751cc9d78e

Yes, it's a slight improvement over current behavior. But it could be
much better. There are request waterfalls several levels deep, and
plenty of CLS still.

Unless there is some very big tradeoff that I'm not aware of, I think
it's worth exposing the Payload object and the user to the server in
order to get the correct state on the first render.

And if that's not possible and the request has to be made on the client,
I think initializing the state as collapsed and then expanding it is
better than not showing it at all.

Another observation that is evident from the video, is that there are
several sources or causes of CLS besides the expanded/collapsed state of
the blocks.

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
2024-10-17 19:36:14 +00:00
Paul
1f0d8da182 fix(plugin-seo): description and title fields now respect given minLength and maxLength rules for passing validation (#8765)
Previously minLength or maxLength was not being respected and the
components would use default values only.
2024-10-17 19:28:54 +00:00
Said Akhrarov
c91f21bb78 docs: fix incorrect link for outside-nextjs in local-api importing it section (#8764)
Currently in the `beta` docs at the bottom of [Local API Overview Import
It
section](https://payloadcms.com/docs/beta/local-api/overview#importing-it)
there is a link for _Outside Nextjs_ which incorrectly sends you to
`/docs/beta/beta/local-api/outside-nextjs` instead of
`docs/beta/local-api/outside-nextjs`.

Interestingly enough, a `Not Found` component/message is not rendered
and instead you see a blank screen.

---------

Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2024-10-17 13:13:37 -06:00
24 changed files with 159 additions and 44 deletions

View File

@@ -145,7 +145,7 @@ Instead, we utilize component paths to reference React Components. This method e
When constructing the `ClientConfig`, Payload uses the component paths as keys to fetch the corresponding React Component imports from the Import Map. It then substitutes the `PayloadComponent` with a `MappedComponent`. A `MappedComponent` includes the React Component and additional metadata, such as whether it's a server or a client component and which props it should receive. These components are then rendered through the `<RenderComponent />` component within the Payload Admin Panel.
Import maps are regenerated whenever you modify any element related to component paths. This regeneration occurs at startup and whenever Hot Module Replacement (HMR) runs. If the import maps fail to regenerate during HMR, you can restart your application and execute the `payload generate:importmap` command to manually create a new import map. If you encounter any errors running this command, see the [Troubleshooting](/docs/beta/local-api/outside-nextjs#troubleshooting) section.
Import maps are regenerated whenever you modify any element related to component paths. This regeneration occurs at startup and whenever Hot Module Replacement (HMR) runs. If the import maps fail to regenerate during HMR, you can restart your application and execute the `payload generate:importmap` command to manually create a new import map. If you encounter any errors running this command, see the [Troubleshooting](../local-api/outside-nextjs#troubleshooting) section.
### Component paths in external packages

View File

@@ -97,7 +97,7 @@ Cookies can cross subdomains without being considered third party cookies, for e
##### 2. Configure cookies
If option 1 isn't possible, then you can get around this limitation by [configuring your cookies](https://payloadcms.com/docs/beta/authentication/overview#config-options) on your authentication collection to achieve the following setup:
If option 1 isn't possible, then you can get around this limitation by [configuring your cookies](./overview#config-options) on your authentication collection to achieve the following setup:
```
SameSite: None // allows the cookie to cross domains
@@ -122,7 +122,7 @@ Configuration example:
},
```
If you're configuring [cors](https://payloadcms.com/docs/beta/production/preventing-abuse#cross-origin-resource-sharing-cors) in your Payload config, you won't be able to use a wildcard anymore, you'll need to specify the list of allowed domains.
If you're configuring [cors](../production/preventing-abuse#cross-origin-resource-sharing-cors) in your Payload config, you won't be able to use a wildcard anymore, you'll need to specify the list of allowed domains.
<Banner type="success">

View File

@@ -84,7 +84,7 @@ export default buildConfig({
## Email
Powered by [Resend](https://resend.com), Payload Cloud comes with integrated email support out of the box. No configuration is needed, and you can use `payload.sendEmail()` to send email right from your Payload app. To learn more about sending email with Payload, checkout the [Email Configuration](https://payloadcms.com/docs/email/overview) overview.
Powered by [Resend](https://resend.com), Payload Cloud comes with integrated email support out of the box. No configuration is needed, and you can use `payload.sendEmail()` to send email right from your Payload app. To learn more about sending email with Payload, checkout the [Email Configuration](../email/overview) overview.
If you are on the Pro or Enterprise plan, you can add your own custom Email domain name. From the Email page of your projects Settings, add the domain you wish to use for email delivery. This will generate a set of DNS records. Add these records to your DNS provider and click verify to check that your records are resolving properly. Once verified, your emails will now be sent from your custom domain name.

View File

@@ -157,7 +157,7 @@ You can disable this setting and solely use migrations to manage your local deve
For this reason, we suggest that you leave `push` as its default setting and treat your local dev database as a sandbox.
For more information about push mode and prototyping in development, [click here](/docs/beta/database/postgres#prototyping-in-dev-mode).
For more information about push mode and prototyping in development, [click here](./postgres#prototyping-in-dev-mode).
The typical workflow in Payload is to build out your Payload configs, install plugins, and make progress in development mode - allowing Drizzle to push your changes to your local database for you. Once you're finished, you can create a migration.

View File

@@ -99,7 +99,7 @@ Alternatively, you can disable `push` and rely solely on migrations to keep your
In Postgres, migrations are a fundamental aspect of working with Payload and you should become familiar with how they work.
For more information about migrations, [click here](/docs/beta/database/migrations#when-to-run-migrations).
For more information about migrations, [click here](./migrations#when-to-run-migrations).
## Drizzle schema hooks

View File

@@ -78,7 +78,7 @@ Alternatively, you can disable `push` and rely solely on migrations to keep your
In SQLite, migrations are a fundamental aspect of working with Payload and you should become familiar with how they work.
For more information about migrations, [click here](/docs/beta/database/migrations#when-to-run-migrations).
For more information about migrations, [click here](./migrations#when-to-run-migrations).
## Drizzle schema hooks

View File

@@ -71,7 +71,7 @@ import config from '@payload-config'
const payload = await getPayload({ config })
```
Both options function in exactly the same way outside of one having HMR support and the other not. For more information about using Payload outside of Next.js, [click here](/docs/beta/local-api/outside-nextjs).
Both options function in exactly the same way outside of one having HMR support and the other not. For more information about using Payload outside of Next.js, [click here](./outside-nextjs).
## Local options available

View File

@@ -138,7 +138,7 @@ const beforeEmail: BeforeEmail<FormSubmission> = (emailsToSend, beforeChangePara
### `defaultToEmail`
Provide a fallback for the email address to send form submissions to. If the email in form configuration does not have a to email set, this email address will be used. If this is not provided then it falls back to the `defaultFromAddress` in your [email configuration](https://payloadcms.com/docs/beta/email/overview).
Provide a fallback for the email address to send form submissions to. If the email in form configuration does not have a to email set, this email address will be used. If this is not provided then it falls back to the `defaultFromAddress` in your [email configuration](../email/overview).
```ts
// payload.config.ts
@@ -412,7 +412,7 @@ formBuilder({
## Email
This plugin relies on the [email configuration](https://payloadcms.com/docs/beta/email/overview) defined in your payload configuration. It will read from your config and attempt to send your emails using the credentials provided.
This plugin relies on the [email configuration](../email/overview) defined in your payload configuration. It will read from your config and attempt to send your emails using the credentials provided.
### Email formatting

View File

@@ -276,6 +276,10 @@ OverviewField({
})
```
<Banner type="info">
Tip: You can override the length rules by changing the minLength and maxLength props on the fields. In the case of the OverviewField you can use `titleOverrides` and `descriptionOverrides` to override the length rules.
</Banner>
## TypeScript
All types can be directly imported:

View File

@@ -246,6 +246,10 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.fields.forEach((subField: Field) => {
if (fieldIsVirtual(subField)) {
return
}
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
@@ -501,6 +505,10 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
buildSchemaOptions: BuildSchemaOptions,
): void => {
field.fields.forEach((subField: Field) => {
if (fieldIsVirtual(subField)) {
return
}
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {
@@ -545,6 +553,9 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
): void => {
field.tabs.forEach((tab) => {
if (tabHasName(tab)) {
if (fieldIsVirtual(tab)) {
return
}
const baseSchema = {
type: buildSchema(config, tab.fields, {
disableUnique: buildSchemaOptions.disableUnique,
@@ -562,6 +573,9 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
})
} else {
tab.fields.forEach((subField: Field) => {
if (fieldIsVirtual(subField)) {
return
}
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
if (addFieldSchema) {

View File

@@ -9,7 +9,7 @@ import type {
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { DrizzleConfig } from 'drizzle-orm'
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
import type { Pool, PoolConfig } from 'pg'
import type { Client, Pool, PoolConfig } from 'pg'
export type Args = {
/**
@@ -62,7 +62,9 @@ declare module 'payload' {
afterSchemaInit: PostgresSchemaHook[]
beforeSchemaInit: PostgresSchemaHook[]
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
drizzle: PostgresDB
drizzle: {
$client: Client | Pool
} & PostgresDB
enums: Record<string, GenericEnum>
/**
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name

View File

@@ -1,5 +1,4 @@
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { Connect } from 'payload'
import { createClient } from '@libsql/client'

View File

@@ -167,7 +167,9 @@ declare module 'payload' {
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
DrizzleAdapter {
beginTransaction: (options?: SQLiteTransactionConfig) => Promise<null | number | string>
drizzle: LibSQLDatabase
drizzle: { $client: Client } & LibSQLDatabase<
Record<string, GenericRelation | GenericTable> & Record<string, unknown>
>
/**
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
* Used for returning properly formed errors from unique fields

View File

@@ -7,7 +7,7 @@ import type {
PostgresSchemaHook,
} from '@payloadcms/drizzle/postgres'
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { VercelPool, VercelPostgresPoolConfig } from '@vercel/postgres'
import type { VercelClient, VercelPool, VercelPostgresPoolConfig } from '@vercel/postgres'
import type { DrizzleConfig } from 'drizzle-orm'
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
@@ -56,6 +56,9 @@ export type Args = {
}
export type VercelPostgresAdapter = {
drizzle: {
$client: VercelClient | VercelPool
} & PostgresDB
pool?: VercelPool
poolOptions?: Args['pool']
} & BasePostgresAdapter
@@ -67,7 +70,9 @@ declare module 'payload' {
afterSchemaInit: PostgresSchemaHook[]
beforeSchemaInit: PostgresSchemaHook[]
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
drizzle: PostgresDB
drizzle: {
$client: VercelClient | VercelPool
} & PostgresDB
enums: Record<string, GenericEnum>
/**
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name

View File

@@ -21,7 +21,7 @@ import type {
} from 'drizzle-orm/pg-core'
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
import type { Payload, PayloadRequest } from 'payload'
import type { ClientConfig, QueryResult } from 'pg'
import type { Client, ClientConfig, Pool, QueryResult } from 'pg'
import type { extendDrizzleTable, Operators } from '../index.js'
import type { BuildQueryJoinAliases, DrizzleAdapter, TransactionPg } from '../types.js'
@@ -134,7 +134,9 @@ export type BasePostgresAdapter = {
defaultDrizzleSnapshot: DrizzleSnapshotJSON
deleteWhere: DeleteWhere
disableCreateDatabase: boolean
drizzle: PostgresDB
drizzle: {
$client: Client | Pool
} & PostgresDB
dropDatabase: DropDatabase
enums: Record<string, GenericEnum>
execute: Execute<unknown>

View File

@@ -22,7 +22,7 @@ import type { GenerateDescription } from '../../types.js'
import { defaults } from '../../defaults.js'
import { LengthIndicator } from '../../ui/LengthIndicator.js'
const { maxLength, minLength } = defaults.description
const { maxLength: maxLengthDefault, minLength: minLengthDefault } = defaults.description
type MetaDescriptionProps = {
readonly hasGenerateDescriptionFn: boolean
@@ -35,6 +35,8 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
components: { Label },
},
label,
maxLength: maxLengthFromProps,
minLength: minLengthFromProps,
required,
},
hasGenerateDescriptionFn,
@@ -55,6 +57,9 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
const { getData } = useForm()
const docInfo = useDocumentInfo()
const maxLength = maxLengthFromProps || maxLengthDefault
const minLength = minLengthFromProps || minLengthDefault
const field: FieldType<string> = useField({
path: pathFromContext,
} as Options)

View File

@@ -23,7 +23,7 @@ import { defaults } from '../../defaults.js'
import { LengthIndicator } from '../../ui/LengthIndicator.js'
import '../index.scss'
const { maxLength, minLength } = defaults.title
const { maxLength: maxLengthDefault, minLength: minLengthDefault } = defaults.title
type MetaTitleProps = {
readonly hasGenerateTitleFn: boolean
@@ -36,6 +36,8 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
components: { Label },
},
label,
maxLength: maxLengthFromProps,
minLength: minLengthFromProps,
required,
},
field: fieldFromProps,
@@ -60,6 +62,9 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
const { getData } = useForm()
const docInfo = useDocumentInfo()
const minLength = minLengthFromProps || minLengthDefault
const maxLength = maxLengthFromProps || maxLengthDefault
const { errorMessage, setValue, showError, value } = field
const regenerateTitle = useCallback(async () => {

View File

@@ -10,25 +10,32 @@ import type { PluginSEOTranslationKeys, PluginSEOTranslations } from '../../tran
import { defaults } from '../../defaults.js'
const {
description: { maxLength: maxDesc, minLength: minDesc },
title: { maxLength: maxTitle, minLength: minTitle },
description: { maxLength: maxDescDefault, minLength: minDescDefault },
title: { maxLength: maxTitleDefault, minLength: minTitleDefault },
} = defaults
type OverviewProps = {
descriptionOverrides?: {
maxLength?: number
minLength?: number
}
descriptionPath?: string
imagePath?: string
titleOverrides?: {
maxLength?: number
minLength?: number
}
titlePath?: string
} & UIField
export const OverviewComponent: React.FC<OverviewProps> = ({
descriptionOverrides,
descriptionPath: descriptionPathFromContext,
imagePath: imagePathFromContext,
titleOverrides,
titlePath: titlePathFromContext,
}) => {
const {
// dispatchFields,
getFields,
} = useForm()
const { getFields } = useForm()
const descriptionPath = descriptionPathFromContext || 'meta.description'
const titlePath = titlePathFromContext || 'meta.title'
@@ -47,6 +54,11 @@ export const OverviewComponent: React.FC<OverviewProps> = ({
const [descIsValid, setDescIsValid] = useState<boolean | undefined>()
const [imageIsValid, setImageIsValid] = useState<boolean | undefined>()
const minDesc = descriptionOverrides?.minLength || minDescDefault
const maxDesc = descriptionOverrides?.maxLength || maxDescDefault
const minTitle = titleOverrides?.minLength || minTitleDefault
const maxTitle = titleOverrides?.maxLength || maxTitleDefault
const resetAll = useCallback(() => {
const fields = getFields()
const fieldsWithoutMeta = fields

View File

@@ -1,6 +1,10 @@
import type { UIField } from 'payload'
interface FieldFunctionProps {
descriptionOverrides?: {
maxLength?: number
minLength?: number
}
/**
* Path to the description field to use for the preview
*
@@ -14,6 +18,10 @@ interface FieldFunctionProps {
*/
imagePath?: string
overrides?: Partial<UIField>
titleOverrides?: {
maxLength?: number
minLength?: number
}
/**
* Path to the title field to use for the preview
*
@@ -25,9 +33,11 @@ interface FieldFunctionProps {
type FieldFunction = ({ overrides }: FieldFunctionProps) => UIField
export const OverviewField: FieldFunction = ({
descriptionOverrides,
descriptionPath,
imagePath,
overrides,
titleOverrides,
titlePath,
}) => {
return {
@@ -37,8 +47,10 @@ export const OverviewField: FieldFunction = ({
components: {
Field: {
clientProps: {
descriptionOverrides,
descriptionPath,
imagePath,
titleOverrides,
titlePath,
},
path: '@payloadcms/plugin-seo/client#OverviewComponent',

View File

@@ -17,7 +17,7 @@ import {
} from '@payloadcms/ui'
import { dequal } from 'dequal/lite'
import { $getNodeByKey } from 'lexical'
import React, { useCallback } from 'react'
import React, { useCallback, useEffect } from 'react'
import type { LexicalRichTextFieldProps } from '../../../../types.js'
import type { BlockFields } from '../../server/nodes/BlocksNode.js'
@@ -70,21 +70,15 @@ export const BlockContent: React.FC<Props> = (props) => {
// is important to consider for the data path used in setDocFieldPreferences
const { getDocPreferences, setDocFieldPreferences } = useDocumentInfo()
const [isCollapsed, setIsCollapsed] = React.useState<boolean>(() => {
let initialState = false
const [isCollapsed, setIsCollapsed] = React.useState<boolean>()
useEffect(() => {
void getDocPreferences().then((currentDocPreferences) => {
const currentFieldPreferences = currentDocPreferences?.fields[field.name]
const collapsedArray = currentFieldPreferences?.collapsed
if (collapsedArray && collapsedArray.includes(formData.id)) {
initialState = true
setIsCollapsed(true)
}
setIsCollapsed(collapsedArray ? collapsedArray.includes(formData.id) : false)
})
return initialState
})
}, [field.name, formData.id, getDocPreferences])
const hasSubmitted = useFormSubmitted()
@@ -180,6 +174,10 @@ export const BlockContent: React.FC<Props> = (props) => {
})
}, [editor, nodeKey])
if (typeof isCollapsed !== 'boolean') {
return null
}
return (
<React.Fragment>
<Collapsible

View File

@@ -13,6 +13,10 @@
flex-wrap: nowrap;
}
.rs__menu-notice {
padding: base(0.5) base(0.6);
}
.rs__indicators {
gap: calc(var(--base) / 4);
}

View File

@@ -318,6 +318,42 @@ export default buildConfigWithDefaults({
virtual: true,
fields: [],
},
{
type: 'row',
fields: [
{
type: 'text',
name: 'textWithinRow',
virtual: true,
},
],
},
{
type: 'collapsible',
fields: [
{
type: 'text',
name: 'textWithinCollapsible',
virtual: true,
},
],
label: 'Colllapsible',
},
{
type: 'tabs',
tabs: [
{
label: 'tab',
fields: [
{
type: 'text',
name: 'textWithinTabs',
virtual: true,
},
],
},
],
},
],
},
],

View File

@@ -810,6 +810,21 @@ describe('database', () => {
expect(resLocal.textHooked).toBe('hooked')
})
it('should not save a nested field to tabs/row/collapsible with virtual: true to the db', async () => {
const res = await payload.create({
data: {
textWithinCollapsible: '1',
textWithinRow: '2',
textWithinTabs: '3',
},
collection: 'fields-persistance',
})
expect(res.textWithinCollapsible).toBeUndefined()
expect(res.textWithinRow).toBeUndefined()
expect(res.textWithinTabs).toBeUndefined()
})
})
it('should not allow to query by a field with `virtual: true`', async () => {

View File

@@ -60,6 +60,7 @@ export interface UserAuthOperations {
export interface Post {
id: string;
title: string;
hasTransaction?: boolean | null;
throwAfterChange?: boolean | null;
updatedAt: string;
createdAt: string;
@@ -225,6 +226,9 @@ export interface FieldsPersistance {
id?: string | null;
}[]
| null;
textWithinRow?: string | null;
textWithinCollapsible?: string | null;
textWithinTabs?: string | null;
updatedAt: string;
createdAt: string;
}
@@ -289,14 +293,10 @@ export interface PayloadLockedDocument {
value: string | User;
} | null);
globalSlug?: string | null;
_lastEdited: {
user: {
relationTo: 'users';
value: string | User;
};
editedAt?: string | null;
user: {
relationTo: 'users';
value: string | User;
};
isLocked?: boolean | null;
updatedAt: string;
createdAt: string;
}