Skip to content

Commit 09e01df

Browse files
fix(plugin-multi-tenant): apply tenant on confirm (#17350)
Identical to #17348, back-ported for 3.x. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent cc81ef0 commit 09e01df

4 files changed

Lines changed: 155 additions & 68 deletions

File tree

‎packages/plugin-multi-tenant/src/components/AssignTenantFieldModal/index.client.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ export const AssignTenantFieldTrigger: React.FC = () => {
4747
}
4848

4949
export const AssignTenantFieldModal: React.FC<{
50-
afterModalClose: () => void
51-
afterModalOpen: () => void
50+
afterModalClose?: () => void
51+
afterModalOpen?: () => void
5252
children: React.ReactNode
5353
onCancel?: () => void
5454
onConfirm?: () => void

‎packages/plugin-multi-tenant/src/components/TenantField/index.client.tsx‎

Lines changed: 92 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
'use client'
22

3+
import type { FieldType } from '@payloadcms/ui'
34
import type { RelationshipFieldClientProps } from 'payload'
45

56
import {
7+
FieldContext,
8+
FieldPathContext,
69
Pill,
710
RelationshipField,
811
useDocumentInfo,
@@ -13,6 +16,8 @@ import {
1316
} from '@payloadcms/ui'
1417
import React from 'react'
1518

19+
import type { TenantValue } from '../../types.js'
20+
1621
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
1722
import {
1823
AssignTenantFieldModal,
@@ -29,77 +34,84 @@ type Props = {
2934

3035
export const TenantField = ({ debug, unique, ...fieldArgs }: Props) => {
3136
const { entityType, options, selectedTenantID, setEntityType, setTenant } = useTenantSelection()
32-
const { setValue, showError, value } = useField<(number | string)[] | (number | string)>()
33-
const modified = useFormModified()
34-
const { isValid: isFormValid, setModified } = useForm()
37+
const tenantField = useField<TenantValue>()
38+
const { setValue, showError, value } = tenantField
39+
const { isValid: isFormValid } = useForm()
3540
const { id: docID, collectionSlug } = useDocumentInfo()
3641
const { isModalOpen, openModal } = useModal()
3742
const isEditManyModalOpen = collectionSlug
3843
? isModalOpen(`edit-${collectionSlug}`) || isModalOpen(`edit-${collectionSlug}-bulk-uploads`)
3944
: false
40-
const isConfirmingRef = React.useRef<boolean>(false)
41-
const prevModified = React.useRef(modified)
42-
const prevValue = React.useRef<typeof value>(value)
45+
const [modalValue, setModalValue] = React.useState<TenantValue | undefined>(value)
46+
const localTenantField = React.useMemo<FieldType<TenantValue>>(
47+
() => ({
48+
...tenantField,
49+
initialValue: modalValue,
50+
setValue: (nextValue) => {
51+
setModalValue(nextValue as TenantValue | undefined)
52+
},
53+
value: modalValue as TenantValue,
54+
}),
55+
[modalValue, tenantField],
56+
)
4357
const showField =
4458
(options.length > 1 && !fieldArgs.field.admin?.hidden && !fieldArgs.field.hidden) || debug
4559

46-
const onConfirm = React.useCallback(() => {
47-
isConfirmingRef.current = true
48-
}, [])
49-
50-
const afterModalOpen = React.useCallback(() => {
51-
prevModified.current = modified
52-
prevValue.current = value
53-
}, [modified, value])
54-
55-
const afterModalClose = React.useCallback(() => {
56-
let didChange = true
57-
if (isConfirmingRef.current) {
58-
// did the values actually change?
59-
if (fieldArgs.field.hasMany) {
60-
const prev = (prevValue.current || []) as (number | string)[]
61-
const newValue = (value || []) as (number | string)[]
62-
if (prev.length !== newValue.length) {
63-
didChange = true
64-
} else {
65-
const allMatch = newValue.every((val) => prev.includes(val))
66-
if (allMatch) {
67-
didChange = false
68-
}
69-
}
70-
} else if (value === prevValue.current) {
71-
didChange = false
60+
function getTenantIDToSelect({
61+
selectedTenantID,
62+
value,
63+
}: {
64+
selectedTenantID: number | string | undefined
65+
value: TenantValue | undefined
66+
}): number | string | undefined {
67+
if (Array.isArray(value)) {
68+
if (!value.length) {
69+
return undefined
7270
}
7371

74-
if (didChange) {
75-
prevModified.current = true
76-
prevValue.current = value
72+
if (!selectedTenantID || !value.includes(selectedTenantID)) {
73+
return value[0]
7774
}
75+
76+
return undefined
7877
}
7978

80-
setValue(prevValue.current, true)
81-
setModified(prevModified.current)
79+
if (value && selectedTenantID !== value) {
80+
return value
81+
}
82+
83+
return undefined
84+
}
85+
86+
const syncTenantSelectionFromValue = React.useCallback(
87+
(tenantValue: TenantValue | undefined) => {
88+
const tenantID = getTenantIDToSelect({ selectedTenantID, value: tenantValue })
89+
90+
if (tenantID) {
91+
setTenant({ id: tenantID, refresh: false })
92+
}
93+
},
94+
[selectedTenantID, setTenant],
95+
)
96+
97+
const onConfirm = React.useCallback(() => {
98+
if (hasTenantValueChanged({ hasMany: fieldArgs.field.hasMany, modalValue, value })) {
99+
setValue(modalValue)
100+
syncTenantSelectionFromValue(modalValue)
101+
}
102+
}, [fieldArgs.field.hasMany, modalValue, setValue, syncTenantSelectionFromValue, value])
82103

83-
isConfirmingRef.current = false
84-
}, [setValue, setModified, value, fieldArgs.field.hasMany])
104+
const afterModalOpen = React.useCallback(() => {
105+
setModalValue(value)
106+
}, [value])
85107

86108
React.useEffect(() => {
87109
if (!entityType) {
88110
setEntityType(unique ? 'global' : 'document')
89111
} else {
90112
// unique documents are controlled from the global TenantSelector
91-
if (!unique && value) {
92-
if (Array.isArray(value)) {
93-
if (value.length) {
94-
if (!selectedTenantID) {
95-
setTenant({ id: value[0], refresh: false })
96-
} else if (!value.includes(selectedTenantID)) {
97-
setTenant({ id: value[0], refresh: false })
98-
}
99-
}
100-
} else if (selectedTenantID !== value) {
101-
setTenant({ id: value, refresh: false })
102-
}
113+
if (!unique) {
114+
syncTenantSelectionFromValue(value)
103115
}
104116
}
105117

@@ -108,7 +120,7 @@ export const TenantField = ({ debug, unique, ...fieldArgs }: Props) => {
108120
setEntityType(undefined)
109121
}
110122
}
111-
}, [unique, options, selectedTenantID, setTenant, value, setEntityType, entityType])
123+
}, [entityType, setEntityType, syncTenantSelectionFromValue, unique, value])
112124

113125
React.useEffect(() => {
114126
if (unique || debug || isEditManyModalOpen) {
@@ -138,12 +150,12 @@ export const TenantField = ({ debug, unique, ...fieldArgs }: Props) => {
138150
if (!unique) {
139151
/** Editing a non-global tenant document */
140152
return (
141-
<AssignTenantFieldModal
142-
afterModalClose={afterModalClose}
143-
afterModalOpen={afterModalOpen}
144-
onConfirm={onConfirm}
145-
>
146-
<TenantRelationshipField fieldArgs={fieldArgs} unique={unique} />
153+
<AssignTenantFieldModal afterModalOpen={afterModalOpen} onConfirm={onConfirm}>
154+
<FieldPathContext value={tenantField.path}>
155+
<FieldContext value={localTenantField}>
156+
<TenantRelationshipField fieldArgs={fieldArgs} unique={unique} />
157+
</FieldContext>
158+
</FieldPathContext>
147159
</AssignTenantFieldModal>
148160
)
149161
}
@@ -154,6 +166,28 @@ export const TenantField = ({ debug, unique, ...fieldArgs }: Props) => {
154166
return null
155167
}
156168

169+
const hasTenantValueChanged = ({
170+
hasMany,
171+
modalValue,
172+
value,
173+
}: {
174+
hasMany?: boolean
175+
modalValue: TenantValue | undefined
176+
value: TenantValue | undefined
177+
}): boolean => {
178+
if (hasMany) {
179+
const currentValue = (value || []) as (number | string)[]
180+
const nextValue = (modalValue || []) as (number | string)[]
181+
182+
return (
183+
currentValue.length !== nextValue.length ||
184+
nextValue.some((nextValueItem) => !currentValue.includes(nextValueItem))
185+
)
186+
}
187+
188+
return value !== modalValue
189+
}
190+
157191
const TenantRelationshipField: React.FC<{
158192
debug?: boolean
159193
fieldArgs: RelationshipFieldClientProps

‎packages/plugin-multi-tenant/src/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {
1111
TypedUser,
1212
} from 'payload'
1313

14+
export type TenantValue = (number | string)[] | (number | string)
15+
1416
export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
1517
/**
1618
* After a tenant is deleted, the plugin will attempt to clean up related documents

‎test/plugin-multi-tenant/e2e.spec.ts‎

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -352,14 +352,39 @@ test.describe('Multi Tenant', () => {
352352
urlUtil: menuItemsURL,
353353
})
354354

355-
await selectDocumentTenant({
356-
action: 'cancel',
355+
await closeNav(page)
356+
await openAssignTenantModal({ page, payload })
357+
await selectInput({
357358
page,
358-
payload,
359-
tenant: 'Steel Cat',
359+
multiSelect: false,
360+
option: 'Steel Cat',
361+
selectLocator: page.locator('.tenantField'),
360362
})
361363

362364
await expect(page.locator('#action-save')).toBeDisabled()
365+
await expect
366+
.poll(async () => {
367+
return await getSelectedTenantFilterName({ page, payload })
368+
})
369+
.toBe('Blue Dog')
370+
371+
const assignTenantModal = page.locator('#assign-tenant-field-modal')
372+
await assignTenantModal.locator('button', { hasText: 'Cancel' }).click()
373+
await expect(assignTenantModal).toBeHidden()
374+
375+
await expect(page.locator('#action-save')).toBeDisabled()
376+
377+
await closeNav(page)
378+
await openAssignTenantModal({ page, payload })
379+
await expect
380+
.poll(async () =>
381+
getSelectInputValue({
382+
multiSelect: false,
383+
selectLocator: page.locator('.tenantField'),
384+
selectType: 'relationship',
385+
}),
386+
)
387+
.toBe('Blue Dog')
363388

364389
await page.goto(menuItemsURL.list)
365390
await expect
@@ -386,12 +411,33 @@ test.describe('Multi Tenant', () => {
386411
urlUtil: menuItemsURL,
387412
})
388413

389-
await selectDocumentTenant({
414+
await closeNav(page)
415+
await openAssignTenantModal({ page, payload })
416+
await selectInput({
390417
page,
391-
payload,
392-
tenant: 'Steel Cat',
418+
multiSelect: false,
419+
option: 'Steel Cat',
420+
selectLocator: page.locator('.tenantField'),
393421
})
394422

423+
await expect(page.locator('#action-save')).toBeDisabled()
424+
await expect
425+
.poll(async () => {
426+
return await getSelectedTenantFilterName({ page, payload })
427+
})
428+
.toBe('Blue Dog')
429+
430+
const assignTenantModal = page.locator('#assign-tenant-field-modal')
431+
await assignTenantModal.locator('button', { hasText: 'Confirm' }).click()
432+
await expect(assignTenantModal).toBeHidden()
433+
434+
await expect(page.locator('#action-save')).toBeEnabled()
435+
await expect
436+
.poll(async () => {
437+
return await getSelectedTenantFilterName({ page, payload })
438+
})
439+
.toBe('Steel Cat')
440+
395441
await saveDocAndAssert(page)
396442
})
397443

@@ -1044,7 +1090,12 @@ async function openAssignTenantModal({
10441090
// Open the assign tenant modal
10451091
const docControlsPopup = page.locator('.popup__content')
10461092
const docControlsButton = page.locator('.doc-controls__popup .popup-button')
1047-
await expect(docControlsButton).toBeVisible()
1093+
1094+
if (!(await docControlsButton.isVisible())) {
1095+
await expect(assignTenantModal).toBeVisible()
1096+
return
1097+
}
1098+
10481099
await docControlsButton.click()
10491100

10501101
const assignTenantButtonLocator = docControlsPopup.locator('button', { hasText: 'Assign Site' })

0 commit comments

Comments
 (0)