Skip to content

Commit 995f96b

Browse files
feat(plugin-multi-tenant): allow tenant field overrides (#13316)
Allows user to override more of the tenant field config. Now you can override most of the field config with: ### At the root level ```ts /** * Field configuration for the field added to all tenant enabled collections */ tenantField?: RootTenantFieldConfigOverrides ``` ### At the collection level Setting collection level overrides will replace the root level overrides shown above. ```ts collections: { [key in CollectionSlug]?: { // ... rest of the types /** * Overrides for the tenant field, will override the entire tenantField configuration */ tenantFieldOverrides?: CollectionTenantFieldConfigOverrides } } ```
1 parent 306b7f6 commit 995f96b

File tree

52 files changed

+478
-303
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+478
-303
lines changed

docs/plugins/multi-tenant.mdx

Lines changed: 61 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,15 @@ The plugin accepts an object with the following properties:
5454
```ts
5555
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
5656
/**
57-
* After a tenant is deleted, the plugin will attempt
58-
* to clean up related documents
57+
* Base path for your application
58+
*
59+
* https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath
60+
*
61+
* @default undefined
62+
*/
63+
basePath?: string
64+
/**
65+
* After a tenant is deleted, the plugin will attempt to clean up related documents
5966
* - removing documents with the tenant ID
6067
* - removing the tenant from users
6168
*
@@ -68,15 +75,18 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
6875
collections: {
6976
[key in CollectionSlug]?: {
7077
/**
71-
* Set to `true` if you want the collection to
72-
* behave as a global
78+
* Set to `true` if you want the collection to behave as a global
7379
*
7480
* @default false
7581
*/
7682
isGlobal?: boolean
7783
/**
78-
* Set to `false` if you want to manually apply
79-
* the baseFilter
84+
* Overrides for the tenant field, will override the entire tenantField configuration
85+
*/
86+
tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
87+
/**
88+
* Set to `false` if you want to manually apply the baseListFilter
89+
* Set to `false` if you want to manually apply the baseFilter
8090
*
8191
* @default true
8292
*/
@@ -94,8 +104,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
94104
*/
95105
useBaseListFilter?: boolean
96106
/**
97-
* Set to `false` if you want to handle collection access
98-
* manually without the multi-tenant constraints applied
107+
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
99108
*
100109
* @default true
101110
*/
@@ -104,8 +113,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
104113
}
105114
/**
106115
* Enables debug mode
107-
* - Makes the tenant field visible in the
108-
* admin UI within applicable collections
116+
* - Makes the tenant field visible in the admin UI within applicable collections
109117
*
110118
* @default false
111119
*/
@@ -117,27 +125,41 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
117125
*/
118126
enabled?: boolean
119127
/**
120-
* Field configuration for the field added
121-
* to all tenant enabled collections
128+
* Localization for the plugin
122129
*/
123-
tenantField?: {
124-
access?: RelationshipField['access']
125-
/**
126-
* The name of the field added to all tenant
127-
* enabled collections
128-
*
129-
* @default 'tenant'
130-
*/
131-
name?: string
130+
i18n?: {
131+
translations: {
132+
[key in AcceptedLanguages]?: {
133+
/**
134+
* @default 'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>'
135+
*/
136+
'confirm-modal-tenant-switch--body'?: string
137+
/**
138+
* `tenantLabel` defaults to the value of the `nav-tenantSelector-label` translation
139+
*
140+
* @default 'Confirm {{tenantLabel}} change'
141+
*/
142+
'confirm-modal-tenant-switch--heading'?: string
143+
/**
144+
* @default 'Assigned Tenant'
145+
*/
146+
'field-assignedTenant-label'?: string
147+
/**
148+
* @default 'Tenant'
149+
*/
150+
'nav-tenantSelector-label'?: string
151+
}
152+
}
132153
}
133154
/**
134-
* Field configuration for the field added
135-
* to the users collection
155+
* Field configuration for the field added to all tenant enabled collections
156+
*/
157+
tenantField?: RootTenantFieldConfigOverrides
158+
/**
159+
* Field configuration for the field added to the users collection
136160
*
137-
* If `includeDefaultField` is `false`, you must
138-
* include the field on your users collection manually
139-
* This is useful if you want to customize the field
140-
* or place the field in a specific location
161+
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
162+
* This is useful if you want to customize the field or place the field in a specific location
141163
*/
142164
tenantsArrayField?:
143165
| {
@@ -158,8 +180,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
158180
*/
159181
arrayTenantFieldName?: string
160182
/**
161-
* When `includeDefaultField` is `true`, the field will
162-
* be added to the users collection automatically
183+
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
163184
*/
164185
includeDefaultField?: true
165186
/**
@@ -176,8 +197,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
176197
arrayFieldName?: string
177198
arrayTenantFieldName?: string
178199
/**
179-
* When `includeDefaultField` is `false`, you must
180-
* include the field on your users collection manually
200+
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
181201
*/
182202
includeDefaultField?: false
183203
rowFields?: never
@@ -186,8 +206,9 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
186206
/**
187207
* Customize tenant selector label
188208
*
189-
* Either a string or an object where the keys are i18n
190-
* codes and the values are the string labels
209+
* Either a string or an object where the keys are i18n codes and the values are the string labels
210+
*
211+
* @deprecated Use `i18n.translations` instead.
191212
*/
192213
tenantSelectorLabel?:
193214
| Partial<{
@@ -201,27 +222,25 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
201222
*/
202223
tenantsSlug?: string
203224
/**
204-
* Function that determines if a user has access
205-
* to _all_ tenants
225+
* Function that determines if a user has access to _all_ tenants
206226
*
207227
* Useful for super-admin type users
208228
*/
209229
userHasAccessToAllTenants?: (
210-
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
230+
user: ConfigTypes extends { user: unknown }
231+
? ConfigTypes['user']
232+
: TypedUser,
211233
) => boolean
212234
/**
213-
* Opt out of adding access constraints to
214-
* the tenants collection
235+
* Opt out of adding access constraints to the tenants collection
215236
*/
216237
useTenantsCollectionAccess?: boolean
217238
/**
218-
* Opt out including the baseFilter to filter
219-
* tenants by selected tenant
239+
* Opt out including the baseListFilter to filter tenants by selected tenant
220240
*/
221241
useTenantsListFilter?: boolean
222242
/**
223-
* Opt out including the baseFilter to filter
224-
* users by selected tenant
243+
* Opt out including the baseListFilter to filter users by selected tenant
225244
*/
226245
useUsersTenantFilter?: boolean
227246
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
9292
<div className="tenant-selector">
9393
<SelectInput
9494
isClearable={viewType === 'list'}
95-
label={getTranslation(label, i18n)}
95+
label={
96+
label ? getTranslation(label, i18n) : t('plugin-multi-tenant:nav-tenantSelector-label')
97+
}
9698
name="setTenant"
9799
onChange={onChange}
98100
options={options}
@@ -110,16 +112,18 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
110112
}}
111113
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
112114
// @ts-expect-error
113-
i18nKey="plugin-multi-tenant:confirm-tenant-switch--body"
115+
i18nKey="plugin-multi-tenant:confirm-modal-tenant-switch--body"
114116
t={t}
115117
variables={{
116118
fromTenant: selectedValue?.label,
117119
toTenant: newSelectedValue?.label,
118120
}}
119121
/>
120122
}
121-
heading={t('plugin-multi-tenant:confirm-tenant-switch--heading', {
122-
tenantLabel: getTranslation(label, i18n),
123+
heading={t('plugin-multi-tenant:confirm-modal-tenant-switch--heading', {
124+
tenantLabel: label
125+
? getTranslation(label, i18n)
126+
: t('plugin-multi-tenant:nav-tenantSelector-label'),
123127
})}
124128
modalSlug={confirmSwitchTenantSlug}
125129
onConfirm={() => {
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
export { tenantField } from '../fields/tenantField/index.js'
21
export { tenantsArrayField } from '../fields/tenantsArrayField/index.js'
Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,83 @@
1-
import { type RelationshipField } from 'payload'
1+
import type { RelationshipFieldSingleValidation, SingleRelationshipField } from 'payload'
2+
23
import { APIError } from 'payload'
34

5+
import type { RootTenantFieldConfigOverrides } from '../../types.js'
6+
47
import { defaults } from '../../defaults.js'
58
import { getCollectionIDType } from '../../utilities/getCollectionIDType.js'
69
import { getTenantFromCookie } from '../../utilities/getTenantFromCookie.js'
710

811
type Args = {
9-
access?: RelationshipField['access']
1012
debug?: boolean
1113
name: string
14+
overrides?: RootTenantFieldConfigOverrides
1215
tenantsCollectionSlug: string
1316
unique: boolean
1417
}
1518
export const tenantField = ({
1619
name = defaults.tenantFieldName,
17-
access = undefined,
1820
debug,
21+
overrides: _overrides = {},
1922
tenantsCollectionSlug = defaults.tenantCollectionSlug,
2023
unique,
21-
}: Args): RelationshipField => ({
22-
name,
23-
type: 'relationship',
24-
access,
25-
admin: {
26-
allowCreate: false,
27-
allowEdit: false,
28-
components: {
29-
Field: {
30-
clientProps: {
31-
debug,
32-
unique,
24+
}: Args): SingleRelationshipField => {
25+
const { validate, ...overrides } = _overrides || {}
26+
return {
27+
...(overrides || {}),
28+
name,
29+
type: 'relationship',
30+
access: overrides?.access || {},
31+
admin: {
32+
allowCreate: false,
33+
allowEdit: false,
34+
disableListColumn: true,
35+
disableListFilter: true,
36+
...(overrides?.admin || {}),
37+
components: {
38+
...(overrides?.admin?.components || {}),
39+
Field: {
40+
path: '@payloadcms/plugin-multi-tenant/client#TenantField',
41+
...(typeof overrides?.admin?.components?.Field !== 'string'
42+
? overrides?.admin?.components?.Field || {}
43+
: {}),
44+
clientProps: {
45+
...(typeof overrides?.admin?.components?.Field !== 'string'
46+
? (overrides?.admin?.components?.Field || {})?.clientProps
47+
: {}),
48+
debug,
49+
unique,
50+
},
3351
},
34-
path: '@payloadcms/plugin-multi-tenant/client#TenantField',
3552
},
3653
},
37-
disableListColumn: true,
38-
disableListFilter: true,
39-
},
40-
hasMany: false,
41-
hooks: {
42-
beforeChange: [
43-
({ req, value }) => {
44-
const idType = getCollectionIDType({
45-
collectionSlug: tenantsCollectionSlug,
46-
payload: req.payload,
47-
})
48-
if (!value) {
49-
const tenantFromCookie = getTenantFromCookie(req.headers, idType)
50-
if (tenantFromCookie) {
51-
return tenantFromCookie
54+
hasMany: false,
55+
hooks: {
56+
...(overrides.hooks || []),
57+
beforeChange: [
58+
({ req, value }) => {
59+
const idType = getCollectionIDType({
60+
collectionSlug: tenantsCollectionSlug,
61+
payload: req.payload,
62+
})
63+
if (!value) {
64+
const tenantFromCookie = getTenantFromCookie(req.headers, idType)
65+
if (tenantFromCookie) {
66+
return tenantFromCookie
67+
}
68+
throw new APIError('You must select a tenant', 400, null, true)
5269
}
53-
throw new APIError('You must select a tenant', 400, null, true)
54-
}
5570

56-
return idType === 'number' ? parseFloat(value) : value
57-
},
58-
],
59-
},
60-
index: true,
61-
// @ts-expect-error translations are not typed for this plugin
62-
label: ({ t }) => t('plugin-multi-tenant:field-assignedTentant-label'),
63-
relationTo: tenantsCollectionSlug,
64-
unique,
65-
})
71+
return idType === 'number' ? parseFloat(value) : value
72+
},
73+
...(overrides?.hooks?.beforeChange || []),
74+
],
75+
},
76+
index: true,
77+
validate: (validate as RelationshipFieldSingleValidation) || undefined,
78+
// @ts-expect-error translations are not typed for this plugin
79+
label: overrides?.label || (({ t }) => t('plugin-multi-tenant:field-assignedTenant-label')),
80+
relationTo: tenantsCollectionSlug,
81+
unique,
82+
}
83+
}

0 commit comments

Comments
 (0)