feat: prevent querying relationship when filterOptions returns false (#4392)
fix: hidden collections showing in lexical and slate relationships feat: prevent querying relationship when filterOptions returns false fix: hidden collections appear in richtext internal link options Co-authored-by: Alessio Gravili <70709113+AlessioGr@users.noreply.github.com>
This commit is contained in:
@@ -12,10 +12,10 @@ keywords: relationship, fields, config, configuration, documentation, Content Ma
|
|||||||
</Banner>
|
</Banner>
|
||||||
|
|
||||||
<LightDarkImage
|
<LightDarkImage
|
||||||
srcLight="https://payloadcms.com/images/docs/fields/relationship.png"
|
srcLight="https://payloadcms.com/images/docs/fields/relationship.png"
|
||||||
srcDark="https://payloadcms.com/images/docs/fields/relationship-dark.png"
|
srcDark="https://payloadcms.com/images/docs/fields/relationship-dark.png"
|
||||||
alt="Shows a relationship field in the Payload admin panel"
|
alt="Shows a relationship field in the Payload admin panel"
|
||||||
caption="Admin panel screenshot of a Relationship field"
|
caption="Admin panel screenshot of a Relationship field"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
**Example uses:**
|
**Example uses:**
|
||||||
@@ -26,28 +26,28 @@ keywords: relationship, fields, config, configuration, documentation, Content Ma
|
|||||||
|
|
||||||
### Config
|
### Config
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||||
| **`relationTo`** \* | Provide one or many collection `slug`s to be able to assign relationships to. |
|
| **`relationTo`** \* | Provide one or many collection `slug`s to be able to assign relationships to. |
|
||||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-relationship-options). |
|
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-relationship-options). |
|
||||||
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many relations instead of only one. |
|
| **`hasMany`** | Boolean when, if set to `true`, allows this field to have many relations instead of only one. |
|
||||||
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with `hasMany`. |
|
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with `hasMany`. |
|
||||||
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with `hasMany`. |
|
| **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with `hasMany`. |
|
||||||
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](/docs/getting-started/concepts#depth) |
|
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](/docs/getting-started/concepts#depth) |
|
||||||
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
|
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
|
||||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||||
| **`index`** | Build a an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
| **`index`** | Build a an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
|
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
|
||||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||||
| **`required`** | Require this field to have a value. |
|
| **`required`** | Require this field to have a value. |
|
||||||
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
|
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
|
||||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||||
|
|
||||||
_\* An asterisk denotes that a property is required._
|
_\* An asterisk denotes that a property is required._
|
||||||
|
|
||||||
@@ -60,47 +60,62 @@ _\* An asterisk denotes that a property is required._
|
|||||||
|
|
||||||
### Admin config
|
### Admin config
|
||||||
|
|
||||||
In addition to the default [field admin config](/docs/fields/overview#admin-config), the Relationship field type also allows for the following admin-specific properties:
|
In addition to the default [field admin config](/docs/fields/overview#admin-config), the Relationship field type also
|
||||||
|
allows for the following admin-specific properties:
|
||||||
|
|
||||||
**`isSortable`**
|
**`isSortable`**
|
||||||
|
|
||||||
Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop (only works when `hasMany` is set to `true`).
|
Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop (only works when `hasMany`
|
||||||
|
is set to `true`).
|
||||||
|
|
||||||
**`allowCreate`**
|
**`allowCreate`**
|
||||||
|
|
||||||
Set to `false` if you'd like to disable the ability to create new documents from within the relationship field (hides the "Add new" button in the admin UI).
|
Set to `false` if you'd like to disable the ability to create new documents from within the relationship field (hides
|
||||||
|
the "Add new" button in the admin UI).
|
||||||
|
|
||||||
**`sortOptions`**
|
**`sortOptions`**
|
||||||
|
|
||||||
The `sortOptions` property allows you to define a default sorting order for the options within a Relationship field's dropdown. This can be particularly useful for ensuring that the most relevant options are presented first to the user.
|
The `sortOptions` property allows you to define a default sorting order for the options within a Relationship field's
|
||||||
|
dropdown. This can be particularly useful for ensuring that the most relevant options are presented first to the user.
|
||||||
|
|
||||||
You can specify `sortOptions` in two ways:
|
You can specify `sortOptions` in two ways:
|
||||||
|
|
||||||
**As a string:**
|
**As a string:**
|
||||||
|
|
||||||
Provide a string to define a global default sort field for all relationship field dropdowns across different collections. You can prefix the field name with a minus symbol ("-") to sort in descending order.
|
Provide a string to define a global default sort field for all relationship field dropdowns across different
|
||||||
|
collections. You can prefix the field name with a minus symbol ("-") to sort in descending order.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
sortOptions: 'fieldName',
|
sortOptions: 'fieldName',
|
||||||
```
|
```
|
||||||
|
|
||||||
This configuration will sort all relationship field dropdowns by `"fieldName"` in ascending order.
|
This configuration will sort all relationship field dropdowns by `"fieldName"` in ascending order.
|
||||||
|
|
||||||
**As an object :**
|
**As an object :**
|
||||||
|
|
||||||
Specify an object where keys are collection slugs and values are strings representing the field names to sort by. This allows for different sorting fields for each collection's relationship dropdown.
|
Specify an object where keys are collection slugs and values are strings representing the field names to sort by. This
|
||||||
|
allows for different sorting fields for each collection's relationship dropdown.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
sortOptions: {
|
sortOptions: {
|
||||||
"pages": "fieldName1",
|
"pages"
|
||||||
"posts": "-fieldName2",
|
:
|
||||||
"categories": "fieldName3"
|
"fieldName1",
|
||||||
|
"posts"
|
||||||
|
:
|
||||||
|
"-fieldName2",
|
||||||
|
"categories"
|
||||||
|
:
|
||||||
|
"fieldName3"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In this configuration:
|
In this configuration:
|
||||||
|
|
||||||
- Dropdowns related to `pages` will be sorted by `"fieldName1"` in ascending order.
|
- Dropdowns related to `pages` will be sorted by `"fieldName1"` in ascending order.
|
||||||
- Dropdowns for `posts` will use `"fieldName2"` for sorting in descending order (noted by the "-" prefix).
|
- Dropdowns for `posts` will use `"fieldName2"` for sorting in descending order (noted by the "-" prefix).
|
||||||
- Dropdowns associated with `categories` will sort based on `"fieldName3"` in ascending order.
|
- Dropdowns associated with `categories` will sort based on `"fieldName3"` in ascending order.
|
||||||
@@ -109,12 +124,15 @@ Note: If `sortOptions` is not defined, the default sorting behavior of the Relat
|
|||||||
|
|
||||||
### Filtering relationship options
|
### Filtering relationship options
|
||||||
|
|
||||||
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both for validating input and filtering available relationships in the UI.
|
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both
|
||||||
|
for validating input and filtering available relationships in the UI.
|
||||||
|
|
||||||
The `filterOptions` property can either be a `Where` query directly, or a function (synchronous or asynchronous) that returns one. When using a function, it will be called with an argument object containing the following properties:
|
The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to
|
||||||
|
prevent all, or a `Where` query. When using a function, it will be
|
||||||
|
called with an argument object with the following properties:
|
||||||
|
|
||||||
| Property | Description |
|
| Property | Description |
|
||||||
| ------------- | ------------------------------------------------------------------------------------ |
|
|---------------|--------------------------------------------------------------------------------------|
|
||||||
| `relationTo` | The `relationTo` to filter against (as defined on the field) |
|
| `relationTo` | The `relationTo` to filter against (as defined on the field) |
|
||||||
| `data` | An object of the full collection or global document currently being edited |
|
| `data` | An object of the full collection or global document currently being edited |
|
||||||
| `siblingData` | An object of the document data limited to fields within the same parent to the field |
|
| `siblingData` | An object of the document data limited to fields within the same parent to the field |
|
||||||
@@ -165,16 +183,21 @@ You can learn more about writing queries [here](/docs/queries/overview).
|
|||||||
|
|
||||||
### How the data is saved
|
### How the data is saved
|
||||||
|
|
||||||
Given the variety of options possible within the `relationship` field type, the shape of the data needed for creating and updating these fields can vary. The following sections will describe the variety of data shapes that can arise from this field.
|
Given the variety of options possible within the `relationship` field type, the shape of the data needed for creating
|
||||||
|
and updating these fields can vary. The following sections will describe the variety of data shapes that can arise from
|
||||||
|
this field.
|
||||||
|
|
||||||
#### Has One
|
#### Has One
|
||||||
|
|
||||||
The most simple pattern of a relationship is to use `hasMany: false` with a `relationTo` that allows for only one type of collection.
|
The most simple pattern of a relationship is to use `hasMany: false` with a `relationTo` that allows for only one type
|
||||||
|
of collection.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
{
|
{
|
||||||
slug: 'example-collection',
|
slug: 'example-collection',
|
||||||
fields: [
|
fields
|
||||||
|
:
|
||||||
|
[
|
||||||
{
|
{
|
||||||
name: 'owner', // required
|
name: 'owner', // required
|
||||||
type: 'relationship', // required
|
type: 'relationship', // required
|
||||||
@@ -200,12 +223,15 @@ When querying documents in this collection via REST API, you could query as foll
|
|||||||
|
|
||||||
#### Has One - Polymorphic
|
#### Has One - Polymorphic
|
||||||
|
|
||||||
Also known as **dynamic references**, in this configuration, the `relationTo` field is an array of Collection slugs that tells Payload which Collections are valid to reference.
|
Also known as **dynamic references**, in this configuration, the `relationTo` field is an array of Collection slugs that
|
||||||
|
tells Payload which Collections are valid to reference.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
{
|
{
|
||||||
slug: 'example-collection',
|
slug: 'example-collection',
|
||||||
fields: [
|
fields
|
||||||
|
:
|
||||||
|
[
|
||||||
{
|
{
|
||||||
name: 'owner', // required
|
name: 'owner', // required
|
||||||
type: 'relationship', // required
|
type: 'relationship', // required
|
||||||
@@ -244,7 +270,9 @@ The `hasMany` tells Payload that there may be more than one collection saved to
|
|||||||
```ts
|
```ts
|
||||||
{
|
{
|
||||||
slug: 'example-collection',
|
slug: 'example-collection',
|
||||||
fields: [
|
fields
|
||||||
|
:
|
||||||
|
[
|
||||||
{
|
{
|
||||||
name: 'owners', // required
|
name: 'owners', // required
|
||||||
type: 'relationship', // required
|
type: 'relationship', // required
|
||||||
@@ -259,7 +287,10 @@ To save the to `hasMany` relationship field we need to send an array of IDs:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"owners": ["6031ac9e1289176380734024", "602c3c327b811235943ee12b"]
|
"owners": [
|
||||||
|
"6031ac9e1289176380734024",
|
||||||
|
"602c3c327b811235943ee12b"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -272,7 +303,9 @@ When querying documents, the format does not change for arrays:
|
|||||||
```ts
|
```ts
|
||||||
{
|
{
|
||||||
slug: 'example-collection',
|
slug: 'example-collection',
|
||||||
fields: [
|
fields
|
||||||
|
:
|
||||||
|
[
|
||||||
{
|
{
|
||||||
name: 'owners', // required
|
name: 'owners', // required
|
||||||
type: 'relationship', // required
|
type: 'relationship', // required
|
||||||
@@ -284,7 +317,8 @@ When querying documents, the format does not change for arrays:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Relationship fields with `hasMany` set to more than one kind of collections save their data as an array of objects—each containing the Collection `slug` as the `relationTo` value, and the related document `id` for the `value`:
|
Relationship fields with `hasMany` set to more than one kind of collections save their data as an array of objects—each
|
||||||
|
containing the Collection `slug` as the `relationTo` value, and the related document `id` for the `value`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -305,12 +339,14 @@ Querying is done in the same way as the earlier Polymorphic example:
|
|||||||
|
|
||||||
`?where[owners.value][equals]=6031ac9e1289176380734024`.
|
`?where[owners.value][equals]=6031ac9e1289176380734024`.
|
||||||
|
|
||||||
|
|
||||||
#### Querying and Filtering Polymorphic Relationships
|
#### Querying and Filtering Polymorphic Relationships
|
||||||
|
|
||||||
Polymorphic and non-polymorphic relationships must be queried differently because of how the related data is stored and may be inconsistent across different collections. Because of this, filtering polymorphic relationship fields from the Collection List admin UI is limited to the `id` value.
|
Polymorphic and non-polymorphic relationships must be queried differently because of how the related data is stored and
|
||||||
|
may be inconsistent across different collections. Because of this, filtering polymorphic relationship fields from the
|
||||||
|
Collection List admin UI is limited to the `id` value.
|
||||||
|
|
||||||
For a polymorphic relationship, the response will always be an array of objects. Each object will contain the `relationTo` and `value` properties.
|
For a polymorphic relationship, the response will always be an array of objects. Each object will contain
|
||||||
|
the `relationTo` and `value` properties.
|
||||||
|
|
||||||
The data can be queried by the related document ID:
|
The data can be queried by the related document ID:
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ keywords: upload, images media, fields, config, configuration, documentation, Co
|
|||||||
</Banner>
|
</Banner>
|
||||||
|
|
||||||
<LightDarkImage
|
<LightDarkImage
|
||||||
srcLight="https://payloadcms.com/images/docs/fields/upload.png"
|
srcLight="https://payloadcms.com/images/docs/fields/upload.png"
|
||||||
srcDark="https://payloadcms.com/images/docs/fields/upload-dark.png"
|
srcDark="https://payloadcms.com/images/docs/fields/upload-dark.png"
|
||||||
alt="Shows an upload field in the Payload admin panel"
|
alt="Shows an upload field in the Payload admin panel"
|
||||||
caption="Admin panel screenshot of an Upload field"
|
caption="Admin panel screenshot of an Upload field"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
**Example uses:**
|
**Example uses:**
|
||||||
@@ -34,25 +34,25 @@ keywords: upload, images media, fields, config, configuration, documentation, Co
|
|||||||
|
|
||||||
### Config
|
### Config
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
|
||||||
| **`*relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. <strong>Note: the related collection must be configured to support Uploads.</strong> |
|
| **`*relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. <strong>Note: the related collection must be configured to support Uploads.</strong> |
|
||||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
|
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
|
||||||
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](/docs/getting-started/concepts#depth) |
|
| **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](/docs/getting-started/concepts#depth) |
|
||||||
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
|
| **`label`** | Text used as a field label in the Admin panel or an object with keys for each language. |
|
||||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||||
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
|
||||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||||
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
| **`hooks`** | Provide field-based hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||||
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
| **`access`** | Provide field-based access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
|
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin panel. |
|
||||||
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
||||||
| **`required`** | Require this field to have a value. |
|
| **`required`** | Require this field to have a value. |
|
||||||
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
|
| **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/fields/overview#admin-config) for more details. |
|
||||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
||||||
|
|
||||||
_\* An asterisk denotes that a property is required._
|
_\* An asterisk denotes that a property is required._
|
||||||
|
|
||||||
@@ -78,12 +78,15 @@ export const ExampleCollection: CollectionConfig = {
|
|||||||
|
|
||||||
### Filtering upload options
|
### Filtering upload options
|
||||||
|
|
||||||
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both for validating input and filtering available uploads in the UI.
|
Options can be dynamically limited by supplying a [query constraint](/docs/queries/overview), which will be used both
|
||||||
|
for validating input and filtering available uploads in the UI.
|
||||||
|
|
||||||
The `filterOptions` property can either be a `Where` query directly, or a function that returns one. When using a function, it will be called with an argument object with the following properties:
|
The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to
|
||||||
|
prevent all, or a `Where` query. When using a function, it will be
|
||||||
|
called with an argument object with the following properties:
|
||||||
|
|
||||||
| Property | Description |
|
| Property | Description |
|
||||||
| ------------- | ------------------------------------------------------------------------------------ |
|
|---------------|--------------------------------------------------------------------------------------|
|
||||||
| `relationTo` | The `relationTo` to filter against (as defined on the field) |
|
| `relationTo` | The `relationTo` to filter against (as defined on the field) |
|
||||||
| `data` | An object of the full collection or global document currently being edited |
|
| `data` | An object of the full collection or global document currently being edited |
|
||||||
| `siblingData` | An object of the document data limited to fields within the same parent to the field |
|
| `siblingData` | An object of the document data limited to fields within the same parent to the field |
|
||||||
|
|||||||
@@ -144,9 +144,10 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
|||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
let copyOfWhere = { ...(where || {}) }
|
let copyOfWhere = { ...(where || {}) }
|
||||||
|
const filterOption = filterOptions?.[slug]
|
||||||
|
|
||||||
if (filterOptions) {
|
if (filterOptions && typeof filterOption !== 'boolean') {
|
||||||
copyOfWhere = hoistQueryParamsToAnd(copyOfWhere, filterOptions[slug])
|
copyOfWhere = hoistQueryParamsToAnd(copyOfWhere, filterOption)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ const Relationship: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
if (!errorLoading) {
|
if (!errorLoading) {
|
||||||
relationsToFetch.reduce(async (priorRelation, relation) => {
|
relationsToFetch.reduce(async (priorRelation, relation) => {
|
||||||
|
const relationFilterOption = filterOptionsResult?.[relation]
|
||||||
let lastLoadedPageToUse
|
let lastLoadedPageToUse
|
||||||
if (search !== searchArg) {
|
if (search !== searchArg) {
|
||||||
lastLoadedPageToUse = 1
|
lastLoadedPageToUse = 1
|
||||||
@@ -138,6 +139,11 @@ const Relationship: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
await priorRelation
|
await priorRelation
|
||||||
|
|
||||||
|
if (relationFilterOption === false) {
|
||||||
|
setLastFullyLoadedRelation(relations.indexOf(relation))
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
if (resultsFetched < 10) {
|
if (resultsFetched < 10) {
|
||||||
const collection = collections.find((coll) => coll.slug === relation)
|
const collection = collections.find((coll) => coll.slug === relation)
|
||||||
let fieldToSearch = collection?.defaultSort || collection?.admin?.useAsTitle || 'id'
|
let fieldToSearch = collection?.defaultSort || collection?.admin?.useAsTitle || 'id'
|
||||||
@@ -177,8 +183,8 @@ const Relationship: React.FC<Props> = (props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterOptionsResult?.[relation]) {
|
if (relationFilterOption && typeof relationFilterOption !== 'boolean') {
|
||||||
query.where.and.push(filterOptionsResult[relation])
|
query.where.and.push(relationFilterOption)
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
||||||
|
|||||||
@@ -61,5 +61,5 @@ export type GetResults = (args: {
|
|||||||
}) => Promise<void>
|
}) => Promise<void>
|
||||||
|
|
||||||
export type FilterOptionsResult = {
|
export type FilterOptionsResult = {
|
||||||
[relation: string]: Where
|
[relation: string]: Where | boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ export const getFilterOptionsQuery = async (
|
|||||||
typeof filterOptions === 'function'
|
typeof filterOptions === 'function'
|
||||||
? await filterOptions({ ...options, relationTo: relation })
|
? await filterOptions({ ...options, relationTo: relation })
|
||||||
: filterOptions
|
: filterOptions
|
||||||
|
if (query[relation] === true) {
|
||||||
|
query[relation] = {}
|
||||||
|
}
|
||||||
|
// this is an ugly way to prevent results from being returned
|
||||||
|
if (query[relation] === false) {
|
||||||
|
query[relation] = { id: { exists: false } }
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export type FilterOptionsProps<T = any> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FilterOptions<T = any> =
|
export type FilterOptions<T = any> =
|
||||||
| ((options: FilterOptionsProps<T>) => Promise<Where> | Where)
|
| ((options: FilterOptionsProps<T>) => Promise<Where | boolean> | Where | boolean)
|
||||||
| Where
|
| Where
|
||||||
| null
|
| null
|
||||||
|
|
||||||
|
|||||||
@@ -253,12 +253,13 @@ const validateFilterOptions: Validate = async (
|
|||||||
[collection: string]: (number | string)[]
|
[collection: string]: (number | string)[]
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
|
const falseCollections: string[] = []
|
||||||
const collections = typeof relationTo === 'string' ? [relationTo] : relationTo
|
const collections = typeof relationTo === 'string' ? [relationTo] : relationTo
|
||||||
const values = Array.isArray(value) ? value : [value]
|
const values = Array.isArray(value) ? value : [value]
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
collections.map(async (collection) => {
|
collections.map(async (collection) => {
|
||||||
const optionFilter =
|
let optionFilter =
|
||||||
typeof filterOptions === 'function'
|
typeof filterOptions === 'function'
|
||||||
? await filterOptions({
|
? await filterOptions({
|
||||||
id,
|
id,
|
||||||
@@ -269,6 +270,10 @@ const validateFilterOptions: Validate = async (
|
|||||||
})
|
})
|
||||||
: filterOptions
|
: filterOptions
|
||||||
|
|
||||||
|
if (optionFilter === true) {
|
||||||
|
optionFilter = null
|
||||||
|
}
|
||||||
|
|
||||||
const valueIDs: (number | string)[] = []
|
const valueIDs: (number | string)[] = []
|
||||||
|
|
||||||
values.forEach((val) => {
|
values.forEach((val) => {
|
||||||
@@ -288,6 +293,10 @@ const validateFilterOptions: Validate = async (
|
|||||||
|
|
||||||
if (optionFilter) findWhere.and.push(optionFilter)
|
if (optionFilter) findWhere.and.push(optionFilter)
|
||||||
|
|
||||||
|
if (optionFilter === false) {
|
||||||
|
falseCollections.push(optionFilter)
|
||||||
|
}
|
||||||
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection,
|
collection,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
@@ -321,6 +330,10 @@ const validateFilterOptions: Validate = async (
|
|||||||
requestedID = val.value
|
requestedID = val.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (falseCollections.find((slug) => relationTo === slug)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return options[collection].indexOf(requestedID) === -1
|
return options[collection].indexOf(requestedID) === -1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import type { User } from 'payload/auth'
|
||||||
import type { Config } from 'payload/config'
|
import type { Config } from 'payload/config'
|
||||||
import type { Field } from 'payload/types'
|
import type { Field, RadioField, TextField } from 'payload/types'
|
||||||
import type { RadioField, TextField } from 'payload/types'
|
|
||||||
|
|
||||||
import { extractTranslations } from 'payload/utilities'
|
import { extractTranslations } from 'payload/utilities'
|
||||||
|
|
||||||
@@ -36,7 +36,12 @@ export const getBaseFields = (
|
|||||||
.map(({ slug }) => slug)
|
.map(({ slug }) => slug)
|
||||||
} else {
|
} else {
|
||||||
enabledRelations = config.collections
|
enabledRelations = config.collections
|
||||||
.filter(({ admin: { enableRichTextLink } }) => enableRichTextLink)
|
.filter(({ admin: { enableRichTextLink, hidden } }) => {
|
||||||
|
if (typeof hidden !== 'function' && hidden) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return enableRichTextLink
|
||||||
|
})
|
||||||
.map(({ slug }) => slug)
|
.map(({ slug }) => slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +112,16 @@ export const getBaseFields = (
|
|||||||
return fields?.linkType === 'internal'
|
return fields?.linkType === 'internal'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// when admin.hidden is a function we need to dynamically call hidden with the user to know if the collection should be shown
|
||||||
|
filterOptions:
|
||||||
|
!enabledCollections && !disabledCollections
|
||||||
|
? ({ relationTo, user }) => {
|
||||||
|
const hidden = config.collections.find(({ slug }) => slug === relationTo).admin.hidden
|
||||||
|
if (typeof hidden === 'function' && hidden({ user } as { user: User })) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
label: translations['fields:chooseDocumentToLink'],
|
label: translations['fields:chooseDocumentToLink'],
|
||||||
relationTo: enabledRelations,
|
relationTo: enabledRelations,
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
|
import type { User } from 'payload/auth'
|
||||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
import { useConfig } from 'payload/components/utilities'
|
import { useAuth, useConfig } from 'payload/components/utilities'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
type options = { uploads: boolean }
|
type options = {
|
||||||
|
uploads: boolean
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
|
||||||
type FilteredCollectionsT = (
|
type FilteredCollectionsT = (
|
||||||
collections: SanitizedCollectionConfig[],
|
collections: SanitizedCollectionConfig[],
|
||||||
options?: options,
|
options?: options,
|
||||||
) => SanitizedCollectionConfig[]
|
) => SanitizedCollectionConfig[]
|
||||||
const filterRichTextCollections: FilteredCollectionsT = (collections, options) => {
|
const filterRichTextCollections: FilteredCollectionsT = (collections, options) => {
|
||||||
return collections.filter(({ admin: { enableRichTextRelationship }, upload }) => {
|
return collections.filter(({ admin: { enableRichTextRelationship, hidden }, upload }) => {
|
||||||
|
if (hidden === true || (typeof hidden === 'function' && hidden({ user: options.user }))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (options?.uploads) {
|
if (options?.uploads) {
|
||||||
return enableRichTextRelationship && Boolean(upload) === true
|
return enableRichTextRelationship && Boolean(upload) === true
|
||||||
}
|
}
|
||||||
@@ -22,8 +29,9 @@ const filterRichTextCollections: FilteredCollectionsT = (collections, options) =
|
|||||||
export const EnabledRelationshipsCondition: React.FC<any> = (props) => {
|
export const EnabledRelationshipsCondition: React.FC<any> = (props) => {
|
||||||
const { children, uploads = false, ...rest } = props
|
const { children, uploads = false, ...rest } = props
|
||||||
const { collections } = useConfig()
|
const { collections } = useConfig()
|
||||||
|
const { user } = useAuth()
|
||||||
const [enabledCollectionSlugs] = React.useState(() =>
|
const [enabledCollectionSlugs] = React.useState(() =>
|
||||||
filterRichTextCollections(collections, { uploads }).map(({ slug }) => slug),
|
filterRichTextCollections(collections, { uploads, user }).map(({ slug }) => slug),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!enabledCollectionSlugs.length) {
|
if (!enabledCollectionSlugs.length) {
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import type { User } from 'payload/auth'
|
||||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
import { useConfig } from 'payload/components/utilities'
|
import { useAuth, useConfig } from 'payload/components/utilities'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
type options = { uploads: boolean }
|
type options = {
|
||||||
|
uploads: boolean
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
|
||||||
type FilteredCollectionsT = (
|
type FilteredCollectionsT = (
|
||||||
collections: SanitizedCollectionConfig[],
|
collections: SanitizedCollectionConfig[],
|
||||||
options?: options,
|
options?: options,
|
||||||
) => SanitizedCollectionConfig[]
|
) => SanitizedCollectionConfig[]
|
||||||
const filterRichTextCollections: FilteredCollectionsT = (collections, options) => {
|
const filterRichTextCollections: FilteredCollectionsT = (collections, options) => {
|
||||||
return collections.filter(({ admin: { enableRichTextRelationship }, upload }) => {
|
return collections.filter(({ admin: { enableRichTextRelationship, hidden }, upload }) => {
|
||||||
|
if (hidden === true || (typeof hidden === 'function' && hidden({ user: options.user }))) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (options?.uploads) {
|
if (options?.uploads) {
|
||||||
return enableRichTextRelationship && Boolean(upload) === true
|
return enableRichTextRelationship && Boolean(upload) === true
|
||||||
}
|
}
|
||||||
@@ -24,8 +31,9 @@ const filterRichTextCollections: FilteredCollectionsT = (collections, options) =
|
|||||||
export const EnabledRelationshipsCondition: React.FC<any> = (props) => {
|
export const EnabledRelationshipsCondition: React.FC<any> = (props) => {
|
||||||
const { children, uploads = false, ...rest } = props
|
const { children, uploads = false, ...rest } = props
|
||||||
const { collections } = useConfig()
|
const { collections } = useConfig()
|
||||||
|
const { user } = useAuth()
|
||||||
const [enabledCollectionSlugs] = React.useState(() =>
|
const [enabledCollectionSlugs] = React.useState(() =>
|
||||||
filterRichTextCollections(collections, { uploads }).map(({ slug }) => slug),
|
filterRichTextCollections(collections, { uploads, user }).map(({ slug }) => slug),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!enabledCollectionSlugs.length) {
|
if (!enabledCollectionSlugs.length) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { User } from 'payload/auth'
|
||||||
import type { Config } from 'payload/config'
|
import type { Config } from 'payload/config'
|
||||||
import type { Field } from 'payload/types'
|
import type { Field } from 'payload/types'
|
||||||
|
|
||||||
@@ -57,9 +58,21 @@ export const getBaseFields = (config: Config): Field[] => [
|
|||||||
return linkType === 'internal'
|
return linkType === 'internal'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// when admin.hidden is a function we need to dynamically call hidden with the user to know if the collection should be shown
|
||||||
|
filterOptions: ({ relationTo, user }) => {
|
||||||
|
const hidden = config.collections.find(({ slug }) => slug === relationTo).admin.hidden
|
||||||
|
if (typeof hidden === 'function' && hidden({ user } as { user: User })) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
label: translations['fields:chooseDocumentToLink'],
|
label: translations['fields:chooseDocumentToLink'],
|
||||||
relationTo: config.collections
|
relationTo: config.collections
|
||||||
.filter(({ admin: { enableRichTextLink } }) => enableRichTextLink)
|
.filter(({ admin: { enableRichTextLink, hidden } }) => {
|
||||||
|
if (typeof hidden !== 'function' && hidden) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return enableRichTextLink
|
||||||
|
})
|
||||||
.map(({ slug }) => slug),
|
.map(({ slug }) => slug),
|
||||||
required: true,
|
required: true,
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export const slug = 'fields-relationship'
|
export const slug = 'fields-relationship'
|
||||||
|
|
||||||
export const relationOneSlug = 'relation-one'
|
export const relationOneSlug = 'relation-one'
|
||||||
|
export const relationTrueFilterOptionSlug = 'relation-filter-true'
|
||||||
|
export const relationFalseFilterOptionSlug = 'relation-filter-false'
|
||||||
export const relationTwoSlug = 'relation-two'
|
export const relationTwoSlug = 'relation-two'
|
||||||
export const relationRestrictedSlug = 'relation-restricted'
|
export const relationRestrictedSlug = 'relation-restricted'
|
||||||
export const relationWithTitleSlug = 'relation-with-title'
|
export const relationWithTitleSlug = 'relation-with-title'
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import { PrePopulateFieldUI } from './PrePopulateFieldUI'
|
|||||||
import {
|
import {
|
||||||
collection1Slug,
|
collection1Slug,
|
||||||
collection2Slug,
|
collection2Slug,
|
||||||
|
relationFalseFilterOptionSlug,
|
||||||
relationOneSlug,
|
relationOneSlug,
|
||||||
relationRestrictedSlug,
|
relationRestrictedSlug,
|
||||||
|
relationTrueFilterOptionSlug,
|
||||||
relationTwoSlug,
|
relationTwoSlug,
|
||||||
relationUpdatedExternallySlug,
|
relationUpdatedExternallySlug,
|
||||||
relationWithTitleSlug,
|
relationWithTitleSlug,
|
||||||
@@ -32,6 +34,7 @@ export interface RelationOne {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RelationTwo = RelationOne
|
export type RelationTwo = RelationOne
|
||||||
export type RelationRestricted = RelationOne
|
export type RelationRestricted = RelationOne
|
||||||
export type RelationWithTitle = RelationOne
|
export type RelationWithTitle = RelationOne
|
||||||
@@ -46,7 +49,6 @@ const baseRelationshipFields: CollectionConfig['fields'] = [
|
|||||||
export default buildConfigWithDefaults({
|
export default buildConfigWithDefaults({
|
||||||
collections: [
|
collections: [
|
||||||
{
|
{
|
||||||
slug,
|
|
||||||
admin: {
|
admin: {
|
||||||
defaultColumns: [
|
defaultColumns: [
|
||||||
'id',
|
'id',
|
||||||
@@ -58,41 +60,39 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
|
||||||
name: 'relationship',
|
name: 'relationship',
|
||||||
relationTo: relationOneSlug,
|
relationTo: relationOneSlug,
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
|
||||||
name: 'relationshipHasMany',
|
name: 'relationshipHasMany',
|
||||||
relationTo: relationOneSlug,
|
|
||||||
hasMany: true,
|
hasMany: true,
|
||||||
|
relationTo: relationOneSlug,
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
|
||||||
name: 'relationshipMultiple',
|
name: 'relationshipMultiple',
|
||||||
relationTo: [relationOneSlug, relationTwoSlug],
|
relationTo: [relationOneSlug, relationTwoSlug],
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
|
||||||
name: 'relationshipHasManyMultiple',
|
name: 'relationshipHasManyMultiple',
|
||||||
hasMany: true,
|
hasMany: true,
|
||||||
relationTo: [relationOneSlug, relationTwoSlug],
|
relationTo: [relationOneSlug, relationTwoSlug],
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
|
||||||
name: 'relationshipRestricted',
|
name: 'relationshipRestricted',
|
||||||
relationTo: relationRestrictedSlug,
|
relationTo: relationRestrictedSlug,
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
|
||||||
name: 'relationshipWithTitle',
|
name: 'relationshipWithTitle',
|
||||||
relationTo: relationWithTitleSlug,
|
relationTo: relationWithTitleSlug,
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
|
||||||
name: 'relationshipFiltered',
|
name: 'relationshipFiltered',
|
||||||
relationTo: relationOneSlug,
|
|
||||||
filterOptions: (args: FilterOptionsProps<FieldsRelationship>) => {
|
filterOptions: (args: FilterOptionsProps<FieldsRelationship>) => {
|
||||||
return {
|
return {
|
||||||
id: {
|
id: {
|
||||||
@@ -100,11 +100,11 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
relationTo: relationOneSlug,
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
|
||||||
name: 'relationshipFilteredAsync',
|
name: 'relationshipFilteredAsync',
|
||||||
relationTo: relationOneSlug,
|
|
||||||
filterOptions: async (args: FilterOptionsProps<FieldsRelationship>) => {
|
filterOptions: async (args: FilterOptionsProps<FieldsRelationship>) => {
|
||||||
return {
|
return {
|
||||||
id: {
|
id: {
|
||||||
@@ -112,57 +112,84 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
relationTo: relationOneSlug,
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'relationship',
|
|
||||||
name: 'relationshipManyFiltered',
|
name: 'relationshipManyFiltered',
|
||||||
relationTo: [relationWithTitleSlug, relationOneSlug],
|
|
||||||
hasMany: true,
|
|
||||||
filterOptions: ({ relationTo, siblingData }: any) => {
|
filterOptions: ({ relationTo, siblingData }: any) => {
|
||||||
if (relationTo === relationOneSlug) {
|
if (relationTo === relationOneSlug) {
|
||||||
return { name: { equals: 'include' } }
|
return { name: { equals: 'include' } }
|
||||||
}
|
}
|
||||||
|
if (relationTo === relationTrueFilterOptionSlug) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (relationTo === relationFalseFilterOptionSlug) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (siblingData.filter) {
|
if (siblingData.filter) {
|
||||||
return { name: { contains: siblingData.filter } }
|
return { name: { contains: siblingData.filter } }
|
||||||
}
|
}
|
||||||
return { and: [] }
|
return { and: [] }
|
||||||
},
|
},
|
||||||
|
hasMany: true,
|
||||||
|
relationTo: [
|
||||||
|
relationWithTitleSlug,
|
||||||
|
relationFalseFilterOptionSlug,
|
||||||
|
relationTrueFilterOptionSlug,
|
||||||
|
relationOneSlug,
|
||||||
|
],
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
|
||||||
name: 'filter',
|
name: 'filter',
|
||||||
|
type: 'text',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'relationshipReadOnly',
|
name: 'relationshipReadOnly',
|
||||||
type: 'relationship',
|
|
||||||
relationTo: relationOneSlug,
|
|
||||||
admin: {
|
admin: {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
|
relationTo: relationOneSlug,
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
slug,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: relationOneSlug,
|
|
||||||
fields: baseRelationshipFields,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: relationTwoSlug,
|
|
||||||
fields: baseRelationshipFields,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: relationRestrictedSlug,
|
|
||||||
admin: {
|
admin: {
|
||||||
useAsTitle: 'name',
|
useAsTitle: 'name',
|
||||||
},
|
},
|
||||||
fields: baseRelationshipFields,
|
fields: baseRelationshipFields,
|
||||||
access: {
|
slug: relationFalseFilterOptionSlug,
|
||||||
read: () => false,
|
},
|
||||||
create: () => false,
|
{
|
||||||
},
|
admin: {
|
||||||
|
useAsTitle: 'name',
|
||||||
|
},
|
||||||
|
fields: baseRelationshipFields,
|
||||||
|
slug: relationTrueFilterOptionSlug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: baseRelationshipFields,
|
||||||
|
slug: relationOneSlug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: baseRelationshipFields,
|
||||||
|
slug: relationTwoSlug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
access: {
|
||||||
|
create: () => false,
|
||||||
|
read: () => false,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'name',
|
||||||
|
},
|
||||||
|
fields: baseRelationshipFields,
|
||||||
|
slug: relationRestrictedSlug,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: relationWithTitleSlug,
|
|
||||||
admin: {
|
admin: {
|
||||||
useAsTitle: 'meta.title',
|
useAsTitle: 'meta.title',
|
||||||
},
|
},
|
||||||
@@ -170,7 +197,6 @@ export default buildConfigWithDefaults({
|
|||||||
...baseRelationshipFields,
|
...baseRelationshipFields,
|
||||||
{
|
{
|
||||||
name: 'meta',
|
name: 'meta',
|
||||||
type: 'group',
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
@@ -178,110 +204,112 @@ export default buildConfigWithDefaults({
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
type: 'group',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
slug: relationWithTitleSlug,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: relationUpdatedExternallySlug,
|
|
||||||
admin: {
|
admin: {
|
||||||
useAsTitle: 'name',
|
useAsTitle: 'name',
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'row',
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'relationPrePopulate',
|
name: 'relationPrePopulate',
|
||||||
type: 'relationship',
|
|
||||||
relationTo: collection1Slug,
|
|
||||||
admin: {
|
admin: {
|
||||||
width: '75%',
|
width: '75%',
|
||||||
},
|
},
|
||||||
|
relationTo: collection1Slug,
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'ui',
|
|
||||||
name: 'prePopulate',
|
name: 'prePopulate',
|
||||||
admin: {
|
admin: {
|
||||||
width: '25%',
|
|
||||||
components: {
|
components: {
|
||||||
Field: () => PrePopulateFieldUI({ path: 'relationPrePopulate', hasMany: false }),
|
Field: () => PrePopulateFieldUI({ hasMany: false, path: 'relationPrePopulate' }),
|
||||||
},
|
},
|
||||||
|
width: '25%',
|
||||||
},
|
},
|
||||||
|
type: 'ui',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
type: 'row',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'row',
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'relationHasMany',
|
name: 'relationHasMany',
|
||||||
type: 'relationship',
|
|
||||||
relationTo: collection1Slug,
|
|
||||||
hasMany: true,
|
|
||||||
admin: {
|
admin: {
|
||||||
width: '75%',
|
width: '75%',
|
||||||
},
|
},
|
||||||
|
hasMany: true,
|
||||||
|
relationTo: collection1Slug,
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'ui',
|
|
||||||
name: 'prePopulateRelationHasMany',
|
name: 'prePopulateRelationHasMany',
|
||||||
admin: {
|
admin: {
|
||||||
width: '25%',
|
|
||||||
components: {
|
components: {
|
||||||
Field: () =>
|
Field: () =>
|
||||||
PrePopulateFieldUI({ path: 'relationHasMany', hasMultipleRelations: false }),
|
PrePopulateFieldUI({ hasMultipleRelations: false, path: 'relationHasMany' }),
|
||||||
},
|
},
|
||||||
|
width: '25%',
|
||||||
},
|
},
|
||||||
|
type: 'ui',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
type: 'row',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'row',
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'relationToManyHasMany',
|
name: 'relationToManyHasMany',
|
||||||
type: 'relationship',
|
|
||||||
relationTo: [collection1Slug, collection2Slug],
|
|
||||||
hasMany: true,
|
|
||||||
admin: {
|
admin: {
|
||||||
width: '75%',
|
width: '75%',
|
||||||
},
|
},
|
||||||
|
hasMany: true,
|
||||||
|
relationTo: [collection1Slug, collection2Slug],
|
||||||
|
type: 'relationship',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'ui',
|
|
||||||
name: 'prePopulateToMany',
|
name: 'prePopulateToMany',
|
||||||
admin: {
|
admin: {
|
||||||
width: '25%',
|
|
||||||
components: {
|
components: {
|
||||||
Field: () =>
|
Field: () =>
|
||||||
PrePopulateFieldUI({
|
PrePopulateFieldUI({
|
||||||
path: 'relationToManyHasMany',
|
|
||||||
hasMultipleRelations: true,
|
hasMultipleRelations: true,
|
||||||
|
path: 'relationToManyHasMany',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
width: '25%',
|
||||||
},
|
},
|
||||||
|
type: 'ui',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
type: 'row',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
slug: relationUpdatedExternallySlug,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
slug: collection1Slug,
|
slug: collection1Slug,
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
name: 'name',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
slug: collection2Slug,
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
|
||||||
name: 'name',
|
name: 'name',
|
||||||
|
type: 'text',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
slug: collection2Slug,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
@@ -358,11 +386,11 @@ export default buildConfigWithDefaults({
|
|||||||
collection: slug,
|
collection: slug,
|
||||||
data: {
|
data: {
|
||||||
relationship: relationOneDocId,
|
relationship: relationOneDocId,
|
||||||
relationshipRestricted: restrictedDocId,
|
|
||||||
relationshipHasManyMultiple: relationOneIDs.map((id) => ({
|
relationshipHasManyMultiple: relationOneIDs.map((id) => ({
|
||||||
relationTo: relationOneSlug,
|
relationTo: relationOneSlug,
|
||||||
value: id,
|
value: id,
|
||||||
})),
|
})),
|
||||||
|
relationshipRestricted: restrictedDocId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -374,10 +402,10 @@ export default buildConfigWithDefaults({
|
|||||||
collection: slug,
|
collection: slug,
|
||||||
data: {
|
data: {
|
||||||
relationship: relationOneDocId,
|
relationship: relationOneDocId,
|
||||||
relationshipRestricted: restrictedDocId,
|
|
||||||
relationshipHasMany: [relationOneID],
|
relationshipHasMany: [relationOneID],
|
||||||
relationshipHasManyMultiple: [{ relationTo: relationTwoSlug, value: relationTwoID }],
|
relationshipHasManyMultiple: [{ relationTo: relationTwoSlug, value: relationTwoID }],
|
||||||
relationshipReadOnly: relationOneID,
|
relationshipReadOnly: relationOneID,
|
||||||
|
relationshipRestricted: restrictedDocId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import { initPageConsoleErrorCatch, openDocControls, saveDocAndAssert } from '..
|
|||||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||||
import {
|
import {
|
||||||
|
relationFalseFilterOptionSlug,
|
||||||
relationOneSlug,
|
relationOneSlug,
|
||||||
relationRestrictedSlug,
|
relationRestrictedSlug,
|
||||||
|
relationTrueFilterOptionSlug,
|
||||||
relationTwoSlug,
|
relationTwoSlug,
|
||||||
relationUpdatedExternallySlug,
|
relationUpdatedExternallySlug,
|
||||||
relationWithTitleSlug,
|
relationWithTitleSlug,
|
||||||
@@ -112,9 +114,9 @@ describe('fields - relationship', () => {
|
|||||||
data: {
|
data: {
|
||||||
name: 'with-existing-relations',
|
name: 'with-existing-relations',
|
||||||
relationship: relationOneDoc.id,
|
relationship: relationOneDoc.id,
|
||||||
|
relationshipReadOnly: relationOneDoc.id,
|
||||||
relationshipRestricted: restrictedRelation.id,
|
relationshipRestricted: restrictedRelation.id,
|
||||||
relationshipWithTitle: relationWithTitle.id,
|
relationshipWithTitle: relationWithTitle.id,
|
||||||
relationshipReadOnly: relationOneDoc.id,
|
|
||||||
},
|
},
|
||||||
})) as any
|
})) as any
|
||||||
})
|
})
|
||||||
@@ -322,6 +324,41 @@ describe('fields - relationship', () => {
|
|||||||
await expect(options).not.toContainText('exclude')
|
await expect(options).not.toContainText('exclude')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should not query for a relationship when filterOptions returns false', async () => {
|
||||||
|
await payload.create({
|
||||||
|
collection: relationFalseFilterOptionSlug,
|
||||||
|
data: {
|
||||||
|
name: 'whatever',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url.create)
|
||||||
|
|
||||||
|
// select relationshipMany field that relies on siblingData field above
|
||||||
|
await page.locator('#field-relationshipManyFiltered .rs__control').click()
|
||||||
|
|
||||||
|
const options = page.locator('#field-relationshipManyFiltered .rs__menu')
|
||||||
|
await expect(options).toContainText('Relation With Titles')
|
||||||
|
await expect(options).not.toContainText('whatever')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should show a relationship when filterOptions returns true', async () => {
|
||||||
|
await payload.create({
|
||||||
|
collection: relationTrueFilterOptionSlug,
|
||||||
|
data: {
|
||||||
|
name: 'truth',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(url.create)
|
||||||
|
|
||||||
|
// select relationshipMany field that relies on siblingData field above
|
||||||
|
await page.locator('#field-relationshipManyFiltered .rs__control').click()
|
||||||
|
|
||||||
|
const options = page.locator('#field-relationshipManyFiltered .rs__menu')
|
||||||
|
await expect(options).toContainText('truth')
|
||||||
|
})
|
||||||
|
|
||||||
test('should open document drawer from read-only relationships', async () => {
|
test('should open document drawer from read-only relationships', async () => {
|
||||||
await page.goto(url.edit(docWithExistingRelations.id))
|
await page.goto(url.edit(docWithExistingRelations.id))
|
||||||
|
|
||||||
@@ -492,6 +529,6 @@ async function clearCollectionDocs(collectionSlug: string): Promise<void> {
|
|||||||
(doc) => doc.id,
|
(doc) => doc.id,
|
||||||
)
|
)
|
||||||
await mapAsync(ids, async (id) => {
|
await mapAsync(ids, async (id) => {
|
||||||
await payload.delete({ collection: collectionSlug, id })
|
await payload.delete({ id, collection: collectionSlug })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user