Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add option for using the ListDrawer for selecting related item in relationship field #11553

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
15 changes: 9 additions & 6 deletions docs/fields/relationship.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,15 @@ export const MyRelationshipField: Field = {

The Relationship Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:

| Property | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **`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`). |
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sort-options) |

| Property | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **`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`). |
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sort-options) |
| **`selectionType`**| Set to `drawer` to use the drawer instead of the dropdown. |


### Sort Options

Expand Down
3 changes: 2 additions & 1 deletion packages/payload/src/fields/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1149,10 +1149,11 @@ type RelationshipAdmin = {
>
} & Admin['components']
isSortable?: boolean
selectionType?: 'drawer' | 'dropdown'
} & Admin

type RelationshipAdminClient = AdminClient &
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'isSortable'>
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'isSortable' | 'selectionType'>

export type PolymorphicRelationshipField = {
admin?: {
Expand Down
110 changes: 87 additions & 23 deletions packages/ui/src/fields/Relationship/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import * as qs from 'qs-esm'
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'

import type { DocumentDrawerProps } from '../../elements/DocumentDrawer/types.js'
import type { ListDrawerProps } from '../../elements/ListDrawer/types.js'
import type { ReactSelectAdapterProps } from '../../elements/ReactSelect/types.js'
import type { GetResults, Option, Value } from './types.js'

import { AddNewRelation } from '../../elements/AddNewRelation/index.js'
import { useDocumentDrawer } from '../../elements/DocumentDrawer/index.js'
import { useListDrawer } from '../../elements/ListDrawer/index.js'
import { ReactSelect } from '../../elements/ReactSelect/index.js'
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
import { FieldDescription } from '../../fields/FieldDescription/index.js'
Expand Down Expand Up @@ -48,6 +50,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
className,
description,
isSortable = true,
selectionType = 'dropdown',
sortOptions,
} = {},
hasMany,
Expand Down Expand Up @@ -120,6 +123,49 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
collectionSlug: currentlyOpenRelationship.collectionSlug,
})

const [
ListDrawer,
,
{ closeDrawer: closeListDrawer, isDrawerOpen: isListDrawerOpen, openDrawer: openListDrawer },
] = useListDrawer({
collectionSlugs: hasMultipleRelations ? relationTo : [relationTo],
filterOptions,
})

const onListSelect = useCallback<NonNullable<ListDrawerProps['onSelect']>>(
({ collectionSlug, doc }) => {
const formattedSelection = hasMultipleRelations
? {
relationTo: collectionSlug,
value: doc.id,
}
: doc.id

if (hasMany) {
const existingValues = Array.isArray(value) ? value : []

// Filter out the existing value if it already exists. Maybe better to filter our the existing values from the list drawer beforehand?
const filteredValues = existingValues.filter((existing) => {
if (hasMultipleRelations) {
return !(
typeof existing === 'object' &&
existing.relationTo === collectionSlug &&
existing.value === doc.id
)
}
return existing !== doc.id
})

setValue([...filteredValues, formattedSelection])
} else {
setValue(formattedSelection)
}

closeListDrawer()
},
[hasMany, hasMultipleRelations, setValue, closeListDrawer, value],
)

const openDrawerWhenRelationChanges = useRef(false)

const getResults: GetResults = useCallback(
Expand Down Expand Up @@ -600,18 +646,26 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
{!errorLoading && (
<div className={`${baseClass}__wrap`}>
<ReactSelect
backspaceRemovesValue={!isDrawerOpen}
components={{
MultiValueLabel,
SingleValue,
}}
backspaceRemovesValue={!(isDrawerOpen || isListDrawerOpen)}
components={
selectionType === 'drawer'
? {
DropdownIndicator: null,
MultiValueLabel,
SingleValue,
}
: {
MultiValueLabel,
SingleValue,
}
}
customProps={{
disableKeyDown: isDrawerOpen,
disableMouseDown: isDrawerOpen,
disableKeyDown: isDrawerOpen || isListDrawerOpen,
disableMouseDown: isDrawerOpen || isListDrawerOpen,
onDocumentDrawerOpen,
onSave,
}}
disabled={readOnly || isDrawerOpen}
disabled={readOnly || isDrawerOpen || isListDrawerOpen}
filterOption={enableWordBoundarySearch ? filterOption : undefined}
getOptionValue={(option) => {
if (!option) {
Expand All @@ -621,9 +675,11 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
? `${option.relationTo}_${option.value}`
: (option.value as string)
}}
isLoading={isLoading}
isLoading={selectionType === 'dropdown' && isLoading}
isMulti={hasMany}
isSearchable={selectionType === 'dropdown' && undefined}
isSortable={isSortable}
menuIsOpen={selectionType === 'dropdown' && menuIsOpen}
onChange={
!readOnly
? (selected) => {
Expand Down Expand Up @@ -660,19 +716,22 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
setMenuIsOpen(false)
}}
onMenuOpen={() => {
setMenuIsOpen(true)

if (!hasLoadedFirstPageRef.current) {
setIsLoading(true)
void getResults({
filterOptions,
lastLoadedPage: {},
onSuccess: () => {
hasLoadedFirstPageRef.current = true
setIsLoading(false)
},
value: initialValue,
})
if (selectionType === 'drawer') {
openListDrawer()
} else {
setMenuIsOpen(true)
if (!hasLoadedFirstPageRef.current) {
setIsLoading(true)
void getResults({
filterOptions,
lastLoadedPage: {},
onSuccess: () => {
hasLoadedFirstPageRef.current = true
setIsLoading(false)
},
value: initialValue,
})
}
}
}}
onMenuScrollToBottom={() => {
Expand All @@ -689,7 +748,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
showError={showError}
value={valueToRender ?? null}
/>
{!readOnly && allowCreate && (
{!readOnly && allowCreate && selectionType === 'dropdown' && (
<AddNewRelation
hasMany={hasMany}
path={path}
Expand All @@ -710,6 +769,11 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
{currentlyOpenRelationship.collectionSlug && currentlyOpenRelationship.hasReadPermission && (
<DocumentDrawer onDelete={onDelete} onDuplicate={onDuplicate} onSave={onSave} />
)}
<ListDrawer
allowCreate={!readOnly && allowCreate}
enableRowSelections={false}
onSelect={onListSelect}
/>
</div>
)
}
Expand Down
48 changes: 48 additions & 0 deletions test/relationships/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,54 @@ export default buildConfigWithDefaults({
},
],
},
{
slug: 'relationship-using-list-drawer',
fields: [
{
type: 'relationship',
name: 'relationship',
admin: { selectionType: 'drawer' },
relationTo: ['movies'],
},
{
type: 'relationship',
name: 'hasManyRelationship',
admin: { selectionType: 'drawer' },
hasMany: true,
relationTo: ['movies'],
},
{
name: 'polymorphicRelationship',
admin: { selectionType: 'drawer' },
type: 'relationship',
relationTo: ['movies', 'directors'],
},
{
name: 'polymorphicHasManyRelationship',
admin: { selectionType: 'drawer' },
type: 'relationship',
hasMany: true,
relationTo: ['movies', 'directors'],
},
{
name: 'polymorphicHasManyFiltered',
admin: { selectionType: 'drawer' },
type: 'relationship',
hasMany: true,
relationTo: ['movies', 'directors', 'relation'],
filterOptions: ({ relationTo }) => {
// returns a Where query dynamically by the type of relationship
if (relationTo === 'relation') {
return {
disableRelation: { not_equals: true },
}
} else {
return true
}
},
},
],
},
],
onInit: async (payload) => {
await payload.create({
Expand Down