Skip to content

Commit 5cf215d

Browse files
feat(plugin-multi-tenant): visible tenant field on documents (#13379)
The goal of this PR is to show the selected tenant on the document level instead of using the global selector to sync the state to the document. Should merge #13316 before this one. ### Video of what this PR implements **Would love feedback!** https://github.com/user-attachments/assets/93ca3d2c-d479-4555-ab38-b77a5a9955e8
1 parent 393b4a0 commit 5cf215d

File tree

24 files changed

+842
-383
lines changed

24 files changed

+842
-383
lines changed

docs/plugins/multi-tenant.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
8585
*/
8686
tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
8787
/**
88-
* Set to `false` if you want to manually apply the baseListFilter
89-
* Set to `false` if you want to manually apply the baseFilter
88+
* Set to `false` if you want to manually apply
89+
* the baseFilter
9090
*
9191
* @default true
9292
*/

packages/next/src/views/Logout/LogoutClient.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ export const LogoutClient: React.FC<{
2626

2727
const { startRouteTransition } = useRouteTransition()
2828

29-
const [isLoggedOut, setIsLoggedOut] = React.useState<boolean>(!user)
29+
const isLoggedIn = React.useMemo(() => {
30+
return Boolean(user?.id)
31+
}, [user?.id])
3032

31-
const logOutSuccessRef = React.useRef(false)
33+
const navigatingToLoginRef = React.useRef(false)
3234

3335
const [loginRoute] = React.useState(() =>
3436
formatAdminURL({
@@ -45,26 +47,26 @@ export const LogoutClient: React.FC<{
4547
const router = useRouter()
4648

4749
const handleLogOut = React.useCallback(async () => {
48-
const loggedOut = await logOut()
49-
setIsLoggedOut(loggedOut)
50+
await logOut()
5051

51-
if (!inactivity && loggedOut && !logOutSuccessRef.current) {
52+
if (!inactivity && !navigatingToLoginRef.current) {
5253
toast.success(t('authentication:loggedOutSuccessfully'))
53-
logOutSuccessRef.current = true
54+
navigatingToLoginRef.current = true
5455
startRouteTransition(() => router.push(loginRoute))
5556
return
5657
}
5758
}, [inactivity, logOut, loginRoute, router, startRouteTransition, t])
5859

5960
useEffect(() => {
60-
if (!isLoggedOut) {
61+
if (isLoggedIn) {
6162
void handleLogOut()
62-
} else {
63+
} else if (!navigatingToLoginRef.current) {
64+
navigatingToLoginRef.current = true
6365
startRouteTransition(() => router.push(loginRoute))
6466
}
65-
}, [handleLogOut, isLoggedOut, loginRoute, router, startRouteTransition])
67+
}, [handleLogOut, isLoggedIn, loginRoute, router, startRouteTransition])
6668

67-
if (isLoggedOut && inactivity) {
69+
if (!isLoggedIn && inactivity) {
6870
return (
6971
<div className={`${baseClass}__wrap`}>
7072
<h2>{t('authentication:loggedOutInactivity')}</h2>
Lines changed: 172 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
'use client'
22

3-
import type { RelationshipFieldClientProps } from 'payload'
3+
import type { RelationshipFieldClientProps, StaticLabel } from 'payload'
44

5-
import { RelationshipField, useField, useFormModified } from '@payloadcms/ui'
5+
import { getTranslation } from '@payloadcms/translations'
6+
import {
7+
ConfirmationModal,
8+
RelationshipField,
9+
Translation,
10+
useField,
11+
useForm,
12+
useFormModified,
13+
useModal,
14+
useTranslation,
15+
} from '@payloadcms/ui'
616
import React from 'react'
717

8-
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
18+
import type {
19+
PluginMultiTenantTranslationKeys,
20+
PluginMultiTenantTranslations,
21+
} from '../../translations/index.js'
22+
923
import './index.scss'
24+
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
1025

1126
const baseClass = 'tenantField'
1227

@@ -16,62 +31,172 @@ type Props = {
1631
} & RelationshipFieldClientProps
1732

1833
export const TenantField = (args: Props) => {
19-
const { debug, unique } = args
20-
const { setValue, value } = useField<number | string>()
21-
const modified = useFormModified()
22-
const {
23-
options,
24-
selectedTenantID,
25-
setEntityType: setEntityType,
26-
setModified,
27-
setTenant,
28-
} = useTenantSelection()
29-
30-
const hasSetValueRef = React.useRef(false)
34+
const { entityType, options, selectedTenantID, setEntityType, setTenant } = useTenantSelection()
35+
const { value } = useField<number | string>()
3136

3237
React.useEffect(() => {
33-
if (!hasSetValueRef.current) {
34-
// set value on load
35-
if (value && value !== selectedTenantID) {
36-
setTenant({ id: value, refresh: unique })
37-
} else {
38-
// in the document view, the tenant field should always have a value
39-
const defaultValue = selectedTenantID || options[0]?.value
40-
setTenant({ id: defaultValue, refresh: unique })
38+
if (!entityType) {
39+
setEntityType(args.unique ? 'global' : 'document')
40+
} else {
41+
// unique documents are controlled from the global TenantSelector
42+
if (!args.unique && value) {
43+
if (!selectedTenantID || value !== selectedTenantID) {
44+
setTenant({ id: value, refresh: false })
45+
}
4146
}
42-
hasSetValueRef.current = true
43-
} else if (!value || value !== selectedTenantID) {
44-
// Update the field on the document value when the tenant is changed
45-
setValue(selectedTenantID, !value || value === selectedTenantID)
4647
}
47-
}, [value, selectedTenantID, setTenant, setValue, options, unique])
4848

49-
React.useEffect(() => {
50-
setEntityType(unique ? 'global' : 'document')
5149
return () => {
52-
setEntityType(undefined)
53-
}
54-
}, [unique, setEntityType])
55-
56-
React.useEffect(() => {
57-
// sync form modified state with the tenant selection provider context
58-
setModified(modified)
59-
60-
return () => {
61-
setModified(false)
50+
if (entityType) {
51+
setEntityType(undefined)
52+
}
6253
}
63-
}, [modified, setModified])
54+
}, [args.unique, options, selectedTenantID, setTenant, value, setEntityType, entityType])
6455

65-
if (debug) {
56+
if (options.length > 1) {
6657
return (
67-
<div className={baseClass}>
68-
<div className={`${baseClass}__wrapper`}>
69-
<RelationshipField {...args} />
58+
<>
59+
<div className={baseClass}>
60+
<div className={`${baseClass}__wrapper`}>
61+
<RelationshipField
62+
{...args}
63+
field={{
64+
...args.field,
65+
required: true,
66+
}}
67+
readOnly={args.readOnly || args.unique}
68+
/>
69+
</div>
7070
</div>
71-
<div className={`${baseClass}__hr`} />
72-
</div>
71+
{args.unique ? (
72+
<SyncFormModified />
73+
) : (
74+
<ConfirmTenantChange fieldLabel={args.field.label} fieldPath={args.path} />
75+
)}
76+
</>
7377
)
7478
}
7579

7680
return null
7781
}
82+
83+
const confirmSwitchTenantSlug = 'confirm-switch-tenant'
84+
85+
const ConfirmTenantChange = ({
86+
fieldLabel,
87+
fieldPath,
88+
}: {
89+
fieldLabel?: StaticLabel
90+
fieldPath: string
91+
}) => {
92+
const { options, selectedTenantID, setTenant } = useTenantSelection()
93+
const { setValue: setTenantFormValue, value: tenantFormValue } = useField<null | number | string>(
94+
{ path: fieldPath },
95+
)
96+
const { setModified } = useForm()
97+
const modified = useFormModified()
98+
const { i18n, t } = useTranslation<
99+
PluginMultiTenantTranslations,
100+
PluginMultiTenantTranslationKeys
101+
>()
102+
const { isModalOpen, openModal } = useModal()
103+
104+
const prevTenantValueRef = React.useRef<null | number | string>(tenantFormValue || null)
105+
const [tenantToConfirm, setTenantToConfirm] = React.useState<null | number | string>(
106+
tenantFormValue || null,
107+
)
108+
109+
const fromTenantOption = React.useMemo(() => {
110+
if (tenantFormValue) {
111+
return options.find((option) => option.value === tenantFormValue)
112+
}
113+
return undefined
114+
}, [options, tenantFormValue])
115+
116+
const toTenantOption = React.useMemo(() => {
117+
if (tenantToConfirm) {
118+
return options.find((option) => option.value === tenantToConfirm)
119+
}
120+
return undefined
121+
}, [options, tenantToConfirm])
122+
123+
const modalIsOpen = isModalOpen(confirmSwitchTenantSlug)
124+
const testRef = React.useRef<boolean>(false)
125+
126+
React.useEffect(() => {
127+
// the form value changed
128+
if (
129+
!modalIsOpen &&
130+
tenantFormValue &&
131+
prevTenantValueRef.current &&
132+
tenantFormValue !== prevTenantValueRef.current
133+
) {
134+
// revert the form value change temporarily
135+
setTenantFormValue(prevTenantValueRef.current, true)
136+
// save the tenant to confirm in modal
137+
setTenantToConfirm(tenantFormValue)
138+
// open confirmation modal
139+
openModal(confirmSwitchTenantSlug)
140+
}
141+
}, [
142+
tenantFormValue,
143+
setTenantFormValue,
144+
openModal,
145+
setTenant,
146+
selectedTenantID,
147+
modalIsOpen,
148+
modified,
149+
])
150+
151+
return (
152+
<ConfirmationModal
153+
body={
154+
<Translation
155+
elements={{
156+
0: ({ children }) => {
157+
return <b>{children}</b>
158+
},
159+
}}
160+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
161+
// @ts-expect-error
162+
i18nKey="plugin-multi-tenant:confirm-modal-tenant-switch--body"
163+
t={t}
164+
variables={{
165+
fromTenant: fromTenantOption?.label,
166+
toTenant: toTenantOption?.label,
167+
}}
168+
/>
169+
}
170+
heading={t('plugin-multi-tenant:confirm-modal-tenant-switch--heading', {
171+
tenantLabel: fieldLabel
172+
? getTranslation(fieldLabel, i18n)
173+
: t('plugin-multi-tenant:nav-tenantSelector-label'),
174+
})}
175+
modalSlug={confirmSwitchTenantSlug}
176+
onCancel={() => {
177+
setModified(testRef.current)
178+
}}
179+
onConfirm={() => {
180+
// set the form value to the tenant to confirm
181+
prevTenantValueRef.current = tenantToConfirm
182+
setTenantFormValue(tenantToConfirm)
183+
}}
184+
/>
185+
)
186+
}
187+
188+
/**
189+
* Tells the global selector when the form has been modified
190+
* so it can display the "Leave without saving" confirmation modal
191+
* if modified and attempting to change the tenant
192+
*/
193+
const SyncFormModified = () => {
194+
const modified = useFormModified()
195+
const { setModified } = useTenantSelection()
196+
197+
React.useEffect(() => {
198+
setModified(modified)
199+
}, [modified, setModified])
200+
201+
return null
202+
}
Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,48 @@
1-
.tenantField {
2-
&__wrapper {
3-
margin-top: calc(-.75 * var(--spacing-field));
4-
margin-bottom: var(--spacing-field);
5-
width: 25%;
1+
.document-fields__main {
2+
--tenant-gutter-h-right: var(--main-gutter-h-right);
3+
--tenant-gutter-h-left: var(--main-gutter-h-left);
4+
}
5+
.document-fields__sidebar-fields {
6+
--tenant-gutter-h-right: var(--sidebar-gutter-h-right);
7+
--tenant-gutter-h-left: var(--sidebar-gutter-h-left);
8+
}
9+
.document-fields__sidebar-fields,
10+
.document-fields__main {
11+
.render-fields {
12+
.tenantField {
13+
width: calc(100% + var(--tenant-gutter-h-right) + var(--tenant-gutter-h-left));
14+
margin-left: calc(-1 * var(--tenant-gutter-h-left));
15+
border-bottom: 1px solid var(--theme-elevation-100);
16+
padding-top: calc(var(--base) * 1);
17+
padding-bottom: calc(var(--base) * 1.75);
18+
19+
&__wrapper {
20+
padding-left: var(--tenant-gutter-h-left);
21+
padding-right: var(--tenant-gutter-h-right);
22+
}
23+
24+
[dir='rtl'] & {
25+
margin-right: calc(-1 * var(--tenant-gutter-h-right));
26+
background-image: repeating-linear-gradient(
27+
-120deg,
28+
var(--theme-elevation-50) 0px,
29+
var(--theme-elevation-50) 1px,
30+
transparent 1px,
31+
transparent 5px
32+
);
33+
}
34+
35+
&:not(:first-child) {
36+
border-top: 1px solid var(--theme-elevation-100);
37+
margin-top: calc(var(--base) * 1.25);
38+
}
39+
&:not(:last-child) {
40+
margin-bottom: var(--spacing-field);
41+
}
42+
&:first-child {
43+
margin-top: calc(var(--base) * -1.5);
44+
padding-top: calc(var(--base) * 1.5);
45+
}
46+
}
647
}
7-
&__hr {
8-
width: calc(100% + 2 * var(--gutter-h));
9-
margin-left: calc(-1 * var(--gutter-h));
10-
background-color: var(--theme-elevation-100);
11-
height: 1px;
12-
margin-bottom: var(--spacing-field);
13-
}
14-
}
48+
}

0 commit comments

Comments
 (0)