diff --git a/docs/configuration/collections.mdx b/docs/configuration/collections.mdx index b9336c53d..eac4efcbe 100644 --- a/docs/configuration/collections.mdx +++ b/docs/configuration/collections.mdx @@ -142,7 +142,7 @@ The following options are available: | `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). | | `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). | | `pagination` | Set pagination-specific options for this Collection. [More details](#pagination). | -| `baseListFilter` | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. | +| `baseFilter` | Defines a default base filter which will be applied to the List View (along with any other filters applied by the user) and internal links in Lexical Editor, | **Note:** If you set `useAsTitle` to a relationship or join field, it will use diff --git a/docs/plugins/multi-tenant.mdx b/docs/plugins/multi-tenant.mdx index 32dcfb86c..406e20a2f 100644 --- a/docs/plugins/multi-tenant.mdx +++ b/docs/plugins/multi-tenant.mdx @@ -76,7 +76,19 @@ type MultiTenantPluginConfig = { isGlobal?: boolean /** * Set to `false` if you want to manually apply - * the baseListFilter + * the baseFilter + * + * @default true + */ + useBaseFilter?: boolean + /** + * @deprecated Use `useBaseFilter` instead. If both are defined, + * `useBaseFilter` will take precedence. This property remains only + * for backward compatibility and may be removed in a future version. + * + * Originally, `baseListFilter` was intended to filter only the List View + * in the admin panel. However, base filtering is often required in other areas + * such as internal link relationships in the Lexical editor. * * @default true */ @@ -203,12 +215,12 @@ type MultiTenantPluginConfig = { */ useTenantsCollectionAccess?: boolean /** - * Opt out including the baseListFilter to filter + * Opt out including the baseFilter to filter * tenants by selected tenant */ useTenantsListFilter?: boolean /** - * Opt out including the baseListFilter to filter + * Opt out including the baseFilter to filter * users by selected tenant */ useUsersTenantFilter?: boolean diff --git a/packages/next/src/views/List/index.tsx b/packages/next/src/views/List/index.tsx index 599f36939..4b07f1713 100644 --- a/packages/next/src/views/List/index.tsx +++ b/packages/next/src/views/List/index.tsx @@ -138,16 +138,14 @@ export const renderListView = async ( throw new Error('not-found') } - let baseListFilter = undefined - - if (typeof collectionConfig.admin?.baseListFilter === 'function') { - baseListFilter = await collectionConfig.admin.baseListFilter({ - limit: query.limit, - page: query.page, - req, - sort: query.sort, - }) - } + const baseFilterConstraint = await ( + collectionConfig.admin?.baseFilter ?? collectionConfig.admin?.baseListFilter + )?.({ + limit: query.limit, + page: query.page, + req, + sort: query.sort, + }) let queryPreset: QueryPreset | undefined let queryPresetPermissions: SanitizedCollectionPermission | undefined @@ -155,7 +153,7 @@ export const renderListView = async ( let whereWithMergedSearch = mergeListSearchAndWhere({ collectionConfig, search: typeof query?.search === 'string' ? query.search : undefined, - where: combineWhereConstraints([query?.where, baseListFilter]), + where: combineWhereConstraints([query?.where, baseFilterConstraint]), }) if (trash === true) { diff --git a/packages/payload/src/collections/config/client.ts b/packages/payload/src/collections/config/client.ts index ffd29798a..fb2e03fc5 100644 --- a/packages/payload/src/collections/config/client.ts +++ b/packages/payload/src/collections/config/client.ts @@ -29,7 +29,7 @@ export type ServerOnlyCollectionProperties = keyof Pick< export type ServerOnlyCollectionAdminProperties = keyof Pick< SanitizedCollectionConfig['admin'], - 'baseListFilter' | 'components' | 'hidden' + 'baseFilter' | 'baseListFilter' | 'components' | 'hidden' > export type ServerOnlyUploadProperties = keyof Pick< @@ -94,6 +94,7 @@ const serverOnlyUploadProperties: Partial[] = [ const serverOnlyCollectionAdminProperties: Partial[] = [ 'hidden', + 'baseFilter', 'baseListFilter', 'components', // 'preview' is handled separately diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index de2eb29d0..dc59bc900 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -270,7 +270,7 @@ export type EnableFoldersOptions = { debug?: boolean } -export type BaseListFilter = (args: { +export type BaseFilter = (args: { limit: number locale?: TypedLocale page: number @@ -278,7 +278,31 @@ export type BaseListFilter = (args: { sort: string }) => null | Promise | Where +/** + * @deprecated Use `BaseFilter` instead. + */ +export type BaseListFilter = BaseFilter + export type CollectionAdminOptions = { + /** + * Defines a default base filter which will be applied in the following parts of the admin panel: + * - List View + * - Relationship fields for internal links within the Lexical editor + * + * This is especially useful for plugins like multi-tenant. For example, + * a user may have access to multiple tenants, but should only see content + * related to the currently active or selected tenant in those places. + */ + baseFilter?: BaseFilter + /** + * @deprecated Use `baseFilter` instead. If both are defined, + * `baseFilter` will take precedence. This property remains only + * for backward compatibility and may be removed in a future version. + * + * Originally, `baseListFilter` was intended to filter only the List View + * in the admin panel. However, base filtering is often required in other areas + * such as internal link relationships in the Lexical editor. + */ baseListFilter?: BaseListFilter /** * Custom admin components diff --git a/packages/payload/src/folders/utils/buildFolderWhereConstraints.ts b/packages/payload/src/folders/utils/buildFolderWhereConstraints.ts index 667b91ab7..272ca9fe3 100644 --- a/packages/payload/src/folders/utils/buildFolderWhereConstraints.ts +++ b/packages/payload/src/folders/utils/buildFolderWhereConstraints.ts @@ -28,20 +28,20 @@ export async function buildFolderWhereConstraints({ }), ] - if (typeof collectionConfig.admin?.baseListFilter === 'function') { - const baseListFilterConstraint = await collectionConfig.admin.baseListFilter({ - limit: 0, - locale: localeCode, - page: 1, - req, - sort: - sort || - (typeof collectionConfig.defaultSort === 'string' ? collectionConfig.defaultSort : 'id'), - }) + const baseFilterConstraint = await ( + collectionConfig.admin?.baseFilter ?? collectionConfig.admin?.baseListFilter + )?.({ + limit: 0, + locale: localeCode, + page: 1, + req, + sort: + sort || + (typeof collectionConfig.defaultSort === 'string' ? collectionConfig.defaultSort : 'id'), + }) - if (baseListFilterConstraint) { - constraints.push(baseListFilterConstraint) - } + if (baseFilterConstraint) { + constraints.push(baseFilterConstraint) } if (folderID) { diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 542f18c71..a780a3128 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -1168,6 +1168,7 @@ export type { AfterRefreshHook as CollectionAfterRefreshHook, AuthCollection, AuthOperationsFromCollectionSlug, + BaseFilter, BaseListFilter, BeforeChangeHook as CollectionBeforeChangeHook, BeforeDeleteHook as CollectionBeforeDeleteHook, diff --git a/packages/plugin-multi-tenant/README.md b/packages/plugin-multi-tenant/README.md index aefc5ffeb..74cc95335 100644 --- a/packages/plugin-multi-tenant/README.md +++ b/packages/plugin-multi-tenant/README.md @@ -36,11 +36,11 @@ type MultiTenantPluginConfig = { */ isGlobal?: boolean /** - * Set to `false` if you want to manually apply the baseListFilter + * Set to `false` if you want to manually apply the baseFilter * * @default true */ - useBaseListFilter?: boolean + useBaseFilter?: boolean /** * Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied * diff --git a/packages/plugin-multi-tenant/src/index.ts b/packages/plugin-multi-tenant/src/index.ts index 0f630f889..88383982b 100644 --- a/packages/plugin-multi-tenant/src/index.ts +++ b/packages/plugin-multi-tenant/src/index.ts @@ -15,7 +15,7 @@ import { addTenantCleanup } from './hooks/afterTenantDelete.js' import { translations } from './translations/index.js' import { addCollectionAccess } from './utilities/addCollectionAccess.js' import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js' -import { combineListFilters } from './utilities/combineListFilters.js' +import { combineFilters } from './utilities/combineFilters.js' export const multiTenantPlugin = (pluginConfig: MultiTenantPluginConfig) => @@ -143,8 +143,10 @@ export const multiTenantPlugin = adminUsersCollection.admin = {} } - adminUsersCollection.admin.baseListFilter = combineListFilters({ - baseListFilter: adminUsersCollection.admin?.baseListFilter, + const baseFilter = + adminUsersCollection.admin?.baseFilter ?? adminUsersCollection.admin?.baseListFilter + adminUsersCollection.admin.baseFilter = combineFilters({ + baseFilter, customFilter: (args) => filterDocumentsByTenants({ filterFieldName: `${tenantsArrayFieldName}.${tenantsArrayTenantFieldName}`, @@ -207,8 +209,9 @@ export const multiTenantPlugin = collection.admin = {} } - collection.admin.baseListFilter = combineListFilters({ - baseListFilter: collection.admin?.baseListFilter, + const baseFilter = collection.admin?.baseFilter ?? collection.admin?.baseListFilter + collection.admin.baseFilter = combineFilters({ + baseFilter, customFilter: (args) => filterDocumentsByTenants({ filterFieldName: 'id', @@ -296,7 +299,9 @@ export const multiTenantPlugin = }), ) - if (pluginConfig.collections[collection.slug]?.useBaseListFilter !== false) { + const { useBaseFilter, useBaseListFilter } = pluginConfig.collections[collection.slug] || {} + + if (useBaseFilter ?? useBaseListFilter ?? true) { /** * Add list filter to enabled collections * - filters results by selected tenant @@ -305,8 +310,9 @@ export const multiTenantPlugin = collection.admin = {} } - collection.admin.baseListFilter = combineListFilters({ - baseListFilter: collection.admin?.baseListFilter, + const baseFilter = collection.admin?.baseFilter ?? collection.admin?.baseListFilter + collection.admin.baseFilter = combineFilters({ + baseFilter, customFilter: (args) => filterDocumentsByTenants({ filterFieldName: tenantFieldName, diff --git a/packages/plugin-multi-tenant/src/types.ts b/packages/plugin-multi-tenant/src/types.ts index 6ccc06642..1f0e37236 100644 --- a/packages/plugin-multi-tenant/src/types.ts +++ b/packages/plugin-multi-tenant/src/types.ts @@ -30,7 +30,19 @@ export type MultiTenantPluginConfig = { */ isGlobal?: boolean /** - * Set to `false` if you want to manually apply the baseListFilter + * Set to `false` if you want to manually apply the baseFilter + * + * @default true + */ + useBaseFilter?: boolean + /** + * @deprecated Use `useBaseFilter` instead. If both are defined, + * `useBaseFilter` will take precedence. This property remains only + * for backward compatibility and may be removed in a future version. + * + * Originally, `baseListFilter` was intended to filter only the List View + * in the admin panel. However, base filtering is often required in other areas + * such as internal link relationships in the Lexical editor. * * @default true */ diff --git a/packages/plugin-multi-tenant/src/utilities/combineListFilters.ts b/packages/plugin-multi-tenant/src/utilities/combineFilters.ts similarity index 59% rename from packages/plugin-multi-tenant/src/utilities/combineListFilters.ts rename to packages/plugin-multi-tenant/src/utilities/combineFilters.ts index c8024330f..f6d15dffb 100644 --- a/packages/plugin-multi-tenant/src/utilities/combineListFilters.ts +++ b/packages/plugin-multi-tenant/src/utilities/combineFilters.ts @@ -1,24 +1,24 @@ -import type { BaseListFilter, Where } from 'payload' +import type { BaseFilter, Where } from 'payload' type Args = { - baseListFilter?: BaseListFilter - customFilter: BaseListFilter + baseFilter?: BaseFilter + customFilter: BaseFilter } /** - * Combines a base list filter with a tenant list filter + * Combines a base filter with a tenant list filter * * Combines where constraints inside of an AND operator */ -export const combineListFilters = - ({ baseListFilter, customFilter }: Args): BaseListFilter => +export const combineFilters = + ({ baseFilter, customFilter }: Args): BaseFilter => async (args) => { const filterConstraints = [] - if (typeof baseListFilter === 'function') { - const baseListFilterResult = await baseListFilter(args) + if (typeof baseFilter === 'function') { + const baseFilterResult = await baseFilter(args) - if (baseListFilterResult) { - filterConstraints.push(baseListFilterResult) + if (baseFilterResult) { + filterConstraints.push(baseFilterResult) } } diff --git a/packages/richtext-lexical/src/features/link/server/baseFields.ts b/packages/richtext-lexical/src/features/link/server/baseFields.ts index 93461025b..8ce0ec596 100644 --- a/packages/richtext-lexical/src/features/link/server/baseFields.ts +++ b/packages/richtext-lexical/src/features/link/server/baseFields.ts @@ -117,13 +117,23 @@ export const getBaseFields = ( type: 'relationship', filterOptions: !enabledCollections && !disabledCollections - ? ({ relationTo, user }) => { - const hidden = config.collections.find(({ slug }) => slug === relationTo)?.admin - .hidden + ? async ({ relationTo, req, user }) => { + const admin = config.collections.find(({ slug }) => slug === relationTo)?.admin + + const hidden = admin?.hidden if (typeof hidden === 'function' && hidden({ user } as { user: TypedUser })) { return false } - return true + + const baseFilter = admin?.baseFilter ?? admin?.baseListFilter + return ( + (await baseFilter?.({ + limit: 0, + page: 1, + req, + sort: 'id', + })) ?? true + ) } : null, label: ({ t }) => t('fields:chooseDocumentToLink'), diff --git a/test/localization/payload-types.ts b/test/localization/payload-types.ts index 8b4a53317..54b0d0903 100644 --- a/test/localization/payload-types.ts +++ b/test/localization/payload-types.ts @@ -1499,6 +1499,6 @@ export interface Auth { declare module 'payload' { - // @ts-ignore + // @ts-ignore export interface GeneratedTypes extends Config {} -} \ No newline at end of file +} diff --git a/test/plugin-multi-tenant/collections/MenuItems.ts b/test/plugin-multi-tenant/collections/MenuItems.ts index 6eb717788..b74f8b954 100644 --- a/test/plugin-multi-tenant/collections/MenuItems.ts +++ b/test/plugin-multi-tenant/collections/MenuItems.ts @@ -90,5 +90,9 @@ export const MenuItems: CollectionConfig = { type: 'text', required: true, }, + { + name: 'content', + type: 'richText', + }, ], } diff --git a/test/plugin-multi-tenant/credentials.ts b/test/plugin-multi-tenant/credentials.ts index a6c330a90..9e546c8cf 100644 --- a/test/plugin-multi-tenant/credentials.ts +++ b/test/plugin-multi-tenant/credentials.ts @@ -11,4 +11,4 @@ export const credentials = { email: 'owner@anchorAndBlueDog.com', password: 'test', }, -} +} as const diff --git a/test/plugin-multi-tenant/e2e.spec.ts b/test/plugin-multi-tenant/e2e.spec.ts index 7e34810fb..3c1429611 100644 --- a/test/plugin-multi-tenant/e2e.spec.ts +++ b/test/plugin-multi-tenant/e2e.spec.ts @@ -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', () => { diff --git a/test/plugin-multi-tenant/payload-types.ts b/test/plugin-multi-tenant/payload-types.ts index e985ac336..bf03e81ce 100644 --- a/test/plugin-multi-tenant/payload-types.ts +++ b/test/plugin-multi-tenant/payload-types.ts @@ -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 { export interface FoodItemsSelect { tenant?: T; name?: T; + content?: T; updatedAt?: T; createdAt?: T; }