### Multi Tenant Plugin
This PR adds a `@payloadcms/plugin-multi-tenant` package. The goal is to
consolidate a source of truth for multi-tenancy. Currently we are
maintaining different implementations for clients, users in discord and
our examples repo. When updates or new paradigms arise we need to
communicate this with everyone and update code examples which is hard to
maintain.
### What does it do?
- adds a tenant selector to the sidebar, above the nav links
- adds a hidden tenant field to every collection that you specify
- adds an array field to your users collection, allowing you to assign
users to tenants
- by default combines the access control (to enabled collections) that
you define, with access control based on the tenants assigned to user on
the request
- by default adds a baseListFilter that filters the documents shown in
the list view with the selected tenant in the admin panel
### What does it not do?
- it does not implement multi-tenancy for your frontend. You will need
to query data for specific tenants to build your website/application
- it does not add a tenants collection, you **NEED** to add a tenants
collection, where you can define what types of fields you would like on
it
### The plugin config
Most of the options listed below are _optional_, but it is easier to
just lay out all of the configuration options.
**TS Type**
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
*
* @default true
*/
cleanupAfterTenantDelete?: boolean
/**
* Automatically
*/
collections: {
[key in CollectionSlug]?: {
/**
* Set to `true` if you want the collection to behave as a global
*
* @default false
*/
isGlobal?: boolean
/**
* Set to `false` if you want to manually apply the baseListFilter
*
* @default true
*/
useBaseListFilter?: boolean
/**
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
*
* @default true
*/
useTenantAccess?: boolean
}
}
/**
* Enables debug mode
* - Makes the tenant field visible in the admin UI within applicable collections
*
* @default false
*/
debug?: boolean
/**
* Enables the multi-tenant plugin
*
* @default true
*/
enabled?: boolean
/**
* Field configuration for the field added to all tenant enabled collections
*/
tenantField?: {
access?: RelationshipField['access']
/**
* The name of the field added to all tenant enabled collections
*
* @default 'tenant'
*/
name?: string
}
/**
* Field configuration for the field added to the users collection
*
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
* This is useful if you want to customize the field or place the field in a specific location
*/
tenantsArrayField?:
| {
/**
* Access configuration for the array field
*/
arrayFieldAccess?: ArrayField['access']
/**
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
*/
includeDefaultField?: true
/**
* Additional fields to include on the tenants array field
*/
rowFields?: Field[]
/**
* Access configuration for the tenant field
*/
tenantFieldAccess?: RelationshipField['access']
}
| {
arrayFieldAccess?: never
/**
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
*/
includeDefaultField?: false
rowFields?: never
tenantFieldAccess?: never
}
/**
* The slug for the tenant collection
*
* @default 'tenants'
*/
tenantsSlug?: string
/**
* Function that determines if a user has access to _all_ tenants
*
* Useful for super-admin type users
*/
userHasAccessToAllTenants?: (
user: ConfigTypes extends { user: User } ? ConfigTypes['user'] : User,
) => boolean
}
```
**Example usage**
```ts
import type { Config } from './payload-types'
import { buildConfig } from 'payload'
export default buildConfig({
plugins: [
multiTenantPlugin<Config>({
collections: {
pages: {},
},
userHasAccessToAllTenants: (user) => isSuperAdmin(user),
}),
],
})
```
### How to configure Collections as Globals for multi-tenant
When using multi-tenant, globals need to actually be configured as
collections so the content can be specific per tenant.
To do that, you can mark a collection with `isGlobal` and it will behave
like a global and users will not see the list view.
```ts
multiTenantPlugin({
collections: {
navigation: {
isGlobal: true,
},
},
})
```
### What?
When a document gets deleted we are not cleaning up jobs that would fail
if the document doesn't exist. This change makes an extra call to the DB
to delete any incomplete jobs for the document.
### Why?
The jobs queue will error and retry needlessly unless these are purged.
### How?
Adds a call to delete jobs from the delete operation.
This PR makes the "test" folder strict in typescript.
`pnpm build:test` before: Found 3275 errors in 174 files.
`pnpm build:test` after: Found 4912 errors in 268 files.
At some point we should bring that number to 0 and make it a requirement
in the CI. Currently `pnpm build:test` is not run anywhere in the CI.
Additionally, I took the opportunity to combine the duplicate
configurations from `tsconfig.json` and `tsconfig.typecheck.json` using
"extend".
declaration, declarationMap and sourceMap have been removed as they have
no reason to exist in noEmit.
The settings I left in `tsconfig.typecheck.json` are ones that I'm not
sure why they are there. Perhaps the file could be removed or at least
reduced further.
This significantly optimizes the form state, reducing its size by up to
more than 3x and improving overall response times. This change also has
rolling effects on initial page size as well, where the initial state
for the entire form is sent through the request. To achieve this, we do
the following:
- Remove `$undefined` strings that are potentially attached to
properties like `value`, `initialValue`, `fieldSchema`, etc.
- Remove unnecessary properties like empty `errorPaths` arrays and empty
`customComponents` objects, which only need to exist if used
- Remove unnecessary properties like `valid`, `passesCondition`, etc.
which only need to be returned if explicitly `false`
- Remove unused properties like `isSidebar`, which simply don't need to
exist at all, as they can be easily calculated during render
## Results
The following results were gathered by booting up each test suite listed
below using the existing seed data, navigating to a document in the
relevant collection, then typing a single letter into the noted field in
order to invoke new form-state. The result is then saved to the file
system for comparison.
| Test Suite | Collection | Field | Before | After | Percentage Change |
|------|------|---------|--------|--------|--------|
| `field-perf` | `blocks-collection` | `layout.0.field1` | 227kB | 110
kB | ~52% smaller |
| `fields` | `array-fields` | `items.0.text` | 14 kB | 4 kB | ~72%
smaller |
| `fields` | `block-fields` | `blocks.0.richText` | 25 kB | 14 kB | ~44%
smaller |
### What?
Allows a user to delete a scheduled publish event after it has been
added:

### Why?
Previously a user had no control over making changes once scheduled.
### How?
Extends the `scheduledPublishHandler` server action to accept a
`deleteID` for the event that should be removed and exposes this to the
user via the admin UI in a new column in the Upcoming Events table.
Fixes#10529. The `req.locale` property within collection and global
access control functions does not reflect the current locale. This was
because we were attaching the locale to the req only _after_ running
`payload.auth`, which attempts to get access control without a
fully-formed req. The fix is to first authenticate the user using the
`executeAuthStrategies` operation directly, then determine the request
locale with that user, and finally get access results with the proper
locale.
This PR adds `cacheTags: boolean` (default `true`) to allow users to
disable the appended document updatedAt value in the case of hosting
with third party CDNs which may not allow additional search params and
throw an error.
It also fixes how we append this value to consider the case where the
URL already contains parameters and appends it with `&` instead.
In the future `cacheTags` can be made an object to allow granularity for
disabling `eTag` headers used for caching as well.
The cache tag control should help with these two issues:
- Fixes https://github.com/payloadcms/payload/issues/9880
- Fixes https://github.com/payloadcms/payload/issues/9993
The appending of the value correctly addresses this:
- Fixes https://github.com/payloadcms/payload/issues/10139
Fixes https://github.com/payloadcms/payload/issues/10436
Fixes an issue where drafts' updatedAt timestamp is not being updated.
We weren't updating the `versionData` to have the right timestamp in the
saveVersion operation when drafts were being updated.
Added e2e tests to make sure 'Last Modified' is always different in both
autosave and non-autosave drafts.
This should fix it https://github.com/payloadcms/payload/issues/10387
I don't know why we had 2 different copies of normalizeMarkdown.
Also, the most up-to-date one still had a bug where lines were
considered as if they were inside codeblocks when they weren't.
How I tested that it works:
1. I copied the `normalizeMarkdown` implementation from this PR into the
website repo, and made sure it is called before the conversion to
editorState.
2. In the admin panel, sync docs.
3. In the admin panel, refresh mdx to lexical (new button, below sync
docs).
4. Look for the examples from bug #10387 and verify that they have been
resolved.
An extra pair of eyes would be nice to make sure I'm not getting
confused with the imports.
Fixes https://github.com/payloadcms/payload/issues/10462
This behavior caused the fixed toolbar of the lexical editor within the
drawer to trigger overlap behavior of the fixed toolbar belonging to the
lexical editor behind the drawer.
Editors within drawers should be treated as separate, instead of being
able to form parent-child relationships between editors behind or in
nested drawers
Previously, this config:
```ts
import type { CollectionConfig } from 'payload'
export const tabSlug = 'tabs'
export const Tab: CollectionConfig = {
slug: tabSlug,
fields: [
{
type: 'tabs',
tabs: [
{
name: 'tabLocalized',
localized: true,
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'array',
type: 'array',
fields: [
{
name: 'title',
type: 'text',
},
],
},
],
},
],
},
],
}
```
This call
```ts
const result = await payload.create({
collection: tabSlug,
locale: englishLocale,
data: {
tabLocalized: {},
},
})
```
Led to the following crash with the MongoDB adapter
<img width="741" alt="image"
src="https://github.com/user-attachments/assets/8d1d37de-a685-4a30-bd37-58af164108a2"
/>
This is due to how Mongoose, apparently just ignores the `minimize:
false` configuration
a83a430a3a/packages/db-mongodb/src/models/buildSchema.ts (L571)
and we, instead of `tabLocalized: { en: { } }` receive just
`tabLocalized: {}`.
This isn't an issue with group fields because we have fallback for them
a83a430a3a/packages/payload/src/fields/hooks/afterChange/promise.ts (L203)
This PR adds the same for tabs.
Editing fields during a locale change on slow networks can lead to
changes being reset when the new form state is returned. This is because
the fields receive new values from context when the new locale loads in,
which may have occurred _after_ changes were made to the fields. The fix
is to subscribe to a new `localeIsLoading` context which is set
immediately after changing locales, and then reset once the new locale
loads in. This also removes the misleading `@deprecated` flag from the
`useLocale` hook itself.
Previously, updates of the node fields from outside the form using
setFields did not trigger re-fetching the initial state, and thus
providing updated values to the form. This is to avoid unnecessary
re-renders of the form and unnecessary requests when setFields is
triggered from within the form.
This PR resets the initial state, thus triggering a re-render and
re-fetch of the initial state, if node.setFields is called from outside
the form. This preserves the performance optimization
Currently, unless a locale is present in the URL search params, the
locale context is instantiated using the default locale until prefs load
in client-side. This causes the locale selector to briefly render in
with the incorrect (default) locale before being replaced by the proper
locale of the request. For example, if the default locale is `en`, and
the page is requested in `es`, the locale selector will flash with
English before changing to the correct locale, even though the page data
itself is properly loaded in Spanish. This is especially evident within
slow networks.
The fix is to query the user's locale preference server-side and thread
it into the locale provider to initialize state. Because search params
are not available within server layouts, we cannot pass the locale param
in the same way, so we rely on the provider itself to read them from the
`useSearchParams` hook. If present, this takes precedence over the
user's preference if it exists.
Since the root page also queries the user's locale preference to
determine the proper locale across navigation, we use React's cache
function to dedupe these function calls and ensure only a single query
is made to the db for each request.
The localization e2e test is notorious for flaking, consuming a lot of
time and resources continually retrying. This was because the test was
attempting to click DOM elements using selectors that never resolve, or
attempting to click inaccessible DOM nodes such as those behind a modal.
The fix is to ensure that the dot nav, for example, is disabled while
form state loads, and that modals are properly closed prior to executing
subsequent tests, etc. Tests also needed to explicitly check for
_enabled_ states before performing click actions, rather than simply
awaiting their visibility.
Fixes#10018. When toggling columns, then sorting them, the table is
reset to the collection's default columns instead of the user's
preferred columns. This is because when sorting columns, a stale
client-side cache of the user's preferences is used to update their sort
preference. This is because when column state is constructed
server-side, it completely bypasses the client-side cache. To fix this,
sort preferences are now also set on the server right alongside column
preferences, which performs an upsert-like operation to ensure that no
existing preferences are lost.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
-->
### What?
This patch implements the functionality in `imageResizer` to omit the
generation of the image when either width or height is undefined and
`withoutEnlargement` is set to `undefined`
### Why?
#9986: `withoutEnlargement` doesn't work when `height` is undefined in
`upload.imageSizes`
### How?
This code checks if `withoutEnlargement` is undefined and either
`targetWidth` or `targetHeight` is missing. If so, it further checks
whether the target dimensions (if provided) are larger than the original
image dimensions. If the target would enlarge the image, it returns
'omit', skipping the resizing to prevent enlargement
Fixes#9986
---------
Co-authored-by: Patrik Kozak <patrik@payloadcms.com>
To reproduce the bug:
1. Within a Lexical editor, insert a relationship field.
2. In the drawer, change the selected collection.
3. The table below changes correctly, but the title and the "create new"
button quickly revert to the original option.
https://github.com/user-attachments/assets/e4b7c615-4b98-4c11-a4b9-a828606edb6f
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
Fixes#9882 and #9691
In 2.0, we would accept data coming back from an update operation and
then reflect those changes in UI.
However, in 3.0, we did not do that anymore - meaning you could change a
document with hooks in `beforeChange` or `afterChange`, but then not see
the changes made on the server.
This PR updates the way that `mergeServerFormState` works, and adds a
property to optionally allow values from server form state - which can
then be used in the `onSuccess` form handler which may need to define
new field values.
Allows to set `maxDepth: 0` for join fields and improves JSDoc about
`maxDepth`, adds tests to confirm that
https://github.com/payloadcms/payload/issues/10243 is not an issue, but
just happens because the default `maxDepth` is `1`.
Similar to #10331. Since React 19, refs can now be passed directly
through props without the need for `React.forwardRef`. This greatly
simplifies components types and overall syntax.
This PR adds a button to the `/account` view which allows users to reset
their preferences on-demand. This is to that editors can quickly reset their
preferences via the admin ui without the need of accessing the db directly,
which was simply not possible before. To do this, we add a new button at
the bottom of the account view which performs a standard `DELETE`
request using the REST API to the `'payload-preferences'` collection.
Related: #9949
Demo: [Posts-reset-prefs--Payload.webm](https://github.com/user-attachments/assets/560cbbe3-06ef-4b7c-b3c2-9702883b1fc6)
Whenever form state fails, like when field conditions, validations, or
default value functions throw errors, blocks and array rows are stuck
within an infinite loading state. Examples of this might be when
accessing properties of undefined within these functions, etc. Although
these errors are logged to the server console, the UI is be misleading,
where the user often waits for the request to resolve rather than
understanding that an underlying API error has occurred. Now, we safely
execute these functions within a `try...catch` block and handle their
failures accordingly. On the client, form state will resolve as expected
using the default return values for these functions.
### What?
Previously, while uploading a file - if the uploading process took a bit
of time, users could still save the document prior to the upload fully
completing.
### Why?
During the uploading process - the save button should be disabled until
the upload is complete to prevent premature saving of an upload
document.
### How?
Now, we keep track of the state of the upload in a provider and disable
the save button until the file is fully uploaded.
Fixes an issue where if a checkbox field was in the first position of a
collection, and you tried to filter on it via the List view, the page
would crash.
Fixes#10234. Some fields, such as focal point fields for upload enabled
collections, were rendering in the condition selector despite being
hidden from the column selector. This was because the logic for the
column selector was filtering fields without labels, but the same was
not being done for the filter conditions. This, however, is not a good
way to filter these fields as it requires this specific logic to be
written in multiple places. Instead, they need to explicitly check for
`hidden` and `disabled` in addition to `disableListFilter` and
`disableListColumn`. The actual filtering logic has been improved across
the two instances as well, removing multiple duplicative loops.
This change has also exposed a underlying issue with the way columns
were handled within the table columns provider. When row selections were
enabled, the selector columns were present in column state. This caused
problems when interacting with column indices, such as when reordering
columns. Instead of needing to manually filter these out every time we
need to work with column state, they no longer appear there in the first
place. Instead, we inject the row selectors directly into the table
itself, completely isolating these row selectors from the column state.
There were a handful of list view e2e tests written into the text and
email field test suite, making them hard to find as they were isolated
from other related tests. A few of these tests were also duplicative
across suites, making CI run them twice unnecessarily.
Bumps Next.js to the latest version `15.1.3`. This affects only internal
`package.json` files (in the root dir and test)
Fixes errors from here https://github.com/payloadcms/payload/pull/10209
Improves the admin e2e test splitting by grouping them by type with
semantic names as opposed to numerically. This will provide much needed
clarity to exactly _where_ new admin tests should be written and help to
quickly distinguish the areas of failure within the CI overview.
Adds a feature to allow editors to schedule publish / unpublish events
in the future. Must be enabled by setting
`versions.drafts.schedulePublish: true` in your Collection / Global
configs.
https://github.com/user-attachments/assets/ca1d7a8b-946a-4eac-b911-c2177dbe3b1c
Todo:
- [x] Translate new i18n keys
- [x] Wire up locale-specific scheduled publish / unpublish actions