fix(richtext-lexical, plugin-multi-tenant): text editor exposes documents from other tenants (#13229)

## What

Before this PR, an internal link in the Lexical editor could reference a
document from a different tenant than the active one.

Reproduction:
1. `pnpm dev plugin-multi-tenant`
2. Log in with `dev@payloadcms.com` and password `test`
3. Go to `http://localhost:3000/admin/collections/food-items` and switch
between the `Blue Dog` and `Steel Cat` tenants to see which food items
each tenant has.
4. Go to http://localhost:3000/admin/collections/food-items/create, and
in the new richtext field enter an internal link
5. In the relationship select menu, you will see the 6 food items at
once (3 of each of those tenants). In the relationship select menu, you
would previously see all 6 food items at once (3 from each of those
tenants). Now, you'll only see the 3 from the active tenant.

The new test verifies that this is fixed.

## How

`baseListFilter` is used, but now it's called `baseFilter` for obvious
reasons: it doesn't just filter the List View. Having two different
properties where the same function was supposed to be placed wasn't
feasible. `baseListFilter` is still supported for backwards
compatibility. It's used as a fallback if `baseFilter` isn't defined,
and it's documented as deprecated.

`baseFilter` is injected into `filterOptions` of the internal link field
in the Lexical Editor.
This commit is contained in:
German Jablonski
2025-08-07 11:24:15 -04:00
committed by GitHub
parent 161769e50c
commit 9c8f3202e4
17 changed files with 167 additions and 58 deletions

View File

@@ -1499,6 +1499,6 @@ export interface Auth {
declare module 'payload' {
// @ts-ignore
// @ts-ignore
export interface GeneratedTypes extends Config {}
}
}

View File

@@ -90,5 +90,9 @@ export const MenuItems: CollectionConfig = {
type: 'text',
required: true,
},
{
name: 'content',
type: 'richText',
},
],
}

View File

@@ -11,4 +11,4 @@ export const credentials = {
email: 'owner@anchorAndBlueDog.com',
password: 'test',
},
}
} as const

View File

@@ -311,6 +311,31 @@ test.describe('Multi Tenant', () => {
confirmationModal.getByText('You are about to change ownership from Blue Dog to Steel Cat'),
).toBeVisible()
})
test('should filter internal links in Lexical editor', async () => {
await loginClientSide({
page,
serverURL,
data: credentials.admin,
})
await selectTenant({
page,
tenant: 'Blue Dog',
})
await page.goto(menuItemsURL.create)
const editor = page.locator('[data-lexical-editor="true"]')
await editor.focus()
await page.keyboard.type('Hello World')
await page.keyboard.down('Shift')
for (let i = 0; i < 'World'.length; i++) {
await page.keyboard.press('ArrowLeft')
}
await page.keyboard.up('Shift')
await page.locator('.toolbar-popup__button-link').click()
await page.locator('.radio-input__styled-radio').last().click()
await page.locator('.drawer__content').locator('.rs__input').click()
await expect(page.getByText('Chorizo Con Queso')).toBeVisible()
await expect(page.getByText('Pretzel Bites')).toBeHidden()
})
})
test.describe('Globals', () => {

View File

@@ -177,6 +177,21 @@ export interface FoodItem {
id: string;
tenant?: (string | null) | Tenant;
name: string;
content?: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
updatedAt: string;
createdAt: string;
}
@@ -315,6 +330,7 @@ export interface UsersSelect<T extends boolean = true> {
export interface FoodItemsSelect<T extends boolean = true> {
tenant?: T;
name?: T;
content?: T;
updatedAt?: T;
createdAt?: T;
}