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>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/relationship.png"
|
||||
srcDark="https://payloadcms.com/images/docs/fields/relationship-dark.png"
|
||||
alt="Shows a relationship field in the Payload admin panel"
|
||||
caption="Admin panel screenshot of a Relationship field"
|
||||
srcLight="https://payloadcms.com/images/docs/fields/relationship.png"
|
||||
srcDark="https://payloadcms.com/images/docs/fields/relationship-dark.png"
|
||||
alt="Shows a relationship field in the Payload admin panel"
|
||||
caption="Admin panel screenshot of a Relationship field"
|
||||
/>
|
||||
|
||||
**Example uses:**
|
||||
@@ -27,7 +27,7 @@ keywords: relationship, fields, config, configuration, documentation, Content Ma
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`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. |
|
||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-relationship-options). |
|
||||
@@ -60,47 +60,62 @@ _\* An asterisk denotes that a property is required._
|
||||
|
||||
### 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`**
|
||||
|
||||
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`**
|
||||
|
||||
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`**
|
||||
|
||||
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:
|
||||
|
||||
**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:
|
||||
|
||||
```ts
|
||||
sortOptions: 'fieldName',
|
||||
```
|
||||
|
||||
This configuration will sort all relationship field dropdowns by `"fieldName"` in ascending order.
|
||||
|
||||
**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:
|
||||
|
||||
```ts
|
||||
sortOptions: {
|
||||
"pages": "fieldName1",
|
||||
"posts": "-fieldName2",
|
||||
"categories": "fieldName3"
|
||||
"pages"
|
||||
:
|
||||
"fieldName1",
|
||||
"posts"
|
||||
:
|
||||
"-fieldName2",
|
||||
"categories"
|
||||
:
|
||||
"fieldName3"
|
||||
}
|
||||
```
|
||||
|
||||
In this configuration:
|
||||
|
||||
- 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 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
|
||||
|
||||
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 |
|
||||
| ------------- | ------------------------------------------------------------------------------------ |
|
||||
|---------------|--------------------------------------------------------------------------------------|
|
||||
| `relationTo` | The `relationTo` to filter against (as defined on the field) |
|
||||
| `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 |
|
||||
@@ -165,16 +183,21 @@ You can learn more about writing queries [here](/docs/queries/overview).
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
{
|
||||
slug: 'example-collection',
|
||||
fields: [
|
||||
fields
|
||||
:
|
||||
[
|
||||
{
|
||||
name: 'owner', // 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
|
||||
|
||||
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
|
||||
{
|
||||
slug: 'example-collection',
|
||||
fields: [
|
||||
fields
|
||||
:
|
||||
[
|
||||
{
|
||||
name: 'owner', // required
|
||||
type: 'relationship', // required
|
||||
@@ -244,7 +270,9 @@ The `hasMany` tells Payload that there may be more than one collection saved to
|
||||
```ts
|
||||
{
|
||||
slug: 'example-collection',
|
||||
fields: [
|
||||
fields
|
||||
:
|
||||
[
|
||||
{
|
||||
name: 'owners', // required
|
||||
type: 'relationship', // required
|
||||
@@ -259,7 +287,10 @@ To save the to `hasMany` relationship field we need to send an array of IDs:
|
||||
|
||||
```json
|
||||
{
|
||||
"owners": ["6031ac9e1289176380734024", "602c3c327b811235943ee12b"]
|
||||
"owners": [
|
||||
"6031ac9e1289176380734024",
|
||||
"602c3c327b811235943ee12b"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -272,7 +303,9 @@ When querying documents, the format does not change for arrays:
|
||||
```ts
|
||||
{
|
||||
slug: 'example-collection',
|
||||
fields: [
|
||||
fields
|
||||
:
|
||||
[
|
||||
{
|
||||
name: 'owners', // 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
|
||||
{
|
||||
@@ -305,12 +339,14 @@ Querying is done in the same way as the earlier Polymorphic example:
|
||||
|
||||
`?where[owners.value][equals]=6031ac9e1289176380734024`.
|
||||
|
||||
|
||||
#### 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:
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ keywords: upload, images media, fields, config, configuration, documentation, Co
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/upload.png"
|
||||
srcDark="https://payloadcms.com/images/docs/fields/upload-dark.png"
|
||||
alt="Shows an upload field in the Payload admin panel"
|
||||
caption="Admin panel screenshot of an Upload field"
|
||||
srcLight="https://payloadcms.com/images/docs/fields/upload.png"
|
||||
srcDark="https://payloadcms.com/images/docs/fields/upload-dark.png"
|
||||
alt="Shows an upload field in the Payload admin panel"
|
||||
caption="Admin panel screenshot of an Upload field"
|
||||
/>
|
||||
|
||||
**Example uses:**
|
||||
@@ -35,7 +35,7 @@ keywords: upload, images media, fields, config, configuration, documentation, Co
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`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> |
|
||||
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More](#filtering-upload-options). |
|
||||
@@ -78,12 +78,15 @@ export const ExampleCollection: CollectionConfig = {
|
||||
|
||||
### 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 |
|
||||
| ------------- | ------------------------------------------------------------------------------------ |
|
||||
|---------------|--------------------------------------------------------------------------------------|
|
||||
| `relationTo` | The `relationTo` to filter against (as defined on the field) |
|
||||
| `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 |
|
||||
|
||||
@@ -144,9 +144,10 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
} = {}
|
||||
|
||||
let copyOfWhere = { ...(where || {}) }
|
||||
const filterOption = filterOptions?.[slug]
|
||||
|
||||
if (filterOptions) {
|
||||
copyOfWhere = hoistQueryParamsToAnd(copyOfWhere, filterOptions[slug])
|
||||
if (filterOptions && typeof filterOption !== 'boolean') {
|
||||
copyOfWhere = hoistQueryParamsToAnd(copyOfWhere, filterOption)
|
||||
}
|
||||
|
||||
if (search) {
|
||||
|
||||
@@ -130,6 +130,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
|
||||
if (!errorLoading) {
|
||||
relationsToFetch.reduce(async (priorRelation, relation) => {
|
||||
const relationFilterOption = filterOptionsResult?.[relation]
|
||||
let lastLoadedPageToUse
|
||||
if (search !== searchArg) {
|
||||
lastLoadedPageToUse = 1
|
||||
@@ -138,6 +139,11 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}
|
||||
await priorRelation
|
||||
|
||||
if (relationFilterOption === false) {
|
||||
setLastFullyLoadedRelation(relations.indexOf(relation))
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
if (resultsFetched < 10) {
|
||||
const collection = collections.find((coll) => coll.slug === relation)
|
||||
let fieldToSearch = collection?.defaultSort || collection?.admin?.useAsTitle || 'id'
|
||||
@@ -177,8 +183,8 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (filterOptionsResult?.[relation]) {
|
||||
query.where.and.push(filterOptionsResult[relation])
|
||||
if (relationFilterOption && typeof relationFilterOption !== 'boolean') {
|
||||
query.where.and.push(relationFilterOption)
|
||||
}
|
||||
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
||||
|
||||
@@ -61,5 +61,5 @@ export type GetResults = (args: {
|
||||
}) => Promise<void>
|
||||
|
||||
export type FilterOptionsResult = {
|
||||
[relation: string]: Where
|
||||
[relation: string]: Where | boolean
|
||||
}
|
||||
|
||||
@@ -15,6 +15,13 @@ export const getFilterOptionsQuery = async (
|
||||
typeof filterOptions === 'function'
|
||||
? await filterOptions({ ...options, relationTo: relation })
|
||||
: 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> =
|
||||
| ((options: FilterOptionsProps<T>) => Promise<Where> | Where)
|
||||
| ((options: FilterOptionsProps<T>) => Promise<Where | boolean> | Where | boolean)
|
||||
| Where
|
||||
| null
|
||||
|
||||
|
||||
@@ -253,12 +253,13 @@ const validateFilterOptions: Validate = async (
|
||||
[collection: string]: (number | string)[]
|
||||
} = {}
|
||||
|
||||
const falseCollections: string[] = []
|
||||
const collections = typeof relationTo === 'string' ? [relationTo] : relationTo
|
||||
const values = Array.isArray(value) ? value : [value]
|
||||
|
||||
await Promise.all(
|
||||
collections.map(async (collection) => {
|
||||
const optionFilter =
|
||||
let optionFilter =
|
||||
typeof filterOptions === 'function'
|
||||
? await filterOptions({
|
||||
id,
|
||||
@@ -269,6 +270,10 @@ const validateFilterOptions: Validate = async (
|
||||
})
|
||||
: filterOptions
|
||||
|
||||
if (optionFilter === true) {
|
||||
optionFilter = null
|
||||
}
|
||||
|
||||
const valueIDs: (number | string)[] = []
|
||||
|
||||
values.forEach((val) => {
|
||||
@@ -288,6 +293,10 @@ const validateFilterOptions: Validate = async (
|
||||
|
||||
if (optionFilter) findWhere.and.push(optionFilter)
|
||||
|
||||
if (optionFilter === false) {
|
||||
falseCollections.push(optionFilter)
|
||||
}
|
||||
|
||||
const result = await payload.find({
|
||||
collection,
|
||||
depth: 0,
|
||||
@@ -321,6 +330,10 @@ const validateFilterOptions: Validate = async (
|
||||
requestedID = val.value
|
||||
}
|
||||
|
||||
if (falseCollections.find((slug) => relationTo === slug)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return options[collection].indexOf(requestedID) === -1
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { User } from 'payload/auth'
|
||||
import type { Config } from 'payload/config'
|
||||
import type { Field } from 'payload/types'
|
||||
import type { RadioField, TextField } from 'payload/types'
|
||||
import type { Field, RadioField, TextField } from 'payload/types'
|
||||
|
||||
import { extractTranslations } from 'payload/utilities'
|
||||
|
||||
@@ -36,7 +36,12 @@ export const getBaseFields = (
|
||||
.map(({ slug }) => slug)
|
||||
} else {
|
||||
enabledRelations = config.collections
|
||||
.filter(({ admin: { enableRichTextLink } }) => enableRichTextLink)
|
||||
.filter(({ admin: { enableRichTextLink, hidden } }) => {
|
||||
if (typeof hidden !== 'function' && hidden) {
|
||||
return false
|
||||
}
|
||||
return enableRichTextLink
|
||||
})
|
||||
.map(({ slug }) => slug)
|
||||
}
|
||||
|
||||
@@ -107,6 +112,16 @@ export const getBaseFields = (
|
||||
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'],
|
||||
relationTo: enabledRelations,
|
||||
required: true,
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import type { User } from 'payload/auth'
|
||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||
|
||||
import { useConfig } from 'payload/components/utilities'
|
||||
import { useAuth, useConfig } from 'payload/components/utilities'
|
||||
import * as React from 'react'
|
||||
|
||||
type options = { uploads: boolean }
|
||||
type options = {
|
||||
uploads: boolean
|
||||
user: User
|
||||
}
|
||||
|
||||
type FilteredCollectionsT = (
|
||||
collections: SanitizedCollectionConfig[],
|
||||
options?: options,
|
||||
) => SanitizedCollectionConfig[]
|
||||
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) {
|
||||
return enableRichTextRelationship && Boolean(upload) === true
|
||||
}
|
||||
@@ -22,8 +29,9 @@ const filterRichTextCollections: FilteredCollectionsT = (collections, options) =
|
||||
export const EnabledRelationshipsCondition: React.FC<any> = (props) => {
|
||||
const { children, uploads = false, ...rest } = props
|
||||
const { collections } = useConfig()
|
||||
const { user } = useAuth()
|
||||
const [enabledCollectionSlugs] = React.useState(() =>
|
||||
filterRichTextCollections(collections, { uploads }).map(({ slug }) => slug),
|
||||
filterRichTextCollections(collections, { uploads, user }).map(({ slug }) => slug),
|
||||
)
|
||||
|
||||
if (!enabledCollectionSlugs.length) {
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import type { User } from 'payload/auth'
|
||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||
|
||||
import { useConfig } from 'payload/components/utilities'
|
||||
import { useAuth, useConfig } from 'payload/components/utilities'
|
||||
import * as React from 'react'
|
||||
|
||||
type options = { uploads: boolean }
|
||||
type options = {
|
||||
uploads: boolean
|
||||
user: User
|
||||
}
|
||||
|
||||
type FilteredCollectionsT = (
|
||||
collections: SanitizedCollectionConfig[],
|
||||
options?: options,
|
||||
) => SanitizedCollectionConfig[]
|
||||
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) {
|
||||
return enableRichTextRelationship && Boolean(upload) === true
|
||||
}
|
||||
@@ -24,8 +31,9 @@ const filterRichTextCollections: FilteredCollectionsT = (collections, options) =
|
||||
export const EnabledRelationshipsCondition: React.FC<any> = (props) => {
|
||||
const { children, uploads = false, ...rest } = props
|
||||
const { collections } = useConfig()
|
||||
const { user } = useAuth()
|
||||
const [enabledCollectionSlugs] = React.useState(() =>
|
||||
filterRichTextCollections(collections, { uploads }).map(({ slug }) => slug),
|
||||
filterRichTextCollections(collections, { uploads, user }).map(({ slug }) => slug),
|
||||
)
|
||||
|
||||
if (!enabledCollectionSlugs.length) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { User } from 'payload/auth'
|
||||
import type { Config } from 'payload/config'
|
||||
import type { Field } from 'payload/types'
|
||||
|
||||
@@ -57,9 +58,21 @@ export const getBaseFields = (config: Config): Field[] => [
|
||||
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'],
|
||||
relationTo: config.collections
|
||||
.filter(({ admin: { enableRichTextLink } }) => enableRichTextLink)
|
||||
.filter(({ admin: { enableRichTextLink, hidden } }) => {
|
||||
if (typeof hidden !== 'function' && hidden) {
|
||||
return false
|
||||
}
|
||||
return enableRichTextLink
|
||||
})
|
||||
.map(({ slug }) => slug),
|
||||
required: true,
|
||||
type: 'relationship',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export const slug = 'fields-relationship'
|
||||
|
||||
export const relationOneSlug = 'relation-one'
|
||||
export const relationTrueFilterOptionSlug = 'relation-filter-true'
|
||||
export const relationFalseFilterOptionSlug = 'relation-filter-false'
|
||||
export const relationTwoSlug = 'relation-two'
|
||||
export const relationRestrictedSlug = 'relation-restricted'
|
||||
export const relationWithTitleSlug = 'relation-with-title'
|
||||
|
||||
@@ -8,8 +8,10 @@ import { PrePopulateFieldUI } from './PrePopulateFieldUI'
|
||||
import {
|
||||
collection1Slug,
|
||||
collection2Slug,
|
||||
relationFalseFilterOptionSlug,
|
||||
relationOneSlug,
|
||||
relationRestrictedSlug,
|
||||
relationTrueFilterOptionSlug,
|
||||
relationTwoSlug,
|
||||
relationUpdatedExternallySlug,
|
||||
relationWithTitleSlug,
|
||||
@@ -32,6 +34,7 @@ export interface RelationOne {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type RelationTwo = RelationOne
|
||||
export type RelationRestricted = RelationOne
|
||||
export type RelationWithTitle = RelationOne
|
||||
@@ -46,7 +49,6 @@ const baseRelationshipFields: CollectionConfig['fields'] = [
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [
|
||||
{
|
||||
slug,
|
||||
admin: {
|
||||
defaultColumns: [
|
||||
'id',
|
||||
@@ -58,41 +60,39 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationship',
|
||||
relationTo: relationOneSlug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipHasMany',
|
||||
relationTo: relationOneSlug,
|
||||
hasMany: true,
|
||||
relationTo: relationOneSlug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipMultiple',
|
||||
relationTo: [relationOneSlug, relationTwoSlug],
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipHasManyMultiple',
|
||||
hasMany: true,
|
||||
relationTo: [relationOneSlug, relationTwoSlug],
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipRestricted',
|
||||
relationTo: relationRestrictedSlug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipWithTitle',
|
||||
relationTo: relationWithTitleSlug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipFiltered',
|
||||
relationTo: relationOneSlug,
|
||||
filterOptions: (args: FilterOptionsProps<FieldsRelationship>) => {
|
||||
return {
|
||||
id: {
|
||||
@@ -100,11 +100,11 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
}
|
||||
},
|
||||
relationTo: relationOneSlug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipFilteredAsync',
|
||||
relationTo: relationOneSlug,
|
||||
filterOptions: async (args: FilterOptionsProps<FieldsRelationship>) => {
|
||||
return {
|
||||
id: {
|
||||
@@ -112,57 +112,84 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
}
|
||||
},
|
||||
relationTo: relationOneSlug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'relationshipManyFiltered',
|
||||
relationTo: [relationWithTitleSlug, relationOneSlug],
|
||||
hasMany: true,
|
||||
filterOptions: ({ relationTo, siblingData }: any) => {
|
||||
if (relationTo === relationOneSlug) {
|
||||
return { name: { equals: 'include' } }
|
||||
}
|
||||
if (relationTo === relationTrueFilterOptionSlug) {
|
||||
return true
|
||||
}
|
||||
if (relationTo === relationFalseFilterOptionSlug) {
|
||||
return false
|
||||
}
|
||||
if (siblingData.filter) {
|
||||
return { name: { contains: siblingData.filter } }
|
||||
}
|
||||
return { and: [] }
|
||||
},
|
||||
hasMany: true,
|
||||
relationTo: [
|
||||
relationWithTitleSlug,
|
||||
relationFalseFilterOptionSlug,
|
||||
relationTrueFilterOptionSlug,
|
||||
relationOneSlug,
|
||||
],
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'filter',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'relationshipReadOnly',
|
||||
type: 'relationship',
|
||||
relationTo: relationOneSlug,
|
||||
admin: {
|
||||
readOnly: true,
|
||||
},
|
||||
relationTo: relationOneSlug,
|
||||
type: 'relationship',
|
||||
},
|
||||
],
|
||||
slug,
|
||||
},
|
||||
{
|
||||
slug: relationOneSlug,
|
||||
fields: baseRelationshipFields,
|
||||
},
|
||||
{
|
||||
slug: relationTwoSlug,
|
||||
fields: baseRelationshipFields,
|
||||
},
|
||||
{
|
||||
slug: relationRestrictedSlug,
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: baseRelationshipFields,
|
||||
access: {
|
||||
read: () => false,
|
||||
create: () => false,
|
||||
},
|
||||
slug: relationFalseFilterOptionSlug,
|
||||
},
|
||||
{
|
||||
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: {
|
||||
useAsTitle: 'meta.title',
|
||||
},
|
||||
@@ -170,7 +197,6 @@ export default buildConfigWithDefaults({
|
||||
...baseRelationshipFields,
|
||||
{
|
||||
name: 'meta',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -178,110 +204,112 @@ export default buildConfigWithDefaults({
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
type: 'group',
|
||||
},
|
||||
],
|
||||
slug: relationWithTitleSlug,
|
||||
},
|
||||
{
|
||||
slug: relationUpdatedExternallySlug,
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'relationPrePopulate',
|
||||
type: 'relationship',
|
||||
relationTo: collection1Slug,
|
||||
admin: {
|
||||
width: '75%',
|
||||
},
|
||||
relationTo: collection1Slug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'prePopulate',
|
||||
admin: {
|
||||
width: '25%',
|
||||
components: {
|
||||
Field: () => PrePopulateFieldUI({ path: 'relationPrePopulate', hasMany: false }),
|
||||
Field: () => PrePopulateFieldUI({ hasMany: false, path: 'relationPrePopulate' }),
|
||||
},
|
||||
width: '25%',
|
||||
},
|
||||
type: 'ui',
|
||||
},
|
||||
],
|
||||
type: 'row',
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'relationHasMany',
|
||||
type: 'relationship',
|
||||
relationTo: collection1Slug,
|
||||
hasMany: true,
|
||||
admin: {
|
||||
width: '75%',
|
||||
},
|
||||
hasMany: true,
|
||||
relationTo: collection1Slug,
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'prePopulateRelationHasMany',
|
||||
admin: {
|
||||
width: '25%',
|
||||
components: {
|
||||
Field: () =>
|
||||
PrePopulateFieldUI({ path: 'relationHasMany', hasMultipleRelations: false }),
|
||||
PrePopulateFieldUI({ hasMultipleRelations: false, path: 'relationHasMany' }),
|
||||
},
|
||||
width: '25%',
|
||||
},
|
||||
type: 'ui',
|
||||
},
|
||||
],
|
||||
type: 'row',
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'relationToManyHasMany',
|
||||
type: 'relationship',
|
||||
relationTo: [collection1Slug, collection2Slug],
|
||||
hasMany: true,
|
||||
admin: {
|
||||
width: '75%',
|
||||
},
|
||||
hasMany: true,
|
||||
relationTo: [collection1Slug, collection2Slug],
|
||||
type: 'relationship',
|
||||
},
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'prePopulateToMany',
|
||||
admin: {
|
||||
width: '25%',
|
||||
components: {
|
||||
Field: () =>
|
||||
PrePopulateFieldUI({
|
||||
path: 'relationToManyHasMany',
|
||||
hasMultipleRelations: true,
|
||||
path: 'relationToManyHasMany',
|
||||
}),
|
||||
},
|
||||
width: '25%',
|
||||
},
|
||||
type: 'ui',
|
||||
},
|
||||
],
|
||||
type: 'row',
|
||||
},
|
||||
],
|
||||
slug: relationUpdatedExternallySlug,
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
slug: collection1Slug,
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: collection2Slug,
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
@@ -358,11 +386,11 @@ export default buildConfigWithDefaults({
|
||||
collection: slug,
|
||||
data: {
|
||||
relationship: relationOneDocId,
|
||||
relationshipRestricted: restrictedDocId,
|
||||
relationshipHasManyMultiple: relationOneIDs.map((id) => ({
|
||||
relationTo: relationOneSlug,
|
||||
value: id,
|
||||
})),
|
||||
relationshipRestricted: restrictedDocId,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -374,10 +402,10 @@ export default buildConfigWithDefaults({
|
||||
collection: slug,
|
||||
data: {
|
||||
relationship: relationOneDocId,
|
||||
relationshipRestricted: restrictedDocId,
|
||||
relationshipHasMany: [relationOneID],
|
||||
relationshipHasManyMultiple: [{ relationTo: relationTwoSlug, value: relationTwoID }],
|
||||
relationshipReadOnly: relationOneID,
|
||||
relationshipRestricted: restrictedDocId,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,8 +17,10 @@ import { initPageConsoleErrorCatch, openDocControls, saveDocAndAssert } from '..
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
|
||||
import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import {
|
||||
relationFalseFilterOptionSlug,
|
||||
relationOneSlug,
|
||||
relationRestrictedSlug,
|
||||
relationTrueFilterOptionSlug,
|
||||
relationTwoSlug,
|
||||
relationUpdatedExternallySlug,
|
||||
relationWithTitleSlug,
|
||||
@@ -112,9 +114,9 @@ describe('fields - relationship', () => {
|
||||
data: {
|
||||
name: 'with-existing-relations',
|
||||
relationship: relationOneDoc.id,
|
||||
relationshipReadOnly: relationOneDoc.id,
|
||||
relationshipRestricted: restrictedRelation.id,
|
||||
relationshipWithTitle: relationWithTitle.id,
|
||||
relationshipReadOnly: relationOneDoc.id,
|
||||
},
|
||||
})) as any
|
||||
})
|
||||
@@ -322,6 +324,41 @@ describe('fields - relationship', () => {
|
||||
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 () => {
|
||||
await page.goto(url.edit(docWithExistingRelations.id))
|
||||
|
||||
@@ -492,6 +529,6 @@ async function clearCollectionDocs(collectionSlug: string): Promise<void> {
|
||||
(doc) => doc.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