Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 61 additions & 42 deletions docs/plugins/multi-tenant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,15 @@ The plugin accepts an object with the following properties:
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* After a tenant is deleted, the plugin will attempt
* to clean up related documents
* Base path for your application
*
* https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath
*
* @default undefined
*/
basePath?: string
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
*
Expand All @@ -68,15 +75,18 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
collections: {
[key in CollectionSlug]?: {
/**
* Set to `true` if you want the collection to
* behave as a global
* Set to `true` if you want the collection to behave as a global
*
* @default false
*/
isGlobal?: boolean
/**
* Set to `false` if you want to manually apply
* the baseFilter
* Overrides for the tenant field, will override the entire tenantField configuration
*/
tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
/**
* Set to `false` if you want to manually apply the baseListFilter
* Set to `false` if you want to manually apply the baseFilter
*
* @default true
*/
Expand All @@ -94,8 +104,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
useBaseListFilter?: boolean
/**
* Set to `false` if you want to handle collection access
* manually without the multi-tenant constraints applied
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
*
* @default true
*/
Expand All @@ -104,8 +113,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
}
/**
* Enables debug mode
* - Makes the tenant field visible in the
* admin UI within applicable collections
* - Makes the tenant field visible in the admin UI within applicable collections
*
* @default false
*/
Expand All @@ -117,27 +125,41 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
enabled?: boolean
/**
* Field configuration for the field added
* to all tenant enabled collections
* Localization for the plugin
*/
tenantField?: {
access?: RelationshipField['access']
/**
* The name of the field added to all tenant
* enabled collections
*
* @default 'tenant'
*/
name?: string
i18n?: {
translations: {
[key in AcceptedLanguages]?: {
/**
* @default 'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>'
*/
'confirm-modal-tenant-switch--body'?: string
/**
* `tenantLabel` defaults to the value of the `nav-tenantSelector-label` translation
*
* @default 'Confirm {{tenantLabel}} change'
*/
'confirm-modal-tenant-switch--heading'?: string
/**
* @default 'Assigned Tenant'
*/
'field-assignedTenant-label'?: string
/**
* @default 'Tenant'
*/
'nav-tenantSelector-label'?: string
}
}
}
/**
* Field configuration for the field added
* to the users collection
* Field configuration for the field added to all tenant enabled collections
*/
tenantField?: RootTenantFieldConfigOverrides
/**
* Field configuration for the field added to the users collection
*
* If `includeDefaultField` is `false`, you must
* include the field on your users collection manually
* This is useful if you want to customize the field
* or place the field in a specific location
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
* This is useful if you want to customize the field or place the field in a specific location
*/
tenantsArrayField?:
| {
Expand All @@ -158,8 +180,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
arrayTenantFieldName?: string
/**
* When `includeDefaultField` is `true`, the field will
* be added to the users collection automatically
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
*/
includeDefaultField?: true
/**
Expand All @@ -176,8 +197,7 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
arrayFieldName?: string
arrayTenantFieldName?: string
/**
* When `includeDefaultField` is `false`, you must
* include the field on your users collection manually
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
*/
includeDefaultField?: false
rowFields?: never
Expand All @@ -186,8 +206,9 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* Customize tenant selector label
*
* Either a string or an object where the keys are i18n
* codes and the values are the string labels
* Either a string or an object where the keys are i18n codes and the values are the string labels
*
* @deprecated Use `i18n.translations` instead.
*/
tenantSelectorLabel?:
| Partial<{
Expand All @@ -201,27 +222,25 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
tenantsSlug?: string
/**
* Function that determines if a user has access
* to _all_ tenants
* Function that determines if a user has access to _all_ tenants
*
* Useful for super-admin type users
*/
userHasAccessToAllTenants?: (
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
user: ConfigTypes extends { user: unknown }
? ConfigTypes['user']
: TypedUser,
) => boolean
/**
* Opt out of adding access constraints to
* the tenants collection
* Opt out of adding access constraints to the tenants collection
*/
useTenantsCollectionAccess?: boolean
/**
* Opt out including the baseFilter to filter
* tenants by selected tenant
* Opt out including the baseListFilter to filter tenants by selected tenant
*/
useTenantsListFilter?: boolean
/**
* Opt out including the baseFilter to filter
* users by selected tenant
* Opt out including the baseListFilter to filter users by selected tenant
*/
useUsersTenantFilter?: boolean
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
<div className="tenant-selector">
<SelectInput
isClearable={viewType === 'list'}
label={getTranslation(label, i18n)}
label={
label ? getTranslation(label, i18n) : t('plugin-multi-tenant:nav-tenantSelector-label')
}
name="setTenant"
onChange={onChange}
options={options}
Expand All @@ -110,16 +112,18 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
}}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
i18nKey="plugin-multi-tenant:confirm-tenant-switch--body"
i18nKey="plugin-multi-tenant:confirm-modal-tenant-switch--body"
t={t}
variables={{
fromTenant: selectedValue?.label,
toTenant: newSelectedValue?.label,
}}
/>
}
heading={t('plugin-multi-tenant:confirm-tenant-switch--heading', {
tenantLabel: getTranslation(label, i18n),
heading={t('plugin-multi-tenant:confirm-modal-tenant-switch--heading', {
tenantLabel: label
? getTranslation(label, i18n)
: t('plugin-multi-tenant:nav-tenantSelector-label'),
})}
modalSlug={confirmSwitchTenantSlug}
onConfirm={() => {
Expand Down
1 change: 0 additions & 1 deletion packages/plugin-multi-tenant/src/exports/fields.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export { tenantField } from '../fields/tenantField/index.js'
export { tenantsArrayField } from '../fields/tenantsArrayField/index.js'
104 changes: 61 additions & 43 deletions packages/plugin-multi-tenant/src/fields/tenantField/index.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,83 @@
import { type RelationshipField } from 'payload'
import type { RelationshipFieldSingleValidation, SingleRelationshipField } from 'payload'

import { APIError } from 'payload'

import type { RootTenantFieldConfigOverrides } from '../../types.js'

import { defaults } from '../../defaults.js'
import { getCollectionIDType } from '../../utilities/getCollectionIDType.js'
import { getTenantFromCookie } from '../../utilities/getTenantFromCookie.js'

type Args = {
access?: RelationshipField['access']
debug?: boolean
name: string
overrides?: RootTenantFieldConfigOverrides
tenantsCollectionSlug: string
unique: boolean
}
export const tenantField = ({
name = defaults.tenantFieldName,
access = undefined,
debug,
overrides: _overrides = {},
tenantsCollectionSlug = defaults.tenantCollectionSlug,
unique,
}: Args): RelationshipField => ({
name,
type: 'relationship',
access,
admin: {
allowCreate: false,
allowEdit: false,
components: {
Field: {
clientProps: {
debug,
unique,
}: Args): SingleRelationshipField => {
const { validate, ...overrides } = _overrides || {}
return {
...(overrides || {}),
name,
type: 'relationship',
access: overrides?.access || {},
admin: {
allowCreate: false,
allowEdit: false,
disableListColumn: true,
disableListFilter: true,
...(overrides?.admin || {}),
components: {
...(overrides?.admin?.components || {}),
Field: {
path: '@payloadcms/plugin-multi-tenant/client#TenantField',
...(typeof overrides?.admin?.components?.Field !== 'string'
? overrides?.admin?.components?.Field || {}
: {}),
clientProps: {
...(typeof overrides?.admin?.components?.Field !== 'string'
? (overrides?.admin?.components?.Field || {})?.clientProps
: {}),
debug,
unique,
},
},
path: '@payloadcms/plugin-multi-tenant/client#TenantField',
},
},
disableListColumn: true,
disableListFilter: true,
},
hasMany: false,
hooks: {
beforeChange: [
({ req, value }) => {
const idType = getCollectionIDType({
collectionSlug: tenantsCollectionSlug,
payload: req.payload,
})
if (!value) {
const tenantFromCookie = getTenantFromCookie(req.headers, idType)
if (tenantFromCookie) {
return tenantFromCookie
hasMany: false,
hooks: {
...(overrides.hooks || []),
beforeChange: [
({ req, value }) => {
const idType = getCollectionIDType({
collectionSlug: tenantsCollectionSlug,
payload: req.payload,
})
if (!value) {
const tenantFromCookie = getTenantFromCookie(req.headers, idType)
if (tenantFromCookie) {
return tenantFromCookie
}
throw new APIError('You must select a tenant', 400, null, true)
}
throw new APIError('You must select a tenant', 400, null, true)
}

return idType === 'number' ? parseFloat(value) : value
},
],
},
index: true,
// @ts-expect-error translations are not typed for this plugin
label: ({ t }) => t('plugin-multi-tenant:field-assignedTentant-label'),
relationTo: tenantsCollectionSlug,
unique,
})
return idType === 'number' ? parseFloat(value) : value
},
...(overrides?.hooks?.beforeChange || []),
],
},
index: true,
validate: (validate as RelationshipFieldSingleValidation) || undefined,
// @ts-expect-error translations are not typed for this plugin
label: overrides?.label || (({ t }) => t('plugin-multi-tenant:field-assignedTenant-label')),
relationTo: tenantsCollectionSlug,
unique,
}
}
Loading
Loading