Skip to content

Commit e6d0260

Browse files
fix(plugin-multi-tenant): selected tenant could become incorrect when navigating out of doc (#10723)
### What? When switching tenants from within a document and then navigating back out to the list view, the tenant would not be set correctly. ### Why? This was because we handle the tenant selector selection differently when viewing a document. ### How? Now when you navigate out, the page will refresh the cookie. Also adds test suite config that shows how the dom can be used to manipulate styles per tenant.
1 parent c1b912d commit e6d0260

File tree

14 files changed

+145
-33
lines changed

14 files changed

+145
-33
lines changed

examples/multi-tenant/src/payload-types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,17 @@ export interface Page {
7878
export interface Tenant {
7979
id: string;
8080
name: string;
81+
/**
82+
* Used for domain-based tenant handling
83+
*/
8184
domain?: string | null;
85+
/**
86+
* Used for url paths, example: /tenant-slug/page-slug
87+
*/
8288
slug: string;
89+
/**
90+
* If checked, logging in is not required to read. Useful for building public pages.
91+
*/
8392
allowPublicRead?: boolean | null;
8493
updatedAt: string;
8594
createdAt: string;

packages/plugin-multi-tenant/src/components/GlobalViewRedirect/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CollectionSlug, ServerProps, ViewTypes } from 'payload'
22

3+
import { headers as getHeaders } from 'next/headers.js'
34
import { redirect } from 'next/navigation.js'
45

56
import { getGlobalViewRedirect } from '../../utilities/getGlobalViewRedirect.js'
@@ -16,9 +17,11 @@ export const GlobalViewRedirect = async (args: Args) => {
1617
const collectionSlug = args?.collectionSlug
1718

1819
if (collectionSlug && args.globalSlugs?.includes(collectionSlug)) {
20+
const headers = await getHeaders()
1921
const redirectRoute = await getGlobalViewRedirect({
2022
slug: collectionSlug,
2123
docID: args.docID,
24+
headers,
2225
payload: args.payload,
2326
tenantFieldName: args.tenantFieldName,
2427
view: args.viewType,

packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export const TenantSelectionProviderClient = ({
3737
const [preventRefreshOnChange, setPreventRefreshOnChange] = React.useState(false)
3838
const { user } = useAuth()
3939
const userID = React.useMemo(() => user?.id, [user?.id])
40+
const selectedTenantLabel = React.useMemo(
41+
() => tenantOptions.find((option) => option.value === selectedTenantID)?.label,
42+
[selectedTenantID, tenantOptions],
43+
)
4044

4145
const router = useRouter()
4246

@@ -80,16 +84,21 @@ export const TenantSelectionProviderClient = ({
8084
}, [userID, router])
8185

8286
return (
83-
<Context.Provider
84-
value={{
85-
options: tenantOptions,
86-
selectedTenantID,
87-
setPreventRefreshOnChange,
88-
setTenant,
89-
}}
87+
<span
88+
data-selected-tenant-id={selectedTenantID}
89+
data-selected-tenant-title={selectedTenantLabel}
9090
>
91-
{children}
92-
</Context.Provider>
91+
<Context.Provider
92+
value={{
93+
options: tenantOptions,
94+
selectedTenantID,
95+
setPreventRefreshOnChange,
96+
setTenant,
97+
}}
98+
>
99+
{children}
100+
</Context.Provider>
101+
</span>
93102
)
94103
}
95104

packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,10 @@ export const TenantSelectionProvider = async ({
4141

4242
const cookies = await getCookies()
4343
const tenantCookie = cookies.get('payload-tenant')?.value
44-
const selectedTenant =
45-
tenantOptions.find((option) => option.value === tenantCookie)?.label || tenantCookie
4644

4745
return (
48-
<span data-selected-tenant-id={tenantCookie} data-selected-tenant-title={selectedTenant}>
49-
<TenantSelectionProviderClient initialValue={tenantCookie} tenantOptions={tenantOptions}>
50-
{children}
51-
</TenantSelectionProviderClient>
52-
</span>
46+
<TenantSelectionProviderClient initialValue={tenantCookie} tenantOptions={tenantOptions}>
47+
{children}
48+
</TenantSelectionProviderClient>
5349
)
5450
}

packages/plugin-multi-tenant/src/utilities/getGlobalViewRedirect.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import type { Payload, ViewTypes } from 'payload'
22

3-
import { headers as getHeaders } from 'next/headers.js'
4-
53
import { SELECT_ALL } from '../constants.js'
64
import { getTenantFromCookie } from './getTenantFromCookie.js'
75

86
type Args = {
97
docID?: number | string
8+
headers: Headers
109
payload: Payload
1110
slug: string
1211
tenantFieldName: string
@@ -15,11 +14,11 @@ type Args = {
1514
export async function getGlobalViewRedirect({
1615
slug,
1716
docID,
17+
headers,
1818
payload,
1919
tenantFieldName,
2020
view,
2121
}: Args): Promise<string | void> {
22-
const headers = await getHeaders()
2322
const tenant = getTenantFromCookie(headers, payload.db.defaultIDType)
2423
let redirectRoute
2524

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CollectionConfig } from 'payload'
22

33
import { postsSlug } from '../shared.js'
4+
import { userFilterOptions } from './Users/filterOptions.js'
45

56
export const Posts: CollectionConfig = {
67
slug: postsSlug,
@@ -18,20 +19,16 @@ export const Posts: CollectionConfig = {
1819
type: 'text',
1920
required: true,
2021
},
21-
{
22-
name: 'excerpt',
23-
label: 'Excerpt',
24-
type: 'text',
25-
},
26-
{
27-
type: 'text',
28-
name: 'slug',
29-
localized: true,
30-
},
3122
{
3223
name: 'relatedLinks',
3324
relationTo: 'links',
3425
type: 'relationship',
3526
},
27+
{
28+
name: 'author',
29+
relationTo: 'users',
30+
type: 'relationship',
31+
filterOptions: userFilterOptions,
32+
},
3633
],
3734
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { FilterOptions, Where } from 'payload'
2+
3+
import { getTenantFromCookie } from '@payloadcms/plugin-multi-tenant/utilities'
4+
5+
export const userFilterOptions: FilterOptions = ({ req }) => {
6+
const selectedTenant = getTenantFromCookie(req.headers, req.payload.db.defaultIDType)
7+
if (!selectedTenant) {
8+
return false
9+
}
10+
11+
return {
12+
or: [
13+
{
14+
'tenants.tenant': {
15+
equals: selectedTenant,
16+
},
17+
},
18+
{
19+
roles: {
20+
in: ['admin'],
21+
},
22+
},
23+
],
24+
} as Where
25+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
import { usersSlug } from '../../shared.js'
4+
5+
export const Users: CollectionConfig = {
6+
slug: usersSlug,
7+
auth: true,
8+
admin: {
9+
useAsTitle: 'email',
10+
},
11+
access: {
12+
read: () => true,
13+
},
14+
fields: [
15+
// Email added by default
16+
// Add more fields as needed
17+
{
18+
type: 'select',
19+
name: 'roles',
20+
hasMany: true,
21+
options: [
22+
{
23+
label: 'Admin',
24+
value: 'admin',
25+
},
26+
{
27+
label: 'User',
28+
value: 'user',
29+
},
30+
],
31+
saveToJWT: true,
32+
},
33+
],
34+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import './styles.css'
2+
3+
export function Icon() {
4+
return <div id="tenant-icon" />
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#tenant-icon {
2+
border-radius: 100%;
3+
height: 18px;
4+
width: 18px;
5+
}
6+
7+
[data-selected-tenant-title="Blue Dog"] #tenant-icon {
8+
background-color: var(--theme-success-300);
9+
}
10+
11+
[data-selected-tenant-title="Steel Cat"] #tenant-icon {
12+
background-color: var(--theme-warning-300);
13+
}

0 commit comments

Comments
 (0)