Skip to content

Commit 16f5538

Browse files
fix(plugin-multi-tenant): unnecessary modal appearing (#12854)
Fixes #12826 Leave without saving was being triggered when no changes were made to the tenant. This should only happen if the value in form state differs from that of the selected tenant, i.e. after changing tenants. Adds tenant selector syncing so the selector updates when a tenant is added or the name is edited. Also adds E2E for most multi-tenant admin functionality. --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210562742356842
1 parent 9f60306 commit 16f5538

File tree

14 files changed

+744
-106
lines changed

14 files changed

+744
-106
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ jobs:
315315
- plugin-cloud-storage
316316
- plugin-form-builder
317317
- plugin-import-export
318+
- plugin-multi-tenant
318319
- plugin-nested-docs
319320
- plugin-seo
320321
- sort
@@ -451,6 +452,7 @@ jobs:
451452
- plugin-cloud-storage
452453
- plugin-form-builder
453454
- plugin-import-export
455+
- plugin-multi-tenant
454456
- plugin-nested-docs
455457
- plugin-seo
456458
- sort

docs/fields/overview.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,8 @@ import type { Field } from 'payload'
315315
export const MyField: Field = {
316316
type: 'text',
317317
name: 'myField',
318-
validate: (value, {req: { t }}) => Boolean(value) || t('validation:required'), // highlight-line
318+
validate: (value, { req: { t } }) =>
319+
Boolean(value) || t('validation:required'), // highlight-line
319320
}
320321
```
321322

examples/localization/src/components/Card/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { cn } from '@/utilities/ui'
33
import useClickableCard from '@/utilities/useClickableCard'
44
import Link from 'next/link'
5-
import { useLocale } from 'next-intl';
5+
import { useLocale } from 'next-intl'
66
import React, { Fragment } from 'react'
77

88
import type { Post } from '@/payload-types'
@@ -17,7 +17,7 @@ export const Card: React.FC<{
1717
showCategories?: boolean
1818
title?: string
1919
}> = (props) => {
20-
const locale = useLocale();
20+
const locale = useLocale()
2121
const { card, link } = useClickableCard({})
2222
const { className, doc, relationTo, showCategories, title: titleFromProps } = props
2323

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import type { RelationshipFieldClientProps } from 'payload'
44

5-
import { RelationshipField, useField } from '@payloadcms/ui'
5+
import { RelationshipField, useField, useFormModified } from '@payloadcms/ui'
66
import React from 'react'
77

88
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
@@ -18,7 +18,14 @@ type Props = {
1818
export const TenantField = (args: Props) => {
1919
const { debug, unique } = args
2020
const { setValue, value } = useField<number | string>()
21-
const { options, selectedTenantID, setPreventRefreshOnChange, setTenant } = useTenantSelection()
21+
const modified = useFormModified()
22+
const {
23+
options,
24+
selectedTenantID,
25+
setEntityType: setEntityType,
26+
setModified,
27+
setTenant,
28+
} = useTenantSelection()
2229

2330
const hasSetValueRef = React.useRef(false)
2431

@@ -35,18 +42,25 @@ export const TenantField = (args: Props) => {
3542
hasSetValueRef.current = true
3643
} else if (!value || value !== selectedTenantID) {
3744
// Update the field on the document value when the tenant is changed
38-
setValue(selectedTenantID)
45+
setValue(selectedTenantID, !value || value === selectedTenantID)
3946
}
4047
}, [value, selectedTenantID, setTenant, setValue, options, unique])
4148

4249
React.useEffect(() => {
43-
if (!unique) {
44-
setPreventRefreshOnChange(true)
50+
setEntityType(unique ? 'global' : 'document')
51+
return () => {
52+
setEntityType(undefined)
4553
}
54+
}, [unique, setEntityType])
55+
56+
React.useEffect(() => {
57+
// sync form modified state with the tenant selection provider context
58+
setModified(modified)
59+
4660
return () => {
47-
setPreventRefreshOnChange(false)
61+
setModified(false)
4862
}
49-
}, [unique, setPreventRefreshOnChange])
63+
}, [modified, setModified])
5064

5165
if (debug) {
5266
return (

packages/plugin-multi-tenant/src/components/TenantSelector/index.tsx

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import type {
2020
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
2121
import './index.scss'
2222

23-
const confirmSwitchTenantSlug = 'confirmSwitchTenant'
23+
const confirmSwitchTenantSlug = 'confirm-switch-tenant'
24+
const confirmLeaveWithoutSavingSlug = 'confirm-leave-without-saving'
2425

2526
export const TenantSelector = ({ label, viewType }: { label: string; viewType?: ViewTypes }) => {
26-
const { options, preventRefreshOnChange, selectedTenantID, setTenant } = useTenantSelection()
27-
const { openModal } = useModal()
27+
const { entityType, modified, options, selectedTenantID, setTenant } = useTenantSelection()
28+
const { closeModal, openModal } = useModal()
2829
const { i18n, t } = useTranslation<
2930
PluginMultiTenantTranslations,
3031
PluginMultiTenantTranslationKeys
@@ -60,15 +61,27 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
6061

6162
const onChange = React.useCallback(
6263
(option: ReactSelectOption | ReactSelectOption[]) => {
63-
if (!preventRefreshOnChange) {
64-
switchTenant(option)
64+
if (option && 'value' in option && option.value === selectedTenantID) {
65+
// If the selected option is the same as the current tenant, do nothing
6566
return
67+
}
68+
69+
if (entityType !== 'document') {
70+
if (entityType === 'global' && modified) {
71+
// If the entityType is 'global' and there are unsaved changes, prompt for confirmation
72+
setTenantSelection(option)
73+
openModal(confirmLeaveWithoutSavingSlug)
74+
} else {
75+
// If the entityType is not 'document', switch tenant without confirmation
76+
switchTenant(option)
77+
}
6678
} else {
79+
// non-unique documents should always prompt for confirmation
6780
setTenantSelection(option)
6881
openModal(confirmSwitchTenantSlug)
6982
}
7083
},
71-
[openModal, preventRefreshOnChange, switchTenant],
84+
[selectedTenantID, entityType, modified, switchTenant, openModal],
7285
)
7386

7487
if (options.length <= 1) {
@@ -105,22 +118,28 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
105118
}}
106119
/>
107120
}
108-
heading={
109-
<Translation
110-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
111-
// @ts-expect-error
112-
i18nKey="plugin-multi-tenant:confirm-tenant-switch--heading"
113-
t={t}
114-
variables={{
115-
tenantLabel: getTranslation(label, i18n),
116-
}}
117-
/>
118-
}
121+
heading={t('plugin-multi-tenant:confirm-tenant-switch--heading', {
122+
tenantLabel: getTranslation(label, i18n),
123+
})}
119124
modalSlug={confirmSwitchTenantSlug}
120125
onConfirm={() => {
121126
switchTenant(tenantSelection)
122127
}}
123128
/>
129+
130+
<ConfirmationModal
131+
body={t('general:changesNotSaved')}
132+
cancelLabel={t('general:stayOnThisPage')}
133+
confirmLabel={t('general:leaveAnyway')}
134+
heading={t('general:leaveWithoutSaving')}
135+
modalSlug={confirmLeaveWithoutSavingSlug}
136+
onCancel={() => {
137+
closeModal(confirmLeaveWithoutSavingSlug)
138+
}}
139+
onConfirm={() => {
140+
switchTenant(tenantSelection)
141+
}}
142+
/>
124143
</div>
125144
)
126145
}

packages/plugin-multi-tenant/src/components/WatchTenantCollection/index.tsx

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,46 @@ import {
88
useDocumentTitle,
99
useEffectEvent,
1010
useFormFields,
11+
useFormSubmitted,
12+
useOperation,
1113
} from '@payloadcms/ui'
1214
import React from 'react'
1315

1416
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
1517

1618
export const WatchTenantCollection = () => {
1719
const { id, collectionSlug } = useDocumentInfo()
20+
const operation = useOperation()
21+
const submitted = useFormSubmitted()
1822
const { title } = useDocumentTitle()
19-
const addedNewTenant = React.useRef(false)
2023

2124
const { getEntityConfig } = useConfig()
2225
const [useAsTitleName] = React.useState(
2326
() => (getEntityConfig({ collectionSlug }) as ClientCollectionConfig).admin.useAsTitle,
2427
)
2528
const titleField = useFormFields(([fields]) => (useAsTitleName ? fields[useAsTitleName] : {}))
2629

27-
const { options, updateTenants } = useTenantSelection()
30+
const { syncTenants, updateTenants } = useTenantSelection()
2831

2932
const syncTenantTitle = useEffectEvent(() => {
3033
if (id) {
3134
updateTenants({ id, label: title })
3235
}
3336
})
3437

35-
React.useEffect(() => {
36-
if (!id || !title || addedNewTenant.current) {
37-
return
38-
}
39-
// Track tenant creation and add it to the tenant selector
40-
const exists = options.some((opt) => opt.value === id)
41-
if (!exists) {
42-
addedNewTenant.current = true
43-
updateTenants({ id, label: title })
44-
}
45-
// eslint-disable-next-line react-compiler/react-compiler
46-
// eslint-disable-next-line react-hooks/exhaustive-deps
47-
}, [id])
48-
4938
React.useEffect(() => {
5039
// only update the tenant selector when the document saves
5140
// → aka when initial value changes
5241
if (id && titleField?.initialValue) {
53-
syncTenantTitle()
42+
void syncTenantTitle()
43+
}
44+
}, [id, titleField?.initialValue, syncTenants])
45+
46+
React.useEffect(() => {
47+
if (operation === 'create' && submitted) {
48+
void syncTenants()
5449
}
55-
}, [id, titleField?.initialValue])
50+
}, [operation, submitted, syncTenants, id])
5651

5752
return null
5853
}

0 commit comments

Comments
 (0)