Commit Graph

5 Commits

Author SHA1 Message Date
Alessio Gravili
d8dace6509 feat: conditional blocks (#13801)
This PR introduces support for conditionally setting allowable block
types via a new `field.filterOptions` property on the blocks field.

Closes the following feature requests:
https://github.com/payloadcms/payload/discussions/5348,
https://github.com/payloadcms/payload/discussions/4668 (partly)

## Example

```ts
fields: [
  {
      name: 'enabledBlocks',
      type: 'text',
      admin: {
        description:
          "Change the value of this field to change the enabled blocks of the blocksWithDynamicFilterOptions field. If it's empty, all blocks are enabled.",
      },
    },
    {
      name: 'blocksWithFilterOptions',
      type: 'blocks',
      filterOptions: ['block1', 'block2'],
      blocks: [
        {
          slug: 'block1',
          fields: [
            {
              type: 'text',
              name: 'block1Text',
            },
          ],
        },
        {
          slug: 'block2',
          fields: [
            {
              type: 'text',
              name: 'block2Text',
            },
          ],
        },
        {
          slug: 'block3',
          fields: [
            {
              type: 'text',
              name: 'block3Text',
            },
          ],
        },
      ],
    },
    {
      name: 'blocksWithDynamicFilterOptions',
      type: 'blocks',
      filterOptions: ({ siblingData: _siblingData, data }) => {
        const siblingData = _siblingData as { enabledBlocks: string }

        if (siblingData?.enabledBlocks !== data?.enabledBlocks) {
          // Just an extra assurance that the field is working as intended
          throw new Error('enabledBlocks and siblingData.enabledBlocks must be identical')
        }
        return siblingData?.enabledBlocks?.length ? [siblingData.enabledBlocks] : true
      },
      blocks: [
        {
          slug: 'block1',
          fields: [
            {
              type: 'text',
              name: 'block1Text',
            },
          ],
        },
        {
          slug: 'block2',
          fields: [
            {
              type: 'text',
              name: 'block2Text',
            },
          ],
        },
        {
          slug: 'block3',
          fields: [
            {
              type: 'text',
              name: 'block3Text',
            },
          ],
        },
      ],
    },
]
```


https://github.com/user-attachments/assets/e38a804f-22fa-4fd2-a6af-ba9b0a5a04d2

# Rationale

## Why not `block.condition`?

- Individual blocks are often reused in multiple contexts, where the
logic for when they should be available may differ. It’s more
appropriate for the blocks field (typically tied to a single collection)
to determine availability.
- Hiding existing blocks when they no longer satisfy a condition would
cause issues - for example, reordering blocks would break or cause block
data to disappear. Instead, this implementation ensures consistency by
throwing a validation error if a block is no longer allowed. This aligns
with the behavior of `filterOptions` in relationship fields, rather than
`condition`.

## Why not call it `blocksFilterOptions`?

Although the type differs from relationship fields, this property is
named `filterOptions` (and not `blocksFilterOptions`) for consistency
across field types. For example, the Select field also uses
`filterOptions` despite its type being unique.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211334752795631
2025-09-22 14:20:25 -07:00
Alessio Gravili
545d870650 chore: fix various e2e test setup issues (#12670)
I noticed a few issues when running e2e tests that will be resolved by
this PR:

- Most important: for some test suites (fields, fields-relationship,
versions, queues, lexical), the database was cleared and seeded
**twice** in between each test run. This is because the onInit function
was running the clear and seed script, when it should only have been
running the seed script. Clearing the database / the snapshot workflow
is being done by the reInit endpoint, which then calls onInit to seed
the actual data.
- The slowest part of `clearAndSeedEverything` is recreating indexes on
mongodb. This PR slightly improves performance here by:
- Skipping this process for the built-in `['payload-migrations',
'payload-preferences', 'payload-locked-documents']` collections
- Previously we were calling both `createIndexes` and `ensureIndexes`.
This was unnecessary - `ensureIndexes` is a deprecated alias of
`createIndexes`. This PR changes it to only call `createIndexes`
- Makes the reinit endpoint accept GET requests instead of POST requests
- this makes it easier to debug right in the browser
- Some typescript fixes
- Adds a `dev:memorydb` script to the package.json. For some reason,
`dev` is super unreliable on mongodb locally when running e2e tests - it
frequently fails during index creation. Using the memorydb fixes this
issue, with the bonus of more closely resembling the CI environment
- Previously, you were unable to run test suites using turbopack +
postgres. This fixes it, by explicitly installing `pg` as devDependency
in our monorepo
- Fixes jest open handles warning
2025-06-04 17:34:37 -03:00
Germán Jabloñski
a66f90ebb6 chore: separate Lexical tests into dedicated suite (#12047)
Lexical tests comprise almost half of the collections in the fields
suite, and are starting to become complex to manage.

They are sometimes related to other auxiliary collections, so
refactoring one test sometimes breaks another, seemingly unrelated one.

In addition, the fields suite is very large, taking a long time to
compile. This will make it faster.

Some ideas for future refactorings:
- 3 main collections: defaultFeatures, fully featured, and legacy.
Legacy is the current one that has multiple editors and could later be
migrated to the first two.
- Avoid collections with more than 1 editor.
- Create reseed buttons to restore the editor to certain states, to
avoid a proliferation of collections and documents.
- Reduce the complexity of the three auxiliary collections (text, array,
upload), which are rarely or never used and have many fields designed
for tests in the fields suite.
2025-04-10 20:47:26 -03: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