Compare commits
121 Commits
feat/impro
...
feat/form-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb76180659 | ||
|
|
d126c2bf80 | ||
|
|
b646485388 | ||
|
|
6b9d81a746 | ||
|
|
513ba636af | ||
|
|
2ae670e0e4 | ||
|
|
779f511fbf | ||
|
|
35d845cea5 | ||
|
|
dc36572fbf | ||
|
|
cba5c7bcac | ||
|
|
70db44f964 | ||
|
|
077fb3ab30 | ||
|
|
b65ae073d2 | ||
|
|
480113a4fe | ||
|
|
b1734b0d38 | ||
|
|
84c838cde1 | ||
|
|
0a3820a487 | ||
|
|
dd28959916 | ||
|
|
12f51bad5f | ||
|
|
4c8cafd6a6 | ||
|
|
152a9b6adf | ||
|
|
d47c980509 | ||
|
|
7f124cfe93 | ||
|
|
6901b2639d | ||
|
|
16d75a7c7b | ||
|
|
de68ef4548 | ||
|
|
f4639c418f | ||
|
|
24da30ab74 | ||
|
|
4be410cc4f | ||
|
|
cd1117515b | ||
|
|
3f550bc0ec | ||
|
|
706410e693 | ||
|
|
3131dba039 | ||
|
|
6bfa66c9ff | ||
|
|
6eee787493 | ||
|
|
30c77d8137 | ||
|
|
707e85ebcf | ||
|
|
09ada20ce8 | ||
|
|
9068bdacae | ||
|
|
155f9f80fe | ||
|
|
2056e9b740 | ||
|
|
44be433d44 | ||
|
|
002e921ede | ||
|
|
3098f35537 | ||
|
|
d7dee225fc | ||
|
|
c31bff7e57 | ||
|
|
5d199587a3 | ||
|
|
ececa65c78 | ||
|
|
7a400a7a79 | ||
|
|
2a0094def7 | ||
|
|
48471b7210 | ||
|
|
480c6e7c09 | ||
|
|
da77f99df4 | ||
|
|
ae4a78b298 | ||
|
|
da6511eba9 | ||
|
|
1f3ccb82d9 | ||
|
|
d6a03eeaba | ||
|
|
3f80c5993c | ||
|
|
c18c58e1fb | ||
|
|
36168184b5 | ||
|
|
98fec35368 | ||
|
|
fde526e07f | ||
|
|
5dadccea39 | ||
|
|
d2fe9b0807 | ||
|
|
95ec57575d | ||
|
|
430ebd42ff | ||
|
|
3415ba81ac | ||
|
|
fa18923317 | ||
|
|
91a0f90649 | ||
|
|
b15a7e3c72 | ||
|
|
d56de79671 | ||
|
|
87ba7f77aa | ||
|
|
9fb7160c2c | ||
|
|
c6c65ac842 | ||
|
|
dc56acbdaf | ||
|
|
6a99677d15 | ||
|
|
6d48cf9bbf | ||
|
|
d7a7fbf93a | ||
|
|
5a5385423e | ||
|
|
ac6f4e2c86 | ||
|
|
886bd94fc3 | ||
|
|
a80c6b5212 | ||
|
|
6f53747040 | ||
|
|
b820a75ec5 | ||
|
|
49d94d53e0 | ||
|
|
feea444867 | ||
|
|
257cad71ce | ||
|
|
04dad9d7a6 | ||
|
|
098fe10ade | ||
|
|
7277f17f14 | ||
|
|
7a73265bd6 | ||
|
|
ec593b453e | ||
|
|
a63a3d0518 | ||
|
|
57143b37d0 | ||
|
|
3ad56cd86f | ||
|
|
05e6f3326b | ||
|
|
8b6ba625b8 | ||
|
|
2b76a0484c | ||
|
|
66318697dd | ||
|
|
8940726601 | ||
|
|
ae32c555ac | ||
|
|
8ed410456c | ||
|
|
824f9a7f4d | ||
|
|
f25acb801c | ||
|
|
5f58daffd0 | ||
|
|
e413e1df1c | ||
|
|
bdbb99972c | ||
|
|
e29ac523d3 | ||
|
|
d8cfdc7bcb | ||
|
|
694c76d51a | ||
|
|
09721d4c20 | ||
|
|
834fdde088 | ||
|
|
45913e41f1 | ||
|
|
42da87b6e9 | ||
|
|
2a1ddf1e89 | ||
|
|
8af8befbd4 | ||
|
|
2118c6c47f | ||
|
|
a07fd9eba3 | ||
|
|
ea9abfdef3 | ||
|
|
b671fd5a6d | ||
|
|
ae0736b738 |
21
.github/CODEOWNERS
vendored
21
.github/CODEOWNERS
vendored
@@ -1,24 +1,35 @@
|
||||
# Order matters. The last matching pattern takes precedence.
|
||||
|
||||
### Package Exports ###
|
||||
### Package Exports
|
||||
|
||||
**/exports/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Packages ###
|
||||
### Packages
|
||||
|
||||
/packages/plugin-cloud*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/email-*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/live-preview*/src/ @jacobsfletch
|
||||
/packages/plugin-stripe/src/ @jacobsfletch
|
||||
/packages/plugin-multi-tenant/src/ @JarrodMFlesch
|
||||
/packages/richtext-*/src/ @AlessioGr
|
||||
/packages/next/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
|
||||
/packages/ui/src/ @jmikrut @jacobsfletch @AlessioGr @JarrodMFlesch
|
||||
/packages/storage-*/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/create-payload-app/src/ @denolfe @jmikrut @DanRibbens
|
||||
/packages/eslint-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Templates ###
|
||||
### Templates
|
||||
|
||||
/templates/_data/ @denolfe @jmikrut @DanRibbens
|
||||
/templates/_template/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
### Build Files ###
|
||||
### Build Files
|
||||
|
||||
**/tsconfig*.json @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
**/jest.config.js @denolfe @jmikrut @DanRibbens @AlessioGr
|
||||
|
||||
### Root ###
|
||||
### Root
|
||||
|
||||
/package.json @denolfe @jmikrut @DanRibbens
|
||||
/tools/ @denolfe @jmikrut @DanRibbens
|
||||
/.husky/ @denolfe @jmikrut @DanRibbens
|
||||
|
||||
2
.github/workflows/lock-issues.yml
vendored
2
.github/workflows/lock-issues.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: dessant/lock-threads@v5
|
||||
with:
|
||||
process-only: 'issues'
|
||||
issue-inactive-days: '1'
|
||||
issue-inactive-days: '7'
|
||||
exclude-any-issue-labels: 'status: awaiting-reply'
|
||||
log-output: true
|
||||
issue-comment: >
|
||||
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -283,6 +283,7 @@ jobs:
|
||||
- fields-relationship
|
||||
- fields__collections__Array
|
||||
- fields__collections__Blocks
|
||||
- fields__collections__Blocks#config.blockreferences.ts
|
||||
- fields__collections__Checkbox
|
||||
- fields__collections__Collapsible
|
||||
- fields__collections__ConditionalLogic
|
||||
@@ -293,6 +294,7 @@ jobs:
|
||||
- fields__collections__JSON
|
||||
- fields__collections__Lexical__e2e__main
|
||||
- fields__collections__Lexical__e2e__blocks
|
||||
- fields__collections__Lexical__e2e__blocks#config.blockreferences.ts
|
||||
- fields__collections__Number
|
||||
- fields__collections__Point
|
||||
- fields__collections__Radio
|
||||
|
||||
@@ -54,7 +54,7 @@ const config = buildConfig({
|
||||
|
||||
In order to ensure the Payload Config is fully Node.js compatible and as lightweight as possible, components are not directly imported into your config. Instead, they are identified by their file path for the Admin Panel to resolve on its own.
|
||||
|
||||
Component Paths, by default, are relative to your project's base directory. This is either your current working directory, or the directory specified in `config.admin.baseDir`. To simplify Component Paths, you can also configure the base directory using the `admin.importMap.baseDir` property.
|
||||
Component Paths, by default, are relative to your project's base directory. This is either your current working directory, or the directory specified in `config.admin.importMap.baseDir`.
|
||||
|
||||
Components using named exports are identified either by appending `#` followed by the export name, or using the `exportName` property. If the component is the default export, this can be omitted.
|
||||
|
||||
|
||||
@@ -654,6 +654,26 @@ const ExampleCollection = {
|
||||
]}
|
||||
/>
|
||||
|
||||
## useDocumentForm
|
||||
|
||||
The `useDocumentForm` hook works the same way as the [useForm](#useform) hook, but it always gives you access to the top-level `Form` of a document. This is useful if you need to access the document's `Form` context from within a child `Form`.
|
||||
|
||||
An example where this could happen would be custom components within lexical blocks, as lexical blocks initialize their own child `Form`.
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useDocumentForm } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
const { fields: parentDocumentFields } = useDocumentForm()
|
||||
|
||||
return (
|
||||
<p>The document's Form has ${Object.keys(parentDocumentFields).length} fields</p>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## useCollapsible
|
||||
|
||||
The `useCollapsible` hook allows you to control parent collapsibles:
|
||||
@@ -1016,3 +1036,117 @@ export const MySetStepNavComponent: React.FC<{
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
## usePayloadAPI
|
||||
|
||||
The `usePayloadAPI` hook is a useful tool for making REST API requests to your Payload instance and handling responses reactively. It allows you to fetch and interact with data while automatically updating when parameters change.
|
||||
|
||||
This hook returns an array with two elements:
|
||||
|
||||
1. An object containing the API response.
|
||||
2. A set of methods to modify request parameters.
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { usePayloadAPI } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// Fetch data from a collection item using its ID
|
||||
const [{ data, error, isLoading }, { setParams }] = usePayloadAPI(
|
||||
'/api/posts/123',
|
||||
{ initialParams: { depth: 1 } }
|
||||
)
|
||||
|
||||
if (isLoading) return <p>Loading...</p>
|
||||
if (error) return <p>Error: {error.message}</p>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{data?.title}</h1>
|
||||
<button onClick={() => setParams({ cacheBust: Date.now() })}>
|
||||
Refresh Data
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Property | Description |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| **`url`** | The API endpoint to fetch data from. Relative URLs will be prefixed with the Payload API route. |
|
||||
| **`options`** | An object containing initial request parameters and initial state configuration. |
|
||||
|
||||
The `options` argument accepts the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **`initialData`** | Uses this data instead of making an initial request. If not provided, the request runs immediately. |
|
||||
| **`initialParams`** | Defines the initial parameters to use in the request. Defaults to an empty object `{}`. |
|
||||
|
||||
**Returned Value:**
|
||||
|
||||
The first item in the returned array is an object containing the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | -------------------------------------------------------- |
|
||||
| **`data`** | The API response data. |
|
||||
| **`error`** | If an error occurs, this contains the error object. |
|
||||
| **`isLoading`** | A boolean indicating whether the request is in progress. |
|
||||
|
||||
The second item is an object with the following methods:
|
||||
|
||||
| Property | Description |
|
||||
| --------------- | ----------------------------------------------------------- |
|
||||
| **`setParams`** | Updates request parameters, triggering a refetch if needed. |
|
||||
|
||||
#### Updating Data
|
||||
|
||||
The `setParams` function can be used to update the request and trigger a refetch:
|
||||
|
||||
```tsx
|
||||
setParams({ depth: 2 })
|
||||
```
|
||||
|
||||
This is useful for scenarios where you need to trigger another fetch regardless of the `url` argument changing.
|
||||
|
||||
## useRouteTransition
|
||||
|
||||
Route transitions are useful in showing immediate visual feedback to the user when navigating between pages. This is especially useful on slow networks when navigating to data heavy or process intensive pages.
|
||||
|
||||
By default, any instances of `Link` from `@payloadcms/ui` will trigger route transitions dy default.
|
||||
|
||||
```tsx
|
||||
import { Link } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent = () => {
|
||||
return (
|
||||
<Link href="/somewhere">
|
||||
Go Somewhere
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
You can also trigger route transitions programmatically, such as when using `router.push` from `next/router`. To do this, wrap your function calls with the `startRouteTransition` method provided by the `useRouteTransition` hook.
|
||||
|
||||
```ts
|
||||
'use client'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTransition } from '@payloadcms/ui'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const { startRouteTransition } = useRouteTransition()
|
||||
|
||||
const redirectSomewhere = useCallback(() => {
|
||||
startRouteTransition(() => router.push('/somewhere'))
|
||||
}, [startRouteTransition, router])
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
@@ -57,7 +57,7 @@ The `admin` directory contains all the _pages_ related to the interface itself,
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:**
|
||||
If you don't intend to use the Admin Panel, [REST API](../rest/overview), or [GraphQL API](../graphql/overview), you can opt-out by simply deleting their corresponding directories within your Next.js app. The overhead, however, is completely constrained to these routes, and will not slow down or affect Payload outside when not in use.
|
||||
If you don't intend to use the Admin Panel, [REST API](../rest-api/overview), or [GraphQL API](../graphql/overview), you can opt-out by simply deleting their corresponding directories within your Next.js app. The overhead, however, is completely constrained to these routes, and will not slow down or affect Payload outside when not in use.
|
||||
</Banner>
|
||||
|
||||
Finally, the `custom.scss` file is where you can add or override globally-oriented styles in the Admin Panel, such as modify the color palette. Customizing the look and feel through CSS alone is a powerful feature of the Admin Panel, [more on that here](./customizing-css).
|
||||
@@ -99,6 +99,7 @@ The following options are available:
|
||||
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
|
||||
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
|
||||
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
|
||||
| **`timezones`** | Configure the timezone settings for the admin panel. [More details](#timezones) |
|
||||
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
|
||||
|
||||
<Banner type="success">
|
||||
@@ -242,3 +243,21 @@ The Payload Admin Panel is translated in over [30 languages and counting](https:
|
||||
## Light and Dark Modes
|
||||
|
||||
Users in the Admin Panel have the ability to choose between light mode and dark mode for their editing experience. Users can select their preferred theme from their account page. Once selected, it is saved to their user's preferences and persisted across sessions and devices. If no theme was selected, the Admin Panel will automatically detect the operation system's theme and use that as the default.
|
||||
|
||||
## Timezones
|
||||
|
||||
The `admin.timezones` configuration allows you to configure timezone settings for the Admin Panel. You can customise the available list of timezones and in the future configure the default timezone for the Admin Panel and for all users.
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | ----------------------------------------------- |
|
||||
| `supportedTimezones` | An array of label/value options for selectable timezones where the value is the IANA name eg. `America/Detroit` |
|
||||
| `defaultTimezone` | The `value` of the default selected timezone. eg. `America/Los_Angeles` |
|
||||
|
||||
We validate the supported timezones array by checking the value against the list of IANA timezones supported via the Intl API, specifically `Intl.supportedValuesOf('timeZone')`.
|
||||
|
||||
<Banner type="info">
|
||||
**Important**
|
||||
You must enable timezones on each individual date field via `timezone: true`. See [Date Fields](../fields/overview#date) for more information.
|
||||
</Banner>
|
||||
|
||||
@@ -10,7 +10,13 @@ The [Admin Panel](../admin/overview) is translated in over [30 languages and cou
|
||||
|
||||
By default, Payload comes preinstalled with English, but you can easily load other languages into your own application. Languages are automatically detected based on the request. If no language is detected, or if the user's language is not yet supported by your application, English will be chosen.
|
||||
|
||||
To configure I18n, use the `i18n` key in your [Payload Config](./overview):
|
||||
To add I18n to your project, you first need to install the `@payloadcms/translations` package:
|
||||
|
||||
```bash
|
||||
pnpm install @payloadcms/translations
|
||||
```
|
||||
|
||||
Once installed, it can be configured using the `i18n` key in your [Payload Config](./overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
@@ -166,7 +172,11 @@ export const Articles: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
## Node
|
||||
## Changing Languages
|
||||
|
||||
Users can change their preferred language in their account settings or by otherwise manipulating their [User Preferences](../admin/preferences).
|
||||
|
||||
## Node.js#node
|
||||
|
||||
Payload's backend sets the language on incoming requests before they are handled. This allows backend validation to return error messages in the user's own language or system generated emails to be sent using the correct translation. You can make HTTP requests with the `accept-language` header and Payload will use that language.
|
||||
|
||||
@@ -174,7 +184,7 @@ Anywhere in your Payload app that you have access to the `req` object, you can a
|
||||
|
||||
## TypeScript
|
||||
|
||||
In order to use custom translations in your project, you need to provide the types for the translations.
|
||||
In order to use [Custom Translations](#custom-translations) in your project, you need to provide the types for the translations.
|
||||
|
||||
Here we create a shareable translations object. We will import this in both our custom components and in our Payload config.
|
||||
|
||||
@@ -253,4 +263,3 @@ const field: Field = {
|
||||
) => t('fields:addLabel'),
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -77,11 +77,12 @@ export default buildConfig({
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **`locales`** | Array of all the languages that you would like to support. [More details](#locales) |
|
||||
| **`defaultLocale`** | Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale. |
|
||||
| **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated unless a fallback is explicitly provided in the request. True by default. |
|
||||
| Option | Description |
|
||||
|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`locales`** | Array of all the languages that you would like to support. [More details](#locales) |
|
||||
| **`defaultLocale`** | Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale. |
|
||||
| **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated unless a fallback is explicitly provided in the request. True by default. |
|
||||
| **`filterAvailableLocales`** | A function that is called with the array of `locales` and the `req`, it should return locales to show in admin UI selector. [See more](#filter-available-options). |
|
||||
|
||||
### Locales
|
||||
|
||||
@@ -100,6 +101,35 @@ The locale codes do not need to be in any specific format. It's up to you to def
|
||||
|
||||
_* An asterisk denotes that a property is required._
|
||||
|
||||
#### Filter Available Options
|
||||
In some projects you may want to filter the available locales shown in the admin UI selector. You can do this by providing a `filterAvailableLocales` function in your Payload Config. This is called on the server side and is passed the array of locales. This means that you can determine what locales are visible in the localizer selection menu at the top of the admin panel. You could do this per user, or implement a function that scopes these to tenants and more. Here is an example using request headers in a multi-tenant application:
|
||||
|
||||
```ts
|
||||
// ... rest of payload config
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'es'],
|
||||
filterAvailableLocales: async ({ req, locales }) => {
|
||||
if (getTenantFromCookie(req.headers, 'text')) {
|
||||
const fullTenant = await req.payload.findByID({
|
||||
id: getTenantFromCookie(req.headers, 'text') as string,
|
||||
collection: 'tenants',
|
||||
req,
|
||||
})
|
||||
if (fullTenant && fullTenant.supportedLocales?.length) {
|
||||
return locales.filter((locale) => {
|
||||
return fullTenant.supportedLocales?.includes(locale.code as 'en' | 'es')
|
||||
})
|
||||
}
|
||||
}
|
||||
return locales
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Since the filtering happens at the root level of the application and its result is not calculated every time you navigate to a new page, you may want to call `router.refresh` in a custom component that watches when values that affect the result change. In the example above, you would want to do this when `supportedLocales` changes on the tenant document.
|
||||
|
||||
|
||||
## Field Localization
|
||||
|
||||
Payload Localization works on a **field** level—not a document level. In addition to configuring the base Payload Config to support Localization, you need to specify each field that you would like to localize.
|
||||
|
||||
@@ -87,6 +87,7 @@ The following options are available:
|
||||
| **`upload`** | Base Payload upload configuration. [More details](../upload/overview#payload-wide-upload-options). |
|
||||
| **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). |
|
||||
| **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). |
|
||||
| **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. |
|
||||
| **`debug`** | Enable to expose more detailed error information. |
|
||||
| **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). |
|
||||
| **`rateLimit`** | Control IP-based rate limiting for all Payload resources. Used to prevent DDoS attacks, etc. [More details](../production/preventing-abuse#rate-limiting-requests). |
|
||||
@@ -181,7 +182,7 @@ If none was in either location, Payload will finally check the `dist` directory.
|
||||
|
||||
### Customizing the Config Location
|
||||
|
||||
In addition to the above automated detection, you can specify your own location for the Payload Config. This can be useful in situations where your config is not in a standard location, or you wish to switch between multiple configurations. To do this, Payload exposes an [Environment Variable](./environment-variables) to bypass all automatic config detection.
|
||||
In addition to the above automated detection, you can specify your own location for the Payload Config. This can be useful in situations where your config is not in a standard location, or you wish to switch between multiple configurations. To do this, Payload exposes an [Environment Variable](../configuration/environment-vars) to bypass all automatic config detection.
|
||||
|
||||
To use a custom config location, set the `PAYLOAD_CONFIG_PATH` environment variable:
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: The Blocks Field is a great layout build and can be used to construct any
|
||||
keywords: blocks, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Blocks Field is **incredibly powerful** storing an array of objects based on the fields that your define, where each item in the array is a "block" with its own unique schema.
|
||||
The Blocks Field is **incredibly powerful**, storing an array of objects based on the fields that you define, where each item in the array is a "block" with its own unique schema.
|
||||
|
||||
Blocks are a great way to create a flexible content model that can be used to build a wide variety of content types, including:
|
||||
|
||||
@@ -64,7 +64,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Blocks Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Blocks Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
@@ -295,6 +295,70 @@ export const CustomBlocksFieldLabelClient: BlocksFieldLabelClientComponent = ({
|
||||
}
|
||||
```
|
||||
|
||||
## Block References
|
||||
|
||||
If you have multiple blocks used in multiple places, your Payload Config can grow in size, potentially sending more data to the client and requiring more processing on the server. However, you can optimize performance by defining each block **once** in your Payload Config and then referencing its slug wherever it's used instead of passing the entire block config.
|
||||
|
||||
To do this, define the block in the `blocks` array of the Payload Config. Then, in the Blocks Field, pass the block slug to the `blockReferences` array - leaving the `blocks` array 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'],
|
||||
})
|
||||
})
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Reminder:**
|
||||
Blocks referenced in the `blockReferences` array are treated as isolated from the collection / global config. This has the following implications:
|
||||
|
||||
1. The block config cannot be modified or extended in the collection config. It will be identical everywhere it's referenced.
|
||||
2. Access control for blocks referenced in the `blockReferences` are run only once - data from the collection will not be available in the block's access control.
|
||||
</Banner>
|
||||
|
||||
## TypeScript
|
||||
|
||||
As you build your own Block configs, you might want to store them in separate files but retain typing accordingly. To do so, you can import and use Payload's `Block` type:
|
||||
|
||||
@@ -54,7 +54,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Code Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Code Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -44,7 +44,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Collapsible Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Collapsible Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -43,6 +43,7 @@ export const MyDateField: Field = {
|
||||
| **`required`** | Require this field to have a value. |
|
||||
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
| **`timezone`** * | Set to `true` to enable timezone selection on this field. [More details](#timezones). |
|
||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
||||
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
|
||||
|
||||
@@ -50,7 +51,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Date Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Date Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
@@ -222,3 +223,23 @@ export const CustomDateFieldLabelClient: DateFieldLabelClientComponent = ({
|
||||
}
|
||||
```
|
||||
|
||||
## Timezones
|
||||
|
||||
To enable timezone selection on a Date field, set the `timezone` property to `true`:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: 'date',
|
||||
type: 'date',
|
||||
timezone: true,
|
||||
}
|
||||
```
|
||||
|
||||
This will add a dropdown to the date picker that allows users to select a timezone. The selected timezone will be saved in the database along with the date in a new column named `date_tz`.
|
||||
|
||||
You can customise the available list of timezones in the [global admin config](../admin/overview#timezones).
|
||||
|
||||
<Banner type='info'>
|
||||
**Good to know:**
|
||||
The date itself will be stored in UTC so it's up to you to handle the conversion to the user's timezone when displaying the date in your frontend.
|
||||
</Banner>
|
||||
|
||||
@@ -51,7 +51,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Email Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Email Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -55,7 +55,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Group Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Group Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -53,7 +53,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the JSON Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the JSON Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -56,7 +56,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Number Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Number Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -57,7 +57,7 @@ There are three main categories of fields in Payload:
|
||||
- [Presentational Fields](#presentational-fields)
|
||||
- [Virtual Fields](#virtual-fields)
|
||||
|
||||
To begin writing fields, first determine which [Field Type](#field-types) best supports your application. Then to author your field accordingly using the [Field Options](#field-options) for your chosen field type.
|
||||
To begin writing fields, first determine which [Field Type](#field-types) best supports your application. Then author your field accordingly using the [Field Options](#field-options) for your chosen field type.
|
||||
|
||||
### Data Fields
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: The Point field type stores coordinates in the database. Learn how to use
|
||||
keywords: point, geolocation, geospatial, geojson, 2dsphere, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Point Field saves a pair of coordinates in the database and assigns an index for location related queries. The data structure in the database matches the GeoJSON structure to represent point. The Payload APIs simplifies the object data to only the [longitude, latitude] location.
|
||||
The Point Field saves a pair of coordinates in the database and assigns an index for location related queries. The data structure in the database matches the GeoJSON structure to represent point. The Payload API simplifies the object data to only the [longitude, latitude] location.
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/point.png"
|
||||
|
||||
@@ -66,7 +66,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Radio Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Radio Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: The Relationship field provides the ability to relate documents together.
|
||||
keywords: relationship, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Relationship Field is one of the most powerful fields Payload features. It provides for the ability to easily relate documents together.
|
||||
The Relationship Field is one of the most powerful fields Payload features. It provides the ability to easily relate documents together.
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/relationship.png"
|
||||
@@ -71,8 +71,7 @@ _* An asterisk denotes that a property is required._
|
||||
</Banner>
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Relationship Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To the appearance and behavior of the Relationship Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
@@ -136,21 +135,19 @@ Note: If `sortOptions` is not defined, the default sorting behavior of the Relat
|
||||
|
||||
## Filtering relationship options
|
||||
|
||||
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both
|
||||
for validating input and filtering available relationships in the UI.
|
||||
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both for validating input and filtering available relationships in the UI.
|
||||
|
||||
The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to
|
||||
prevent all, or a `Where` query. When using a function, it will be
|
||||
called with an argument object with the following properties:
|
||||
The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to prevent all, or a `Where` query. When using a function, it will be called with an argument object with the following properties:
|
||||
|
||||
| Property | Description |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property |
|
||||
| `data` | An object containing the full collection or global document currently being edited |
|
||||
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field |
|
||||
| `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation |
|
||||
| `user` | An object containing the currently authenticated user |
|
||||
| `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. |
|
||||
| Property | Description |
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `blockData` | The data of the nearest parent block. Will be `undefined` if the field is not within a block or when called on a `Filter` component within the list view. |
|
||||
| `data` | An object containing the full collection or global document currently being edited. Will be an empty object when called on a `Filter` component within the list view. |
|
||||
| `id` | The `id` of the current document being edited. Will be `undefined` during the `create` operation or when called on a `Filter` component within the list view. |
|
||||
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property. |
|
||||
| `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. |
|
||||
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. Will be an emprt object when called on a `Filter` component within the list view. |
|
||||
| `user` | An object containing the currently authenticated user. |
|
||||
|
||||
## Example
|
||||
|
||||
@@ -296,7 +293,7 @@ The `hasMany` tells Payload that there may be more than one collection saved to
|
||||
}
|
||||
```
|
||||
|
||||
To save the to `hasMany` relationship field we need to send an array of IDs:
|
||||
To save to the `hasMany` relationship field we need to send an array of IDs:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -68,7 +68,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Select Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Select Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -56,7 +56,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Text Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Text Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -53,7 +53,7 @@ _* An asterisk denotes that a property is required._
|
||||
|
||||
## Admin Options
|
||||
|
||||
The customize the appearance and behavior of the Textarea Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
To customize the appearance and behavior of the Textarea Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
|
||||
|
||||
```ts
|
||||
import type { Field } from 'payload'
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Context allows you to pass in extra data that can be shared between hooks
|
||||
keywords: hooks, context, payload context, payloadcontext, data, extra data, shared data, shared, extra
|
||||
---
|
||||
|
||||
The `context` object is used to share data across different Hooks. This persists throughout the entire lifecycle of a request and is available within every Hook. By setting properties to `req.context`, you can effectively logic across multiple Hooks.
|
||||
The `context` object is used to share data across different Hooks. This persists throughout the entire lifecycle of a request and is available within every Hook. By setting properties to `req.context`, you can effectively share logic across multiple Hooks.
|
||||
|
||||
## When To Use Context
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ The following arguments are provided to all Field Hooks:
|
||||
| **`schemaPath`** | The path of the [Field](../fields/overview) in the schema. |
|
||||
| **`siblingData`** | The data of sibling fields adjacent to the field that the Hook is running against. |
|
||||
| **`siblingDocWithLocales`** | The sibling data of the Document with all [Locales](../configuration/localization). |
|
||||
| **`siblingFields`** | The sibling fields of the field which the hook is running against.
|
||||
|
|
||||
| **`value`** | The value of the [Field](../fields/overview). |
|
||||
|
||||
<Banner type="success">
|
||||
|
||||
@@ -29,7 +29,7 @@ As mentioned above, you can queue jobs, but the jobs won't run unless a worker p
|
||||
|
||||
#### Cron jobs
|
||||
|
||||
You can use the jobs.autoRun property to configure cron jobs:
|
||||
You can use the `jobs.autoRun` property to configure cron jobs:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
@@ -47,6 +47,12 @@ export default buildConfig({
|
||||
},
|
||||
// add as many cron jobs as you want
|
||||
],
|
||||
shouldAutoRun: async (payload) => {
|
||||
// Tell Payload if it should run jobs or not.
|
||||
// This function will be invoked each time Payload goes to pick up and run jobs.
|
||||
// If this function ever returns false, the cron schedule will be stopped.
|
||||
return true
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ The most important aspect of a Workflow is the `handler`, where you can declare
|
||||
|
||||
However, importantly, tasks that have successfully been completed will simply re-return the cached and saved output without running again. The Workflow will pick back up where it failed and only task from the failure point onward will be re-executed.
|
||||
|
||||
To define a JS-based workflow, simply add a workflow to the `jobs.wokflows` array in your Payload config. A workflow consists of the following fields:
|
||||
To define a JS-based workflow, simply add a workflow to the `jobs.workflows` array in your Payload config. A workflow consists of the following fields:
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -109,10 +109,12 @@ The following arguments are provided to the `url` function:
|
||||
| **`globalConfig`** | The Global Admin Config of the Document being edited. [More details](../configuration/globals#admin-options). |
|
||||
| **`req`** | The Payload Request object. |
|
||||
|
||||
If your application requires a fully qualified URL, such as within deploying to Vercel Preview Deployments, you can use the `req` property to build this URL:
|
||||
You can return either an absolute URL or relative URL from this function. If you don't know the URL of your frontend at build-time, you can return a relative URL, and in that case, Payload will automatically construct an absolute URL by injecting the protocol, domain, and port from your browser window. Returning a relative URL is helpful for platforms like Vercel where you may have preview deployment URLs that are unknown at build time.
|
||||
|
||||
If your application requires a fully qualified URL, or you are attempting to preview with a frontend on a different domain, you can use the `req` property to build this URL:
|
||||
|
||||
```ts
|
||||
url: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlight-line
|
||||
url: ({ data, req }) => `${req.protocol}//${req.host}/${data.slug}` // highlight-line
|
||||
```
|
||||
|
||||
### Breakpoints
|
||||
|
||||
@@ -122,6 +122,18 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
* Access configuration for the array field
|
||||
*/
|
||||
arrayFieldAccess?: ArrayField['access']
|
||||
/**
|
||||
* Name of the array field
|
||||
*
|
||||
* @default 'tenants'
|
||||
*/
|
||||
arrayFieldName?: string
|
||||
/**
|
||||
* Name of the tenant field
|
||||
*
|
||||
* @default 'tenant'
|
||||
*/
|
||||
arrayTenantFieldName?: string
|
||||
/**
|
||||
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
|
||||
*/
|
||||
@@ -137,6 +149,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
}
|
||||
| {
|
||||
arrayFieldAccess?: never
|
||||
arrayFieldName?: string
|
||||
arrayTenantFieldName?: string
|
||||
/**
|
||||
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Plugins provide a great way to modularize Payload functionalities into eas
|
||||
keywords: plugins, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Payload Plugins take full advantage of the modularity of the [Payload Config](../configuration/overview), allowing developers to easily inject custom—sometimes complex—functionality into Payload apps from a very small touch-point. This is especially useful is sharing your work across multiple projects or with the greater Payload community.
|
||||
Payload Plugins take full advantage of the modularity of the [Payload Config](../configuration/overview), allowing developers to easily inject custom—sometimes complex—functionality into Payload apps from a very small touch-point. This is especially useful for sharing your work across multiple projects or with the greater Payload community.
|
||||
|
||||
There are many [Official Plugins](#official-plugins) available that solve for some of the most common uses cases, such as the [Form Builder Plugin](./form-builder) or [SEO Plugin](./seo). There are also [Community Plugins](#community-plugins) available, maintained entirely by contributing members. To extend Payload's functionality in some other way, you can easily [build your own plugin](./build-your-own).
|
||||
|
||||
|
||||
@@ -101,8 +101,7 @@ unsupported `$facet` aggregation.
|
||||
### CosmosDB
|
||||
|
||||
When using Azure Cosmos DB, an index is needed for any field you may want to sort on. To add the sort index for all
|
||||
fields that may be sorted in the admin UI use the <a href="/docs/configuration/overview">indexSortableFields</a>
|
||||
configuration option.
|
||||
fields that may be sorted in the admin UI use the [indexSortableFields](/docs/configuration/overview) option.
|
||||
|
||||
## File storage
|
||||
|
||||
|
||||
@@ -578,7 +578,7 @@ Each endpoint object needs to have:
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`path`** | A string for the endpoint route after the collection or globals slug |
|
||||
| **`method`** | The lowercase HTTP verb to use: 'get', 'head', 'post', 'put', 'delete', 'connect' or 'options' |
|
||||
| **`handler`** | A function or array of functions to be called with **req**, **res** and **next** arguments. [Next.js](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) |
|
||||
| **`handler`** | A function that accepts **req** - `PayloadRequest` object which contains [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) properties, currently authenticated `user` and the Local API instance `payload`. |
|
||||
| **`root`** | When `true`, defines the endpoint on the root Next.js app, bypassing Payload handlers and the `routes.api` subpath. Note: this only applies to top-level endpoints of your Payload Config, endpoints defined on `collections` or `globals` cannot be root. |
|
||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
|
||||
// myTextBlock is the slug of the block
|
||||
myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
|
||||
},
|
||||
inlineBlocks: {
|
||||
// myInlineBlock is the slug of the block
|
||||
myInlineBlock: ({ node }) => <span>{node.fields.text}</span>,
|
||||
},
|
||||
})
|
||||
|
||||
export const MyComponent = ({ lexicalData }) => {
|
||||
|
||||
@@ -614,7 +614,7 @@ import {
|
||||
COMMAND_PRIORITY_EDITOR
|
||||
} from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext.js'
|
||||
import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext'
|
||||
import { $insertNodeToNearestRoot } from '@payloadcms/richtext-lexical/lexical/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ Here's an overview of all the included features:
|
||||
| **`CheckListFeature`** | Yes | Adds checklists |
|
||||
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
|
||||
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
|
||||
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes |
|
||||
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
|
||||
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
|
||||
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
|
||||
@@ -174,7 +174,7 @@ Notice how even the toolbars are features? That's how extensible our lexical edi
|
||||
|
||||
## Creating your own, custom Feature
|
||||
|
||||
You can find more information about creating your own feature in our [building custom feature docs](../rich-text/building-custom-features).
|
||||
You can find more information about creating your own feature in our [building custom feature docs](../rich-text/custom-features).
|
||||
|
||||
## TypeScript
|
||||
|
||||
|
||||
@@ -113,7 +113,24 @@ _An asterisk denotes that an option is required._
|
||||
|
||||
### Payload-wide Upload Options
|
||||
|
||||
Upload options are specifiable on a Collection by Collection basis, you can also control app wide options by passing your base Payload Config an `upload` property containing an object supportive of all `Busboy` configuration options. [Click here](https://github.com/mscdex/busboy#api) for more documentation about what you can control.
|
||||
Upload options are specifiable on a Collection by Collection basis, you can also control app wide options by passing your base Payload Config an `upload` property containing an object supportive of all `Busboy` configuration options.
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`abortOnLimit`** | A boolean that, if `true`, returns HTTP 413 if a file exceeds the file size limit. If `false`, the file is truncated. Defaults to `false`. |
|
||||
| **`createParentPath`** | Set to `true` to automatically create a directory path when moving files from a temporary directory or buffer. Defaults to `false`. |
|
||||
| **`debug`** | A boolean that turns upload process logging on if `true`, or off if `false`. Useful for troubleshooting. Defaults to `false`. |
|
||||
| **`limitHandler`** | A function which is invoked if the file is greater than configured limits. |
|
||||
| **`parseNested`** | Set to `true` to turn `req.body` and `req.files` into nested structures. By default `req.body` and `req.files` are flat objects. Defaults to `false`. |
|
||||
| **`preserveExtension`** | Preserves file extensions with the `safeFileNames` option. Limits file names to 3 characters if `true` or a custom length if a `number`, trimming from the start of the extension. |
|
||||
| **`responseOnLimit`** | A `string` that is sent in the Response to a client if the file size limit is exceeded when used with `abortOnLimit`. |
|
||||
| **`safeFileNames`** | Set to `true` to strip non-alphanumeric characters except dashes and underscores. Can also be set to a regex to determine what to strip. Defaults to `false`. |
|
||||
| **`tempFileDir`** | A `string` path to store temporary files used when the `useTempFiles` option is set to `true`. Defaults to `'./tmp'`. |
|
||||
| **`uploadTimeout`** | A `number` that defines how long to wait for data before aborting, specified in milliseconds. Set to `0` to disable timeout checks. Defaults to `60000`. |
|
||||
| **`uriDecodeFileNames`** | Set to `true` to apply uri decoding to file names. Defaults to `false`. |
|
||||
| **`useTempFiles`** | Set to `true` to store files to a temporary directory instead of in RAM, reducing memory usage for large files or many files. |
|
||||
|
||||
[Click here](https://github.com/mscdex/busboy#api) for more documentation about what you can control with `Busboy`.
|
||||
|
||||
A common example of what you might want to customize within Payload-wide Upload options would be to increase the allowed `fileSize` of uploads sent to Payload:
|
||||
|
||||
|
||||
@@ -19,9 +19,10 @@ export const defaultESLintIgnores = [
|
||||
'**/build/',
|
||||
'**/node_modules/',
|
||||
'**/temp/',
|
||||
'**/*.spec.ts',
|
||||
'packages/**/*.spec.ts',
|
||||
'next-env.d.ts',
|
||||
'**/app',
|
||||
'src/**/*.spec.ts',
|
||||
]
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
@@ -29,10 +30,7 @@ export const defaultESLintIgnores = [
|
||||
export const rootParserOptions = {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
projectService: {
|
||||
maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 40,
|
||||
allowDefaultProject: ['scripts/*.ts', '*.js', '*.mjs', '*.d.ts'],
|
||||
},
|
||||
projectService: true,
|
||||
}
|
||||
|
||||
/** @type {Config[]} */
|
||||
|
||||
@@ -10,11 +10,17 @@ To spin up this example locally, follow these steps:
|
||||
|
||||
- `npx create-payload-app --example multi-tenant`
|
||||
|
||||
2. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
2. `cp .env.example .env` to copy the example environment variables
|
||||
|
||||
3. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
|
||||
- Press `y` when prompted to seed the database
|
||||
3. `open http://localhost:3000` to access the home page
|
||||
4. `open http://localhost:3000/admin` to access the admin panel
|
||||
- Login with email `demo@payloadcms.com` and password `demo`
|
||||
4. `open http://localhost:3000` to access the home page
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
|
||||
### Default users
|
||||
|
||||
The seed script seeds 3 tenants.
|
||||
Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -28,7 +34,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
- #### Users
|
||||
|
||||
The `users` collection is auth-enabled and encompass both app-wide and tenant-scoped users based on the value of their `roles` and `tenants` fields. Users with the role `super-admin` can manage your entire application, while users with the _tenant role_ of `admin` have limited access to the platform and can manage only the tenant(s) they are assigned to, see [Tenants](#tenants) for more details.
|
||||
The `users` collection is auth-enabled and encompasses both app-wide and tenant-scoped users based on the value of their `roles` and `tenants` fields. Users with the role `super-admin` can manage your entire application, while users with the _tenant role_ of `admin` have limited access to the platform and can manage only the tenant(s) they are assigned to, see [Tenants](#tenants) for more details.
|
||||
|
||||
For additional help with authentication, see the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/cms#readme) or the [Authentication](https://payloadcms.com/docs/authentication/overview#authentication-overview) docs.
|
||||
|
||||
@@ -40,13 +46,13 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
**Domain-based Tenant Setting**:
|
||||
|
||||
This example also supports domain-based tenant selection, where tenants can be associated with a specific domain. If a tenant is associated with a domain (e.g., `gold.localhost.com:3000`), when a user logs in from that domain, they will be automatically scoped to the matching tenant. This is accomplished through an optional `afterLogin` hook that sets a `payload-tenant` cookie based on the domain.
|
||||
This example also supports domain-based tenant selection, where tenants can be associated with a specific domain. If a tenant is associated with a domain (e.g., `gold.test:3000`), when a user logs in from that domain, they will be automatically scoped to the matching tenant. This is accomplished through an optional `afterLogin` hook that sets a `payload-tenant` cookie based on the domain.
|
||||
|
||||
The seed script seeds 3 tenants, for the domain portion of the example to function properly you will need to add the following entries to your systems `/etc/hosts` file:
|
||||
For the domain portion of the example to function properly, you will need to add the following entries to your system's `/etc/hosts` file:
|
||||
|
||||
- gold.localhost.com:3000
|
||||
- silver.localhost.com:3000
|
||||
- bronze.localhost.com:3000
|
||||
```
|
||||
127.0.0.1 gold.test silver.test bronze.test
|
||||
```
|
||||
|
||||
- #### Pages
|
||||
|
||||
@@ -54,7 +60,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
|
||||
|
||||
## Access control
|
||||
|
||||
Basic role-based access control is setup to determine what users can and cannot do based on their roles, which are:
|
||||
Basic role-based access control is set up to determine what users can and cannot do based on their roles, which are:
|
||||
|
||||
- `super-admin`: They can access the Payload admin panel to manage your multi-tenant application. They can see all tenants and make all operations.
|
||||
- `user`: They can only access the Payload admin panel if they are a tenant-admin, in which case they have a limited access to operations based on their tenant (see below).
|
||||
|
||||
@@ -10,10 +10,10 @@ export default async ({ params: paramsPromise }: { params: Promise<{ slug: strin
|
||||
<p>When you visit a tenant by domain, the domain is used to determine the tenant.</p>
|
||||
<p>
|
||||
For example, visiting{' '}
|
||||
<a href="http://gold.localhost.com:3000/tenant-domains/login">
|
||||
http://gold.localhost.com:3000/tenant-domains/login
|
||||
<a href="http://gold.test:3000/tenant-domains/login">
|
||||
http://gold.test:3000/tenant-domains/login
|
||||
</a>{' '}
|
||||
will show the tenant with the domain "gold.localhost.com".
|
||||
will show the tenant with the domain "gold.test".
|
||||
</p>
|
||||
|
||||
<h2>Slugs</h2>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Access } from 'payload'
|
||||
/**
|
||||
* Tenant admins and super admins can will be allowed access
|
||||
*/
|
||||
export const superAdminOrTeanantAdminAccess: Access = ({ req }) => {
|
||||
export const superAdminOrTenantAdminAccess: Access = ({ req }) => {
|
||||
if (!req.user) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { ensureUniqueSlug } from './hooks/ensureUniqueSlug'
|
||||
import { superAdminOrTeanantAdminAccess } from '@/collections/Pages/access/superAdminOrTenantAdmin'
|
||||
import { superAdminOrTenantAdminAccess } from '@/collections/Pages/access/superAdminOrTenantAdmin'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
access: {
|
||||
create: superAdminOrTeanantAdminAccess,
|
||||
delete: superAdminOrTeanantAdminAccess,
|
||||
create: superAdminOrTenantAdminAccess,
|
||||
delete: superAdminOrTenantAdminAccess,
|
||||
read: () => true,
|
||||
update: superAdminOrTeanantAdminAccess,
|
||||
update: superAdminOrTenantAdminAccess,
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
|
||||
@@ -10,6 +10,9 @@ import { setCookieBasedOnDomain } from './hooks/setCookieBasedOnDomain'
|
||||
import { tenantsArrayField } from '@payloadcms/plugin-multi-tenant/fields'
|
||||
|
||||
const defaultTenantArrayField = tenantsArrayField({
|
||||
tenantsArrayFieldName: 'tenants',
|
||||
tenantsArrayTenantFieldName: 'tenant',
|
||||
tenantsCollectionSlug: 'tenants',
|
||||
arrayFieldAccess: {},
|
||||
tenantFieldAccess: {},
|
||||
rowFields: [
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
|
||||
|
||||
export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 1',
|
||||
slug: 'gold',
|
||||
domain: 'gold.test',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant2 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 2',
|
||||
slug: 'silver',
|
||||
domain: 'silver.test',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant3 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 3',
|
||||
slug: 'bronze',
|
||||
domain: 'bronze.test',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
@@ -10,47 +37,16 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
},
|
||||
})
|
||||
|
||||
const tenant1 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 1',
|
||||
slug: 'gold',
|
||||
domain: 'gold.localhost.com',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant2 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 2',
|
||||
slug: 'silver',
|
||||
domain: 'silver.localhost.com',
|
||||
},
|
||||
})
|
||||
|
||||
const tenant3 = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'Tenant 3',
|
||||
slug: 'bronze',
|
||||
domain: 'bronze.localhost.com',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant1@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
tenant: tenant1.id,
|
||||
},
|
||||
// {
|
||||
// roles: ['tenant-admin'],
|
||||
// tenant: tenant2.id,
|
||||
// },
|
||||
],
|
||||
username: 'tenant1',
|
||||
},
|
||||
@@ -60,7 +56,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant2@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -75,7 +71,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'tenant3@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -90,7 +86,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'multi-admin@payloadcms.com',
|
||||
password: 'test',
|
||||
password: 'demo',
|
||||
tenants: [
|
||||
{
|
||||
roles: ['tenant-admin'],
|
||||
@@ -105,7 +101,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
tenant: tenant3.id,
|
||||
},
|
||||
],
|
||||
username: 'tenant3',
|
||||
username: 'multi-admin',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -54,6 +54,7 @@ const generateEnvContent = (
|
||||
.filter((line) => line.includes('=') && !line.startsWith('#'))
|
||||
.forEach((line) => {
|
||||
const [key, value] = line.split('=')
|
||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||
envVars[key] = value
|
||||
})
|
||||
|
||||
|
||||
@@ -224,12 +224,12 @@ function insertBeforeAndAfter(content: string, loc: Loc): string {
|
||||
}
|
||||
|
||||
// insert ) after end
|
||||
lines[end.line - 1] = insert(lines[end.line - 1], end.column, ')')
|
||||
lines[end.line - 1] = insert(lines[end.line - 1]!, end.column, ')')
|
||||
// insert withPayload before start
|
||||
if (start.line === end.line) {
|
||||
lines[end.line - 1] = insert(lines[end.line - 1], start.column, 'withPayload(')
|
||||
lines[end.line - 1] = insert(lines[end.line - 1]!, start.column, 'withPayload(')
|
||||
} else {
|
||||
lines[start.line - 1] = insert(lines[start.line - 1], start.column, 'withPayload(')
|
||||
lines[start.line - 1] = insert(lines[start.line - 1]!, start.column, 'withPayload(')
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* TODO: remove the following lines */
|
||||
"noUncheckedIndexedAccess": false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -34,11 +34,6 @@ export const buildCollectionSchema = (
|
||||
schema.index(indexDefinition, { unique: true })
|
||||
}
|
||||
|
||||
if (payload.config.indexSortableFields && collection.timestamps !== false) {
|
||||
schema.index({ updatedAt: 1 })
|
||||
schema.index({ createdAt: 1 })
|
||||
}
|
||||
|
||||
schema
|
||||
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
|
||||
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }))
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { IndexOptions, Schema, SchemaOptions, SchemaTypeOptions } from 'mon
|
||||
import mongoose from 'mongoose'
|
||||
import {
|
||||
type ArrayField,
|
||||
type Block,
|
||||
type BlocksField,
|
||||
type CheckboxField,
|
||||
type CodeField,
|
||||
@@ -193,11 +192,12 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, fieldSchema, payload.config.localization),
|
||||
})
|
||||
|
||||
field.blocks.forEach((blockItem: Block) => {
|
||||
;(field.blockReferences ?? field.blocks).forEach((blockItem) => {
|
||||
const blockSchema = new mongoose.Schema({}, { _id: false, id: false })
|
||||
|
||||
blockItem.fields.forEach((blockField) => {
|
||||
const block = typeof blockItem === 'string' ? payload.blocks[blockItem] : blockItem
|
||||
|
||||
block.fields.forEach((blockField) => {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(blockField, blockSchema, payload, buildSchemaOptions)
|
||||
@@ -207,11 +207,11 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
if (field.localized && payload.config.localization) {
|
||||
payload.config.localization.localeCodes.forEach((localeCode) => {
|
||||
// @ts-expect-error Possible incorrect typing in mongoose types, this works
|
||||
schema.path(`${field.name}.${localeCode}`).discriminator(blockItem.slug, blockSchema)
|
||||
schema.path(`${field.name}.${localeCode}`).discriminator(block.slug, blockSchema)
|
||||
})
|
||||
} else {
|
||||
// @ts-expect-error Possible incorrect typing in mongoose types, this works
|
||||
schema.path(field.name).discriminator(blockItem.slug, blockSchema)
|
||||
schema.path(field.name).discriminator(block.slug, blockSchema)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -80,6 +80,10 @@ const hasRelationshipOrUploadField = ({ fields }: { fields: Field[] }): boolean
|
||||
|
||||
if ('blocks' in field) {
|
||||
for (const block of field.blocks) {
|
||||
if (typeof block === 'string') {
|
||||
// Skip - string blocks have been added in v3 and thus don't need to be migrated
|
||||
continue
|
||||
}
|
||||
if (hasRelationshipOrUploadField({ fields: block.fields })) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -65,18 +65,24 @@ export const getLocalizedSortProperty = ({
|
||||
}
|
||||
|
||||
if (matchedField.type === 'blocks') {
|
||||
nextFields = matchedField.blocks.reduce((flattenedBlockFields, block) => {
|
||||
return [
|
||||
...flattenedBlockFields,
|
||||
...block.flattenedFields.filter(
|
||||
(blockField) =>
|
||||
(fieldAffectsData(blockField) &&
|
||||
blockField.name !== 'blockType' &&
|
||||
blockField.name !== 'blockName') ||
|
||||
!fieldAffectsData(blockField),
|
||||
),
|
||||
]
|
||||
}, [])
|
||||
nextFields = (matchedField.blockReferences ?? matchedField.blocks).reduce(
|
||||
(flattenedBlockFields, _block) => {
|
||||
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
|
||||
const block =
|
||||
typeof _block === 'string' ? config.blocks.find((b) => b.slug === _block) : _block
|
||||
return [
|
||||
...flattenedBlockFields,
|
||||
...block.flattenedFields.filter(
|
||||
(blockField) =>
|
||||
(fieldAffectsData(blockField) &&
|
||||
blockField.name !== 'blockType' &&
|
||||
blockField.name !== 'blockName') ||
|
||||
!fieldAffectsData(blockField),
|
||||
),
|
||||
]
|
||||
},
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
const result = incomingResult ? `${incomingResult}.${localizedSegment}` : localizedSegment
|
||||
|
||||
@@ -52,30 +52,37 @@ export async function parseParams({
|
||||
// So we need to loop on keys again here to handle each operator independently
|
||||
const pathOperators = where[relationOrPath]
|
||||
if (typeof pathOperators === 'object') {
|
||||
for (const operator of Object.keys(pathOperators)) {
|
||||
if (validOperatorSet.has(operator as Operator)) {
|
||||
const searchParam = await buildSearchParam({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
incomingPath: relationOrPath,
|
||||
locale,
|
||||
operator,
|
||||
payload,
|
||||
val: pathOperators[operator],
|
||||
})
|
||||
const validOperators = Object.keys(pathOperators).filter((operator) =>
|
||||
validOperatorSet.has(operator as Operator),
|
||||
)
|
||||
for (const operator of validOperators) {
|
||||
const searchParam = await buildSearchParam({
|
||||
collectionSlug,
|
||||
fields,
|
||||
globalSlug,
|
||||
incomingPath: relationOrPath,
|
||||
locale,
|
||||
operator,
|
||||
payload,
|
||||
val: pathOperators[operator],
|
||||
})
|
||||
|
||||
if (searchParam?.value && searchParam?.path) {
|
||||
result = {
|
||||
...result,
|
||||
[searchParam.path]: searchParam.value,
|
||||
if (searchParam?.value && searchParam?.path) {
|
||||
if (validOperators.length > 1) {
|
||||
if (!result.$and) {
|
||||
result.$and = []
|
||||
}
|
||||
} else if (typeof searchParam?.value === 'object') {
|
||||
result = deepMergeWithCombinedArrays(result, searchParam.value, {
|
||||
// dont clone Types.ObjectIDs
|
||||
clone: false,
|
||||
result.$and.push({
|
||||
[searchParam.path]: searchParam.value,
|
||||
})
|
||||
} else {
|
||||
result[searchParam.path] = searchParam.value
|
||||
}
|
||||
} else if (typeof searchParam?.value === 'object') {
|
||||
result = deepMergeWithCombinedArrays(result, searchParam.value, {
|
||||
// dont clone Types.ObjectIDs
|
||||
clone: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { FlattenedBlock, FlattenedField, Payload, RelationshipField } from 'payload'
|
||||
import type {
|
||||
FlattenedBlock,
|
||||
FlattenedBlocksField,
|
||||
FlattenedField,
|
||||
Payload,
|
||||
RelationshipField,
|
||||
} from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { createArrayFromCommaDelineated } from 'payload'
|
||||
@@ -40,14 +46,18 @@ const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => {
|
||||
// returns nestedField Field object from blocks.nestedField path because getLocalizedPaths splits them only for relationships
|
||||
const getFieldFromSegments = ({
|
||||
field,
|
||||
payload,
|
||||
segments,
|
||||
}: {
|
||||
field: FlattenedBlock | FlattenedField
|
||||
payload: Payload
|
||||
segments: string[]
|
||||
}) => {
|
||||
if ('blocks' in field) {
|
||||
for (const block of field.blocks) {
|
||||
const field = getFieldFromSegments({ field: block, segments })
|
||||
if ('blocks' in field || 'blockReferences' in field) {
|
||||
const _field: FlattenedBlocksField = field as FlattenedBlocksField
|
||||
for (const _block of _field.blockReferences ?? _field.blocks) {
|
||||
const block: FlattenedBlock = typeof _block === 'string' ? payload.blocks[_block] : _block
|
||||
const field = getFieldFromSegments({ field: block, payload, segments })
|
||||
if (field) {
|
||||
return field
|
||||
}
|
||||
@@ -67,7 +77,7 @@ const getFieldFromSegments = ({
|
||||
}
|
||||
|
||||
segments.shift()
|
||||
return getFieldFromSegments({ field: foundField, segments })
|
||||
return getFieldFromSegments({ field: foundField, payload, segments })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +101,7 @@ export const sanitizeQueryValue = ({
|
||||
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
|
||||
const segments = path.split('.')
|
||||
segments.shift()
|
||||
const foundField = getFieldFromSegments({ field, segments })
|
||||
const foundField = getFieldFromSegments({ field, payload, segments })
|
||||
|
||||
if (foundField) {
|
||||
field = foundField
|
||||
|
||||
@@ -123,7 +123,8 @@ const traverseFields = ({
|
||||
case 'blocks': {
|
||||
const blocksSelect = select[field.name] as SelectType
|
||||
|
||||
for (const block of field.blocks) {
|
||||
for (const _block of field.blockReferences ?? field.blocks) {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
if (
|
||||
(selectMode === 'include' && blocksSelect[block.slug] === true) ||
|
||||
(selectMode === 'exclude' && typeof blocksSelect[block.slug] === 'undefined')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Field, SanitizedConfig } from 'payload'
|
||||
import { flattenAllFields, type Field, type SanitizedConfig } from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
|
||||
@@ -74,7 +74,28 @@ const relsFields: Field[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const referenceBlockFields: Field[] = [
|
||||
...relsFields,
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: relsFields,
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: relsFields,
|
||||
},
|
||||
]
|
||||
|
||||
const config = {
|
||||
blocks: [
|
||||
{
|
||||
slug: 'reference-block',
|
||||
fields: referenceBlockFields,
|
||||
flattenedFields: flattenAllFields({ fields: referenceBlockFields }),
|
||||
},
|
||||
],
|
||||
collections: [
|
||||
{
|
||||
slug: 'docs',
|
||||
@@ -137,6 +158,11 @@ const config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blockReferences',
|
||||
type: 'blocks',
|
||||
blockReferences: ['reference-block'],
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
@@ -321,6 +347,14 @@ describe('sanitizeRelationshipIDs', () => {
|
||||
group: { ...relsData },
|
||||
},
|
||||
],
|
||||
blockReferences: [
|
||||
{
|
||||
blockType: 'reference-block',
|
||||
array: [{ ...relsData }],
|
||||
group: { ...relsData },
|
||||
...relsData,
|
||||
},
|
||||
],
|
||||
group: {
|
||||
...relsData,
|
||||
array: [{ ...relsData }],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { APIError, traverseFields } from 'payload'
|
||||
import { traverseFields } from 'payload'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
|
||||
type Args = {
|
||||
@@ -150,7 +150,7 @@ export const sanitizeRelationshipIDs = ({
|
||||
}
|
||||
}
|
||||
|
||||
traverseFields({ callback: sanitize, fields, fillEmpty: false, ref: data })
|
||||
traverseFields({ callback: sanitize, config, fields, fillEmpty: false, ref: data })
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ChainedMethods } from '@payloadcms/drizzle/types'
|
||||
|
||||
import { chainMethods } from '@payloadcms/drizzle'
|
||||
import { count } from 'drizzle-orm'
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
|
||||
import type { CountDistinct, SQLiteAdapter } from './types.js'
|
||||
|
||||
@@ -9,6 +9,17 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
this: SQLiteAdapter,
|
||||
{ db, joins, tableName, where },
|
||||
) {
|
||||
// When we don't have any joins - use a simple COUNT(*) query.
|
||||
if (joins.length === 0) {
|
||||
const countResult = await db
|
||||
.select({
|
||||
count: count(),
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
return Number(countResult[0].count)
|
||||
}
|
||||
|
||||
const chainedMethods: ChainedMethods = []
|
||||
|
||||
joins.forEach(({ condition, table }) => {
|
||||
@@ -18,14 +29,20 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
})
|
||||
})
|
||||
|
||||
// When we have any joins, we need to count each individual ID only once.
|
||||
// COUNT(*) doesn't work for this well in this case, as it also counts joined tables.
|
||||
// SELECT (COUNT DISTINCT id) has a very slow performance on large tables.
|
||||
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
|
||||
const countResult = await chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: db
|
||||
.select({
|
||||
count: count(),
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where),
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1),
|
||||
})
|
||||
|
||||
return Number(countResult[0].count)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -16,7 +16,9 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
|
||||
|
||||
const result = await upsertRow<T>({
|
||||
data.createdAt = new Date().toISOString()
|
||||
|
||||
const result = await upsertRow<{ globalType: string } & T>({
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
@@ -26,5 +28,7 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
tableName,
|
||||
})
|
||||
|
||||
result.globalType = slug
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -185,7 +185,8 @@ export const traverseFields = ({
|
||||
}
|
||||
}
|
||||
|
||||
field.blocks.forEach((block) => {
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
const blockKey = `_blocks_${block.slug}`
|
||||
|
||||
let blockSelect: boolean | SelectType | undefined
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { count } from 'drizzle-orm'
|
||||
import { count, sql } from 'drizzle-orm'
|
||||
|
||||
import type { ChainedMethods, TransactionPg } from '../types.js'
|
||||
import type { ChainedMethods } from '../types.js'
|
||||
import type { BasePostgresAdapter, CountDistinct } from './types.js'
|
||||
|
||||
import { chainMethods } from '../find/chainMethods.js'
|
||||
@@ -9,6 +9,17 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
this: BasePostgresAdapter,
|
||||
{ db, joins, tableName, where },
|
||||
) {
|
||||
// When we don't have any joins - use a simple COUNT(*) query.
|
||||
if (joins.length === 0) {
|
||||
const countResult = await db
|
||||
.select({
|
||||
count: count(),
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where)
|
||||
return Number(countResult[0].count)
|
||||
}
|
||||
|
||||
const chainedMethods: ChainedMethods = []
|
||||
|
||||
joins.forEach(({ condition, table }) => {
|
||||
@@ -18,14 +29,20 @@ export const countDistinct: CountDistinct = async function countDistinct(
|
||||
})
|
||||
})
|
||||
|
||||
// When we have any joins, we need to count each individual ID only once.
|
||||
// COUNT(*) doesn't work for this well in this case, as it also counts joined tables.
|
||||
// SELECT (COUNT DISTINCT id) has a very slow performance on large tables.
|
||||
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
|
||||
const countResult = await chainMethods({
|
||||
methods: chainedMethods,
|
||||
query: (db as TransactionPg)
|
||||
query: db
|
||||
.select({
|
||||
count: count(),
|
||||
count: sql`COUNT(1) OVER()`,
|
||||
})
|
||||
.from(this.tables[tableName])
|
||||
.where(where),
|
||||
.where(where)
|
||||
.groupBy(this.tables[tableName].id)
|
||||
.limit(1),
|
||||
})
|
||||
|
||||
return Number(countResult[0].count)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FlattenedField } from 'payload'
|
||||
import type { FlattenedBlock, FlattenedField } from 'payload'
|
||||
|
||||
type Args = {
|
||||
doc: Record<string, unknown>
|
||||
@@ -51,7 +51,10 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
Object.entries(rowData).forEach(([locale, localeRows]) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, i) => {
|
||||
const matchedBlock = field.blocks.find((block) => block.slug === row.blockType)
|
||||
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
|
||||
const matchedBlock = field.blocks.find(
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined
|
||||
|
||||
if (matchedBlock) {
|
||||
return traverseFields({
|
||||
@@ -69,7 +72,10 @@ export const traverseFields = ({ doc, fields, locale, path, rows }: Args) => {
|
||||
|
||||
if (Array.isArray(rowData)) {
|
||||
rowData.forEach((row, i) => {
|
||||
const matchedBlock = field.blocks.find((block) => block.slug === row.blockType)
|
||||
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
|
||||
const matchedBlock = field.blocks.find(
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined
|
||||
|
||||
if (matchedBlock) {
|
||||
return traverseFields({
|
||||
|
||||
@@ -42,6 +42,11 @@ export const traverseFields = (args: Args) => {
|
||||
|
||||
case 'blocks': {
|
||||
return field.blocks.forEach((block) => {
|
||||
// Can ignore string blocks, as those were added in v3 and don't need to be migrated
|
||||
if (typeof block === 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
const newTableName = args.adapter.tableNameMap.get(
|
||||
`${args.rootTableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||
import { type SQL } from 'drizzle-orm'
|
||||
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||
|
||||
import type { GenericTable } from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
@@ -10,16 +10,18 @@ export const addJoinTable = ({
|
||||
type,
|
||||
condition,
|
||||
joins,
|
||||
queryPath,
|
||||
table,
|
||||
}: {
|
||||
condition: SQL
|
||||
joins: BuildQueryJoinAliases
|
||||
queryPath?: string
|
||||
table: GenericTable | PgTableWithColumns<any>
|
||||
type?: 'innerJoin' | 'leftJoin' | 'rightJoin'
|
||||
}) => {
|
||||
const name = getNameFromDrizzleTable(table)
|
||||
|
||||
if (!joins.some((eachJoin) => getNameFromDrizzleTable(eachJoin.table) === name)) {
|
||||
joins.push({ type, condition, table })
|
||||
joins.push({ type, condition, queryPath, table })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { parseParams } from './parseParams.js'
|
||||
|
||||
export type BuildQueryJoinAliases = {
|
||||
condition: SQL
|
||||
queryPath?: string
|
||||
table: GenericTable | PgTableWithColumns<any>
|
||||
type?: 'innerJoin' | 'leftJoin' | 'rightJoin'
|
||||
}[]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'
|
||||
import type { FlattenedField, NumberField, TextField } from 'payload'
|
||||
import type { FlattenedBlock, FlattenedField, NumberField, TextField } from 'payload'
|
||||
|
||||
import { and, eq, like, sql } from 'drizzle-orm'
|
||||
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
|
||||
@@ -176,7 +176,12 @@ export const getTableColumnFromPath = ({
|
||||
// find the block config using the value
|
||||
const blockTypes = Array.isArray(value) ? value : [value]
|
||||
blockTypes.forEach((blockType) => {
|
||||
const block = field.blocks.find((block) => block.slug === blockType)
|
||||
const block =
|
||||
adapter.payload.blocks[blockType] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
newTableName = adapter.tableNameMap.get(
|
||||
`${tableName}_blocks_${toSnakeCase(block.slug)}`,
|
||||
)
|
||||
@@ -201,11 +206,13 @@ export const getTableColumnFromPath = ({
|
||||
}
|
||||
}
|
||||
|
||||
const hasBlockField = field.blocks.some((block) => {
|
||||
const hasBlockField = (field.blockReferences ?? field.blocks).some((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
|
||||
newTableName = adapter.tableNameMap.get(`${tableName}_blocks_${toSnakeCase(block.slug)}`)
|
||||
constraintPath = `${constraintPath}${field.name}.%.`
|
||||
|
||||
let result
|
||||
let result: TableColumn
|
||||
const blockConstraints = []
|
||||
const blockSelectFields = {}
|
||||
try {
|
||||
@@ -363,7 +370,10 @@ export const getTableColumnFromPath = ({
|
||||
const {
|
||||
newAliasTable: aliasRelationshipTable,
|
||||
newAliasTableName: aliasRelationshipTableName,
|
||||
} = getTableAlias({ adapter, tableName: relationTableName })
|
||||
} = getTableAlias({
|
||||
adapter,
|
||||
tableName: relationTableName,
|
||||
})
|
||||
|
||||
if (selectLocale && field.localized && adapter.payload.config.localization) {
|
||||
selectFields._locale = aliasRelationshipTable.locale
|
||||
@@ -380,17 +390,21 @@ export const getTableColumnFromPath = ({
|
||||
conditions.push(eq(aliasRelationshipTable.locale, locale))
|
||||
}
|
||||
|
||||
joins.push({
|
||||
addJoinTable({
|
||||
condition: and(...conditions),
|
||||
joins,
|
||||
queryPath: `${constraintPath}.${field.name}`,
|
||||
table: aliasRelationshipTable,
|
||||
})
|
||||
} else {
|
||||
// Join in the relationships table
|
||||
joins.push({
|
||||
addJoinTable({
|
||||
condition: and(
|
||||
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
|
||||
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
|
||||
),
|
||||
joins,
|
||||
queryPath: `${constraintPath}.${field.name}`,
|
||||
table: aliasRelationshipTable,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,9 +87,11 @@ export const sanitizeQueryValue = ({
|
||||
if (field.type === 'number') {
|
||||
formattedValue = formattedValue.map((arrayVal) => parseFloat(arrayVal))
|
||||
}
|
||||
} else if (typeof formattedValue === 'number') {
|
||||
formattedValue = [formattedValue]
|
||||
}
|
||||
|
||||
if (!Array.isArray(formattedValue) || formattedValue.length === 0) {
|
||||
if (!Array.isArray(formattedValue)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +343,9 @@ export const traverseFields = ({
|
||||
case 'blocks': {
|
||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||
|
||||
field.blocks.forEach((block) => {
|
||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||
|
||||
const blockTableName = createTableName({
|
||||
adapter,
|
||||
config: block,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
|
||||
import type { FlattenedBlock, FlattenedField, JoinQuery, SanitizedConfig } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldIsVirtual } from 'payload/shared'
|
||||
import { fieldIsVirtual } from 'payload/shared'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
|
||||
@@ -216,7 +216,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
|
||||
Object.entries(result[field.name]).forEach(([locale, localizedBlocks]) => {
|
||||
result[field.name][locale] = localizedBlocks.map((row) => {
|
||||
const block = field.blocks.find(({ slug }) => slug === row.blockType)
|
||||
const block =
|
||||
adapter.payload.blocks[row.blockType] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (block) {
|
||||
const blockResult = traverseFields<T>({
|
||||
@@ -265,7 +269,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
||||
row.id = row._uuid
|
||||
delete row._uuid
|
||||
}
|
||||
const block = field.blocks.find(({ slug }) => slug === row.blockType)
|
||||
|
||||
if (typeof row.blockType !== 'string') {
|
||||
return acc
|
||||
}
|
||||
|
||||
const block =
|
||||
adapter.payload.blocks[row.blockType] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === row.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (block) {
|
||||
if (
|
||||
|
||||
@@ -97,6 +97,7 @@ export const transformArray = ({
|
||||
data: arrayRow,
|
||||
fieldPrefix: '',
|
||||
fields: field.flattenedFields,
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: arrayTableName,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FlattenedBlocksField } from 'payload'
|
||||
import type { FlattenedBlock, FlattenedBlocksField } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
@@ -51,7 +51,13 @@ export const transformBlocks = ({
|
||||
if (typeof blockRow.blockType !== 'string') {
|
||||
return
|
||||
}
|
||||
const matchedBlock = field.blocks.find(({ slug }) => slug === blockRow.blockType)
|
||||
|
||||
const matchedBlock =
|
||||
adapter.payload.blocks[blockRow.blockType] ??
|
||||
((field.blockReferences ?? field.blocks).find(
|
||||
(block) => typeof block !== 'string' && block.slug === blockRow.blockType,
|
||||
) as FlattenedBlock | undefined)
|
||||
|
||||
if (!matchedBlock) {
|
||||
return
|
||||
}
|
||||
@@ -101,6 +107,7 @@ export const transformBlocks = ({
|
||||
data: blockRow,
|
||||
fieldPrefix: '',
|
||||
fields: matchedBlock.flattenedFields,
|
||||
insideArrayOrBlock: true,
|
||||
locales: newRow.locales,
|
||||
numbers,
|
||||
parentTableName: blockTableName,
|
||||
|
||||
@@ -42,6 +42,10 @@ type Args = {
|
||||
fieldPrefix: string
|
||||
fields: FlattenedField[]
|
||||
forcedLocale?: string
|
||||
/**
|
||||
* Tracks whether the current traversion context is from array or block.
|
||||
*/
|
||||
insideArrayOrBlock?: boolean
|
||||
locales: {
|
||||
[locale: string]: Record<string, unknown>
|
||||
}
|
||||
@@ -77,6 +81,7 @@ export const traverseFields = ({
|
||||
fieldPrefix,
|
||||
fields,
|
||||
forcedLocale,
|
||||
insideArrayOrBlock = false,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -163,8 +168,8 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
if (field.type === 'blocks') {
|
||||
field.blocks.forEach(({ slug }) => {
|
||||
blocksToDelete.add(toSnakeCase(slug))
|
||||
;(field.blockReferences ?? field.blocks).forEach((block) => {
|
||||
blocksToDelete.add(toSnakeCase(typeof block === 'string' ? block : block.slug))
|
||||
})
|
||||
|
||||
if (field.localized) {
|
||||
@@ -230,6 +235,7 @@ export const traverseFields = ({
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.flattenedFields,
|
||||
forcedLocale: localeKey,
|
||||
insideArrayOrBlock,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -258,6 +264,7 @@ export const traverseFields = ({
|
||||
existingLocales,
|
||||
fieldPrefix: `${fieldName}_`,
|
||||
fields: field.flattenedFields,
|
||||
insideArrayOrBlock,
|
||||
locales,
|
||||
numbers,
|
||||
parentTableName,
|
||||
@@ -420,7 +427,7 @@ export const traverseFields = ({
|
||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||
if (Array.isArray(localeData)) {
|
||||
const newRows = transformSelects({
|
||||
id: data._uuid || data.id,
|
||||
id: insideArrayOrBlock ? data._uuid || data.id : undefined,
|
||||
data: localeData,
|
||||
locale: localeKey,
|
||||
})
|
||||
@@ -431,7 +438,7 @@ export const traverseFields = ({
|
||||
}
|
||||
} else if (Array.isArray(data[field.name])) {
|
||||
const newRows = transformSelects({
|
||||
id: data._uuid || data.id,
|
||||
id: insideArrayOrBlock ? data._uuid || data.id : undefined,
|
||||
data: data[field.name],
|
||||
locale: withinArrayOrBlockLocale,
|
||||
})
|
||||
@@ -472,8 +479,9 @@ export const traverseFields = ({
|
||||
}
|
||||
|
||||
valuesToTransform.forEach(({ localeKey, ref, value }) => {
|
||||
let formattedValue = value
|
||||
|
||||
if (typeof value !== 'undefined') {
|
||||
let formattedValue = value
|
||||
if (value && field.type === 'point' && adapter.name !== 'sqlite') {
|
||||
formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})`
|
||||
}
|
||||
@@ -483,12 +491,16 @@ export const traverseFields = ({
|
||||
formattedValue = new Date(value).toISOString()
|
||||
} else if (value instanceof Date) {
|
||||
formattedValue = value.toISOString()
|
||||
} else if (fieldName === 'updatedAt') {
|
||||
// let the db handle this
|
||||
formattedValue = new Date().toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date' && fieldName === 'updatedAt') {
|
||||
// let the db handle this
|
||||
formattedValue = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (typeof formattedValue !== 'undefined') {
|
||||
if (localeKey) {
|
||||
ref[localeKey][fieldName] = formattedValue
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
|
||||
const existingGlobal = await db.query[tableName].findFirst({})
|
||||
|
||||
const result = await upsertRow<T>({
|
||||
const result = await upsertRow<{ globalType: string } & T>({
|
||||
...(existingGlobal ? { id: existingGlobal.id, operation: 'update' } : { operation: 'create' }),
|
||||
adapter: this,
|
||||
data,
|
||||
@@ -28,5 +28,7 @@ export async function updateGlobal<T extends Record<string, unknown>>(
|
||||
tableName,
|
||||
})
|
||||
|
||||
result.globalType = slug
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"eslint-plugin-jest-dom": "5.4.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-hooks": "5.0.0",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-a4b2d0d5-20250203",
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"globals": "15.12.0",
|
||||
"typescript": "5.7.3",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"eslint-plugin-jest-dom": "5.4.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-hooks": "5.0.0",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-a4b2d0d5-20250203",
|
||||
"eslint-plugin-regexp": "2.6.0",
|
||||
"globals": "15.12.0",
|
||||
"typescript": "5.7.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -108,8 +108,15 @@ export function buildObjectType({
|
||||
}
|
||||
},
|
||||
blocks: (objectTypeConfig: ObjectTypeConfig, field: BlocksField) => {
|
||||
const blockTypes: GraphQLObjectType<any, any>[] = field.blocks.reduce((acc, block) => {
|
||||
if (!graphqlResult.types.blockTypes[block.slug]) {
|
||||
const blockTypes: GraphQLObjectType<any, any>[] = (
|
||||
field.blockReferences ?? field.blocks
|
||||
).reduce((acc, _block) => {
|
||||
const blockSlug = typeof _block === 'string' ? _block : _block.slug
|
||||
if (!graphqlResult.types.blockTypes[blockSlug]) {
|
||||
// TODO: iterate over blocks mapped to block slug in v4, or pass through payload.blocks
|
||||
const block =
|
||||
typeof _block === 'string' ? config.blocks.find((b) => b.slug === _block) : _block
|
||||
|
||||
const interfaceName =
|
||||
block?.interfaceName || block?.graphQL?.singularName || toWords(block.slug, true)
|
||||
|
||||
@@ -133,8 +140,8 @@ export function buildObjectType({
|
||||
}
|
||||
}
|
||||
|
||||
if (graphqlResult.types.blockTypes[block.slug]) {
|
||||
acc.push(graphqlResult.types.blockTypes[block.slug])
|
||||
if (graphqlResult.types.blockTypes[blockSlug]) {
|
||||
acc.push(graphqlResult.types.blockTypes[blockSlug])
|
||||
}
|
||||
|
||||
return acc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.20.0",
|
||||
"version": "3.23.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
'use client'
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { Link } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import { useParams, usePathname, useSearchParams } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
|
||||
export const DocumentTabLink: React.FC<{
|
||||
adminRoute: SanitizedConfig['routes']['admin']
|
||||
ariaLabel?: string
|
||||
|
||||
@@ -34,8 +34,6 @@
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-top: base(0.25);
|
||||
padding-bottom: calc(var(--base) / 1.5);
|
||||
flex-direction: column;
|
||||
|
||||
@@ -4,9 +4,8 @@ import type { groupNavItems } from '@payloadcms/ui/shared'
|
||||
import type { NavPreferences } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { NavGroup, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { Link, NavGroup, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import { usePathname } from 'next/navigation.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
@@ -45,9 +44,6 @@ export const DefaultNavClient: React.FC<{
|
||||
id = `nav-global-${slug}`
|
||||
}
|
||||
|
||||
const Link = (LinkWithDefault.default ||
|
||||
LinkWithDefault) as typeof LinkWithDefault.default
|
||||
|
||||
const LinkElement = Link || 'a'
|
||||
const activeCollection =
|
||||
pathname.startsWith(href) && ['/', undefined].includes(pathname[href.length])
|
||||
|
||||
@@ -6,9 +6,9 @@ import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerCompo
|
||||
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
import { NavHamburger } from './NavHamburger/index.js'
|
||||
import { NavWrapper } from './NavWrapper/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'nav'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { AcceptedLanguages } from '@payloadcms/translations'
|
||||
import type { ImportMap, LanguageOptions, SanitizedConfig, ServerFunctionClient } from 'payload'
|
||||
|
||||
import { rtlLanguages } from '@payloadcms/translations'
|
||||
import { RootProvider } from '@payloadcms/ui'
|
||||
import { ProgressBar, RootProvider } from '@payloadcms/ui'
|
||||
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
|
||||
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
|
||||
import { getPayload, getRequestLanguage, parseCookies } from 'payload'
|
||||
@@ -91,6 +91,20 @@ export const RootLayout = async ({
|
||||
importMap,
|
||||
})
|
||||
|
||||
if (
|
||||
clientConfig.localization &&
|
||||
config.localization &&
|
||||
typeof config.localization.filterAvailableLocales === 'function'
|
||||
) {
|
||||
clientConfig.localization.locales = (
|
||||
await config.localization.filterAvailableLocales({
|
||||
locales: config.localization.locales,
|
||||
req,
|
||||
})
|
||||
).map(({ toString, ...rest }) => rest)
|
||||
clientConfig.localization.localeCodes = config.localization.locales.map(({ code }) => code)
|
||||
}
|
||||
|
||||
const locale = await getRequestLocale({
|
||||
req,
|
||||
})
|
||||
@@ -121,6 +135,7 @@ export const RootLayout = async ({
|
||||
translations={req.i18n.translations}
|
||||
user={req.user}
|
||||
>
|
||||
<ProgressBar />
|
||||
{Array.isArray(config.admin?.components?.providers) &&
|
||||
config.admin?.components?.providers.length > 0 ? (
|
||||
<NestProviders
|
||||
|
||||
@@ -34,7 +34,9 @@ const handlerBuilder =
|
||||
|
||||
const response = await handleEndpoints({
|
||||
config,
|
||||
path: `${awaitedConfig.routes.api}/${awaitedParams.slug.join('/')}`,
|
||||
path: awaitedParams
|
||||
? `${awaitedConfig.routes.api}/${awaitedParams.slug.join('/')}`
|
||||
: undefined,
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,12 @@ export type DashboardProps = {
|
||||
lockDuration?: number
|
||||
slug: string
|
||||
}>
|
||||
Link: React.ComponentType<any>
|
||||
/**
|
||||
* @deprecated
|
||||
* This prop is deprecated and will be removed in the next major version.
|
||||
* Components now import their own `Link` directly from `next/link`.
|
||||
*/
|
||||
Link?: React.ComponentType
|
||||
navGroups?: ReturnType<typeof groupNavItems>
|
||||
permissions: SanitizedPermissions
|
||||
visibleEntities: VisibleEntities
|
||||
@@ -28,7 +33,6 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
globalData,
|
||||
i18n,
|
||||
i18n: { t },
|
||||
Link,
|
||||
locale,
|
||||
navGroups,
|
||||
params,
|
||||
@@ -146,7 +150,6 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
el="link"
|
||||
icon="plus"
|
||||
iconStyle="with-border"
|
||||
Link={Link}
|
||||
round
|
||||
to={createHREF}
|
||||
/>
|
||||
@@ -155,7 +158,6 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
buttonAriaLabel={buttonAriaLabel}
|
||||
href={href}
|
||||
id={`card-${slug}`}
|
||||
Link={Link}
|
||||
title={getTranslation(label, i18n)}
|
||||
titleAs="h3"
|
||||
/>
|
||||
|
||||
@@ -4,15 +4,12 @@ import type { AdminViewProps } from 'payload'
|
||||
import { HydrateAuthProvider, SetStepNav } from '@payloadcms/ui'
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { DefaultDashboard } from './Default/index.js'
|
||||
|
||||
export { generateDashboardMetadata } from './meta.js'
|
||||
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
|
||||
export const Dashboard: React.FC<AdminViewProps> = async ({
|
||||
initPageResult,
|
||||
params,
|
||||
@@ -110,7 +107,6 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
|
||||
<SetStepNav nav={[]} />
|
||||
{RenderServerComponent({
|
||||
clientProps: {
|
||||
Link,
|
||||
locale,
|
||||
},
|
||||
Component: config.admin?.components?.views?.dashboard?.Component,
|
||||
@@ -119,7 +115,6 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
|
||||
serverProps: {
|
||||
globalData,
|
||||
i18n,
|
||||
Link,
|
||||
locale,
|
||||
navGroups,
|
||||
params,
|
||||
|
||||
@@ -91,6 +91,7 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
text(value, {
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'onChange',
|
||||
preferences: { fields: {} },
|
||||
@@ -120,6 +121,7 @@ export const ForgotPasswordForm: React.FC = () => {
|
||||
email(value, {
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
blockData: {},
|
||||
data: {},
|
||||
event: 'onChange',
|
||||
preferences: { fields: {} },
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { Button } from '@payloadcms/ui'
|
||||
import { Button, Link } from '@payloadcms/ui'
|
||||
import { formatAdminURL, Translation } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { FormHeader } from '../../elements/FormHeader/index.js'
|
||||
@@ -10,7 +9,6 @@ import { ForgotPasswordForm } from './ForgotPasswordForm/index.js'
|
||||
|
||||
export { generateForgotPasswordMetadata } from './meta.js'
|
||||
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
export const forgotPasswordBaseClass = 'forgot-password'
|
||||
|
||||
export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult }) => {
|
||||
@@ -54,7 +52,7 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
|
||||
}
|
||||
heading={i18n.t('authentication:alreadyLoggedIn')}
|
||||
/>
|
||||
<Button buttonStyle="secondary" el="link" Link={Link} size="large" to={adminRoute}>
|
||||
<Button buttonStyle="secondary" el="link" size="large" to={adminRoute}>
|
||||
{i18n.t('general:backToDashboard')}
|
||||
</Button>
|
||||
</Fragment>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { isNumber } from 'payload/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { renderListViewSlots } from './renderListViewSlots.js'
|
||||
import { resolveAllFilterOptions } from './resolveAllFilterOptions.js'
|
||||
|
||||
export { generateListMetadata } from './meta.js'
|
||||
|
||||
@@ -149,6 +150,11 @@ export const renderListView = async (
|
||||
|
||||
const renderedFilters = renderFilters(collectionConfig.fields, req.payload.importMap)
|
||||
|
||||
const resolvedFilterOptions = await resolveAllFilterOptions({
|
||||
collectionConfig,
|
||||
req,
|
||||
})
|
||||
|
||||
const staticDescription =
|
||||
typeof collectionConfig.admin.description === 'function'
|
||||
? collectionConfig.admin.description({ t: i18n.t })
|
||||
@@ -192,6 +198,7 @@ export const renderListView = async (
|
||||
enableRowSelections,
|
||||
listPreferences,
|
||||
renderedFilters,
|
||||
resolvedFilterOptions,
|
||||
Table,
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user