Commit Graph

16 Commits

Author SHA1 Message Date
Dallas Opelt
285fc8cf7e fix: presentational-field types incorrectly exposing hooks (#11514)
### What?

Presentational Fields such as
[Row](https://payloadcms.com/docs/fields/row) are described as only
effecting the admin panel. If they do not impact data, their types
should not include hooks in the fields config.

### Why?

Developers can currently assign hooks to these fields, expecting them to
work, when in reality they are not called.

### How?

Omit `hooks` from `FieldBase`

Fixes #11507

---------

Co-authored-by: German Jablonski <43938777+GermanJablo@users.noreply.github.com>
2025-09-10 18:05:59 +00:00
Alessio Gravili
ad2564e5fa fix: ensure scheduling by default only handles default queue, add allQueues config to autoRun (#13395)
By default, `payload.jobs.run` only runs jobs from the `default` queue
(since https://github.com/payloadcms/payload/pull/12799). It exposes an
`allQueues` property to run jobs from all queues.

For handling schedules (`payload.jobs.handleSchedules` and
`config.jobs.autoRun`), this behaves differently - jobs are run from all
queues by default, and no `allQueues` property exists.

This PR adds an `allQueues` property to scheduling, as well as changes
the default behavior to only handle schedules for the `default` queue.
That way, the behavior of running and scheduling jobs matches.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210982048221260
2025-08-12 11:55:17 -04:00
Germán Jabloñski
6ec21a53ff chore: migrate to TypeScript strict in Payload package (enable strictNullChecks) - #3 (#12586)
Important: An intentional effort is being made during migration to not
modify runtime behavior. This implies that there will be several
assertions, non-null assertions, and @ts-expect-error. This philosophy
applies only to migrating old code to TypeScript strict, not to writing
new code. For a more detailed justification for this reasoning,
https://github.com/payloadcms/payload/pull/11840#discussion_r2021975897.

In this PR, instead of following the approach of migrating a subset of
files, I'm migrating all files by disabling a specific rule. In this
case, `strictNullChecks`.

`strictNullChecks` is a good rule to start the migration with because
it's easy to silence with non-null assertions or optional chainings.
Additionally, almost all ts strict errors are due to this rule.

This PR improves 200+ files, leaving only 68 remaining to migrate to
strict mode in the payload package.
2025-06-03 14:43:37 +00:00
Paul
e258cd73ef feat: allow group fields to have an optional name (#12318)
Adds the ability to completely omit `name` from group fields now so that
they're entirely presentational.

New config:
```ts
import type { CollectionConfig } from 'payload'

export const ExampleCollection: CollectionConfig = {
  slug: 'posts',
  fields: [
    {
      label: 'Page header',
      type: 'group', // required
      fields: [
        {
          name: 'title',
          type: 'text',
          required: true,
        },
      ],
    },
  ],
}
```

will create
<img width="332" alt="image"
src="https://github.com/user-attachments/assets/10b4315e-92d6-439e-82dd-7c815a844035"
/>


but the data response will still be

```
{
    "createdAt": "2025-05-05T13:42:20.326Z",
    "updatedAt": "2025-05-05T13:42:20.326Z",
    "title": "example post",
    "id": "6818c03ce92b7f92be1540f0"

}
```

Checklist:
- [x] Added int tests
- [x] Modify mongo, drizzle and graphql packages
- [x] Add type tests
- [x] Add e2e tests
2025-05-14 23:45:34 +00:00
Alessio Gravili
dc9e8fa655 refactor: simplify running field hooks (#11372)
Previously, we were quite frequently using `.reduce()` to sequentially run field hooks. This PR replaces them with simple `for` loops, which is less overhead, less code, less confusing and simpler to understand.

Additionally, it refactors `mergeLocaleActions` which previously was unnecessarily complex. They no longer entail async code, thus we no longer have to juggle with promises
2025-02-24 18:37:33 +00:00
Alessio Gravili
e6fea1d132 fix: localized fields within block references were not handled properly if any parent is localized (#11207)
The `localized` properly was not stripped out of referenced block fields, if any parent was localized. For normal fields, this is done in sanitizeConfig. As the same referenced block config can be used in both a localized and non-localized config, we are not able to strip it out inside sanitizeConfig by modifying the block config.

Instead, this PR had to bring back tedious logic to handle it everywhere the `field.localized` property is accessed. For backwards-compatibility, we need to keep the existing sanitizeConfig logic. In 4.0, we should remove it to benefit from better test coverage of runtime field.localized handling - for now, this is done for our test suite using the `PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY` flag.
2025-02-17 19:50:32 +00:00
Alessio Gravili
4c8cafd6a6 perf: deduplicate blocks used in multiple places using new config.blocks property (#10905)
If you have multiple blocks that are used in multiple places, this can quickly blow up the size of your Payload Config. This will incur a performance hit, as more data is
1.  sent to the client (=> bloated `ClientConfig` and large initial html) and
2. processed on the server (permissions are calculated every single time you navigate to a page - this iterates through all blocks you have defined, even if they're duplicative)

This can be optimized by defining your block **once** in your Payload Config, and just referencing the block slug whenever it's used, instead of passing the entire block config. To do this, the block can be defined in the `blocks` array of the Payload Config. The slug can then be passed to the `blockReferences` array in the Blocks Field - the `blocks` array has to be empty for compatibility reasons.

```ts
import { buildConfig } from 'payload'
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical'

// Payload Config
const config = buildConfig({
  // Define the block once
  blocks: [
    {
      slug: 'TextBlock',
      fields: [
        {
          name: 'text',
          type: 'text',
        },
      ],
    },
  ],
  collections: [
    {
      slug: 'collection1',
      fields: [
        {
          name: 'content',
          type: 'blocks',
          // Reference the block by slug
          blockReferences: ['TextBlock'],
          blocks: [], // Required to be empty, for compatibility reasons
        },
      ],
    },
     {
      slug: 'collection2',
      fields: [
        {
          name: 'editor',
          type: 'richText',
          editor: lexicalEditor({
            BlocksFeature({
              // Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
              blocks: ['TextBlock'],
            })
          })
        },
      ],
    },
  ],
})
```

## v4.0 Plans

In 4.0, we will remove the `blockReferences` property, and allow string block references to be passed directly to the blocks `property`. Essentially, we'd remove the `blocks` property and rename `blockReferences` to `blocks`.

The reason we opted to a new property in this PR is to avoid breaking changes. Allowing strings to be passed to the `blocks` property will prevent plugins that iterate through fields / blocks from compiling.

## PR Changes

- Testing: This PR introduces a plugin that automatically converts blocks to block references. This is done in the fields__blocks test suite, to run our existing test suite using block references.

- Block References support: Most changes are similar. Everywhere we iterate through blocks, we have to now do the following:
1. Check if `field.blockReferences` is provided. If so, only iterate through that.
2. Check if the block is an object (= actual block), or string
3. If it's a string, pull the actual block from the Payload Config or from `payload.blocks`.

The exception is config sanitization and block type generations. This PR optimizes them so that each block is only handled once, instead of every time the block is referenced.

## Benchmarks

60 Block fields, each block field having the same 600 Blocks.

### Before:
**Initial HTML:** 195 kB
**Generated types:** takes 11 minutes, 461,209 lines

https://github.com/user-attachments/assets/11d49a4e-5414-4579-8050-e6346e552f56

### After:
**Initial HTML:** 73.6 kB
**Generated types:** takes 2 seconds, 35,810 lines

https://github.com/user-attachments/assets/3eab1a99-6c29-489d-add5-698df67780a3

### After Permissions Optimization (follow-up PR)
Initial HTML: 73.6 kB

https://github.com/user-attachments/assets/a909202e-45a8-4bf6-9a38-8c85813f1312


## Future Plans

1. This PR does not yet deduplicate block references during permissions calculation. We'll optimize that in a separate PR, as this one is already large enough
2. The same optimization can be done to deduplicate fields. One common use-case would be link field groups that may be referenced in multiple entities, outside of blocks. We might explore adding a new `fieldReferences` property, that allows you to reference those same `config.blocks`.
2025-02-14 00:08:20 +00:00
Germán Jabloñski
6eee787493 chore: add typescript-strict-plugin to the payload package for incremental file-by-file migration [skip lint] (#11133)
### What?

Implement the
[typescript-strict-plugin](https://github.com/allegro/typescript-strict-plugin)
plugin in the payload (core) package.

### Why?

1. One strategy for incremental migration is to enable strictness rules
in tsconfig, fix some errors, and push them without committing the
changes to tsconfig.json. However, this is not feasible for a package as
large as Payload that has over 1000 typescript errors. Until the work is
done, new contributions would undo the work being done.
2. Even if no migration work is done after this PR, this change already
improves the strictness of the package. 89 of the 311 files within the
package already satisfy strict mode. This PR only adds a comment
`@ts-strict-ignore` to files that had at least one compilation error.
This way, the propagation of errors in those files is stopped.
3. New files created in the package are strict by default (this was the
main improvement in version 2 of `typescript-strict-plugin`).

I recommend starting the migration with this package because it is the
one that almost all the others depend on. Once we finish this package,
we can repeat the same strategy on another one, or use the strategy I
mentioned in point 1 if the package is small.

### Note

If you don't see errors in the IDE when you uncomment `//
@ts-strict-ignore`, try restarting the typescript server or VSCode


### How to contribute to the migration ❤️

1. Remove `// @ts-strict-ignore` comments from 1 or more files
2. Fix the pending errors (they should appear in your IDE's intellisense
or when running `cd packages/payload` + `pnpm build:types`
3. Submit your PR!

Important: You don't need to fix everything at once! Furthermore, I
recommend breaking this down into very small PRs to trace potential
issues later if there are any. So if you have 5 minutes, tackle a small
file—every bit counts! 🤗
2025-02-12 11:06:03 -05:00
Alessio Gravili
155f9f80fe feat: add siblingFields arg to field hooks (#11117)
This PR adds a new `siblingFields` argument to field hooks. This allows
us to dramatically simplify the `lexicalHTML` field, which previously
had to use a complex `findFieldPathAndSiblingFields` function that
deeply traverses the entire `CollectionConfig` just to find the sibling
fields.
2025-02-11 14:22:31 -07:00
Alessio Gravili
ae32c555ac fix(richtext-lexical): ensure sub-fields have access to full document data in form state (#9869)
Fixes https://github.com/payloadcms/payload/issues/10940

This PR does the following:
- adds a `useDocumentForm` hook to access the document Form. Useful if
you are within a sub-Form
- ensure the `data` property passed to field conditions, read access
control, validation and filterOptions is always the top-level document
data. Previously, for fields within lexical blocks/links/upload, this
incorrectly was the lexical block-level data.
- adds a `blockData` property to hooks, field conditions,
read/update/create field access control, validation and filterOptions
for all fields. This allows you to access the data of the nearest parent
block, which is especially useful for lexical sub-fields. Users that
were previously depending on the incorrect behavior of the `data`
property in order to access the data of the lexical block can now switch
to the new `blockData` property
2025-02-06 13:49:17 -05:00
Jacob Fletcher
0acaf8a7f7 fix: field paths within hooks (#10638)
Field paths within hooks are not correct.

For example, an unnamed tab containing a group field and nested text
field should have the path:
- `myGroupField.myTextField`

However, within hooks that path is formatted as:
- `_index-1.myGroupField.myTextField`

The leading index shown above should not exist, as this field is
considered top-level since it is located within an unnamed tab.

This discrepancy is only evident through the APIs themselves, such as
when creating a request with invalid data and reading the validation
errors in the response. Form state contains proper field paths, which is
ultimately why this issue was never caught. This is because within the
admin panel we merge the API response with the current form state,
obscuring the underlying issue. This becomes especially obvious in
#10580, where we no longer initialize validation errors within form
state until the form has been submitted, and instead rely solely on the
API response for the initial error state.

Here's comprehensive example of how field paths _should_ be formatted:

```
{
  // ...
  fields: [
    {
      // path: 'topLevelNamedField'
      // schemaPath: 'topLevelNamedField'
      // indexPath: ''
      name: 'topLevelNamedField',
      type: 'text',
    },
    {
      // path: 'array'
      // schemaPath: 'array'
      // indexPath: ''
      name: 'array',
      type: 'array',
      fields: [
        {
          // path: 'array.[n].fieldWithinArray'
          // schemaPath: 'array.fieldWithinArray'
          // indexPath: ''
          name: 'fieldWithinArray',
          type: 'text',
        },
        {
          // path: 'array.[n].nestedArray'
          // schemaPath: 'array.nestedArray'
          // indexPath: ''
          name: 'nestedArray',
          type: 'array',
          fields: [
            {
              // path: 'array.[n].nestedArray.[n].fieldWithinNestedArray'
              // schemaPath: 'array.nestedArray.fieldWithinNestedArray'
              // indexPath: ''
              name: 'fieldWithinNestedArray',
              type: 'text',
            },
          ],
        },
        {
          // path: 'array.[n]._index-2'
          // schemaPath: 'array._index-2'
          // indexPath: '2'
          type: 'row',
          fields: [
            {
              // path: 'array.[n].fieldWithinRowWithinArray'
              // schemaPath: 'array._index-2.fieldWithinRowWithinArray'
              // indexPath: ''
              name: 'fieldWithinRowWithinArray',
              type: 'text',
            },
          ],
        },
      ],
    },
    {
      // path: '_index-2'
      // schemaPath: '_index-2'
      // indexPath: '2'
      type: 'row',
      fields: [
        {
          // path: 'fieldWithinRow'
          // schemaPath: '_index-2.fieldWithinRow'
          // indexPath: ''
          name: 'fieldWithinRow',
          type: 'text',
        },
      ],
    },
    {
      // path: '_index-3'
      // schemaPath: '_index-3'
      // indexPath: '3'
      type: 'tabs',
      tabs: [
        {
          // path: '_index-3-0'
          // schemaPath: '_index-3-0'
          // indexPath: '3-0'
          label: 'Unnamed Tab',
          fields: [
            {
              // path: 'fieldWithinUnnamedTab'
              // schemaPath: '_index-3-0.fieldWithinUnnamedTab'
              // indexPath: ''
              name: 'fieldWithinUnnamedTab',
              type: 'text',
            },
            {
              // path: '_index-3-0-1'
              // schemaPath: '_index-3-0-1'
              // indexPath: '3-0-1'
              type: 'tabs',
              tabs: [
                {
                  // path: '_index-3-0-1-0'
                  // schemaPath: '_index-3-0-1-0'
                  // indexPath: '3-0-1-0'
                  label: 'Nested Unnamed Tab',
                  fields: [
                    {
                      // path: 'fieldWithinNestedUnnamedTab'
                      // schemaPath: '_index-3-0-1-0.fieldWithinNestedUnnamedTab'
                      // indexPath: ''
                      name: 'fieldWithinNestedUnnamedTab',
                      type: 'text',
                    },
                  ],
                },
              ],
            },
          ],
        },
        {
          // path: 'namedTab'
          // schemaPath: '_index-3.namedTab'
          // indexPath: ''
          label: 'Named Tab',
          name: 'namedTab',
          fields: [
            {
              // path: 'namedTab.fieldWithinNamedTab'
              // schemaPath: '_index-3.namedTab.fieldWithinNamedTab'
              // indexPath: ''
              name: 'fieldWithinNamedTab',
              type: 'text',
            },
          ],
        },
      ],
    },
  ]
}
```
2025-01-27 14:41:35 -05:00
Alessio Gravili
03291472d6 chore: bump all eslint dependencies, run lint and prettier (#9128)
This fixes a peer dependency error in our monorepo, as
eslint-plugin-jsx-a11y finally supports eslint v9.

Additionally, this officially adds TypeScript 5.6 support for
typescript-eslint.
2024-11-12 10:18:22 -05:00
Jacob Fletcher
c96fa613bc feat!: on demand rsc (#8364)
Currently, Payload renders all custom components on initial compile of
the admin panel. This is problematic for two key reasons:
1. Custom components do not receive contextual data, i.e. fields do not
receive their field data, edit views do not receive their document data,
etc.
2. Components are unnecessarily rendered before they are used

This was initially required to support React Server Components within
the Payload Admin Panel for two key reasons:
1. Fields can be dynamically rendered within arrays, blocks, etc.
2. Documents can be recursively rendered within a "drawer" UI, i.e.
relationship fields
3. Payload supports server/client component composition 

In order to achieve this, components need to be rendered on the server
and passed as "slots" to the client. Currently, the pattern for this is
to render custom server components in the "client config". Then when a
view or field is needed to be rendered, we first check the client config
for a "pre-rendered" component, otherwise render our client-side
fallback component.

But for the reasons listed above, this pattern doesn't exactly make
custom server components very useful within the Payload Admin Panel,
which is where this PR comes in. Now, instead of pre-rendering all
components on initial compile, we're able to render custom components
_on demand_, only as they are needed.

To achieve this, we've established [this
pattern](https://github.com/payloadcms/payload/pull/8481) of React
Server Functions in the Payload Admin Panel. With Server Functions, we
can iterate the Payload Config and return JSX through React's
`text/x-component` content-type. This means we're able to pass
contextual props to custom components, such as data for fields and
views.

## Breaking Changes

1. Add the following to your root layout file, typically located at
`(app)/(payload)/layout.tsx`:

    ```diff
    /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
    /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
    + import type { ServerFunctionClient } from 'payload'

    import config from '@payload-config'
    import { RootLayout } from '@payloadcms/next/layouts'
    import { handleServerFunctions } from '@payloadcms/next/utilities'
    import React from 'react'

    import { importMap } from './admin/importMap.js'
    import './custom.scss'

    type Args = {
      children: React.ReactNode
    }

+ const serverFunctions: ServerFunctionClient = async function (args) {
    +  'use server'
    +  return handleServerFunctions({
    +    ...args,
    +    config,
    +    importMap,
    +  })
    + }

    const Layout = ({ children }: Args) => (
      <RootLayout
        config={config}
        importMap={importMap}
    +  serverFunctions={serverFunctions}
      >
        {children}
      </RootLayout>
    )

    export default Layout
    ```

2. If you were previously posting to the `/api/form-state` endpoint, it
no longer exists. Instead, you'll need to invoke the `form-state` Server
Function, which can be done through the _new_ `getFormState` utility:

    ```diff
    - import { getFormState } from '@payloadcms/ui'
    - const { state } = await getFormState({
    -   apiRoute: '',
    -   body: {
    -     // ...
    -   },
    -   serverURL: ''
    - })

    + const { getFormState } = useServerFunctions()
    +
    + const { state } = await getFormState({
    +   // ...
    + })
    ```

## Breaking Changes

```diff
- useFieldProps()
- useCellProps()
```

More details coming soon.

---------

Co-authored-by: Alessio Gravili <alessio@gravili.de>
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
Co-authored-by: James <james@trbl.design>
2024-11-11 13:59:05 -05:00
Sasha
f52b7c45c0 fix: type augmentation of RequestContext (#9035)
### What?

Makes this to actually work
```ts
import type { RequestContext as OriginalRequestContext } from 'payload'

declare module 'payload' {
  // Create a new interface that merges your additional fields with the original one
  export interface RequestContext extends OriginalRequestContext {
    myObject?: string
    // ...
  }
}
```
<img width="502" alt="image"
src="https://github.com/user-attachments/assets/38570d3c-e8a8-48aa-a57d-6d11e79394f5">


### Why?
This is described in our docs
https://payloadcms.com/docs/beta/hooks/context#typescript therefore it
should work.

### How?
In order to get the declaration work, we need to reuse the type from the
root file `payload/src/index.js`. Additionally, removes `RequestContext`
type duplication in both `payload/src/types/index.js` and
`payload/src/index.js`.

Fixes https://github.com/payloadcms/payload/issues/8851
2024-11-05 23:14:04 +02:00
James Mikrut
c460868e52 fix: duplication with localized arrays in unnamed tabs (#8236)
Fixes a case where in relational DBs, you can't duplicate documents if
you have localized arrays within unnamed tabs.

The `beforeDuplicate` hooks were not being run for fields within unnamed
tabs.
2024-09-16 11:43:36 -04:00
James Mikrut
5873a3db06 fix: duplicating localized nested arrays (#8220)
Fixes an issue where duplicating documents in Postgres / SQLite would
crash because of a foreign key constraint / unique ID issue when you
have nested arrays / blocks within localized arrays / blocks.

We now run `beforeDuplicate` against all locales prior to
`beforeValidate` and `beforeChange` hooks.

This PR also fixes a series of issues in Postgres / SQLite where you
have localized groups / named tabs, and then arrays / blocks within the
localized groups / named tabs.
2024-09-15 02:51:31 +00:00