Compare commits
4 Commits
fix/revert
...
fix/multi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa3439e89e | ||
|
|
d847da9ed4 | ||
|
|
967b071caa | ||
|
|
30a501ccd6 |
@@ -63,7 +63,7 @@ Each test directory is split up in this way specifically to reduce friction when
|
||||
|
||||
The following command will start Payload with your config: `pnpm dev my-test-dir`. Example: `pnpm dev fields` for the test/`fields` test suite. This command will start up Payload using your config and refresh a test database on every restart. If you're using VS Code, the most common run configs are automatically added to your editor - you should be able to find them in your VS Code launch tab.
|
||||
|
||||
By default, payload will [automatically log you in](https://payloadcms.com/docs/authentication/overview#auto-login) with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
|
||||
By default, payload will [automatically log you in](https://payloadcms.com/docs/authentication/overview#admin-autologin) with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
|
||||
|
||||
The default credentials are `dev@payloadcms.com` as E-Mail and `test` as password. These are used in the auto-login.
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ The following arguments are provided to the `delete` function:
|
||||
|
||||
If the Collection is used to access the [Admin Panel](../admin/overview#the-admin-user-collection), the `Admin` Access Control function determines whether or not the currently logged in user can access the admin UI.
|
||||
|
||||
To add Admin Access Control to a Collection, use the `admin` property in the [Collection Config](../configuration/collections):
|
||||
To add Admin Access Control to a Collection, use the `admin` property in the [Collection Config](../collections/overview):
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
@@ -185,7 +185,7 @@ Each Custom Component receives the following props by default:
|
||||
|
||||
<Banner type="warning">
|
||||
**Reminder:**
|
||||
All Custom Components also receive various other props that are specific component being rendered. See [Root Components](#root-components), [Collection Components](../configuration/collections#custom-components), [Global Components](../configuration/globals#custom-components), or [Field Components](../fields/overview#custom-components) for a complete list of all default props per component.
|
||||
All Custom Components also receive various other props that are specific component being rendered. See [Root Components](#root-components), [Collection Components](../configuration/collections#custom-components), [Global Components](../configuraiton/globals#custom-components), or [Field Components](../fields/overview#custom-components) for a complete list of all default props per component.
|
||||
</Banner>
|
||||
|
||||
### Custom Props
|
||||
@@ -462,7 +462,7 @@ Payload also exports its [SCSS](https://sass-lang.com) library for reuse which i
|
||||
|
||||
## Root Components
|
||||
|
||||
Root Components are those that affect the [Admin Panel](./overview) generally, such as the logo or the main nav.
|
||||
Root Components are those that effect the [Admin Panel](./overview) generally, such as the logo or the main nav.
|
||||
|
||||
To override Root Components, use the `admin.components` property in your [Payload Config](../configuration/overview):
|
||||
|
||||
@@ -511,7 +511,7 @@ The following options are available:
|
||||
|
||||
As you add more and more Custom Components to your [Admin Panel](./overview), you may find it helpful to add additional [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context)(s). Payload allows you to inject your own context providers in your app so you can export your own custom hooks, etc.
|
||||
|
||||
To add a Custom Provider, use the `admin.components.providers` property in your [Payload Config](../configuration/overview):
|
||||
To add a Custom Provider, use the `admin.components.providers` property in your [Payload Config](../getting-started/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
@@ -546,5 +546,5 @@ export const useMyCustomContext = () => useContext(MyCustomContext)
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Reminder:** React Context exists only within Client Components. This means they must include the `use client` directive at the top of their files and cannot contain server-only code. To use a Server Component here, simply _wrap_ your Client Component with it.
|
||||
**Reminder:**React Context exists only within Client Components. This means they must include the `use client` directive at the top of their files and cannot contain server-only code. To use a Server Component here, simply _wrap_ your Client Component with it.
|
||||
</Banner>
|
||||
|
||||
@@ -312,7 +312,7 @@ Payload comes with built-in forgot password functionality. Submitting an email a
|
||||
|
||||
The link to reset the user's password contains a token which is what allows the user to securely reset their password.
|
||||
|
||||
By default, the Forgot Password operations send users to the [Admin Panel](../admin/overview) to reset their password, but you can customize the generated email to send users to the frontend of your app instead by [overriding the email HTML](/docs/authentication/email#forgot-password).
|
||||
By default, the Forgot Password operations send users to the [Admin Panel](../admin/overview) to reset their password, but you can customize the generated email to send users to the frontend of your app instead by [overriding the email HTML](/docs/authentication/overview#forgot-password).
|
||||
|
||||
**Example REST API Forgot Password**:
|
||||
|
||||
@@ -348,9 +348,7 @@ const token = await payload.forgotPassword({
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
**Note:** if you do not have a `config.serverURL` set, Payload will attempt to create one for you if the `forgot-password` operation was triggered via REST or GraphQL by looking at the incoming `req`. But this is not supported if you are calling `payload.forgotPassword()` via the Local API. If you do not have a `serverURL` set, you may want to override your `auth.forgotPassword.generateEmailHTML` function to provide a full URL to link the user to a proper reset-password page.
|
||||
</Banner>
|
||||
**Note:** if you do not have a `config.serverURL` set, Payload will attempt to create one for you if the `forgot-password` operation was triggered via REST or GraphQL by looking at the incoming `req`. But this is not supported if you are calling `payload.forgotPassword()` via the Local API. If you do not have a `serverURL` set, you may want to override your `auth.forgotPassword.generateEmailHTML` function to provide a full URL to link the user to a proper reset-password page.
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
|
||||
@@ -132,7 +132,7 @@ If set to `true`, an email address is required when creating a new user. If set
|
||||
|
||||
For testing and demo purposes you may want to skip forcing the user to login in order to access your application. Typically, all users should be required to login, however, you can speed up local development time by enabling auto-login.
|
||||
|
||||
To enable auto-login, set the `autoLogin` property in the [Payload Config](../admin/overview#admin-options):
|
||||
To enable auto-login, set the `autoLogin` property in the [Admin Config](../configuration/admin):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
@@ -32,7 +32,7 @@ export default buildConfig({
|
||||
|
||||
## Config Options
|
||||
|
||||
It's often best practice to write your Collections in separate files and then import them into the main [Payload Config](./overview).
|
||||
It's often best practice to write your Collections in separate files and then import them into the main [Payload Config](../overview).
|
||||
|
||||
Here is what a simple Collection Config might look like:
|
||||
|
||||
@@ -95,7 +95,7 @@ Fields define the schema of the Documents within a Collection. To learn more, go
|
||||
|
||||
## Admin Options
|
||||
|
||||
The behavior of Collections within the [Admin Panel](../admin/overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](../admin/components), selecting which fields to display in the List View, and more.
|
||||
The behavior of Collections within the [Admin Panel](../admin/overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](./components), selecting which fields to display in the List View, and more.
|
||||
|
||||
To configure Admin Options for Collections, use the `admin` property in your Collection Config:
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ The following options are available:
|
||||
| **`lockDocuments`** | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](../admin/locked-documents). |
|
||||
| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#global-config). |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#globals-config). |
|
||||
|
||||
_* An asterisk denotes that a property is required._
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ The Payload Config is strongly typed and ties directly into Payload's TypeScript
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:**
|
||||
The location of your Payload Config can be customized. [More details](#customizing-the-config-location).
|
||||
The location of your Payload Config can be customized. [More details](#customizing--automating-config-location-detection).
|
||||
</Banner>
|
||||
|
||||
## Config Options
|
||||
@@ -73,7 +73,7 @@ The following options are available:
|
||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||
| **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). |
|
||||
| **`globals`** | An array of Globals for Payload to manage. [More details](./globals). |
|
||||
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). |
|
||||
| **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cross-origin-resource-sharing-cors). |
|
||||
| **`localization`** | Opt-in to translate your content into multiple locales. [More details](./localization). |
|
||||
| **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). |
|
||||
| **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. |
|
||||
@@ -181,7 +181,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](..environment-variables) to bypass all automatic config detection.
|
||||
|
||||
To use a custom config location, set the `PAYLOAD_CONFIG_PATH` environment variable:
|
||||
|
||||
|
||||
@@ -274,5 +274,5 @@ export default buildConfig({
|
||||
Passing your migrations as shown above will tell Payload, in production only, to execute any migrations that need to be run prior to completing the initialization of Payload. This is ideal for long-running services where Payload will only be initialized at startup.
|
||||
|
||||
<Banner type="warning">
|
||||
**Warning:** if Payload is instructed to run migrations in production, this may slow down serverless cold starts on platforms such as Vercel. Generally, this option should only be used for long-running servers / containers.
|
||||
Warning - if Payload is instructed to run migrations in production, this may slow down serverless cold starts on platforms such as Vercel. Generally, this option should only be used for long-running servers / containers.
|
||||
</Banner>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default buildConfig({
|
||||
|
||||
<Banner type="info">
|
||||
**Note:**
|
||||
If you're using `vercelPostgresAdapter` your `process.env.POSTGRES_URL` or `pool.connectionString` points to a local database (e.g hostname has `localhost` or `127.0.0.1`) we use the `pg` module for pooling instead of `@vercel/postgres`. This is because `@vercel/postgres` doesn't work with local databases, if you want to disable that behavior, you can pass `forceUseVercelPostgres: true` to the adapter's args and follow [Vercel guide](https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker) for a Docker Neon DB setup.
|
||||
If you're using `vercelPostgresAdapter` your `process.env.POSTGRES_URL` or `pool.connectionString` points to a local database (e.g hostname has `localhost` or `127.0.0.1`) we use the `pg` module for pooling instead of `@vercel/postgres`. This is because `@vercel/postgres` doesn't work with local databases, if you want to disable that behavior, you can pass `forceUseVercelPostgres: true` to adapter's 'args and follow [Vercel guide](https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker) for a Docker Neon DB setup.
|
||||
</Banner>
|
||||
|
||||
## Options
|
||||
|
||||
@@ -84,7 +84,7 @@ The Array Field inherits all of the default options from the base [Field Admin C
|
||||
| Option | Description |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`initCollapsed`** | Set the initial collapsed state |
|
||||
| **`components.RowLabel`** | React component to be rendered as the label on the array row. [Example](#row-label) |
|
||||
| **`components.RowLabel`** | React component to be rendered as the label on the array row. [Example](#example-of-a-custom-rowlabel-component) |
|
||||
| **`isSortable`** | Disable order sorting by setting this value to `false` |
|
||||
|
||||
## Example
|
||||
|
||||
@@ -123,11 +123,11 @@ powerful Admin UI.
|
||||
|
||||
| Option | Description |
|
||||
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`name`** * | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
|
||||
| **`collection`** * | The `slug`s having the relationship field. |
|
||||
| **`on`** * | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
|
||||
| **`name`** * | To be used as the property name when retrieved from the database. [More](./overview#field-names) |
|
||||
| **`collection`** * | The `slug`s having the relationship field. |
|
||||
| **`on`** * | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
|
||||
| **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. |
|
||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](../queries/depth#max-depth). |
|
||||
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth). |
|
||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||
|
||||
@@ -10,7 +10,7 @@ Fields are the building blocks of Payload. They define the schema of the Documen
|
||||
|
||||
There are many [Field Types](#field-types) to choose from, ranging anywhere from simple text strings to nested arrays and blocks. Most fields save data to the database, while others are strictly presentational. Fields can have [Custom Validations](#validation), [Conditional Logic](./overview#conditional-logic), [Access Control](#field-level-access-control), [Hooks](#field-level-hooks), and so much more.
|
||||
|
||||
Fields can be endlessly customized in their appearance and behavior without affecting their underlying data structure. Fields are designed to withstand heavy modification or even complete replacement through the use of [Custom Field Components](#custom-components).
|
||||
Fields can be endlessly customized in their appearance and behavior without affecting their underlying data structure. Fields are designed to withstand heavy modification or even complete replacement through the use of [Custom Field Components](../admin/components#custom-components).
|
||||
|
||||
To configure fields, use the `fields` property in your [Collection](../configuration/collections) or [Global](../configuration/globals) config:
|
||||
|
||||
@@ -154,7 +154,7 @@ The following field names are forbidden and cannot be used:
|
||||
|
||||
### Field-level Hooks
|
||||
|
||||
In addition to being able to define [Hooks](../hooks/overview) on a document-level, you can define extremely granular logic field-by-field.
|
||||
In addition to being able to define [Hooks](.../admin/hooks/overview) on a document-level, you can define extremely granular logic field-by-field.
|
||||
|
||||
To define Field-level Hooks, use the `hooks` property in your Field Config:
|
||||
|
||||
@@ -172,7 +172,7 @@ export const MyField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
For full details on Field-level Hooks, see the [Field Hooks](../hooks/fields) documentation.
|
||||
For full details on Field-level Hooks, see the [Field Hooks](.../admin/hooks/fields) documentation.
|
||||
|
||||
### Field-level Access Control
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Everything Payload does (create, read, update, delete, login, logout, etc.) is e
|
||||
|
||||
- [Local API](#local-api) - Extremely fast, direct-to-database access
|
||||
- [REST API](#rest-api) - Standard HTTP endpoints for querying and mutating data
|
||||
- [GraphQL](#graphql-api) - A full GraphQL API with a GraphQL Playground
|
||||
- [GraphQL](#graphql) - A full GraphQL API with a GraphQL Playground
|
||||
|
||||
<Banner type="success">
|
||||
**Note:**
|
||||
@@ -157,7 +157,7 @@ All of Payload's GraphQL functionality is abstracted into a separate package. Pa
|
||||
|
||||
This is the UI library that Payload's Admin Panel uses. All components are exported from this package and can be re-used as you build extensions to the Payload admin UI, or want to use Payload components in your own React apps. Some exports are server components and some are client components.
|
||||
|
||||
`@payloadcms/db-postgres`, `@payloadcms/db-vercel-postgres`, `@payloadcms/db-mongodb`, `@payloadcms/db-sqlite`
|
||||
`@payloadcms/db-postgres`, `@payloadcms/db-vercel-postgres`, `@payloadcms/db-mongodb`
|
||||
|
||||
You can choose which Database Adapter you'd like to use for your project, and no matter which you choose, the entire data layer for Payload is contained within these packages. You can only use one at a time for any given project.
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ const Header: GlobalConfig = {
|
||||
|
||||
## Preferences
|
||||
|
||||
User [preferences](../admin/preferences) for the [Admin Panel](../admin/overview) are also available to GraphQL the same way as other collection schemas are generated. To query preferences you must supply an authorization token in the header and only the preferences of that user will be accessible.
|
||||
User [preferences](/docs/admin/overview#preferences) for the [Admin Panel](../admin/overview) are also available to GraphQL the same way as other collection schemas are generated. To query preferences you must supply an authorization token in the header and only the preferences of that user will be accessible.
|
||||
|
||||
**Payload will open the following query:**
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ All Field Hooks accept an array of synchronous or asynchronous functions. These
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
Due to GraphQL's typed nature, changing the type of data that you return from a field will produce errors in the [GraphQL API](../graphql/overview). If you need to change the shape or type of data, consider [Collection Hooks](./collections) or [Global Hooks](./globals) instead.
|
||||
Due to GraphQL's typed nature, changing the type of data that you return from a field will produce errors in the [GraphQL API](../graphql/overview). If you need to change the shape or type of data, consider [Collection Hooks](./collections) or [Global Hooks](./hooks) instead.
|
||||
</Banner>
|
||||
|
||||
To add hooks to a Field, use the `hooks` property in your [Field Config](../fields/overview):
|
||||
|
||||
@@ -19,7 +19,7 @@ Here are some common examples of how you can use the Local API:
|
||||
- Fetching Payload data within React Server Components
|
||||
- Seeding data via Node seed scripts that you write and maintain
|
||||
- Opening custom Next.js route handlers which feature additional functionality but still rely on Payload
|
||||
- Within [Access Control](../access-control/overview) and [Hooks](../hooks/overview)
|
||||
- Within [Access Control](../access-control) and [Hooks](../hooks/overview)
|
||||
|
||||
## Accessing Payload
|
||||
|
||||
|
||||
@@ -25,6 +25,18 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
|
||||
- Adds a `tenant` field to each specified collection
|
||||
- Adds a tenant selector to the admin panel, allowing you to switch between tenants
|
||||
- Filters list view results by selected tenant
|
||||
- Filters relationship fields by selected tenant
|
||||
- Ability to create "global" like collections, 1 doc per tenant
|
||||
- Automatically assign a tenant to new documents
|
||||
|
||||
<Banner type="error">
|
||||
**Warning**
|
||||
|
||||
By default this plugin cleans up documents when a tenant is deleted. You should ensure you have
|
||||
strong access control on your tenants collection to prevent deletions by unauthorized users.
|
||||
|
||||
You can disabled this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
|
||||
</Banner>
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -40,7 +52,7 @@ The plugin accepts an object with the following properties:
|
||||
|
||||
```ts
|
||||
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
/**
|
||||
/**
|
||||
* After a tenant is deleted, the plugin will attempt to clean up related documents
|
||||
* - removing documents with the tenant ID
|
||||
* - removing the tenant from users
|
||||
@@ -144,8 +156,12 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
* Useful for super-admin type users
|
||||
*/
|
||||
userHasAccessToAllTenants?: (
|
||||
user: ConfigTypes extends { user } ? ConfigTypes['user'] : User,
|
||||
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
|
||||
) => boolean
|
||||
/**
|
||||
* Opt out of adding access constraints to the tenants collection
|
||||
*/
|
||||
useTenantsCollectionAccess?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ export const Pages: CollectionConfig<'pages'> = {
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
**Important:**
|
||||
<strong>Important:</strong>
|
||||
When using `defaultPopulate` on a collection with [Uploads](/docs/fields/upload) enabled and you want to select the `url` field, it is important to specify `filename: true` as well, otherwise Payload will not be able to construct the correct file URL, instead returning `url: null`.
|
||||
</Banner>
|
||||
|
||||
|
||||
@@ -3,10 +3,7 @@ import { TenantSelector as TenantSelector_d6d5f193a167989e2ee7d14202901e62 } fro
|
||||
import { TenantSelectionProvider as TenantSelectionProvider_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
|
||||
export const importMap = {
|
||||
'@payloadcms/plugin-multi-tenant/client#TenantField':
|
||||
TenantField_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
'@payloadcms/plugin-multi-tenant/rsc#TenantSelector':
|
||||
TenantSelector_d6d5f193a167989e2ee7d14202901e62,
|
||||
'@payloadcms/plugin-multi-tenant/client#TenantSelectionProvider':
|
||||
TenantSelectionProvider_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/rsc#TenantSelector": TenantSelector_d6d5f193a167989e2ee7d14202901e62,
|
||||
"@payloadcms/plugin-multi-tenant/client#TenantSelectionProvider": TenantSelectionProvider_1d0591e3cf4f332c83a86da13a0de59a
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@ export const Account: React.FC<AdminViewProps> = async ({
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: collectionConfig.slug,
|
||||
skipValidation: true,
|
||||
})
|
||||
|
||||
// Fetch document lock state
|
||||
|
||||
@@ -47,7 +47,7 @@ export const CreateFirstUserClient: React.FC<{
|
||||
const collectionConfig = getEntityConfig({ collectionSlug: userSlug })
|
||||
|
||||
const onChange: FormProps['onChange'][0] = React.useCallback(
|
||||
async ({ formState: prevFormState, submitted }) => {
|
||||
async ({ formState: prevFormState }) => {
|
||||
const controller = handleAbortRef(abortOnChangeRef)
|
||||
|
||||
const response = await getFormState({
|
||||
@@ -58,7 +58,6 @@ export const CreateFirstUserClient: React.FC<{
|
||||
operation: 'create',
|
||||
schemaPath: userSlug,
|
||||
signal: controller.signal,
|
||||
skipValidation: !submitted,
|
||||
})
|
||||
|
||||
abortOnChangeRef.current = null
|
||||
|
||||
@@ -63,7 +63,6 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: collectionConfig.slug,
|
||||
skipValidation: true,
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -31,7 +31,7 @@ export const getDocumentPermissions = async (args: {
|
||||
if (collectionConfig) {
|
||||
try {
|
||||
docPermissions = await docAccessOperation({
|
||||
id,
|
||||
id: id?.toString(),
|
||||
collection: {
|
||||
config: collectionConfig,
|
||||
},
|
||||
@@ -46,7 +46,7 @@ export const getDocumentPermissions = async (args: {
|
||||
|
||||
if (collectionConfig.versions?.drafts) {
|
||||
hasPublishPermission = await docAccessOperation({
|
||||
id,
|
||||
id: id?.toString(),
|
||||
collection: {
|
||||
config: collectionConfig,
|
||||
},
|
||||
|
||||
@@ -157,7 +157,6 @@ export const renderDocument = async ({
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: collectionSlug || globalSlug,
|
||||
skipValidation: true,
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
@@ -225,7 +225,6 @@ const PreviewView: React.FC<Props> = ({
|
||||
returnLockStatus: false,
|
||||
schemaPath: entitySlug,
|
||||
signal: controller.signal,
|
||||
skipValidation: true,
|
||||
})
|
||||
|
||||
// Unlock the document after save
|
||||
@@ -268,7 +267,7 @@ const PreviewView: React.FC<Props> = ({
|
||||
)
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState, submitted }) => {
|
||||
async ({ formState: prevFormState }) => {
|
||||
const controller = handleAbortRef(abortOnChangeRef)
|
||||
|
||||
const currentTime = Date.now()
|
||||
@@ -293,7 +292,6 @@ const PreviewView: React.FC<Props> = ({
|
||||
returnLockStatus: isLockingEnabled ? true : false,
|
||||
schemaPath,
|
||||
signal: controller.signal,
|
||||
skipValidation: !submitted,
|
||||
updateLastEdited,
|
||||
})
|
||||
|
||||
|
||||
@@ -100,16 +100,14 @@ const Restore: React.FC<Props> = ({
|
||||
className={[canRestoreAsDraft && `${baseClass}__button`].filter(Boolean).join(' ')}
|
||||
onClick={() => toggleModal(modalSlug)}
|
||||
size="small"
|
||||
SubMenuPopupContent={
|
||||
canRestoreAsDraft
|
||||
? () => (
|
||||
<PopupList.ButtonGroup>
|
||||
<PopupList.Button onClick={() => [setDraft(true), toggleModal(modalSlug)]}>
|
||||
{t('version:restoreAsDraft')}
|
||||
</PopupList.Button>
|
||||
</PopupList.ButtonGroup>
|
||||
)
|
||||
: null
|
||||
SubMenuPopupContent={() =>
|
||||
canRestoreAsDraft && (
|
||||
<PopupList.ButtonGroup>
|
||||
<PopupList.Button onClick={() => [setDraft(true), toggleModal(modalSlug)]}>
|
||||
{t('version:restoreAsDraft')}
|
||||
</PopupList.Button>
|
||||
</PopupList.ButtonGroup>
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('version:restoreThisVersion')}
|
||||
|
||||
@@ -116,7 +116,7 @@ export type BaseRichTextHookArgs<
|
||||
field: FieldAffectingData
|
||||
/** The global which the field belongs to. If the field belongs to a collection, this will be null. */
|
||||
global: null | SanitizedGlobalConfig
|
||||
indexPath: number[]
|
||||
|
||||
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
|
||||
originalDoc?: TData
|
||||
/**
|
||||
|
||||
@@ -84,7 +84,6 @@ export type BuildFormStateArgs = {
|
||||
req: PayloadRequest
|
||||
returnLockStatus?: boolean
|
||||
schemaPath: string
|
||||
skipValidation?: boolean
|
||||
updateLastEdited?: boolean
|
||||
} & (
|
||||
| {
|
||||
|
||||
@@ -239,7 +239,7 @@ export const findOperation = async <
|
||||
doc._isLocked = !!lockedDoc
|
||||
doc._userEditing = lockedDoc ? lockedDoc?.user?.value : null
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch (error) {
|
||||
for (const doc of result.docs) {
|
||||
doc._isLocked = false
|
||||
doc._userEditing = null
|
||||
|
||||
@@ -166,7 +166,6 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
|
||||
findMany?: boolean
|
||||
/** The global which the field belongs to. If the field belongs to a collection, this will be null. */
|
||||
global: null | SanitizedGlobalConfig
|
||||
indexPath: number[]
|
||||
/** A string relating to which operation the field type is currently executing within. Useful within beforeValidate, beforeChange, and afterChange hooks to differentiate between create and update operations. */
|
||||
operation?: 'create' | 'delete' | 'read' | 'update'
|
||||
/** The full original document in `update` operations. In the `afterChange` hook, this is the resulting document of the operation. */
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { ClientField, Field, Tab, TabAsFieldClient } from './config/types.js'
|
||||
import type { ClientField, Field, TabAsField, TabAsFieldClient } from './config/types.js'
|
||||
|
||||
type Args = {
|
||||
field: ClientField | Field | Tab | TabAsFieldClient
|
||||
field: ClientField | Field | TabAsField | TabAsFieldClient
|
||||
index: number
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
}
|
||||
|
||||
type FieldPaths = {
|
||||
type Result = {
|
||||
/**
|
||||
* A string of '-' separated indexes representing where
|
||||
* to find this field in a given field schema array.
|
||||
@@ -16,11 +16,11 @@ type FieldPaths = {
|
||||
*/
|
||||
indexPath: string
|
||||
/**
|
||||
* Path for this field relative to its position in the data.
|
||||
* Path for this field specifically.
|
||||
*/
|
||||
path: string
|
||||
/**
|
||||
* Path for this field relative to its position in the schema.
|
||||
* Schema path for this field specifically.
|
||||
*/
|
||||
schemaPath: string
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export function getFieldPaths({
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
}: Args): FieldPaths {
|
||||
}: Args): Result {
|
||||
if ('name' in field) {
|
||||
return {
|
||||
indexPath: `${parentIndexPath ? parentIndexPath + '-' : ''}${index}`,
|
||||
@@ -48,37 +48,3 @@ export function getFieldPaths({
|
||||
schemaPath: `${parentSchemaPath ? parentSchemaPath + '.' : ''}${indexSuffix}`,
|
||||
}
|
||||
}
|
||||
|
||||
export function getFieldPathsModified({
|
||||
field,
|
||||
index,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
}: Args): FieldPaths {
|
||||
const parentPathSegments = parentPath.split('.')
|
||||
|
||||
const parentIsUnnamed = parentPathSegments[parentPathSegments.length - 1].startsWith('_index-')
|
||||
|
||||
const parentWithoutIndex = parentIsUnnamed
|
||||
? parentPathSegments.slice(0, -1).join('.')
|
||||
: parentPath
|
||||
|
||||
const parentPathToUse = parentIsUnnamed ? parentWithoutIndex : parentPath
|
||||
|
||||
if ('name' in field) {
|
||||
return {
|
||||
indexPath: '',
|
||||
path: `${parentPathToUse ? parentPathToUse + '.' : ''}${field.name}`,
|
||||
schemaPath: `${parentSchemaPath ? parentSchemaPath + '.' : ''}${field.name}`,
|
||||
}
|
||||
}
|
||||
|
||||
const indexSuffix = `_index-${`${parentIndexPath ? parentIndexPath + '-' : ''}${index}`}`
|
||||
|
||||
return {
|
||||
indexPath: `${parentIndexPath ? parentIndexPath + '-' : ''}${index}`,
|
||||
path: `${parentPathToUse ? parentPathToUse + '.' : ''}${indexSuffix}`,
|
||||
schemaPath: `${!parentIsUnnamed && parentSchemaPath ? parentSchemaPath + '.' : ''}${indexSuffix}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,12 +44,11 @@ export const afterChange = async <T extends JsonObject>({
|
||||
fields: collection?.fields || global?.fields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
parentPath: '',
|
||||
parentSchemaPath: '',
|
||||
path: [],
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc,
|
||||
req,
|
||||
schemaPath: [],
|
||||
siblingData: data,
|
||||
siblingDoc: incomingDoc,
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args = {
|
||||
@@ -19,9 +19,14 @@ type Args = {
|
||||
fieldIndex: number
|
||||
global: null | SanitizedGlobalConfig
|
||||
operation: 'create' | 'update'
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
/**
|
||||
* The parent's path
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
previousDoc: JsonObject
|
||||
previousSiblingDoc: JsonObject
|
||||
req: PayloadRequest
|
||||
@@ -41,7 +46,6 @@ export const promise = async ({
|
||||
fieldIndex,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
previousDoc,
|
||||
@@ -50,17 +54,15 @@ export const promise = async ({
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args): Promise<void> => {
|
||||
const { indexPath, path, schemaPath } = getFieldPaths({
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
|
||||
const pathSegments = path ? path.split('.') : []
|
||||
const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
|
||||
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// Execute hooks
|
||||
@@ -74,15 +76,14 @@ export const promise = async ({
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
indexPath: indexPathSegments,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
previousValue: previousDoc[field.name],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
@@ -101,7 +102,7 @@ export const promise = async ({
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
collection,
|
||||
@@ -111,20 +112,18 @@ export const promise = async ({
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: [...fieldPath, i],
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[rowIndex] || ({} as JsonObject),
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject),
|
||||
req,
|
||||
siblingData: siblingData?.[field.name]?.[rowIndex] || {},
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||
siblingDoc: row ? { ...row } : {},
|
||||
}),
|
||||
)
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -133,8 +132,7 @@ export const promise = async ({
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
@@ -149,19 +147,17 @@ export const promise = async ({
|
||||
fields: block.fields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath + '.' + block.slug,
|
||||
path: [...fieldPath, i],
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[rowIndex] || ({} as JsonObject),
|
||||
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject),
|
||||
req,
|
||||
siblingData: siblingData?.[field.name]?.[rowIndex] || {},
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData?.[field.name]?.[i] || {},
|
||||
siblingDoc: row ? { ...row } : {},
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -178,19 +174,17 @@ export const promise = async ({
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData || {},
|
||||
siblingDoc: { ...siblingDoc },
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
await traverseFields({
|
||||
collection,
|
||||
@@ -200,12 +194,11 @@ export const promise = async ({
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: previousDoc[field.name] as JsonObject,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: (siblingData?.[field.name] as JsonObject) || {},
|
||||
siblingDoc: siblingDoc[field.name] as JsonObject,
|
||||
})
|
||||
@@ -217,7 +210,6 @@ export const promise = async ({
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
@@ -234,15 +226,14 @@ export const promise = async ({
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
indexPath: indexPathSegments,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
previousValue: previousDoc[field.name],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingDoc[field.name],
|
||||
})
|
||||
@@ -260,9 +251,7 @@ export const promise = async ({
|
||||
let tabSiblingDoc = siblingDoc
|
||||
let tabPreviousSiblingDoc = siblingDoc
|
||||
|
||||
const isNamedTab = tabHasName(field)
|
||||
|
||||
if (isNamedTab) {
|
||||
if (tabHasName(field)) {
|
||||
tabSiblingData = (siblingData[field.name] as JsonObject) ?? {}
|
||||
tabSiblingDoc = (siblingDoc[field.name] as JsonObject) ?? {}
|
||||
tabPreviousSiblingDoc = (previousDoc[field.name] as JsonObject) ?? {}
|
||||
@@ -276,12 +265,11 @@ export const promise = async ({
|
||||
fields: field.fields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: isNamedTab ? '' : indexPath,
|
||||
parentPath: isNamedTab ? path : parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: tabPreviousSiblingDoc,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
})
|
||||
@@ -298,16 +286,14 @@ export const promise = async ({
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...previousSiblingDoc },
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData || {},
|
||||
siblingDoc: { ...siblingDoc },
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,11 @@ type Args = {
|
||||
fields: (Field | TabAsField)[]
|
||||
global: null | SanitizedGlobalConfig
|
||||
operation: 'create' | 'update'
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
path: (number | string)[]
|
||||
previousDoc: JsonObject
|
||||
previousSiblingDoc: JsonObject
|
||||
req: PayloadRequest
|
||||
schemaPath: string[]
|
||||
siblingData: JsonObject
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
@@ -32,12 +31,11 @@ export const traverseFields = async ({
|
||||
fields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
path,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
req,
|
||||
schemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args): Promise<void> => {
|
||||
@@ -54,9 +52,8 @@ export const traverseFields = async ({
|
||||
fieldIndex,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
previousDoc,
|
||||
previousSiblingDoc,
|
||||
req,
|
||||
|
||||
@@ -83,12 +83,11 @@ export async function afterRead<T extends JsonObject>(args: Args<T>): Promise<T>
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: '',
|
||||
parentSchemaPath: '',
|
||||
path: [],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: [],
|
||||
select,
|
||||
selectMode: select ? getSelectMode(select) : undefined,
|
||||
showHiddenFields,
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getDefaultValue } from '../../getDefaultValue.js'
|
||||
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { relationshipPopulationPromise } from './relationshipPopulationPromise.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
@@ -37,9 +37,14 @@ type Args = {
|
||||
global: null | SanitizedGlobalConfig
|
||||
locale: null | string
|
||||
overrideAccess: boolean
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
/**
|
||||
* The parent's path.
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
populate?: PopulateType
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
@@ -75,7 +80,6 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
populate,
|
||||
@@ -88,17 +92,15 @@ export const promise = async ({
|
||||
triggerAccessControl = true,
|
||||
triggerHooks = true,
|
||||
}: Args): Promise<void> => {
|
||||
const { indexPath, path, schemaPath } = getFieldPaths({
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
|
||||
const pathSegments = path ? path.split('.') : []
|
||||
const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
|
||||
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (
|
||||
fieldAffectsData(field) &&
|
||||
@@ -109,7 +111,6 @@ export const promise = async ({
|
||||
delete siblingDoc[field.name]
|
||||
}
|
||||
|
||||
// Strip unselected fields
|
||||
if (fieldAffectsData(field) && select && selectMode) {
|
||||
if (selectMode === 'include') {
|
||||
if (!select[field.name]) {
|
||||
@@ -245,13 +246,12 @@ export const promise = async ({
|
||||
field,
|
||||
findMany,
|
||||
global,
|
||||
indexPath: indexPathSegments,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
value,
|
||||
@@ -275,13 +275,12 @@ export const promise = async ({
|
||||
field,
|
||||
findMany,
|
||||
global,
|
||||
indexPath: indexPathSegments,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
value: siblingDoc[field.name],
|
||||
@@ -362,7 +361,7 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
@@ -378,12 +377,11 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: typeof arraySelect === 'object' ? arraySelect : undefined,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
@@ -395,7 +393,7 @@ export const promise = async ({
|
||||
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
||||
Object.values(rows).forEach((localeRows) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, rowIndex) => {
|
||||
localeRows.forEach((row, i) => {
|
||||
traverseFields({
|
||||
collection,
|
||||
context,
|
||||
@@ -411,12 +409,11 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: (row as JsonObject) || {},
|
||||
triggerAccessControl,
|
||||
@@ -437,7 +434,7 @@ export const promise = async ({
|
||||
let blocksSelect = select?.[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
@@ -490,12 +487,11 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath + '.' + block.slug,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: typeof blockSelect === 'object' ? blockSelect : undefined,
|
||||
selectMode: blockSelectMode,
|
||||
showHiddenFields,
|
||||
@@ -508,7 +504,7 @@ export const promise = async ({
|
||||
} else if (!shouldHoistLocalizedValue && typeof rows === 'object' && rows !== null) {
|
||||
Object.values(rows).forEach((localeRows) => {
|
||||
if (Array.isArray(localeRows)) {
|
||||
localeRows.forEach((row, rowIndex) => {
|
||||
localeRows.forEach((row, i) => {
|
||||
const block = field.blocks.find(
|
||||
(blockType) => blockType.slug === (row as JsonObject).blockType,
|
||||
)
|
||||
@@ -529,12 +525,11 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath + '.' + block.slug,
|
||||
path: [...fieldPath, i],
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingDoc: (row as JsonObject) || {},
|
||||
triggerAccessControl,
|
||||
@@ -568,12 +563,11 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
@@ -584,10 +578,8 @@ export const promise = async ({
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
let groupDoc = siblingDoc[field.name] as JsonObject
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
groupDoc = {}
|
||||
}
|
||||
@@ -609,12 +601,11 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: typeof groupSelect === 'object' ? groupSelect : undefined,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
@@ -630,7 +621,6 @@ export const promise = async ({
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
@@ -662,16 +652,15 @@ export const promise = async ({
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
indexPath: indexPathSegments,
|
||||
locale,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
triggerAccessControl,
|
||||
@@ -700,16 +689,15 @@ export const promise = async ({
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
indexPath: indexPathSegments,
|
||||
locale,
|
||||
operation: 'read',
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: fieldSchemaPath,
|
||||
showHiddenFields,
|
||||
siblingData: siblingDoc,
|
||||
triggerAccessControl,
|
||||
@@ -729,12 +717,8 @@ export const promise = async ({
|
||||
case 'tab': {
|
||||
let tabDoc = siblingDoc
|
||||
let tabSelect: SelectType | undefined
|
||||
|
||||
const isNamedTab = tabHasName(field)
|
||||
|
||||
if (isNamedTab) {
|
||||
if (tabHasName(field)) {
|
||||
tabDoc = siblingDoc[field.name] as JsonObject
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
tabDoc = {}
|
||||
}
|
||||
@@ -761,12 +745,11 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: isNamedTab ? '' : indexPath,
|
||||
parentPath: isNamedTab ? path : parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select: tabSelect,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
@@ -794,12 +777,11 @@ export const promise = async ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
select,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
@@ -807,9 +789,9 @@ export const promise = async ({
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -30,12 +30,11 @@ type Args = {
|
||||
global: null | SanitizedGlobalConfig
|
||||
locale: null | string
|
||||
overrideAccess: boolean
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
path: (number | string)[]
|
||||
populate?: PopulateType
|
||||
populationPromises: Promise<void>[]
|
||||
req: PayloadRequest
|
||||
schemaPath: string[]
|
||||
select?: SelectType
|
||||
selectMode?: SelectMode
|
||||
showHiddenFields: boolean
|
||||
@@ -59,12 +58,11 @@ export const traverseFields = ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
path,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
schemaPath,
|
||||
select,
|
||||
selectMode,
|
||||
showHiddenFields,
|
||||
@@ -90,9 +88,8 @@ export const traverseFields = ({
|
||||
global,
|
||||
locale,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
populate,
|
||||
populationPromises,
|
||||
req,
|
||||
|
||||
@@ -28,7 +28,6 @@ export type Args<T extends JsonObject> = {
|
||||
* - Transform data for storage
|
||||
* - Unflatten locales. The input `data` is the normal document for one locale. The output result will become the document with locales.
|
||||
*/
|
||||
|
||||
export const beforeChange = async <T extends JsonObject>({
|
||||
id,
|
||||
collection,
|
||||
@@ -57,10 +56,9 @@ export const beforeChange = async <T extends JsonObject>({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
parentPath: '',
|
||||
parentSchemaPath: '',
|
||||
path: [],
|
||||
req,
|
||||
schemaPath: [],
|
||||
siblingData: data,
|
||||
siblingDoc: doc,
|
||||
siblingDocWithLocales: docWithLocales,
|
||||
|
||||
@@ -4,14 +4,14 @@ import type { ValidationFieldError } from '../../../errors/index.js'
|
||||
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, Operation, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, TabAsField } from '../../config/types.js'
|
||||
import type { BaseValidateOptions, Field, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
|
||||
import { getLabelFromPath } from '../../../utilities/getLabelFromPath.js'
|
||||
import { getFormattedLabel } from '../../../utilities/getFormattedLabel.js'
|
||||
import { getTranslatedLabel } from '../../../utilities/getTranslatedLabel.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { getExistingRowDoc } from './getExistingRowDoc.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
@@ -23,14 +23,23 @@ type Args = {
|
||||
docWithLocales: JsonObject
|
||||
errors: ValidationFieldError[]
|
||||
field: Field | TabAsField
|
||||
/**
|
||||
* The index of the field as it appears in the parent's fields array. This is used to construct the field path / schemaPath
|
||||
* for unnamed fields like rows and collapsibles.
|
||||
*/
|
||||
fieldIndex: number
|
||||
global: null | SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
mergeLocaleActions: (() => Promise<void>)[]
|
||||
operation: Operation
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
/**
|
||||
* The parent's path.
|
||||
*/
|
||||
parentPath: (number | string)[]
|
||||
/**
|
||||
* The parent's schemaPath (path without indexes).
|
||||
*/
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequest
|
||||
siblingData: JsonObject
|
||||
siblingDoc: JsonObject
|
||||
@@ -59,7 +68,6 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
@@ -68,14 +76,6 @@ export const promise = async ({
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
}: Args): Promise<void> => {
|
||||
const { indexPath, path, schemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
})
|
||||
|
||||
const passesCondition = field.admin?.condition
|
||||
? Boolean(field.admin.condition(data, siblingData, { user: req.user }))
|
||||
: true
|
||||
@@ -84,9 +84,15 @@ export const promise = async ({
|
||||
const defaultLocale = localization ? localization?.defaultLocale : 'en'
|
||||
const operationLocale = req.locale || defaultLocale
|
||||
|
||||
const pathSegments = path ? path.split('.') : []
|
||||
const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
|
||||
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
// skip validation if the field is localized and the incoming data is null
|
||||
@@ -107,14 +113,13 @@ export const promise = async ({
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
indexPath: indexPathSegments,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData,
|
||||
siblingDocWithLocales,
|
||||
value: siblingData[field.name],
|
||||
@@ -158,17 +163,16 @@ export const promise = async ({
|
||||
|
||||
if (typeof validationResult === 'string') {
|
||||
const label = getTranslatedLabel(field?.label || field?.name, req.i18n)
|
||||
const parentPathSegments = parentPath ? parentPath.split('.') : []
|
||||
|
||||
const fieldLabel =
|
||||
Array.isArray(parentPathSegments) && parentPathSegments.length > 0
|
||||
? getLabelFromPath(parentPathSegments.concat(label))
|
||||
Array.isArray(parentPath) && parentPath.length > 0
|
||||
? getFormattedLabel([...parentPath, label])
|
||||
: label
|
||||
|
||||
errors.push({
|
||||
label: fieldLabel,
|
||||
message: validationResult,
|
||||
path,
|
||||
path: fieldPath.join('.'),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -212,8 +216,7 @@ export const promise = async ({
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
@@ -227,10 +230,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: getExistingRowDoc(row as JsonObject, siblingDoc[field.name]),
|
||||
siblingDocWithLocales: getExistingRowDoc(
|
||||
@@ -252,10 +254,8 @@ export const promise = async ({
|
||||
const rows = siblingData[field.name]
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name])
|
||||
|
||||
const rowSiblingDocWithLocales = getExistingRowDoc(
|
||||
row as JsonObject,
|
||||
siblingDocWithLocales ? siblingDocWithLocales[field.name] : {},
|
||||
@@ -278,10 +278,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath + '.' + block.slug,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: rowSiblingDoc,
|
||||
siblingDocWithLocales: rowSiblingDocWithLocales,
|
||||
@@ -311,10 +310,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
@@ -328,11 +326,9 @@ export const promise = async ({
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDocWithLocales[field.name] !== 'object') {
|
||||
siblingDocWithLocales[field.name] = {}
|
||||
}
|
||||
@@ -349,10 +345,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath: '',
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: siblingData[field.name] as JsonObject,
|
||||
siblingDoc: siblingDoc[field.name] as JsonObject,
|
||||
siblingDocWithLocales: siblingDocWithLocales[field.name] as JsonObject,
|
||||
@@ -361,7 +356,6 @@ export const promise = async ({
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'point': {
|
||||
// Transform point data for storage
|
||||
if (
|
||||
@@ -385,7 +379,6 @@ export const promise = async ({
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
@@ -404,15 +397,14 @@ export const promise = async ({
|
||||
errors,
|
||||
field,
|
||||
global,
|
||||
indexPath: indexPathSegments,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData,
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
@@ -433,17 +425,13 @@ export const promise = async ({
|
||||
let tabSiblingDoc = siblingDoc
|
||||
let tabSiblingDocWithLocales = siblingDocWithLocales
|
||||
|
||||
const isNamedTab = tabHasName(field)
|
||||
|
||||
if (isNamedTab) {
|
||||
if (tabHasName(field)) {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDocWithLocales[field.name] !== 'object') {
|
||||
siblingDocWithLocales[field.name] = {}
|
||||
}
|
||||
@@ -465,10 +453,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath: isNamedTab ? '' : indexPath,
|
||||
parentPath: isNamedTab ? path : parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
siblingDocWithLocales: tabSiblingDocWithLocales,
|
||||
@@ -491,10 +478,9 @@ export const promise = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
|
||||
@@ -25,10 +25,9 @@ type Args = {
|
||||
id?: number | string
|
||||
mergeLocaleActions: (() => Promise<void>)[]
|
||||
operation: Operation
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
path: (number | string)[]
|
||||
req: PayloadRequest
|
||||
schemaPath: string[]
|
||||
siblingData: JsonObject
|
||||
/**
|
||||
* The original siblingData (not modified by any hooks)
|
||||
@@ -61,10 +60,9 @@ export const traverseFields = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
path,
|
||||
req,
|
||||
schemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocWithLocales,
|
||||
@@ -87,9 +85,8 @@ export const traverseFields = async ({
|
||||
global,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { SanitizedCollectionConfig } from '../../../collections/config/type
|
||||
import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, PayloadRequest } from '../../../types/index.js'
|
||||
|
||||
import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
type Args<T extends JsonObject> = {
|
||||
@@ -34,10 +35,9 @@ export const beforeDuplicate = async <T extends JsonObject>({
|
||||
doc,
|
||||
fields: collection?.fields,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: '',
|
||||
parentSchemaPath: '',
|
||||
path: [],
|
||||
req,
|
||||
schemaPath: [],
|
||||
siblingDoc: doc,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { RequestContext } from '../../../index.js'
|
||||
import type { JsonObject, PayloadRequest } from '../../../types/index.js'
|
||||
import type { Field, FieldHookArgs, TabAsField } from '../../config/types.js'
|
||||
|
||||
import { fieldAffectsData } from '../../config/types.js'
|
||||
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { fieldAffectsData, tabHasName } from '../../config/types.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { runBeforeDuplicateHooks } from './runHook.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
|
||||
@@ -16,9 +16,8 @@ type Args<T> = {
|
||||
fieldIndex: number
|
||||
id?: number | string
|
||||
overrideAccess: boolean
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
parentPath: (number | string)[]
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequest
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
@@ -31,25 +30,40 @@ export const promise = async <T>({
|
||||
field,
|
||||
fieldIndex,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
siblingDoc,
|
||||
}: Args<T>): Promise<void> => {
|
||||
const { indexPath, path, schemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
})
|
||||
|
||||
const { localization } = req.payload.config
|
||||
|
||||
const pathSegments = path ? path.split('.') : []
|
||||
const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
|
||||
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
// Handle unnamed tabs
|
||||
if (field.type === 'tab' && !tabHasName(field)) {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
let fieldData = siblingDoc?.[field.name]
|
||||
@@ -68,12 +82,11 @@ export const promise = async <T>({
|
||||
data: doc,
|
||||
field,
|
||||
global: undefined,
|
||||
indexPath: indexPathSegments,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name]?.[locale],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData: siblingDoc,
|
||||
siblingDocWithLocales: siblingDoc,
|
||||
value: siblingDoc[field.name]?.[locale],
|
||||
@@ -101,12 +114,11 @@ export const promise = async <T>({
|
||||
data: doc,
|
||||
field,
|
||||
global: undefined,
|
||||
indexPath: indexPathSegments,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: parentSchemaPath,
|
||||
siblingData: siblingDoc,
|
||||
siblingDocWithLocales: siblingDoc,
|
||||
value: siblingDoc[field.name],
|
||||
@@ -138,8 +150,7 @@ export const promise = async <T>({
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
@@ -148,26 +159,22 @@ export const promise = async <T>({
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = fieldData[locale]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
const blockTypeToMatch = row.blockType
|
||||
|
||||
const block = field.blocks.find(
|
||||
@@ -182,10 +189,9 @@ export const promise = async <T>({
|
||||
doc,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath + '.' + block.slug,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
@@ -195,6 +201,7 @@ export const promise = async <T>({
|
||||
}
|
||||
|
||||
case 'group':
|
||||
|
||||
case 'tab': {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
@@ -204,10 +211,9 @@ export const promise = async <T>({
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldSchemaPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: fieldData[locale],
|
||||
}),
|
||||
)
|
||||
@@ -229,8 +235,7 @@ export const promise = async <T>({
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
@@ -239,28 +244,23 @@ export const promise = async <T>({
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'blocks': {
|
||||
const rows = siblingDoc[field.name]
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
const blockTypeToMatch = row.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
|
||||
@@ -275,52 +275,28 @@ export const promise = async <T>({
|
||||
doc,
|
||||
fields: block.fields,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath + '.' + block.slug,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: row,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
const groupDoc = siblingDoc[field.name] as JsonObject
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
req,
|
||||
siblingDoc: groupDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
case 'group':
|
||||
|
||||
case 'tab': {
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
|
||||
const tabDoc = siblingDoc[field.name] as JsonObject
|
||||
const groupDoc = siblingDoc[field.name] as Record<string, unknown>
|
||||
|
||||
await traverseFields({
|
||||
id,
|
||||
@@ -329,11 +305,10 @@ export const promise = async <T>({
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
siblingDoc: tabDoc,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc: groupDoc as JsonObject,
|
||||
})
|
||||
|
||||
break
|
||||
@@ -352,31 +327,9 @@ export const promise = async <T>({
|
||||
doc,
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
req,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Unnamed Tab
|
||||
// @ts-expect-error `fieldAffectsData` inferred return type doesn't account for TabAsField
|
||||
case 'tab': {
|
||||
await traverseFields({
|
||||
id,
|
||||
collection,
|
||||
context,
|
||||
doc,
|
||||
// @ts-expect-error `fieldAffectsData` inferred return type doesn't account for TabAsField
|
||||
fields: field.fields,
|
||||
overrideAccess,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
@@ -391,10 +344,9 @@ export const promise = async <T>({
|
||||
doc,
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
overrideAccess,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
|
||||
@@ -12,10 +12,9 @@ type Args<T> = {
|
||||
fields: (Field | TabAsField)[]
|
||||
id?: number | string
|
||||
overrideAccess: boolean
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
path: (number | string)[]
|
||||
req: PayloadRequest
|
||||
schemaPath: string[]
|
||||
siblingDoc: JsonObject
|
||||
}
|
||||
|
||||
@@ -26,14 +25,12 @@ export const traverseFields = async <T>({
|
||||
doc,
|
||||
fields,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
path,
|
||||
req,
|
||||
schemaPath,
|
||||
siblingDoc,
|
||||
}: Args<T>): Promise<void> => {
|
||||
const promises = []
|
||||
|
||||
fields.forEach((field, fieldIndex) => {
|
||||
promises.push(
|
||||
promise({
|
||||
@@ -44,9 +41,8 @@ export const traverseFields = async <T>({
|
||||
field,
|
||||
fieldIndex,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
req,
|
||||
siblingDoc,
|
||||
}),
|
||||
|
||||
@@ -47,10 +47,9 @@ export const beforeValidate = async <T extends JsonObject>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: '',
|
||||
parentSchemaPath: '',
|
||||
path: [],
|
||||
req,
|
||||
schemaPath: [],
|
||||
siblingData: incomingData,
|
||||
siblingDoc: doc,
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Field, TabAsField } from '../../config/types.js'
|
||||
import { MissingEditorProp } from '../../../errors/index.js'
|
||||
import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types.js'
|
||||
import { getDefaultValue } from '../../getDefaultValue.js'
|
||||
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { getFieldPaths } from '../../getFieldPaths.js'
|
||||
import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc.js'
|
||||
import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js'
|
||||
import { traverseFields } from './traverseFields.js'
|
||||
@@ -27,9 +27,8 @@ type Args<T> = {
|
||||
id?: number | string
|
||||
operation: 'create' | 'update'
|
||||
overrideAccess: boolean
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
parentPath: (number | string)[]
|
||||
parentSchemaPath: string[]
|
||||
req: PayloadRequest
|
||||
siblingData: JsonObject
|
||||
/**
|
||||
@@ -56,24 +55,21 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args<T>): Promise<void> => {
|
||||
const { indexPath, path, schemaPath } = getFieldPaths({
|
||||
const { path: _fieldPath, schemaPath: _fieldSchemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
parentIndexPath: '', // Doesn't matter, as unnamed fields do not affect data, and hooks are only run on fields that affect data
|
||||
parentPath: parentPath.join('.'),
|
||||
parentSchemaPath: parentSchemaPath.join('.'),
|
||||
})
|
||||
|
||||
const pathSegments = path ? path.split('.') : []
|
||||
const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
|
||||
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
|
||||
const fieldPath = _fieldPath ? _fieldPath.split('.') : []
|
||||
const fieldSchemaPath = _fieldSchemaPath ? _fieldSchemaPath.split('.') : []
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
if (field.name === 'id') {
|
||||
@@ -275,15 +271,14 @@ export const promise = async <T>({
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
indexPath: indexPathSegments,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingDoc[field.name],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
@@ -330,8 +325,7 @@ export const promise = async <T>({
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
promises.push(
|
||||
traverseFields({
|
||||
id,
|
||||
@@ -343,16 +337,14 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: getExistingRowDoc(row as JsonObject, siblingDoc[field.name]),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
break
|
||||
@@ -363,8 +355,7 @@ export const promise = async <T>({
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
const promises = []
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
rows.forEach((row, i) => {
|
||||
const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name])
|
||||
const blockTypeToMatch = (row as JsonObject).blockType || rowSiblingDoc.blockType
|
||||
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
|
||||
@@ -383,17 +374,15 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path + '.' + rowIndex,
|
||||
parentSchemaPath: schemaPath + '.' + block.slug,
|
||||
path: [...fieldPath, i],
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: row as JsonObject,
|
||||
siblingDoc: rowSiblingDoc,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -412,22 +401,19 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'group': {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
@@ -445,10 +431,9 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: groupData as JsonObject,
|
||||
siblingDoc: groupDoc as JsonObject,
|
||||
})
|
||||
@@ -460,7 +445,6 @@ export const promise = async <T>({
|
||||
if (!field?.editor) {
|
||||
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
}
|
||||
|
||||
if (typeof field?.editor === 'function') {
|
||||
throw new Error('Attempted to access unsanitized rich text editor.')
|
||||
}
|
||||
@@ -477,15 +461,14 @@ export const promise = async <T>({
|
||||
data,
|
||||
field,
|
||||
global,
|
||||
indexPath: indexPathSegments,
|
||||
operation,
|
||||
originalDoc: doc,
|
||||
overrideAccess,
|
||||
path: pathSegments,
|
||||
path: fieldPath,
|
||||
previousSiblingDoc: siblingDoc,
|
||||
previousValue: siblingData[field.name],
|
||||
req,
|
||||
schemaPath: schemaPathSegments,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
value: siblingData[field.name],
|
||||
})
|
||||
@@ -501,14 +484,10 @@ export const promise = async <T>({
|
||||
case 'tab': {
|
||||
let tabSiblingData
|
||||
let tabSiblingDoc
|
||||
|
||||
const isNamedTab = tabHasName(field)
|
||||
|
||||
if (isNamedTab) {
|
||||
if (tabHasName(field)) {
|
||||
if (typeof siblingData[field.name] !== 'object') {
|
||||
siblingData[field.name] = {}
|
||||
}
|
||||
|
||||
if (typeof siblingDoc[field.name] !== 'object') {
|
||||
siblingDoc[field.name] = {}
|
||||
}
|
||||
@@ -530,10 +509,9 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: isNamedTab ? '' : indexPath,
|
||||
parentPath: isNamedTab ? path : parentPath,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
})
|
||||
@@ -552,10 +530,9 @@ export const promise = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath: indexPath,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
path: fieldPath,
|
||||
req,
|
||||
schemaPath: fieldSchemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
})
|
||||
|
||||
@@ -19,10 +19,9 @@ type Args<T> = {
|
||||
id?: number | string
|
||||
operation: 'create' | 'update'
|
||||
overrideAccess: boolean
|
||||
parentIndexPath: string
|
||||
parentPath: string
|
||||
parentSchemaPath: string
|
||||
path: (number | string)[]
|
||||
req: PayloadRequest
|
||||
schemaPath: string[]
|
||||
siblingData: JsonObject
|
||||
/**
|
||||
* The original siblingData (not modified by any hooks)
|
||||
@@ -40,15 +39,13 @@ export const traverseFields = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
path,
|
||||
req,
|
||||
schemaPath,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}: Args<T>): Promise<void> => {
|
||||
const promises = []
|
||||
|
||||
fields.forEach((field, fieldIndex) => {
|
||||
promises.push(
|
||||
promise({
|
||||
@@ -62,15 +59,13 @@ export const traverseFields = async <T>({
|
||||
global,
|
||||
operation,
|
||||
overrideAccess,
|
||||
parentIndexPath,
|
||||
parentPath,
|
||||
parentSchemaPath,
|
||||
parentPath: path,
|
||||
parentSchemaPath: schemaPath,
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
@@ -1152,7 +1152,6 @@ export {
|
||||
type ServerOnlyFieldProperties,
|
||||
} from './fields/config/client.js'
|
||||
export { sanitizeFields } from './fields/config/sanitize.js'
|
||||
|
||||
export type {
|
||||
AdminClient,
|
||||
ArrayField,
|
||||
@@ -1429,6 +1428,7 @@ export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js
|
||||
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'
|
||||
export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js'
|
||||
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'
|
||||
|
||||
export { saveVersion } from './versions/saveVersion.js'
|
||||
export type { SchedulePublishTaskInput } from './versions/schedule/types.js'
|
||||
export type { TypeWithVersion } from './versions/types.js'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const getLabelFromPath = (path: (number | string)[]): string => {
|
||||
export const getFormattedLabel = (path: (number | string)[]): string => {
|
||||
return path
|
||||
.filter((pathSegment) => !(typeof pathSegment === 'string' && pathSegment.includes('_index')))
|
||||
.reduce<string[]>((acc, part) => {
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type RelationshipField } from 'payload'
|
||||
import { APIError } from 'payload'
|
||||
|
||||
import { getCollectionIDType } from '../../utilities/getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from '../../utilities/getTenantFromCookie.js'
|
||||
|
||||
type Args = {
|
||||
@@ -40,19 +39,13 @@ export const tenantField = ({
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ req, value }) => {
|
||||
const idType = getCollectionIDType({
|
||||
collectionSlug: tenantsCollectionSlug,
|
||||
payload: req.payload,
|
||||
})
|
||||
if (!value) {
|
||||
const tenantFromCookie = getTenantFromCookie(req.headers, idType)
|
||||
const tenantFromCookie = getTenantFromCookie(req.headers, req.payload.db.defaultIDType)
|
||||
if (tenantFromCookie) {
|
||||
return tenantFromCookie
|
||||
}
|
||||
throw new APIError('You must select a tenant', 400, null, true)
|
||||
}
|
||||
|
||||
return idType === 'number' ? parseFloat(value) : value
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ export const tenantsArrayField = (args: {
|
||||
arrayFieldAccess?: ArrayField['access']
|
||||
rowFields?: ArrayField['fields']
|
||||
tenantFieldAccess?: RelationshipField['access']
|
||||
tenantsCollectionSlug: string
|
||||
}): ArrayField => ({
|
||||
name: 'tenants',
|
||||
type: 'array',
|
||||
@@ -15,7 +14,7 @@ export const tenantsArrayField = (args: {
|
||||
type: 'relationship',
|
||||
access: args.tenantFieldAccess,
|
||||
index: true,
|
||||
relationTo: args.tenantsCollectionSlug,
|
||||
relationTo: 'tenants',
|
||||
required: true,
|
||||
saveToJWT: true,
|
||||
},
|
||||
|
||||
@@ -9,14 +9,12 @@ import { generateCookie, mergeHeaders } from 'payload'
|
||||
|
||||
import type { UserWithTenantsField } from '../types.js'
|
||||
|
||||
import { getCollectionIDType } from '../utilities/getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from '../utilities/getTenantFromCookie.js'
|
||||
|
||||
type Args = {
|
||||
collection: CollectionConfig
|
||||
enabledSlugs: string[]
|
||||
tenantFieldName: string
|
||||
tenantsCollectionSlug: string
|
||||
usersSlug: string
|
||||
}
|
||||
/**
|
||||
@@ -28,7 +26,6 @@ export const addTenantCleanup = ({
|
||||
collection,
|
||||
enabledSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
usersSlug,
|
||||
}: Args) => {
|
||||
if (!collection.hooks) {
|
||||
@@ -41,7 +38,6 @@ export const addTenantCleanup = ({
|
||||
afterTenantDelete({
|
||||
enabledSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
usersSlug,
|
||||
}),
|
||||
)
|
||||
@@ -51,15 +47,10 @@ export const afterTenantDelete =
|
||||
({
|
||||
enabledSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
usersSlug,
|
||||
}: Omit<Args, 'collection'>): CollectionAfterDeleteHook =>
|
||||
async ({ id, req }) => {
|
||||
const idType = getCollectionIDType({
|
||||
collectionSlug: tenantsCollectionSlug,
|
||||
payload: req.payload,
|
||||
})
|
||||
const currentTenantCookieID = getTenantFromCookie(req.headers, idType)
|
||||
const currentTenantCookieID = getTenantFromCookie(req.headers, req.payload.db.defaultIDType)
|
||||
if (currentTenantCookieID === id) {
|
||||
const newHeaders = new Headers({
|
||||
'Set-Cookie': generateCookie<string>({
|
||||
|
||||
@@ -80,12 +80,7 @@ export const multiTenantPlugin =
|
||||
* Add tenants array field to users collection
|
||||
*/
|
||||
if (pluginConfig?.tenantsArrayField?.includeDefaultField !== false) {
|
||||
adminUsersCollection.fields.push(
|
||||
tenantsArrayField({
|
||||
...(pluginConfig?.tenantsArrayField || {}),
|
||||
tenantsCollectionSlug,
|
||||
}),
|
||||
)
|
||||
adminUsersCollection.fields.push(tenantsArrayField(pluginConfig?.tenantsArrayField || {}))
|
||||
}
|
||||
|
||||
addCollectionAccess({
|
||||
@@ -121,15 +116,17 @@ export const multiTenantPlugin =
|
||||
if (collection.slug === tenantsCollectionSlug) {
|
||||
tenantCollection = collection
|
||||
|
||||
/**
|
||||
* Add access control constraint to tenants collection
|
||||
* - constrains access a users assigned tenants
|
||||
*/
|
||||
addCollectionAccess({
|
||||
collection,
|
||||
fieldName: 'id',
|
||||
userHasAccessToAllTenants,
|
||||
})
|
||||
if (pluginConfig.useTenantsCollectionAccess !== false) {
|
||||
/**
|
||||
* Add access control constraint to tenants collection
|
||||
* - constrains access a users assigned tenants
|
||||
*/
|
||||
addCollectionAccess({
|
||||
collection,
|
||||
fieldName: 'id',
|
||||
userHasAccessToAllTenants,
|
||||
})
|
||||
}
|
||||
|
||||
if (pluginConfig.cleanupAfterTenantDelete !== false) {
|
||||
/**
|
||||
@@ -141,7 +138,6 @@ export const multiTenantPlugin =
|
||||
collection,
|
||||
enabledSlugs: [...collectionSlugs, ...globalCollectionSlugs],
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
usersSlug: adminUsersCollection.slug,
|
||||
})
|
||||
}
|
||||
@@ -159,8 +155,6 @@ export const multiTenantPlugin =
|
||||
fields: collection.fields,
|
||||
tenantEnabledCollectionSlugs: collectionSlugs,
|
||||
tenantEnabledGlobalSlugs: globalCollectionSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -188,7 +182,6 @@ export const multiTenantPlugin =
|
||||
collection.admin.baseListFilter = withTenantListFilter({
|
||||
baseListFilter: collection.admin?.baseListFilter,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,10 @@ export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
userHasAccessToAllTenants?: (
|
||||
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
|
||||
) => boolean
|
||||
/**
|
||||
* Opt out of adding access constraints to the tenants collection
|
||||
*/
|
||||
useTenantsCollectionAccess?: boolean
|
||||
}
|
||||
|
||||
export type Tenant<IDType = number | string> = {
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import type { Field, FilterOptionsProps, RelationshipField } from 'payload'
|
||||
|
||||
import { getCollectionIDType } from './getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from './getTenantFromCookie.js'
|
||||
|
||||
type AddFilterOptionsToFieldsArgs = {
|
||||
fields: Field[]
|
||||
tenantEnabledCollectionSlugs: string[]
|
||||
tenantEnabledGlobalSlugs: string[]
|
||||
tenantFieldName: string
|
||||
tenantsCollectionSlug: string
|
||||
}
|
||||
|
||||
export function addFilterOptionsToFields({
|
||||
fields,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantEnabledGlobalSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
}: AddFilterOptionsToFieldsArgs) {
|
||||
fields.forEach((field) => {
|
||||
if (field.type === 'relationship') {
|
||||
@@ -31,7 +26,7 @@ export function addFilterOptionsToFields({
|
||||
)
|
||||
}
|
||||
if (tenantEnabledCollectionSlugs.includes(field.relationTo)) {
|
||||
addFilter({ field, tenantEnabledCollectionSlugs, tenantFieldName, tenantsCollectionSlug })
|
||||
addFilter(field, tenantEnabledCollectionSlugs)
|
||||
}
|
||||
} else {
|
||||
field.relationTo.map((relationTo) => {
|
||||
@@ -41,12 +36,7 @@ export function addFilterOptionsToFields({
|
||||
)
|
||||
}
|
||||
if (tenantEnabledCollectionSlugs.includes(relationTo)) {
|
||||
addFilter({
|
||||
field,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
})
|
||||
addFilter(field, tenantEnabledCollectionSlugs)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -62,8 +52,6 @@ export function addFilterOptionsToFields({
|
||||
fields: field.fields,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantEnabledGlobalSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -73,8 +61,6 @@ export function addFilterOptionsToFields({
|
||||
fields: block.fields,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantEnabledGlobalSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -85,26 +71,13 @@ export function addFilterOptionsToFields({
|
||||
fields: tab.fields,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantEnabledGlobalSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type AddFilterArgs = {
|
||||
field: RelationshipField
|
||||
tenantEnabledCollectionSlugs: string[]
|
||||
tenantFieldName: string
|
||||
tenantsCollectionSlug: string
|
||||
}
|
||||
function addFilter({
|
||||
field,
|
||||
tenantEnabledCollectionSlugs,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
}: AddFilterArgs) {
|
||||
function addFilter(field: RelationshipField, tenantEnabledCollectionSlugs: string[]) {
|
||||
// User specified filter
|
||||
const originalFilter = field.filterOptions
|
||||
field.filterOptions = async (args) => {
|
||||
@@ -122,11 +95,7 @@ function addFilter({
|
||||
}
|
||||
|
||||
// Custom tenant filter
|
||||
const tenantFilterResults = filterOptionsByTenant({
|
||||
...args,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
})
|
||||
const tenantFilterResults = filterOptionsByTenant(args)
|
||||
|
||||
// If the tenant filter returns true, just use the original filter
|
||||
if (tenantFilterResults === true) {
|
||||
@@ -146,18 +115,9 @@ function addFilter({
|
||||
|
||||
type Args = {
|
||||
tenantFieldName?: string
|
||||
tenantsCollectionSlug: string
|
||||
} & FilterOptionsProps
|
||||
const filterOptionsByTenant = ({
|
||||
req,
|
||||
tenantFieldName = 'tenant',
|
||||
tenantsCollectionSlug,
|
||||
}: Args) => {
|
||||
const idType = getCollectionIDType({
|
||||
collectionSlug: tenantsCollectionSlug,
|
||||
payload: req.payload,
|
||||
})
|
||||
const selectedTenant = getTenantFromCookie(req.headers, idType)
|
||||
const filterOptionsByTenant = ({ req, tenantFieldName = 'tenant' }: Args) => {
|
||||
const selectedTenant = getTenantFromCookie(req.headers, req.payload.db.defaultIDType)
|
||||
if (!selectedTenant) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { CollectionSlug, Payload } from 'payload'
|
||||
|
||||
type Args = {
|
||||
collectionSlug: CollectionSlug
|
||||
payload: Payload
|
||||
}
|
||||
export const getCollectionIDType = ({ collectionSlug, payload }: Args): 'number' | 'text' => {
|
||||
return payload.collections[collectionSlug]?.customIDType ?? payload.db.defaultIDType
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import type { Payload, User, ViewTypes } from 'payload'
|
||||
|
||||
import { SELECT_ALL } from '../constants.js'
|
||||
import { findTenantOptions } from '../queries/findTenantOptions.js'
|
||||
import { getCollectionIDType } from './getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from './getTenantFromCookie.js'
|
||||
|
||||
type Args = {
|
||||
@@ -27,11 +26,7 @@ export async function getGlobalViewRedirect({
|
||||
user,
|
||||
view,
|
||||
}: Args): Promise<string | void> {
|
||||
const idType = getCollectionIDType({
|
||||
collectionSlug: tenantsCollectionSlug,
|
||||
payload,
|
||||
})
|
||||
let tenant = getTenantFromCookie(headers, idType)
|
||||
let tenant = getTenantFromCookie(headers, payload.db.defaultIDType)
|
||||
let redirectRoute
|
||||
|
||||
if (!tenant || tenant === SELECT_ALL) {
|
||||
|
||||
@@ -13,5 +13,5 @@ export function getTenantFromCookie(
|
||||
): null | number | string {
|
||||
const cookies = parseCookies(headers)
|
||||
const selectedTenant = cookies.get('payload-tenant') || null
|
||||
return selectedTenant ? (idType === 'number' ? parseFloat(selectedTenant) : selectedTenant) : null
|
||||
return selectedTenant ? (idType === 'number' ? parseInt(selectedTenant) : selectedTenant) : null
|
||||
}
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import type { PayloadRequest, Where } from 'payload'
|
||||
|
||||
import { SELECT_ALL } from '../constants.js'
|
||||
import { getCollectionIDType } from './getCollectionIDType.js'
|
||||
import { getTenantFromCookie } from './getTenantFromCookie.js'
|
||||
|
||||
type Args = {
|
||||
req: PayloadRequest
|
||||
tenantFieldName: string
|
||||
tenantsCollectionSlug: string
|
||||
}
|
||||
export const getTenantListFilter = ({
|
||||
req,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
}: Args): null | Where => {
|
||||
const idType = getCollectionIDType({
|
||||
collectionSlug: tenantsCollectionSlug,
|
||||
payload: req.payload,
|
||||
})
|
||||
const selectedTenant = getTenantFromCookie(req.headers, idType)
|
||||
export const getTenantListFilter = ({ req, tenantFieldName }: Args): null | Where => {
|
||||
const selectedTenant = getTenantFromCookie(req.headers, req.payload.db.defaultIDType)
|
||||
|
||||
if (selectedTenant === SELECT_ALL) {
|
||||
return {}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { getTenantListFilter } from './getTenantListFilter.js'
|
||||
type Args = {
|
||||
baseListFilter?: BaseListFilter
|
||||
tenantFieldName: string
|
||||
tenantsCollectionSlug: string
|
||||
}
|
||||
/**
|
||||
* Combines a base list filter with a tenant list filter
|
||||
@@ -13,7 +12,7 @@ type Args = {
|
||||
* Combines where constraints inside of an AND operator
|
||||
*/
|
||||
export const withTenantListFilter =
|
||||
({ baseListFilter, tenantFieldName, tenantsCollectionSlug }: Args): BaseListFilter =>
|
||||
({ baseListFilter, tenantFieldName }: Args): BaseListFilter =>
|
||||
async (args) => {
|
||||
const filterConstraints = []
|
||||
|
||||
@@ -28,7 +27,6 @@ export const withTenantListFilter =
|
||||
const tenantListFilter = getTenantListFilter({
|
||||
req: args.req,
|
||||
tenantFieldName,
|
||||
tenantsCollectionSlug,
|
||||
})
|
||||
|
||||
if (tenantListFilter) {
|
||||
|
||||
@@ -22,91 +22,64 @@ type ResaveArgs = {
|
||||
|
||||
const resave = async ({ collection, doc, draft, pluginConfig, req }: ResaveArgs) => {
|
||||
const parentSlug = pluginConfig?.parentFieldSlug || 'parent'
|
||||
const parentDocIsPublished = doc._status === 'published'
|
||||
const children = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
draft: true,
|
||||
limit: 0,
|
||||
locale: req.locale,
|
||||
req,
|
||||
where: {
|
||||
[parentSlug]: {
|
||||
equals: doc.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const breadcrumbSlug = pluginConfig.breadcrumbsFieldSlug || 'breadcrumbs'
|
||||
|
||||
if (draft) {
|
||||
// If the parent is a draft, don't resave children
|
||||
return
|
||||
} else {
|
||||
const initialDraftChildren = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
draft: true,
|
||||
limit: 0,
|
||||
locale: req.locale,
|
||||
req,
|
||||
where: {
|
||||
[parentSlug]: {
|
||||
equals: doc.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
try {
|
||||
for (const child of children.docs) {
|
||||
const childIsPublished =
|
||||
typeof collection.versions === 'object' &&
|
||||
collection.versions.drafts &&
|
||||
child._status === 'published'
|
||||
|
||||
const draftChildren = initialDraftChildren.docs.filter((child) => child._status === 'draft')
|
||||
|
||||
const publishedChildren = await req.payload.find({
|
||||
collection: collection.slug,
|
||||
depth: 0,
|
||||
draft: false,
|
||||
limit: 0,
|
||||
locale: req.locale,
|
||||
req,
|
||||
where: {
|
||||
[parentSlug]: {
|
||||
equals: doc.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const childrenById = [...draftChildren, ...publishedChildren.docs].reduce((acc, child) => {
|
||||
acc[child.id] = acc[child.id] || []
|
||||
acc[child.id].push(child)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const sortedChildren = Object.values(childrenById).flatMap((group: JsonObject[]) => {
|
||||
return group.sort((a, b) => {
|
||||
if (a.updatedAt !== b.updatedAt) {
|
||||
return a.updatedAt > b.updatedAt ? 1 : -1
|
||||
}
|
||||
return a._status === 'published' ? 1 : -1
|
||||
})
|
||||
})
|
||||
|
||||
if (sortedChildren) {
|
||||
try {
|
||||
for (const child of sortedChildren) {
|
||||
const isDraft = child._status !== 'published'
|
||||
|
||||
await req.payload.update({
|
||||
id: child.id,
|
||||
collection: collection.slug,
|
||||
data: {
|
||||
...child,
|
||||
[breadcrumbSlug]: await populateBreadcrumbs(req, pluginConfig, collection, child),
|
||||
},
|
||||
depth: 0,
|
||||
draft: isDraft,
|
||||
locale: req.locale,
|
||||
req,
|
||||
})
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error(
|
||||
`Nested Docs plugin encountered an error while re-saving a child document.`,
|
||||
)
|
||||
req.payload.logger.error(err)
|
||||
|
||||
if (
|
||||
(err as ValidationError)?.name === 'ValidationError' &&
|
||||
(err as ValidationError)?.data?.errors?.length
|
||||
) {
|
||||
throw new APIError(
|
||||
'Could not publish or save changes: One or more children are invalid.',
|
||||
400,
|
||||
)
|
||||
}
|
||||
if (!parentDocIsPublished && childIsPublished) {
|
||||
continue
|
||||
}
|
||||
|
||||
await req.payload.update({
|
||||
id: child.id,
|
||||
collection: collection.slug,
|
||||
data: {
|
||||
...child,
|
||||
[breadcrumbSlug]: await populateBreadcrumbs(req, pluginConfig, collection, child),
|
||||
},
|
||||
depth: 0,
|
||||
draft: !childIsPublished,
|
||||
locale: req.locale,
|
||||
req,
|
||||
})
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error(
|
||||
`Nested Docs plugin has had an error while re-saving a child document${
|
||||
draft ? ' as draft' : ' as published'
|
||||
}.`,
|
||||
)
|
||||
req.payload.logger.error(err)
|
||||
|
||||
// Use type assertion until we can use instanceof reliably with our Error types
|
||||
if (
|
||||
(err as ValidationError)?.name === 'ValidationError' &&
|
||||
(err as ValidationError)?.data?.errors?.length
|
||||
) {
|
||||
throw new APIError(
|
||||
'Could not publish or save changes: One or more children are invalid.',
|
||||
400,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,10 +90,20 @@ export const resaveChildren =
|
||||
await resave({
|
||||
collection,
|
||||
doc,
|
||||
draft: doc._status === 'published' ? false : true,
|
||||
draft: true,
|
||||
pluginConfig,
|
||||
req,
|
||||
})
|
||||
|
||||
if (doc._status === 'published') {
|
||||
await resave({
|
||||
collection,
|
||||
doc,
|
||||
draft: false,
|
||||
pluginConfig,
|
||||
req,
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -190,7 +190,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
context: _context,
|
||||
data,
|
||||
global,
|
||||
indexPath,
|
||||
operation,
|
||||
originalDoc,
|
||||
path,
|
||||
@@ -199,7 +198,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
req,
|
||||
schemaPath,
|
||||
} = args
|
||||
|
||||
let { value } = args
|
||||
if (finalSanitizedEditorConfig?.features?.hooks?.afterChange?.length) {
|
||||
for (const hook of finalSanitizedEditorConfig.features.hooks.afterChange) {
|
||||
@@ -294,12 +292,11 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
fields: subFields,
|
||||
global,
|
||||
operation,
|
||||
parentIndexPath: indexPath.join('-'),
|
||||
parentPath: path.join('.'),
|
||||
parentSchemaPath: schemaPath.join('.'),
|
||||
path,
|
||||
previousDoc,
|
||||
previousSiblingDoc: { ...nodePreviousSiblingDoc },
|
||||
req,
|
||||
schemaPath,
|
||||
siblingData: nodeSiblingData || {},
|
||||
siblingDoc: { ...nodeSiblingDoc },
|
||||
})
|
||||
@@ -325,7 +322,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
findMany,
|
||||
flattenLocales,
|
||||
global,
|
||||
indexPath,
|
||||
locale,
|
||||
originalDoc,
|
||||
overrideAccess,
|
||||
@@ -338,7 +334,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
triggerAccessControl,
|
||||
triggerHooks,
|
||||
} = args
|
||||
|
||||
let { value } = args
|
||||
|
||||
if (finalSanitizedEditorConfig?.features?.hooks?.afterRead?.length) {
|
||||
@@ -413,12 +408,11 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
global,
|
||||
locale: locale!,
|
||||
overrideAccess: overrideAccess!,
|
||||
parentIndexPath: indexPath.join('-'),
|
||||
parentPath: path.join('.'),
|
||||
parentSchemaPath: schemaPath.join('.'),
|
||||
path,
|
||||
populate,
|
||||
populationPromises: populationPromises!,
|
||||
req,
|
||||
schemaPath,
|
||||
showHiddenFields: showHiddenFields!,
|
||||
siblingDoc: nodeSliblingData,
|
||||
triggerAccessControl,
|
||||
@@ -441,7 +435,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
errors,
|
||||
field,
|
||||
global,
|
||||
indexPath,
|
||||
mergeLocaleActions,
|
||||
operation,
|
||||
originalDoc,
|
||||
@@ -453,7 +446,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
siblingDocWithLocales,
|
||||
skipValidation,
|
||||
} = args
|
||||
|
||||
let { value } = args
|
||||
|
||||
if (finalSanitizedEditorConfig?.features?.hooks?.beforeChange?.length) {
|
||||
@@ -574,10 +566,9 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
global,
|
||||
mergeLocaleActions: mergeLocaleActions!,
|
||||
operation: operation!,
|
||||
parentIndexPath: indexPath.join('-'),
|
||||
parentPath: path.join('.'),
|
||||
parentSchemaPath: schemaPath.join('.'),
|
||||
path,
|
||||
req,
|
||||
schemaPath,
|
||||
siblingData: nodeSiblingData,
|
||||
siblingDoc: nodePreviousSiblingDoc,
|
||||
siblingDocWithLocales: nodeSiblingDocWithLocales ?? {},
|
||||
@@ -629,7 +620,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
context,
|
||||
data,
|
||||
global,
|
||||
indexPath,
|
||||
operation,
|
||||
originalDoc,
|
||||
overrideAccess,
|
||||
@@ -638,7 +628,6 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
req,
|
||||
schemaPath,
|
||||
} = args
|
||||
|
||||
let { value } = args
|
||||
if (finalSanitizedEditorConfig?.features?.hooks?.beforeValidate?.length) {
|
||||
for (const hook of finalSanitizedEditorConfig.features.hooks.beforeValidate) {
|
||||
@@ -766,10 +755,9 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
global,
|
||||
operation,
|
||||
overrideAccess: overrideAccess!,
|
||||
parentIndexPath: indexPath.join('-'),
|
||||
parentPath: path.join('.'),
|
||||
parentSchemaPath: schemaPath.join('.'),
|
||||
path,
|
||||
req,
|
||||
schemaPath,
|
||||
siblingData: nodeSiblingData,
|
||||
siblingDoc: nodeSiblingDoc,
|
||||
})
|
||||
|
||||
@@ -59,11 +59,10 @@ export const recursivelyPopulateFieldsForGraphQL = ({
|
||||
global: null, // Pass from core? This is only needed for hooks, so we can leave this null for now
|
||||
locale: req.locale!,
|
||||
overrideAccess,
|
||||
parentIndexPath: '',
|
||||
parentPath: '',
|
||||
parentSchemaPath: '',
|
||||
path: [],
|
||||
populationPromises, // This is not the same as populationPromises passed into this recurseNestedFields. These are just promises resolved at the very end.
|
||||
req,
|
||||
schemaPath: [],
|
||||
showHiddenFields,
|
||||
siblingDoc,
|
||||
triggerHooks: false,
|
||||
|
||||
@@ -39,10 +39,6 @@ export const importDateFNSLocale = async (locale: string): Promise<Locale> => {
|
||||
case 'es':
|
||||
result = (await import('date-fns/locale/es')).es
|
||||
|
||||
break
|
||||
case 'et':
|
||||
result = (await import('date-fns/locale/et')).et
|
||||
|
||||
break
|
||||
case 'fa-IR':
|
||||
result = (await import('date-fns/locale/fa-IR')).faIR
|
||||
|
||||
@@ -443,9 +443,9 @@ export const enTranslations = {
|
||||
autosave: 'Autosave',
|
||||
autosavedSuccessfully: 'Autosaved successfully.',
|
||||
autosavedVersion: 'Autosaved version',
|
||||
changed: 'Changed',
|
||||
changedFieldsCount_one: '{{count}} changed field',
|
||||
changedFieldsCount_other: '{{count}} changed fields',
|
||||
changed: 'Changed',
|
||||
compareVersion: 'Compare version against:',
|
||||
confirmPublish: 'Confirm publish',
|
||||
confirmRevertToSaved: 'Confirm revert to saved',
|
||||
|
||||
@@ -108,7 +108,7 @@ export function EditForm({ submitted }: EditFormProps) {
|
||||
)
|
||||
|
||||
const onChange: NonNullable<FormProps['onChange']>[0] = useCallback(
|
||||
async ({ formState: prevFormState, submitted }) => {
|
||||
async ({ formState: prevFormState }) => {
|
||||
const controller = handleAbortRef(abortOnChangeRef)
|
||||
|
||||
const docPreferences = await getDocPreferences()
|
||||
@@ -121,7 +121,6 @@ export function EditForm({ submitted }: EditFormProps) {
|
||||
operation: 'create',
|
||||
schemaPath,
|
||||
signal: controller.signal,
|
||||
skipValidation: !submitted,
|
||||
})
|
||||
|
||||
abortOnChangeRef.current = null
|
||||
|
||||
@@ -216,7 +216,6 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
|
||||
operation: 'create',
|
||||
renderAllFields: true,
|
||||
schemaPath: collectionSlug,
|
||||
skipValidation: true,
|
||||
})
|
||||
initialStateRef.current = formStateWithoutFiles
|
||||
setHasInitializedState(true)
|
||||
|
||||
@@ -185,7 +185,6 @@ export const EditManyDrawerContent: React.FC<
|
||||
operation: 'update',
|
||||
schemaPath: slug,
|
||||
signal: controller.signal,
|
||||
skipValidation: true,
|
||||
})
|
||||
|
||||
setInitialState(result)
|
||||
|
||||
@@ -198,12 +198,8 @@ const TabsFieldComponent: TabsFieldClientComponent = (props) => {
|
||||
parentSchemaPath={activePathSchemaChildrenPath}
|
||||
path={activeTabPath}
|
||||
permissions={
|
||||
permissions && typeof permissions === 'object' && 'name' in activeTabConfig
|
||||
? permissions[activeTabConfig.name] &&
|
||||
typeof permissions[activeTabConfig.name] === 'object' &&
|
||||
'fields' in permissions[activeTabConfig.name]
|
||||
? permissions[activeTabConfig.name].fields
|
||||
: permissions[activeTabConfig.name]
|
||||
'name' in activeTabConfig && permissions?.[activeTabConfig.name]?.fields
|
||||
? permissions[activeTabConfig.name].fields
|
||||
: permissions
|
||||
}
|
||||
readOnly={readOnly}
|
||||
|
||||
@@ -505,7 +505,6 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
renderAllFields: true,
|
||||
schemaPath: collectionSlug ? collectionSlug : globalSlug,
|
||||
signal: controller.signal,
|
||||
skipValidation: true,
|
||||
})
|
||||
|
||||
contextRef.current = { ...initContextState } as FormContextType
|
||||
@@ -666,7 +665,6 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
// Edit view default onChange is in packages/ui/src/views/Edit/index.tsx. This onChange usually sends a form state request
|
||||
revalidatedFormState = await onChangeFn({
|
||||
formState: deepCopyObjectSimpleWithoutReactComponents(contextRef.current.fields),
|
||||
submitted,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -701,7 +699,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
`fields` updates before `modified`, because setModified is in a setTimeout.
|
||||
So on the first change, modified is false, so we don't trigger the effect even though we should.
|
||||
**/
|
||||
[contextRef.current.fields, modified, submitted],
|
||||
[contextRef.current.fields, modified],
|
||||
[dispatchFields, onChange],
|
||||
{
|
||||
delay: 250,
|
||||
|
||||
@@ -33,6 +33,7 @@ export const mergeServerFormState = ({
|
||||
|
||||
if (acceptValues) {
|
||||
serverPropsToAccept.push('value')
|
||||
serverPropsToAccept.push('initialValue')
|
||||
}
|
||||
|
||||
let changed = false
|
||||
|
||||
@@ -39,7 +39,7 @@ export type FormProps = {
|
||||
initialState?: FormState
|
||||
isInitializing?: boolean
|
||||
log?: boolean
|
||||
onChange?: ((args: { formState: FormState; submitted?: boolean }) => Promise<FormState>)[]
|
||||
onChange?: ((args: { formState: FormState }) => Promise<FormState>)[]
|
||||
onSubmit?: (fields: FormState, data: Data) => void
|
||||
onSuccess?: (json: unknown) => Promise<FormState | void> | void
|
||||
redirect?: string
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
PayloadRequest,
|
||||
SanitizedFieldPermissions,
|
||||
SanitizedFieldsPermissions,
|
||||
Validate,
|
||||
} from 'payload'
|
||||
|
||||
import ObjectIdImport from 'bson-objectid'
|
||||
@@ -168,7 +167,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
return
|
||||
}
|
||||
|
||||
const validate: Validate = field.validate
|
||||
const validate = field.validate
|
||||
|
||||
let validationResult: string | true = true
|
||||
|
||||
@@ -184,21 +183,21 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
}
|
||||
|
||||
try {
|
||||
validationResult = await validate(data?.[field.name], {
|
||||
...field,
|
||||
id,
|
||||
collectionSlug,
|
||||
data: fullData,
|
||||
event: 'onChange',
|
||||
// @AlessioGr added `jsonError` in https://github.com/payloadcms/payload/commit/c7ea62a39473408c3ea912c4fbf73e11be4b538d
|
||||
// @ts-expect-error-next-line
|
||||
jsonError,
|
||||
operation,
|
||||
preferences,
|
||||
previousValue: previousFormState?.[path]?.initialValue,
|
||||
req,
|
||||
siblingData: data,
|
||||
})
|
||||
validationResult = await validate(
|
||||
data?.[field.name] as never,
|
||||
{
|
||||
...field,
|
||||
id,
|
||||
collectionSlug,
|
||||
data: fullData,
|
||||
event: 'onChange',
|
||||
jsonError,
|
||||
operation,
|
||||
preferences,
|
||||
req,
|
||||
siblingData: data,
|
||||
} as any,
|
||||
)
|
||||
} catch (err) {
|
||||
validationResult = `Error validating field at path: ${path}`
|
||||
|
||||
@@ -366,6 +365,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
}
|
||||
|
||||
const parentPath = path + '.' + i
|
||||
const rowSchemaPath = schemaPath + '.' + block.slug
|
||||
|
||||
if (block) {
|
||||
row.id = row?.id || new ObjectId().toHexString()
|
||||
@@ -435,7 +435,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
parentIndexPath: '',
|
||||
parentPassesCondition: passesCondition,
|
||||
parentPath,
|
||||
parentSchemaPath: schemaPath + '.' + block.slug,
|
||||
parentSchemaPath: rowSchemaPath,
|
||||
permissions:
|
||||
fieldPermissions === true
|
||||
? fieldPermissions
|
||||
@@ -741,10 +741,10 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
includeSchema,
|
||||
omitParents,
|
||||
operation,
|
||||
parentIndexPath: isNamedTab ? '' : tabIndexPath,
|
||||
parentIndexPath: tabHasName(tab) ? '' : tabIndexPath,
|
||||
parentPassesCondition: passesCondition,
|
||||
parentPath: isNamedTab ? tabPath : parentPath,
|
||||
parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath,
|
||||
parentPath: tabHasName(tab) ? tabPath : parentPath,
|
||||
parentSchemaPath: tabHasName(tab) ? tabSchemaPath : parentSchemaPath,
|
||||
permissions: childPermissions,
|
||||
preferences,
|
||||
previousFormState,
|
||||
|
||||
@@ -47,33 +47,34 @@ type Args = {
|
||||
renderAllFields: boolean
|
||||
renderFieldFn?: RenderFieldMethod
|
||||
req: PayloadRequest
|
||||
|
||||
schemaPath: string
|
||||
skipValidation?: boolean
|
||||
}
|
||||
|
||||
export const fieldSchemasToFormState = async ({
|
||||
id,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data = {},
|
||||
fields,
|
||||
fieldSchemaMap,
|
||||
operation,
|
||||
permissions,
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
renderFieldFn,
|
||||
req,
|
||||
schemaPath,
|
||||
skipValidation,
|
||||
}: Args): Promise<FormState> => {
|
||||
if (!clientFieldSchemaMap && renderFieldFn) {
|
||||
export const fieldSchemasToFormState = async (args: Args): Promise<FormState> => {
|
||||
if (!args.clientFieldSchemaMap && args.renderFieldFn) {
|
||||
console.warn(
|
||||
'clientFieldSchemaMap is not passed to fieldSchemasToFormState - this will reduce performance',
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
clientFieldSchemaMap,
|
||||
collectionSlug,
|
||||
data = {},
|
||||
fields,
|
||||
fieldSchemaMap,
|
||||
operation,
|
||||
permissions,
|
||||
preferences,
|
||||
previousFormState,
|
||||
renderAllFields,
|
||||
renderFieldFn,
|
||||
req,
|
||||
schemaPath,
|
||||
} = args
|
||||
|
||||
if (fields && fields.length) {
|
||||
const state: FormStateWithoutComponents = {}
|
||||
|
||||
@@ -109,7 +110,6 @@ export const fieldSchemasToFormState = async ({
|
||||
renderAllFields,
|
||||
renderFieldFn,
|
||||
req,
|
||||
skipValidation,
|
||||
state,
|
||||
})
|
||||
|
||||
|
||||
@@ -131,11 +131,8 @@ export const traverseFields = ({
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tabs':
|
||||
field.tabs.map((tab, tabIndex) => {
|
||||
const isNamedTab = tabHasName(tab)
|
||||
|
||||
const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({
|
||||
field: {
|
||||
...tab,
|
||||
@@ -154,8 +151,8 @@ export const traverseFields = ({
|
||||
config,
|
||||
fields: tab.fields,
|
||||
i18n,
|
||||
parentIndexPath: isNamedTab ? '' : tabIndexPath,
|
||||
parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath,
|
||||
parentIndexPath: tabHasName(tab) ? '' : tabIndexPath,
|
||||
parentSchemaPath: tabHasName(tab) ? tabSchemaPath : parentSchemaPath,
|
||||
payload,
|
||||
schemaMap,
|
||||
})
|
||||
|
||||
@@ -98,8 +98,6 @@ export const traverseFields = ({
|
||||
|
||||
case 'tabs':
|
||||
field.tabs.map((tab, tabIndex) => {
|
||||
const isNamedTab = tabHasName(tab)
|
||||
|
||||
const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({
|
||||
field: {
|
||||
...tab,
|
||||
@@ -117,8 +115,8 @@ export const traverseFields = ({
|
||||
config,
|
||||
fields: tab.fields,
|
||||
i18n,
|
||||
parentIndexPath: isNamedTab ? '' : tabIndexPath,
|
||||
parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath,
|
||||
parentIndexPath: tabHasName(tab) ? '' : tabIndexPath,
|
||||
parentSchemaPath: tabHasName(tab) ? tabSchemaPath : parentSchemaPath,
|
||||
schemaMap,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -114,7 +114,6 @@ export const buildFormState = async (
|
||||
},
|
||||
returnLockStatus,
|
||||
schemaPath = collectionSlug || globalSlug,
|
||||
skipValidation,
|
||||
updateLastEdited,
|
||||
} = args
|
||||
|
||||
@@ -195,7 +194,6 @@ export const buildFormState = async (
|
||||
renderFieldFn: renderField,
|
||||
req,
|
||||
schemaPath,
|
||||
skipValidation,
|
||||
})
|
||||
|
||||
// Maintain form state of auth / upload fields
|
||||
|
||||
@@ -280,7 +280,6 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
||||
returnLockStatus: false,
|
||||
schemaPath: schemaPathSegments.join('.'),
|
||||
signal: controller.signal,
|
||||
skipValidation: true,
|
||||
})
|
||||
|
||||
// Unlock the document after save
|
||||
@@ -324,7 +323,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
||||
)
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState, submitted }) => {
|
||||
async ({ formState: prevFormState }) => {
|
||||
const controller = handleAbortRef(abortOnChangeRef)
|
||||
|
||||
const currentTime = Date.now()
|
||||
@@ -346,7 +345,6 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
|
||||
formState: prevFormState,
|
||||
globalSlug,
|
||||
operation,
|
||||
skipValidation: !submitted,
|
||||
// Performance optimization: Setting it to false ensure that only fields that have explicit requireRender set in the form state will be rendered (e.g. new array rows).
|
||||
// We only want to render ALL fields on initial render, not in onChange.
|
||||
renderAllFields: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { FormFieldBlock, Form as FormType } from '@payloadcms/plugin-form-builder/types'
|
||||
import type { Form as FormType } from '@payloadcms/plugin-form-builder/types'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
@@ -8,9 +8,20 @@ import RichText from '@/components/RichText'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import { buildInitialFormState } from './buildInitialFormState'
|
||||
import { fields } from './fields'
|
||||
import { getClientSideURL } from '@/utilities/getURL'
|
||||
|
||||
export type Value = unknown
|
||||
|
||||
export interface Property {
|
||||
[key: string]: Value
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
[key: string]: Property | Property[]
|
||||
}
|
||||
|
||||
export type FormBlockType = {
|
||||
blockName?: string
|
||||
blockType?: 'formBlock'
|
||||
@@ -32,7 +43,7 @@ export const FormBlock: React.FC<
|
||||
} = props
|
||||
|
||||
const formMethods = useForm({
|
||||
defaultValues: formFromProps.fields,
|
||||
defaultValues: buildInitialFormState(formFromProps.fields),
|
||||
})
|
||||
const {
|
||||
control,
|
||||
@@ -47,7 +58,7 @@ export const FormBlock: React.FC<
|
||||
const router = useRouter()
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data: FormFieldBlock[]) => {
|
||||
(data: Data) => {
|
||||
let loadingTimerID: ReturnType<typeof setTimeout>
|
||||
const submitForm = async () => {
|
||||
setError(undefined)
|
||||
|
||||
44
templates/website/src/blocks/Form/buildInitialFormState.tsx
Normal file
44
templates/website/src/blocks/Form/buildInitialFormState.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { FormFieldBlock } from '@payloadcms/plugin-form-builder/types'
|
||||
|
||||
export const buildInitialFormState = (fields: FormFieldBlock[]) => {
|
||||
return fields?.reduce((initialSchema, field) => {
|
||||
if (field.blockType === 'checkbox') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.name]: field.defaultValue,
|
||||
}
|
||||
}
|
||||
if (field.blockType === 'country') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.name]: '',
|
||||
}
|
||||
}
|
||||
if (field.blockType === 'email') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.name]: '',
|
||||
}
|
||||
}
|
||||
if (field.blockType === 'text') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.name]: '',
|
||||
}
|
||||
}
|
||||
if (field.blockType === 'select') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.name]: '',
|
||||
}
|
||||
}
|
||||
if (field.blockType === 'state') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.name]: '',
|
||||
}
|
||||
}
|
||||
|
||||
return initialSchema
|
||||
}, {})
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export interface UserAuthOperations {
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
content?: {
|
||||
richText?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
@@ -217,7 +217,7 @@ export interface PayloadMigration {
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
content?: T;
|
||||
richText?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
@@ -340,6 +340,23 @@ export interface MenuSelect<T extends boolean = true> {
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "ContactBlock".
|
||||
*/
|
||||
export interface ContactBlock {
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
first: string;
|
||||
/**
|
||||
* ...
|
||||
*/
|
||||
two: string;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'contact';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
|
||||
@@ -8,7 +8,7 @@ import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { errorOnUnnamedFieldsSlug, postsSlug } from './shared.js'
|
||||
import { postsSlug } from './shared.js'
|
||||
|
||||
const defaultValueField: TextField = {
|
||||
name: 'defaultValue',
|
||||
@@ -156,32 +156,6 @@ export default buildConfigWithDefaults({
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: errorOnUnnamedFieldsSlug,
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'UnnamedTab',
|
||||
fields: [
|
||||
{
|
||||
name: 'groupWithinUnnamedTab',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'default-values',
|
||||
fields: [
|
||||
|
||||
@@ -26,7 +26,7 @@ import { devUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { isMongoose } from '../helpers/isMongoose.js'
|
||||
import removeFiles from '../helpers/removeFiles.js'
|
||||
import { errorOnUnnamedFieldsSlug, postsSlug } from './shared.js'
|
||||
import { postsSlug } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -895,7 +895,7 @@ describe('database', () => {
|
||||
await expect(errorMessage).toBe('The following field is invalid: Title')
|
||||
})
|
||||
|
||||
it('should return validation errors in response', async () => {
|
||||
it('should return proper deeply nested field validation errors', async () => {
|
||||
try {
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
@@ -921,22 +921,6 @@ describe('database', () => {
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should return validation errors with proper field paths for unnamed fields', async () => {
|
||||
try {
|
||||
await payload.create({
|
||||
collection: errorOnUnnamedFieldsSlug,
|
||||
data: {
|
||||
groupWithinUnnamedTab: {
|
||||
// @ts-expect-error
|
||||
text: undefined,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (e: any) {
|
||||
expect(e.data?.errors?.[0]?.path).toBe('groupWithinUnnamedTab.text')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('defaultValue', () => {
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface Config {
|
||||
};
|
||||
collections: {
|
||||
posts: Post;
|
||||
'error-on-unnamed-fields': ErrorOnUnnamedField;
|
||||
'default-values': DefaultValue;
|
||||
'relation-a': RelationA;
|
||||
'relation-b': RelationB;
|
||||
@@ -31,7 +30,6 @@ export interface Config {
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
'error-on-unnamed-fields': ErrorOnUnnamedFieldsSelect<false> | ErrorOnUnnamedFieldsSelect<true>;
|
||||
'default-values': DefaultValuesSelect<false> | DefaultValuesSelect<true>;
|
||||
'relation-a': RelationASelect<false> | RelationASelect<true>;
|
||||
'relation-b': RelationBSelect<false> | RelationBSelect<true>;
|
||||
@@ -116,18 +114,6 @@ export interface Post {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "error-on-unnamed-fields".
|
||||
*/
|
||||
export interface ErrorOnUnnamedField {
|
||||
id: string;
|
||||
groupWithinUnnamedTab: {
|
||||
text: string;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "default-values".
|
||||
@@ -369,10 +355,6 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'error-on-unnamed-fields';
|
||||
value: string | ErrorOnUnnamedField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'default-values';
|
||||
value: string | DefaultValue;
|
||||
@@ -500,19 +482,6 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "error-on-unnamed-fields_select".
|
||||
*/
|
||||
export interface ErrorOnUnnamedFieldsSelect<T extends boolean = true> {
|
||||
groupWithinUnnamedTab?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "default-values_select".
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export const postsSlug = 'posts'
|
||||
export const errorOnUnnamedFieldsSlug = 'error-on-unnamed-fields'
|
||||
|
||||
@@ -47,8 +47,6 @@ const TabsFields: CollectionConfig = {
|
||||
type: 'array',
|
||||
required: true,
|
||||
fields: [
|
||||
// path: 'array.n.text'
|
||||
// schemaPath: '_index-1-0.array.text'
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
|
||||
@@ -30,7 +30,6 @@ import { clearAndSeedEverything } from './seed.js'
|
||||
import {
|
||||
arrayFieldsSlug,
|
||||
blockFieldsSlug,
|
||||
collapsibleFieldsSlug,
|
||||
groupFieldsSlug,
|
||||
relationshipFieldsSlug,
|
||||
tabsFieldsSlug,
|
||||
@@ -488,7 +487,7 @@ describe('Fields', () => {
|
||||
})
|
||||
|
||||
describe('rows', () => {
|
||||
it('should show proper validation error message on text field within row field', async () => {
|
||||
it('show proper validation error message on text field within row field', async () => {
|
||||
await expect(async () =>
|
||||
payload.create({
|
||||
collection: 'row-fields',
|
||||
@@ -1678,7 +1677,7 @@ describe('Fields', () => {
|
||||
expect(res.id).toBe(doc.id)
|
||||
})
|
||||
|
||||
it('should show proper validation error on text field in nested array', async () => {
|
||||
it('show proper validation error on text field in nested array', async () => {
|
||||
await expect(async () =>
|
||||
payload.create({
|
||||
collection,
|
||||
@@ -1698,7 +1697,7 @@ describe('Fields', () => {
|
||||
).rejects.toThrow('The following field is invalid: Items 1 > SubArray 1 > Second text field')
|
||||
})
|
||||
|
||||
it('should show proper validation error on text field in row field in nested array', async () => {
|
||||
it('show proper validation error on text field in row field in nested array', async () => {
|
||||
await expect(async () =>
|
||||
payload.create({
|
||||
collection,
|
||||
@@ -2507,10 +2506,10 @@ describe('Fields', () => {
|
||||
})
|
||||
|
||||
describe('collapsible', () => {
|
||||
it('should show proper validation error message for fields nested in collapsible', async () => {
|
||||
it('show proper validation error message for fields nested in collapsible', async () => {
|
||||
await expect(async () =>
|
||||
payload.create({
|
||||
collection: collapsibleFieldsSlug,
|
||||
collection: 'collapsible-fields',
|
||||
data: {
|
||||
text: 'required',
|
||||
group: {
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export const beforeValidateSlug = 'before-validate'
|
||||
export const fieldPathsSlug = 'field-paths'
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { beforeValidateSlug } from '../../shared.js'
|
||||
import { beforeValidateSlug } from '../../collectionSlugs.js'
|
||||
|
||||
export const BeforeValidateCollection: CollectionConfig = {
|
||||
slug: beforeValidateSlug,
|
||||
|
||||
@@ -18,6 +18,7 @@ export const DataHooks: CollectionConfig = {
|
||||
return args
|
||||
},
|
||||
],
|
||||
|
||||
beforeChange: [
|
||||
({ context, data, collection }) => {
|
||||
context['collection_beforeChange_collection'] = JSON.stringify(collection)
|
||||
@@ -68,6 +69,7 @@ export const DataHooks: CollectionConfig = {
|
||||
return value
|
||||
},
|
||||
],
|
||||
|
||||
afterRead: [
|
||||
({ collection, field, context }) => {
|
||||
return (
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
import type { CollectionConfig, Field, FieldHook, FieldHookArgs } from 'payload'
|
||||
|
||||
import { fieldPathsSlug } from '../../shared.js'
|
||||
|
||||
const attachPathsToDoc = (
|
||||
label: string,
|
||||
{ value, path, schemaPath, indexPath, data }: FieldHookArgs,
|
||||
): any => {
|
||||
if (!data) {
|
||||
data = {}
|
||||
}
|
||||
|
||||
// attach values to data for `beforeRead` and `beforeChange` hooks
|
||||
data[`${label}_FieldPaths`] = {
|
||||
path,
|
||||
schemaPath,
|
||||
indexPath,
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const attachHooks = (
|
||||
fieldIdentifier: string,
|
||||
): {
|
||||
afterRead: FieldHook[]
|
||||
beforeChange: FieldHook[]
|
||||
beforeDuplicate: FieldHook[]
|
||||
beforeValidate: FieldHook[]
|
||||
} => ({
|
||||
beforeValidate: [(args) => attachPathsToDoc(`${fieldIdentifier}_beforeValidate`, args)],
|
||||
beforeChange: [(args) => attachPathsToDoc(`${fieldIdentifier}_beforeChange`, args)],
|
||||
afterRead: [(args) => attachPathsToDoc(`${fieldIdentifier}_afterRead`, args)],
|
||||
beforeDuplicate: [(args) => attachPathsToDoc(`${fieldIdentifier}_beforeDuplicate`, args)],
|
||||
})
|
||||
|
||||
const createFields = (fieldIdentifiers: string[]): Field[] =>
|
||||
fieldIdentifiers.reduce((acc, fieldIdentifier) => {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
name: `${fieldIdentifier}_beforeValidate_FieldPaths`,
|
||||
type: 'json',
|
||||
},
|
||||
{
|
||||
name: `${fieldIdentifier}_beforeChange_FieldPaths`,
|
||||
type: 'json',
|
||||
},
|
||||
{
|
||||
name: `${fieldIdentifier}_afterRead_FieldPaths`,
|
||||
type: 'json',
|
||||
},
|
||||
{
|
||||
name: `${fieldIdentifier}_beforeDuplicate_FieldPaths`,
|
||||
type: 'json',
|
||||
},
|
||||
]
|
||||
}, [] as Field[])
|
||||
|
||||
export const FieldPaths: CollectionConfig = {
|
||||
slug: fieldPathsSlug,
|
||||
fields: [
|
||||
{
|
||||
// path: 'topLevelNamedField'
|
||||
// schemaPath: 'topLevelNamedField'
|
||||
// indexPath: ''
|
||||
name: 'topLevelNamedField',
|
||||
type: 'text',
|
||||
hooks: attachHooks('topLevelNamedField'),
|
||||
},
|
||||
{
|
||||
// path: 'array'
|
||||
// schemaPath: 'array'
|
||||
// indexPath: ''
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
// path: 'array.[n].fieldWithinArray'
|
||||
// schemaPath: 'array.fieldWithinArray'
|
||||
// indexPath: ''
|
||||
name: 'fieldWithinArray',
|
||||
type: 'text',
|
||||
hooks: attachHooks('fieldWithinArray'),
|
||||
},
|
||||
{
|
||||
// path: 'array.[n].nestedArray'
|
||||
// schemaPath: 'array.nestedArray'
|
||||
// indexPath: ''
|
||||
name: 'nestedArray',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
// path: 'array.[n].nestedArray.[n].fieldWithinNestedArray'
|
||||
// schemaPath: 'array.nestedArray.fieldWithinNestedArray'
|
||||
// indexPath: ''
|
||||
name: 'fieldWithinNestedArray',
|
||||
type: 'text',
|
||||
hooks: attachHooks('fieldWithinNestedArray'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// path: 'array.[n]._index-2'
|
||||
// schemaPath: 'array._index-2'
|
||||
// indexPath: ''
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
// path: 'array.[n].fieldWithinRowWithinArray'
|
||||
// schemaPath: 'array._index-2.fieldWithinRowWithinArray'
|
||||
// indexPath: ''
|
||||
name: 'fieldWithinRowWithinArray',
|
||||
type: 'text',
|
||||
hooks: attachHooks('fieldWithinRowWithinArray'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// path: '_index-2'
|
||||
// schemaPath: '_index-2'
|
||||
// indexPath: '2'
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
// path: 'fieldWithinRow'
|
||||
// schemaPath: '_index-2.fieldWithinRow'
|
||||
// indexPath: ''
|
||||
name: 'fieldWithinRow',
|
||||
type: 'text',
|
||||
hooks: attachHooks('fieldWithinRow'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// path: '_index-3'
|
||||
// schemaPath: '_index-3'
|
||||
// indexPath: '3'
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
// path: '_index-3-0'
|
||||
// schemaPath: '_index-3-0'
|
||||
// indexPath: '3-0'
|
||||
label: 'Unnamed Tab',
|
||||
fields: [
|
||||
{
|
||||
// path: 'fieldWithinUnnamedTab'
|
||||
// schemaPath: '_index-3-0.fieldWithinUnnamedTab'
|
||||
// indexPath: ''
|
||||
name: 'fieldWithinUnnamedTab',
|
||||
type: 'text',
|
||||
hooks: attachHooks('fieldWithinUnnamedTab'),
|
||||
},
|
||||
{
|
||||
// path: '_index-3-0-1'
|
||||
// schemaPath: '_index-3-0-1'
|
||||
// indexPath: '3-0-1'
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
// path: '_index-3-0-1-0'
|
||||
// schemaPath: '_index-3-0-1-0'
|
||||
// indexPath: '3-0-1-0'
|
||||
label: 'Nested Unnamed Tab',
|
||||
fields: [
|
||||
{
|
||||
// path: 'fieldWithinNestedUnnamedTab'
|
||||
// schemaPath: '_index-3-0-1-0.fieldWithinNestedUnnamedTab'
|
||||
// indexPath: ''
|
||||
name: 'fieldWithinNestedUnnamedTab',
|
||||
type: 'text',
|
||||
hooks: attachHooks('fieldWithinNestedUnnamedTab'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// path: 'namedTab'
|
||||
// schemaPath: '_index-3.namedTab'
|
||||
// indexPath: ''
|
||||
label: 'Named Tab',
|
||||
name: 'namedTab',
|
||||
fields: [
|
||||
{
|
||||
// path: 'namedTab.fieldWithinNamedTab'
|
||||
// schemaPath: '_index-3.namedTab.fieldWithinNamedTab'
|
||||
// indexPath: ''
|
||||
name: 'fieldWithinNamedTab',
|
||||
type: 'text',
|
||||
hooks: attachHooks('fieldWithinNamedTab'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// create fields for the hooks to save data to
|
||||
...createFields([
|
||||
'topLevelNamedField',
|
||||
'fieldWithinArray',
|
||||
'fieldWithinNestedArray',
|
||||
'fieldWithinRowWithinArray',
|
||||
'fieldWithinRow',
|
||||
'fieldWithinUnnamedTab',
|
||||
'fieldWithinNestedUnnamedTab',
|
||||
'fieldWithinNamedTab',
|
||||
]),
|
||||
],
|
||||
}
|
||||
@@ -13,14 +13,12 @@ import { BeforeValidateCollection } from './collections/BeforeValidate/index.js'
|
||||
import ChainingHooks from './collections/ChainingHooks/index.js'
|
||||
import ContextHooks from './collections/ContextHooks/index.js'
|
||||
import { DataHooks } from './collections/Data/index.js'
|
||||
import { FieldPaths } from './collections/FieldPaths/index.js'
|
||||
import Hooks, { hooksSlug } from './collections/Hook/index.js'
|
||||
import NestedAfterReadHooks from './collections/NestedAfterReadHooks/index.js'
|
||||
import Relations from './collections/Relations/index.js'
|
||||
import TransformHooks from './collections/Transform/index.js'
|
||||
import Users, { seedHooksUsers } from './collections/Users/index.js'
|
||||
import { DataHooksGlobal } from './globals/Data/index.js'
|
||||
|
||||
export const HooksConfig: Promise<SanitizedConfig> = buildConfigWithDefaults({
|
||||
admin: {
|
||||
importMap: {
|
||||
@@ -39,7 +37,6 @@ export const HooksConfig: Promise<SanitizedConfig> = buildConfigWithDefaults({
|
||||
Relations,
|
||||
Users,
|
||||
DataHooks,
|
||||
FieldPaths,
|
||||
],
|
||||
globals: [DataHooksGlobal],
|
||||
endpoints: [
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert }
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { beforeValidateSlug } from './shared.js'
|
||||
import { beforeValidateSlug } from './collectionSlugs.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { relationsSlug } from './collections/Relations/index.js'
|
||||
import { transformSlug } from './collections/Transform/index.js'
|
||||
import { hooksUsersSlug } from './collections/Users/index.js'
|
||||
import { beforeValidateSlug, fieldPathsSlug } from './shared.js'
|
||||
import { beforeValidateSlug } from './collectionSlugs.js'
|
||||
import { HooksConfig } from './config.js'
|
||||
import { dataHooksGlobalSlug } from './globals/Data/index.js'
|
||||
|
||||
@@ -517,101 +517,6 @@ describe('Hooks', () => {
|
||||
|
||||
expect(doc.field_globalAndField).toStrictEqual(globalAndFieldString + globalAndFieldString)
|
||||
})
|
||||
|
||||
it('should pass correct field paths through field hooks', async () => {
|
||||
const formatExpectedFieldPaths = (
|
||||
fieldIdentifier: string,
|
||||
{
|
||||
path,
|
||||
schemaPath,
|
||||
}: {
|
||||
path: string[]
|
||||
schemaPath: string[]
|
||||
},
|
||||
) => ({
|
||||
[`${fieldIdentifier}_beforeValidate_FieldPaths`]: {
|
||||
path,
|
||||
schemaPath,
|
||||
},
|
||||
[`${fieldIdentifier}_beforeChange_FieldPaths`]: {
|
||||
path,
|
||||
schemaPath,
|
||||
},
|
||||
[`${fieldIdentifier}_afterRead_FieldPaths`]: {
|
||||
path,
|
||||
schemaPath,
|
||||
},
|
||||
[`${fieldIdentifier}_beforeDuplicate_FieldPaths`]: {
|
||||
path,
|
||||
schemaPath,
|
||||
},
|
||||
})
|
||||
|
||||
const originalDoc = await payload.create({
|
||||
collection: fieldPathsSlug,
|
||||
data: {
|
||||
topLevelNamedField: 'Test',
|
||||
array: [
|
||||
{
|
||||
fieldWithinArray: 'Test',
|
||||
nestedArray: [
|
||||
{
|
||||
fieldWithinNestedArray: 'Test',
|
||||
fieldWithinNestedRow: 'Test',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
fieldWithinRow: 'Test',
|
||||
fieldWithinUnnamedTab: 'Test',
|
||||
namedTab: {
|
||||
fieldWithinNamedTab: 'Test',
|
||||
},
|
||||
fieldWithinNestedUnnamedTab: 'Test',
|
||||
},
|
||||
})
|
||||
|
||||
// duplicate the doc to ensure that the beforeDuplicate hook is run
|
||||
const doc = await payload.duplicate({
|
||||
id: originalDoc.id,
|
||||
collection: fieldPathsSlug,
|
||||
})
|
||||
|
||||
expect(doc).toMatchObject({
|
||||
...formatExpectedFieldPaths('topLevelNamedField', {
|
||||
path: ['topLevelNamedField'],
|
||||
schemaPath: ['topLevelNamedField'],
|
||||
}),
|
||||
...formatExpectedFieldPaths('fieldWithinArray', {
|
||||
path: ['array', '0', 'fieldWithinArray'],
|
||||
schemaPath: ['array', 'fieldWithinArray'],
|
||||
}),
|
||||
...formatExpectedFieldPaths('fieldWithinNestedArray', {
|
||||
path: ['array', '0', 'nestedArray', '0', 'fieldWithinNestedArray'],
|
||||
schemaPath: ['array', 'nestedArray', 'fieldWithinNestedArray'],
|
||||
}),
|
||||
...formatExpectedFieldPaths('fieldWithinRowWithinArray', {
|
||||
path: ['array', '0', 'fieldWithinRowWithinArray'],
|
||||
schemaPath: ['array', '_index-2', 'fieldWithinRowWithinArray'],
|
||||
}),
|
||||
...formatExpectedFieldPaths('fieldWithinRow', {
|
||||
path: ['fieldWithinRow'],
|
||||
schemaPath: ['_index-2', 'fieldWithinRow'],
|
||||
}),
|
||||
...formatExpectedFieldPaths('fieldWithinUnnamedTab', {
|
||||
path: ['fieldWithinUnnamedTab'],
|
||||
schemaPath: ['_index-3-0', 'fieldWithinUnnamedTab'],
|
||||
}),
|
||||
...formatExpectedFieldPaths('fieldWithinNestedUnnamedTab', {
|
||||
path: ['fieldWithinNestedUnnamedTab'],
|
||||
schemaPath: ['_index-3-0-1-0', 'fieldWithinNestedUnnamedTab'],
|
||||
}),
|
||||
...formatExpectedFieldPaths('fieldWithinNamedTab', {
|
||||
path: ['namedTab', 'fieldWithinNamedTab'],
|
||||
schemaPath: ['_index-3', 'namedTab', 'fieldWithinNamedTab'],
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('config level after error hook', () => {
|
||||
|
||||
@@ -22,7 +22,6 @@ export interface Config {
|
||||
relations: Relation;
|
||||
'hooks-users': HooksUser;
|
||||
'data-hooks': DataHook;
|
||||
'field-paths': FieldPath;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
@@ -40,7 +39,6 @@ export interface Config {
|
||||
relations: RelationsSelect<false> | RelationsSelect<true>;
|
||||
'hooks-users': HooksUsersSelect<false> | HooksUsersSelect<true>;
|
||||
'data-hooks': DataHooksSelect<false> | DataHooksSelect<true>;
|
||||
'field-paths': FieldPathsSelect<false> | FieldPathsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
@@ -238,286 +236,6 @@ export interface DataHook {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "field-paths".
|
||||
*/
|
||||
export interface FieldPath {
|
||||
id: string;
|
||||
topLevelNamedField?: string | null;
|
||||
array?:
|
||||
| {
|
||||
fieldWithinArray?: string | null;
|
||||
nestedArray?:
|
||||
| {
|
||||
fieldWithinNestedArray?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
fieldWithinRow?: string | null;
|
||||
fieldWithinUnnamedTab?: string | null;
|
||||
fieldWithinNestedUnnamedTab?: string | null;
|
||||
namedTab?: {
|
||||
fieldWithinNamedTab?: string | null;
|
||||
};
|
||||
topLevelNamedField_beforeValidate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
topLevelNamedField_beforeChange_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
topLevelNamedField_afterRead_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
topLevelNamedField_beforeDuplicate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinArray_beforeValidate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinArray_beforeChange_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinArray_afterRead_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinArray_beforeDuplicate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNestedArray_beforeValidate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNestedArray_beforeChange_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNestedArray_afterRead_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNestedArray_beforeDuplicate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinRow_beforeValidate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinRow_beforeChange_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinRow_afterRead_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinRow_beforeDuplicate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinUnnamedTab_beforeValidate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinUnnamedTab_beforeChange_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinUnnamedTab_afterRead_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinUnnamedTab_beforeDuplicate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNestedUnnamedTab_beforeValidate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNestedUnnamedTab_beforeChange_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNestedUnnamedTab_afterRead_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNestedUnnamedTab_beforeDuplicate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNamedTab_beforeValidate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNamedTab_beforeChange_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNamedTab_afterRead_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
fieldWithinNamedTab_beforeDuplicate_FieldPaths?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
@@ -568,10 +286,6 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'data-hooks';
|
||||
value: string | DataHook;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'field-paths';
|
||||
value: string | FieldPath;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@@ -756,63 +470,6 @@ export interface DataHooksSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "field-paths_select".
|
||||
*/
|
||||
export interface FieldPathsSelect<T extends boolean = true> {
|
||||
topLevelNamedField?: T;
|
||||
array?:
|
||||
| T
|
||||
| {
|
||||
fieldWithinArray?: T;
|
||||
nestedArray?:
|
||||
| T
|
||||
| {
|
||||
fieldWithinNestedArray?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
fieldWithinRow?: T;
|
||||
fieldWithinUnnamedTab?: T;
|
||||
fieldWithinNestedUnnamedTab?: T;
|
||||
namedTab?:
|
||||
| T
|
||||
| {
|
||||
fieldWithinNamedTab?: T;
|
||||
};
|
||||
topLevelNamedField_beforeValidate_FieldPaths?: T;
|
||||
topLevelNamedField_beforeChange_FieldPaths?: T;
|
||||
topLevelNamedField_afterRead_FieldPaths?: T;
|
||||
topLevelNamedField_beforeDuplicate_FieldPaths?: T;
|
||||
fieldWithinArray_beforeValidate_FieldPaths?: T;
|
||||
fieldWithinArray_beforeChange_FieldPaths?: T;
|
||||
fieldWithinArray_afterRead_FieldPaths?: T;
|
||||
fieldWithinArray_beforeDuplicate_FieldPaths?: T;
|
||||
fieldWithinNestedArray_beforeValidate_FieldPaths?: T;
|
||||
fieldWithinNestedArray_beforeChange_FieldPaths?: T;
|
||||
fieldWithinNestedArray_afterRead_FieldPaths?: T;
|
||||
fieldWithinNestedArray_beforeDuplicate_FieldPaths?: T;
|
||||
fieldWithinRow_beforeValidate_FieldPaths?: T;
|
||||
fieldWithinRow_beforeChange_FieldPaths?: T;
|
||||
fieldWithinRow_afterRead_FieldPaths?: T;
|
||||
fieldWithinRow_beforeDuplicate_FieldPaths?: T;
|
||||
fieldWithinUnnamedTab_beforeValidate_FieldPaths?: T;
|
||||
fieldWithinUnnamedTab_beforeChange_FieldPaths?: T;
|
||||
fieldWithinUnnamedTab_afterRead_FieldPaths?: T;
|
||||
fieldWithinUnnamedTab_beforeDuplicate_FieldPaths?: T;
|
||||
fieldWithinNestedUnnamedTab_beforeValidate_FieldPaths?: T;
|
||||
fieldWithinNestedUnnamedTab_beforeChange_FieldPaths?: T;
|
||||
fieldWithinNestedUnnamedTab_afterRead_FieldPaths?: T;
|
||||
fieldWithinNestedUnnamedTab_beforeDuplicate_FieldPaths?: T;
|
||||
fieldWithinNamedTab_beforeValidate_FieldPaths?: T;
|
||||
fieldWithinNamedTab_beforeChange_FieldPaths?: T;
|
||||
fieldWithinNamedTab_afterRead_FieldPaths?: T;
|
||||
fieldWithinNamedTab_beforeDuplicate_FieldPaths?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
|
||||
@@ -72,7 +72,6 @@ describe('@payloadcms/plugin-nested-docs', () => {
|
||||
title: `Child ${i + 1}`,
|
||||
slug: `child-${i + 1}`,
|
||||
parent: parentDoc.id,
|
||||
_status: 'published',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -84,7 +83,6 @@ describe('@payloadcms/plugin-nested-docs', () => {
|
||||
data: {
|
||||
title: '11 children updated',
|
||||
slug: '11-children-updated',
|
||||
_status: 'published',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -92,6 +90,7 @@ describe('@payloadcms/plugin-nested-docs', () => {
|
||||
const { docs } = await payload.find({
|
||||
collection: 'pages',
|
||||
limit: 0,
|
||||
draft: true,
|
||||
where: {
|
||||
parent: {
|
||||
equals: parentDoc.id,
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": ["./test/hooks/config.ts"],
|
||||
"@payload-config": ["./test/admin/config.ts"],
|
||||
"@payloadcms/live-preview": ["./packages/live-preview/src"],
|
||||
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
|
||||
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user