Skip to content

feat(plugin-multi-tenant): visible tenant field on documents #13379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
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
93 changes: 57 additions & 36 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,12 +75,15 @@ 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
/**
* Overrides for the tenant field, will override the entire tenantField configuration
*/
tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
/**
* Set to `false` if you want to manually apply
* the baseFilter
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,17 +222,17 @@ 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
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,62 +16,56 @@ type Props = {
} & RelationshipFieldClientProps

export const TenantField = (args: Props) => {
const { debug, unique } = args
const { setValue, value } = useField<number | string>()
const modified = useFormModified()
const {
options,
selectedTenantID,
setEntityType: setEntityType,
setModified,
setTenant,
} = useTenantSelection()

const hasSetValueRef = React.useRef(false)
const { options, selectedTenantID, setEntityType, setTenant } = useTenantSelection()
const { value } = useField<number | string>()

React.useEffect(() => {
if (!hasSetValueRef.current) {
// set value on load
if (value && value !== selectedTenantID) {
setTenant({ id: value, refresh: unique })
} else {
// in the document view, the tenant field should always have a value
const defaultValue = selectedTenantID || options[0]?.value
setTenant({ id: defaultValue, refresh: unique })
setEntityType(args.unique ? 'global' : 'document')

if (!args.unique) {
// unique documents are controlled from the global TenantSelector
if (args.unique && !selectedTenantID) {
// set default tenant for the global
if (options.length > 0) {
setTenant({ id: options[0]?.value, refresh: true })
}
} else if (value) {
if (!selectedTenantID || value !== selectedTenantID) {
setTenant({ id: value, refresh: Boolean(args.unique) })
}
}
hasSetValueRef.current = true
} else if (!value || value !== selectedTenantID) {
// Update the field on the document value when the tenant is changed
setValue(selectedTenantID, !value || value === selectedTenantID)
}
}, [value, selectedTenantID, setTenant, setValue, options, unique])

React.useEffect(() => {
setEntityType(unique ? 'global' : 'document')
return () => {
setEntityType(undefined)
}
}, [unique, setEntityType])

React.useEffect(() => {
// sync form modified state with the tenant selection provider context
setModified(modified)

return () => {
setModified(false)
}
}, [modified, setModified])
}, [args.unique, options, selectedTenantID, setTenant, value, setEntityType])

if (debug) {
if (options.length > 1 && !args.unique) {
return (
<div className={baseClass}>
<div className={`${baseClass}__wrapper`}>
<RelationshipField {...args} />
<>
<div className={baseClass}>
<div className={`${baseClass}__wrapper`}>
<RelationshipField {...args} />
</div>
</div>
<div className={`${baseClass}__hr`} />
</div>
<SyncFormModified />
</>
)
} else if (args.unique) {
return <SyncFormModified />
}

return null
}

const SyncFormModified = () => {
const modified = useFormModified()
const { setModified } = useTenantSelection()

React.useEffect(() => {
setModified(modified)
}, [modified, setModified])

return null
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
.tenantField {
&__wrapper {
margin-top: calc(-.75 * var(--spacing-field));
margin-bottom: var(--spacing-field);
width: 25%;
.document-fields__main {
--tenant-gutter-h-right: var(--main-gutter-h-right);
--tenant-gutter-h-left: var(--main-gutter-h-left);
}
.document-fields__sidebar-fields {
--tenant-gutter-h-right: var(--sidebar-gutter-h-right);
--tenant-gutter-h-left: var(--sidebar-gutter-h-left);
}
.document-fields__sidebar-fields,
.document-fields__main {
.render-fields {
.tenantField {
width: calc(100% + var(--tenant-gutter-h-right) + var(--tenant-gutter-h-left));
margin-left: calc(-1 * var(--tenant-gutter-h-left));
border-bottom: 1px solid var(--theme-elevation-100);
padding-top: calc(var(--base) * 1);
padding-bottom: calc(var(--base) * 1.75);

&__wrapper {
padding-left: var(--tenant-gutter-h-left);
padding-right: var(--tenant-gutter-h-right);
}

[dir='rtl'] & {
margin-right: calc(-1 * var(--tenant-gutter-h-right));
background-image: repeating-linear-gradient(
-120deg,
var(--theme-elevation-50) 0px,
var(--theme-elevation-50) 1px,
transparent 1px,
transparent 5px
);
}

&:not(:first-child) {
border-top: 1px solid var(--theme-elevation-100);
margin-top: calc(var(--base) * 1.25);
}
&:not(:last-child) {
margin-bottom: var(--spacing-field);
}
&:first-child {
margin-top: calc(var(--base) * -1.5);
padding-top: calc(var(--base) * 1.5);
}
}
}
&__hr {
width: calc(100% + 2 * var(--gutter-h));
margin-left: calc(-1 * var(--gutter-h));
background-color: var(--theme-elevation-100);
height: 1px;
margin-bottom: var(--spacing-field);
}
}
}
Loading
Loading