This adds support for calculating and displaying file sizes for JPEG XL
images.
Image resizing is not supported by sharp out-of-the-box yet:
https://github.com/lovell/sharp/issues/2731
### What?
Extends visibility into what view is being shown so custom components
have context as to where they are being rendered.
**This PR does not add React Context.**
### Why?
This was needed for the multi-tenant plugin where the selector is in the
navigation sidebar and has no way to know if it is being shown inside of
a document or the list view.
I assume other users may also want their server components to be aware
of where a component is rendering before hitting the client. An example
would be wanting to redirect on the server instead of on the client,
this is how multi-tenant redirects users from "global" enabled
collections to the document view.
### How?
Adds 2 new variables that are determined by the view being routed to.
`viewType` - which view is being rendered, ie `list`, `document`,
`version`, `account`, `verify`, `reset`
```ts
type ViewTypes =
| 'account'
| 'dashboard'
| 'document'
| 'list'
| 'reset'
| 'verify'
| 'version'
```
`documentSubViewType` - which tells you what sub view you are on, ie
`api`, `livePreview`, `default`, `versions`
```ts
type DocumentSubViewTypes =
| 'api'
| 'default'
| 'livePreview'
| 'version'
| 'versions'
```
- reduces unnecessary shallow copying within operations by removing
unnecessary spreads or .map()'s
- removes unnecessary `deleteMany` call in `deleteUserPreferences` for
auth-enabled collections
- replaces all instances of `validOperators.includes` with
`validOperatorMap[]`. O(n) => O(1)
- optimizes the `sanitizeInternalFields` function. Previously, it was
doing a **lot** of shallow copying
A lot of this deepCopying was just not necessary. This removes the
deepCopying from all field hook operations where I think it's 100% safe.
It does not remove all deepCopying, especially in areas where the input
data was deep copied, and that data pre-modification is then used after
the field hooks have run.
In these cases, further execution of the hook might be intentionally
expecting the unmodified version of that input data
Data for the EditMany view was fetched even though the EditMany Drawer
was not open. This, in combination with the router.replace call to add
the default limit query param, caused the root layout to re-render
### What?
General improvements:
- Disable duplication on tenant collections marked with `isGlobal`
- Simplify cookie setting logic and option loading for the selector
### What?
Updating wording for a sentence.
### Why?
I believe it was missing a word as it read a bit off without it and
causes a reread.
### How?
Doesn't feel like it reads right and causes a reread.
Fixes #
- "If you are building a website that fits within the limits _of_ a tool
like Webflow or Framer"
### 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,
},
},
})
```
🤖 Automated bump of templates for v3.17.1
Triggered by user: @denolfe
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
### 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.
### What?
While working on a custom database adapter (I know I am crazy for this)
I noticed that UpsertArgs is not exported when doing:
```
import {
type UpsertArgs
} from 'payload'
```
it results in:
```
Error: src/index.ts(21,8): error TS2614: Module '"payload"' has no exported member 'UpsertArgs'. Did you mean to use 'import UpsertArgs from "payload"' instead?
```
### Why?
Because index.ts in packages/payload/src/index.ts includes Upsert but
not UpsertArgs in export.
### How?
Add the export from UpsertArgs back.
Previously, every error from MongoDB was logged as "Value must be
unique", as well the response code should not be `BAD_REQUEST` but
`INTERNAL_SERVER_ERROR`. `throw error` preserves the original error so
it can be traced.
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 PR modifies `tsconfig.base.json` by setting the following
strictness properties to true: `strict`, `noUncheckedIndexedAccess` and
`noImplicitOverride`.
In packages where compilation errors were observed, these settings were
opted out, and TODO comments were added to make it easier to track the
roadmap for converting everything to strict mode.
The following packages now have increased strictness, which prevents new
errors from being accidentally introduced:
- storage-vercel-blob
- storage-s3*
- storage-gcs
- plugin-sentry
- payload-cloud*
- email-resend*
- email-nodemailer*
*These packages already had `strict: true`, but now have
`noUncheckedIndexedAccess` and `noImplicitOverride`.
Note that this only affects the `/packages` folder, but not
`/templates`, `/test` or `/examples` which have a different `tsconfig`.
The `access.read` function executed within form state was missing the
`id` arg, and was also incorrectly setting `data` as `doc`. When
building form state, there is no concept of a "doc" because it is
possible to build form state using only a subset of fields. There is
"data", however, which represents the schema path at the entry point of
the function. Similarly, when building form state on within an
`onChange` function, for example, we do not send the original doc
through the request, which is what "doc" would represent. Instead, we
send either `data` or `formState`, both of which could represent a
_modified_ doc. This particular invocation of read access does not
effect the visibility of fields themselves, but rather their return
values from the form state endpoint. Field visibility is determined at
the request level.
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 |
Previously, the url field of a link was stored and outputted despite the
link being an internal link. This PR ensures that either the link url,
or the link doc is stored and outputted - never both.
If you add text to the editor, then delete it using ctrl+a + delete, one
empty paragraph that cannot be removed remains in the editor state.
In order to account for this, we have a `hasText()` function - this,
however, was not used in our JSX and HTML converters. This caused the
converters to incorrectly output a linebreak if said empty editor state
was passed in.
Original issue: #10534
The original issue was partially fixed by #10535, but it missed a case
that overrides a post method with get.
This PR passes the `basePath` to the overridden call.
Better error message when no argument is passed to `pnpm payload`.
Before:
```
Unknown script: "".
```
After:
```
Please provide a command to run
Available commands:
- command-1
- command-2
- etc.
```
After working on this I found a more accurate way to reproduce the bug:
- in the issue repro, type a letter in the select menu.
- delete the letter and wait for the debounce (300ms)
- type another letter.
- in devtools, you should see that the query increases the pagination by
+1. With this change, the pagination is reset when the input changes.
Fixes#10496
I'd like to do integration testing. But since there is no isolated `/ui`
test yet, this requires some planning. I have it pending.
### 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.