feat(plugin-multi-tenant): allow opting out of tenant access control merge (#10888)
### What? In some cases you may want to opt out of using the default access control that this plugin provides on the tenants collection. ### Why? Other collections are able to opt out of this already, but the tenants collection specifically was not configured with an opt out capability. ### How? Adds new property to the plugin config: `useTenantsCollectionAccess`. Setting this to `false` allows users to opt out and write their own access control functions without the plugin merging in its own constraints for the tenant collection. Fixes https://github.com/payloadcms/payload/issues/10882
This commit is contained in:
@@ -25,6 +25,18 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
|
||||
- Adds a `tenant` field to each specified collection
|
||||
- Adds a tenant selector to the admin panel, allowing you to switch between tenants
|
||||
- Filters list view results by selected tenant
|
||||
- Filters relationship fields by selected tenant
|
||||
- Ability to create "global" like collections, 1 doc per tenant
|
||||
- Automatically assign a tenant to new documents
|
||||
|
||||
<Banner type="error">
|
||||
**Warning**
|
||||
|
||||
By default this plugin cleans up documents when a tenant is deleted. You should ensure you have
|
||||
strong access control on your tenants collection to prevent deletions by unauthorized users.
|
||||
|
||||
You can disabled this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
|
||||
</Banner>
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -40,7 +52,7 @@ 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
|
||||
* - removing documents with the tenant ID
|
||||
* - removing the tenant from users
|
||||
@@ -144,8 +156,12 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
* Useful for super-admin type users
|
||||
*/
|
||||
userHasAccessToAllTenants?: (
|
||||
user: ConfigTypes extends { user } ? ConfigTypes['user'] : User,
|
||||
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
|
||||
) => boolean
|
||||
/**
|
||||
* Opt out of adding access constraints to the tenants collection
|
||||
*/
|
||||
useTenantsCollectionAccess?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@ export const multiTenantPlugin =
|
||||
if (collection.slug === tenantsCollectionSlug) {
|
||||
tenantCollection = collection
|
||||
|
||||
if (pluginConfig.useTenantsCollectionAccess !== false) {
|
||||
/**
|
||||
* Add access control constraint to tenants collection
|
||||
* - constrains access a users assigned tenants
|
||||
@@ -137,6 +138,7 @@ export const multiTenantPlugin =
|
||||
fieldName: 'id',
|
||||
userHasAccessToAllTenants,
|
||||
})
|
||||
}
|
||||
|
||||
if (pluginConfig.cleanupAfterTenantDelete !== false) {
|
||||
/**
|
||||
|
||||
@@ -121,6 +121,10 @@ export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
userHasAccessToAllTenants?: (
|
||||
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
|
||||
) => boolean
|
||||
/**
|
||||
* Opt out of adding access constraints to the tenants collection
|
||||
*/
|
||||
useTenantsCollectionAccess?: boolean
|
||||
}
|
||||
|
||||
export type Tenant<IDType = number | string> = {
|
||||
|
||||
@@ -79,6 +79,10 @@ const RichTextComponent: React.FC<
|
||||
const disabled = readOnlyFromProps || formProcessing || formInitializing
|
||||
|
||||
const [isSmallWidthViewport, setIsSmallWidthViewport] = useState<boolean>(false)
|
||||
const [rerenderProviderKey, setRerenderProviderKey] = useState<Date>()
|
||||
|
||||
const prevInitialValueRef = React.useRef<SerializedEditorState | undefined>(initialValue)
|
||||
const prevValueRef = React.useRef<SerializedEditorState | undefined>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const updateViewPortWidth = () => {
|
||||
@@ -113,13 +117,24 @@ const RichTextComponent: React.FC<
|
||||
|
||||
const handleChange = useCallback(
|
||||
(editorState: EditorState) => {
|
||||
setValue(editorState.toJSON())
|
||||
const newState = editorState.toJSON()
|
||||
prevValueRef.current = newState
|
||||
setValue(newState)
|
||||
},
|
||||
[setValue],
|
||||
)
|
||||
|
||||
const styles = useMemo(() => mergeFieldStyles(field), [field])
|
||||
|
||||
useEffect(() => {
|
||||
if (JSON.stringify(initialValue) !== JSON.stringify(prevInitialValueRef.current)) {
|
||||
prevInitialValueRef.current = initialValue
|
||||
if (JSON.stringify(prevValueRef.current) !== JSON.stringify(value)) {
|
||||
setRerenderProviderKey(new Date())
|
||||
}
|
||||
}
|
||||
}, [initialValue, value])
|
||||
|
||||
return (
|
||||
<div className={classes} key={pathWithEditDepth} style={styles}>
|
||||
<RenderCustomComponent
|
||||
@@ -135,7 +150,7 @@ const RichTextComponent: React.FC<
|
||||
editorConfig={editorConfig}
|
||||
fieldProps={props}
|
||||
isSmallWidthViewport={isSmallWidthViewport}
|
||||
key={JSON.stringify({ initialValue, path })} // makes sure lexical is completely re-rendered when initialValue changes, bypassing the lexical-internal value memoization. That way, external changes to the form will update the editor. More infos in PR description (https://github.com/payloadcms/payload/pull/5010)
|
||||
key={JSON.stringify({ path, rerenderProviderKey })} // makes sure lexical is completely re-rendered when initialValue changes, bypassing the lexical-internal value memoization. That way, external changes to the form will update the editor. More infos in PR description (https://github.com/payloadcms/payload/pull/5010)
|
||||
onChange={handleChange}
|
||||
readOnly={disabled}
|
||||
value={value}
|
||||
|
||||
@@ -33,6 +33,7 @@ export const mergeServerFormState = ({
|
||||
|
||||
if (acceptValues) {
|
||||
serverPropsToAccept.push('value')
|
||||
serverPropsToAccept.push('initialValue')
|
||||
}
|
||||
|
||||
let changed = false
|
||||
|
||||
28
test/fields/collections/Lexical/components/ClearState.tsx
Normal file
28
test/fields/collections/Lexical/components/ClearState.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { useForm } from '@payloadcms/ui'
|
||||
import React from 'react'
|
||||
|
||||
export const ClearState = ({ fieldName }: { fieldName: string }) => {
|
||||
const { dispatchFields, fields } = useForm()
|
||||
|
||||
const clearState = React.useCallback(() => {
|
||||
dispatchFields({
|
||||
type: 'REPLACE_STATE',
|
||||
state: {
|
||||
...fields,
|
||||
[fieldName]: {
|
||||
...fields[fieldName],
|
||||
initialValue: null,
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
}, [dispatchFields, fields, fieldName])
|
||||
|
||||
return (
|
||||
<button id={`clear-lexical-${fieldName}`} onClick={clearState} type="button">
|
||||
Clear State
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -264,6 +264,19 @@ describe('lexicalMain', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('should be able to externally mutate editor state', async () => {
|
||||
await navigateToLexicalFields()
|
||||
const richTextField = page.locator('.rich-text-lexical').nth(1).locator('.editor-scroller') // first
|
||||
await expect(richTextField).toBeVisible()
|
||||
await richTextField.click() // Use click, because focus does not work
|
||||
await page.keyboard.type('some text')
|
||||
const spanInEditor = richTextField.locator('span').first()
|
||||
await expect(spanInEditor).toHaveText('some text')
|
||||
await saveDocAndAssert(page)
|
||||
await page.locator('#clear-lexical-lexicalSimple').click()
|
||||
await expect(spanInEditor).not.toBeAttached()
|
||||
})
|
||||
|
||||
test('should be able to bold text using floating select toolbar', async () => {
|
||||
await navigateToLexicalFields()
|
||||
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
|
||||
|
||||
@@ -317,6 +317,20 @@ export const LexicalFields: CollectionConfig = {
|
||||
],
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'clearLexicalState',
|
||||
admin: {
|
||||
components: {
|
||||
Field: {
|
||||
path: '/collections/Lexical/components/ClearState.js#ClearState',
|
||||
clientProps: {
|
||||
fieldName: 'lexicalSimple',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'lexicalWithBlocks',
|
||||
type: 'richText',
|
||||
|
||||
Reference in New Issue
Block a user