Skip to content

Commit 0cc06a1

Browse files
authored
fix(ui): relationship field label not updated when document is updated from other drawer (#14609)
When updating a document from a document drawer that isn’t the one associated with a relationship field, the relationship’s label doesn’t update correctly. ## Reproduction This issue is easily reproducible when two relationship fields link to the same document. If you update the document from the drawer of one field, the other field will not reflect the update. This happens because each field only listens to the `onSave` event of its own document drawer. ## Fix Use the `useDocumentEvents` hook instead of the `onSave` hook. `useDocumentEvents` listens for updates to all saved documents, ensuring that all relationship fields stay in sync regardless of which drawer the update originated from. **Before:** https://github.com/user-attachments/assets/eda152fe-3cda-4111-b0f3-9f0d5a48f37b **After:** https://github.com/user-attachments/assets/da252d3d-c0a9-43ee-91d8-508e302d323e
1 parent 3198bbe commit 0cc06a1

File tree

11 files changed

+178
-57
lines changed

11 files changed

+178
-57
lines changed

packages/payload/src/admin/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
22
import type React from 'react'
33

44
import type { ImportMap } from '../bin/generateImportMap/index.js'
5+
import type { TypeWithID } from '../collections/config/types.js'
56
import type { SanitizedConfig } from '../config/types.js'
67
import type {
78
Block,
@@ -717,7 +718,10 @@ export type ClientFieldSchemaMap = Map<
717718
>
718719

719720
export type DocumentEvent = {
721+
doc?: TypeWithID
722+
drawerSlug?: string
720723
entitySlug: string
721724
id?: number | string
725+
operation: 'create' | 'update'
722726
updatedAt: string
723727
}

packages/ui/src/elements/BulkUpload/EditForm/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function EditForm({
4343
Upload: CustomUpload,
4444
} = useDocumentInfo()
4545

46-
const { onSave: onSaveFromContext } = useDocumentDrawerContext()
46+
const { drawerSlug, onSave: onSaveFromContext } = useDocumentDrawerContext()
4747

4848
const { getFormState } = useServerFunctions()
4949

@@ -64,7 +64,10 @@ export function EditForm({
6464
const onSave = useCallback(
6565
(json) => {
6666
reportUpdate({
67+
doc: json?.doc || json?.result,
68+
drawerSlug,
6769
entitySlug: collectionSlug,
70+
operation: 'create',
6871
updatedAt: json?.result?.updatedAt || new Date().toISOString(),
6972
})
7073

@@ -76,7 +79,7 @@ export function EditForm({
7679
}
7780
resetUploadEdits()
7881
},
79-
[collectionSlug, onSaveFromContext, reportUpdate, resetUploadEdits],
82+
[collectionSlug, onSaveFromContext, reportUpdate, resetUploadEdits, drawerSlug],
8083
)
8184

8285
const onChange: NonNullable<FormProps['onChange']>[0] = useCallback(

packages/ui/src/elements/DocumentDrawer/Provider.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ export const DocumentDrawerContextProvider: React.FC<
4141
children: React.ReactNode
4242
} & DocumentDrawerContextProps
4343
> = ({ children, ...rest }) => {
44-
return <DocumentDrawerCallbacksContext value={rest}>{children}</DocumentDrawerCallbacksContext>
44+
return (
45+
<DocumentDrawerCallbacksContext value={{ ...rest }}>{children}</DocumentDrawerCallbacksContext>
46+
)
4547
}
4648

4749
export const useDocumentDrawerContext = (): DocumentDrawerContextType => {

packages/ui/src/fields/Relationship/Input.tsx

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
'use client'
2-
import type { FilterOptionsResult, PaginatedDocs, ValueWithRelation, Where } from 'payload'
2+
import type {
3+
DocumentEvent,
4+
FilterOptionsResult,
5+
PaginatedDocs,
6+
ValueWithRelation,
7+
Where,
8+
} from 'payload'
39

410
import { dequal } from 'dequal/lite'
511
import { formatAdminURL, wordBoundariesRegex } from 'payload/shared'
@@ -24,15 +30,16 @@ import { useEffectEvent } from '../../hooks/useEffectEvent.js'
2430
import { useQueue } from '../../hooks/useQueue.js'
2531
import { useAuth } from '../../providers/Auth/index.js'
2632
import { useConfig } from '../../providers/Config/index.js'
33+
import { useDocumentEvents } from '../../providers/DocumentEvents/index.js'
2734
import { useLocale } from '../../providers/Locale/index.js'
2835
import { useTranslation } from '../../providers/Translation/index.js'
2936
import { sanitizeFilterOptionsQuery } from '../../utilities/sanitizeFilterOptionsQuery.js'
3037
import { fieldBaseClass } from '../shared/index.js'
3138
import { createRelationMap } from './createRelationMap.js'
3239
import { findOptionsByValue } from './findOptionsByValue.js'
3340
import { optionsReducer } from './optionsReducer.js'
34-
import { MultiValueLabel } from './select-components/MultiValueLabel/index.js'
3541
import './index.scss'
42+
import { MultiValueLabel } from './select-components/MultiValueLabel/index.js'
3643
import { SingleValue } from './select-components/SingleValue/index.js'
3744

3845
const baseClass = 'relationship'
@@ -102,7 +109,7 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
102109

103110
const valueRef = useRef(value)
104111

105-
const [DocumentDrawer, , { isDrawerOpen, openDrawer }] = useDocumentDrawer({
112+
const [DocumentDrawer, , { drawerSlug, isDrawerOpen, openDrawer }] = useDocumentDrawer({
106113
id: currentlyOpenRelationship.id,
107114
collectionSlug: currentlyOpenRelationship.collectionSlug,
108115
})
@@ -467,35 +474,75 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
467474
}, Promise.resolve())
468475
})
469476

470-
const onSave = useCallback<DocumentDrawerProps['onSave']>(
471-
(args) => {
472-
dispatchOptions({
473-
type: 'UPDATE',
474-
collection: args.collectionConfig,
475-
config,
476-
doc: args.doc,
477-
i18n,
478-
})
477+
const { mostRecentUpdate } = useDocumentEvents()
479478

480-
const docID = args.doc.id
479+
const handleDocumentUpdateEvent = useEffectEvent((mostRecentUpdate: DocumentEvent) => {
480+
if (!value) {
481+
return false
482+
}
481483

482-
if (hasMany) {
483-
const currentValue = value ? (Array.isArray(value) ? value : [value]) : []
484+
const docID = mostRecentUpdate.doc.id
484485

485-
const valuesToSet = currentValue.map((option: ValueWithRelation) => {
486-
return {
487-
relationTo: option.value === docID ? args.collectionConfig.slug : option.relationTo,
488-
value: option.value,
489-
}
486+
let isMatchingUpdate = false
487+
if (mostRecentUpdate.operation === 'update') {
488+
if (hasMany === true) {
489+
const currentValue = Array.isArray(value) ? value : [value]
490+
isMatchingUpdate = currentValue.some((option) => {
491+
return option.value === docID && option.relationTo === mostRecentUpdate.entitySlug
490492
})
491-
492-
onChange(valuesToSet)
493493
} else if (hasMany === false) {
494-
onChange({ relationTo: args.collectionConfig.slug, value: docID })
494+
isMatchingUpdate =
495+
value?.value === docID && value?.relationTo === mostRecentUpdate.entitySlug
495496
}
496-
},
497-
[i18n, config, hasMany, onChange, value],
498-
)
497+
} else if (mostRecentUpdate.operation === 'create') {
498+
// "Create New" drawer operations on the same level as this drawer should
499+
// set the value to the newly created document.
500+
// See test "should create document within document drawer > has one"
501+
isMatchingUpdate = mostRecentUpdate.drawerSlug === drawerSlug
502+
}
503+
504+
if (!isMatchingUpdate) {
505+
return
506+
}
507+
508+
const collectionConfig = getEntityConfig({ collectionSlug: mostRecentUpdate.entitySlug })
509+
510+
dispatchOptions({
511+
type: 'UPDATE',
512+
collection: collectionConfig,
513+
config,
514+
doc: mostRecentUpdate.doc,
515+
i18n,
516+
})
517+
518+
if (hasMany) {
519+
const currentValue = value ? (Array.isArray(value) ? value : [value]) : []
520+
521+
const valuesToSet = currentValue.map((option: ValueWithRelation) => {
522+
return {
523+
relationTo: option.value === docID ? mostRecentUpdate.entitySlug : option.relationTo,
524+
value: option.value,
525+
}
526+
})
527+
528+
onChange(valuesToSet)
529+
} else if (hasMany === false) {
530+
onChange({ relationTo: mostRecentUpdate.entitySlug, value: docID })
531+
}
532+
})
533+
534+
/**
535+
* Listen to document update events. If you edit a related document from a drawer and save it, this event
536+
* will be triggered. We then need up update the label of this relationship input, as the useAsLabel field could have changed.
537+
*
538+
* We listen to this event instead of using the onSave callback on the document drawer, as the onSave callback is not triggered
539+
* when you save a document from a drawer opened by a *different* relationship (or any other) field.
540+
*/
541+
useEffect(() => {
542+
if (mostRecentUpdate) {
543+
handleDocumentUpdateEvent(mostRecentUpdate)
544+
}
545+
}, [mostRecentUpdate])
499546

500547
const onDuplicate = useCallback<DocumentDrawerProps['onDuplicate']>(
501548
(args) => {
@@ -729,7 +776,6 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
729776
disableKeyDown: isDrawerOpen || isListDrawerOpen,
730777
disableMouseDown: isDrawerOpen || isListDrawerOpen,
731778
onDocumentOpen,
732-
onSave,
733779
}}
734780
disabled={readOnly || isDrawerOpen || isListDrawerOpen}
735781
filterOption={enableWordBoundarySearch ? filterOption : undefined}
@@ -870,7 +916,7 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
870916
/>
871917
</div>
872918
{currentlyOpenRelationship.collectionSlug && currentlyOpenRelationship.hasReadPermission && (
873-
<DocumentDrawer onDelete={onDelete} onDuplicate={onDuplicate} onSave={onSave} />
919+
<DocumentDrawer onDelete={onDelete} onDuplicate={onDuplicate} />
874920
)}
875921
{appearance === 'drawer' && !readOnly && (
876922
<ListDrawer allowCreate={allowCreate} enableRowSelections={false} onSelect={onListSelect} />

packages/ui/src/fields/Relationship/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { RelationshipFieldClientComponent, ValueWithRelation } from 'payloa
33

44
import React, { useCallback, useMemo } from 'react'
55

6-
import type { PolymorphicRelationValue, Value } from './types.js'
6+
import type { Value } from './types.js'
77

88
import { useField } from '../../forms/useField/index.js'
99
import { withCondition } from '../../forms/withCondition/index.js'
@@ -80,7 +80,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
8080
Array.isArray(value) &&
8181
Array.isArray(newValue) &&
8282
value.length === newValue.length &&
83-
(value as PolymorphicRelationValue[]).every((val, idx) => {
83+
(value as ValueWithRelation[]).every((val, idx) => {
8484
const newVal = newValue[idx]
8585
return val.value === newVal.value && val.relationTo === newVal.relationTo
8686
})
@@ -117,8 +117,8 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
117117
disableFormModification =
118118
value &&
119119
newValue &&
120-
(value as PolymorphicRelationValue).value === newValue.value &&
121-
(value as PolymorphicRelationValue).relationTo === newValue.relationTo
120+
(value as ValueWithRelation).value === newValue.value &&
121+
(value as ValueWithRelation).relationTo === newValue.relationTo
122122
} else {
123123
disableFormModification = value && newValue && value === newValue.value
124124
}

packages/ui/src/fields/Relationship/types.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
LabelFunction,
88
StaticDescription,
99
StaticLabel,
10+
ValueWithRelation,
1011
} from 'payload'
1112

1213
export type Option = {
@@ -22,21 +23,16 @@ export type OptionGroup = {
2223
options: Option[]
2324
}
2425

25-
export type PolymorphicRelationValue = {
26-
relationTo: string
27-
value: number | string
28-
}
29-
3026
export type MonomorphicRelationValue = number | string
3127

3228
export type Value =
3329
| MonomorphicRelationValue
3430
| MonomorphicRelationValue[]
35-
| PolymorphicRelationValue
36-
| PolymorphicRelationValue[]
31+
| ValueWithRelation
32+
| ValueWithRelation[]
3733

3834
type CLEAR = {
39-
exemptValues?: PolymorphicRelationValue | PolymorphicRelationValue[]
35+
exemptValues?: ValueWithRelation | ValueWithRelation[]
4036
type: 'CLEAR'
4137
}
4238

@@ -71,11 +67,11 @@ export type Action = ADD | CLEAR | REMOVE | UPDATE
7167
export type HasManyValueUnion =
7268
| {
7369
hasMany: false
74-
value?: PolymorphicRelationValue
70+
value?: ValueWithRelation
7571
}
7672
| {
7773
hasMany: true
78-
value?: PolymorphicRelationValue[]
74+
value?: ValueWithRelation[]
7975
}
8076

8177
export type UpdateResults = (
@@ -121,13 +117,13 @@ export type RelationshipInputProps = {
121117
type SharedRelationshipInputProps =
122118
| {
123119
readonly hasMany: false
124-
readonly initialValue?: null | PolymorphicRelationValue
125-
readonly onChange: (value: PolymorphicRelationValue) => void
126-
readonly value?: null | PolymorphicRelationValue
120+
readonly initialValue?: null | ValueWithRelation
121+
readonly onChange: (value: ValueWithRelation) => void
122+
readonly value?: null | ValueWithRelation
127123
}
128124
| {
129125
readonly hasMany: true
130-
readonly initialValue?: null | PolymorphicRelationValue[]
131-
readonly onChange: (value: PolymorphicRelationValue[]) => void
132-
readonly value?: null | PolymorphicRelationValue[]
126+
readonly initialValue?: null | ValueWithRelation[]
127+
readonly onChange: (value: ValueWithRelation[]) => void
128+
readonly value?: null | ValueWithRelation[]
133129
}

packages/ui/src/providers/DocumentEvents/index.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import type { DocumentEvent } from 'payload'
33

44
import React, { createContext, use, useState } from 'react'
55

6-
const Context = createContext({
6+
const Context = createContext<{
7+
mostRecentUpdate: DocumentEvent | null
8+
reportUpdate: (event: DocumentEvent) => void
9+
}>({
710
mostRecentUpdate: null,
8-
reportUpdate: (doc: DocumentEvent) => null,
11+
reportUpdate: () => null,
912
})
1013

1114
export const DocumentEventsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
@@ -14,4 +17,11 @@ export const DocumentEventsProvider: React.FC<{ children: React.ReactNode }> = (
1417
return <Context value={{ mostRecentUpdate, reportUpdate }}>{children}</Context>
1518
}
1619

20+
/**
21+
* The useDocumentEvents hook provides a way of subscribing to cross-document events,
22+
* such as updates made to nested documents within a drawer.
23+
* This hook will report document events that are outside the scope of the document currently being edited.
24+
*
25+
* @link https://payloadcms.com/docs/admin/react-hooks#usedocumentevents
26+
*/
1727
export const useDocumentEvents = () => use(Context)

packages/ui/src/views/Edit/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ export function DefaultEditView({
112112
onRestore,
113113
onSave: onSaveFromContext,
114114
} = useDocumentDrawerContext()
115-
const { closeModal } = useModal()
116115

117116
const isInDrawer = Boolean(drawerSlug)
118117

@@ -373,20 +372,33 @@ export function DefaultEditView({
373372

374373
reportUpdate({
375374
id,
375+
doc: document,
376+
drawerSlug,
376377
entitySlug,
378+
operation: 'update',
377379
updatedAt,
378380
})
379381

380382
abortOnSaveRef.current = null
381383

382384
return state
385+
} else {
386+
reportUpdate({
387+
id,
388+
doc: document,
389+
drawerSlug,
390+
entitySlug,
391+
operation: 'create',
392+
updatedAt,
393+
})
383394
}
384395
},
385396
[
386397
reportUpdate,
387398
id,
388399
entitySlug,
389400
user,
401+
drawerSlug,
390402
collectionSlug,
391403
userSlug,
392404
setLastUpdateTime,

test/fields/baseConfig.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import UploadRestricted from './collections/UploadRestricted/index.js'
4141
import Uploads3 from './collections/Uploads3/index.js'
4242
import { seed } from './seed.js'
4343

44-
export const collectionSlugs: CollectionConfig[] = [
44+
export const collections: CollectionConfig[] = [
4545
{
4646
slug: 'users',
4747
admin: {
@@ -92,7 +92,7 @@ export const collectionSlugs: CollectionConfig[] = [
9292
]
9393

9494
export const baseConfig: Partial<Config> = {
95-
collections: collectionSlugs,
95+
collections,
9696
blocks: [
9797
{
9898
slug: 'ConfigBlockTest',

0 commit comments

Comments
 (0)