diff --git a/packages/plugin-multi-tenant/src/utilities/getGlobalViewRedirect.ts b/packages/plugin-multi-tenant/src/utilities/getGlobalViewRedirect.ts index ba5ebd15d..9195ee449 100644 --- a/packages/plugin-multi-tenant/src/utilities/getGlobalViewRedirect.ts +++ b/packages/plugin-multi-tenant/src/utilities/getGlobalViewRedirect.ts @@ -25,7 +25,7 @@ type Args = { view: ViewTypes } export async function getGlobalViewRedirect({ - slug, + slug: collectionSlug, basePath, docID, headers, @@ -64,45 +64,67 @@ export async function getGlobalViewRedirect({ tenant = tenantOptions[0]?.value || null } - try { - const { docs } = await payload.find({ - collection: slug, - depth: 0, - limit: 1, - overrideAccess: false, - pagination: false, - user, - where: { - [tenantFieldName]: { - equals: tenant, + if (tenant) { + try { + const globalTenantDocQuery = await payload.find({ + collection: collectionSlug, + depth: 0, + limit: 1, + pagination: false, + select: { + id: true, }, - }, - }) + where: { + [tenantFieldName]: { + equals: tenant, + }, + }, + }) - const tenantDocID = docs?.[0]?.id + const globalTenantDocID = globalTenantDocQuery?.docs?.[0]?.id - if (view === 'document') { - if (docID && !tenantDocID) { - // viewing a document with an id but does not match the selected tenant, redirect to create route - redirectRoute = `/collections/${slug}/create` - } else if (tenantDocID && docID !== tenantDocID) { - // tenant document already exists but does not match current route doc ID, redirect to matching tenant doc - redirectRoute = `/collections/${slug}/${tenantDocID}` - } - } else if (view === 'list') { - if (tenantDocID) { - // tenant document exists, redirect to edit view - redirectRoute = `/collections/${slug}/${tenantDocID}` - } else { - // tenant document does not exist, redirect to create route - redirectRoute = `/collections/${slug}/create` + if (view === 'document') { + // global tenant document edit view + if (globalTenantDocID && docID !== globalTenantDocID) { + // tenant document already exists but does not match current route docID + // redirect to matching tenant docID from query + redirectRoute = `/collections/${collectionSlug}/${globalTenantDocID}` + } else if (docID && !globalTenantDocID) { + // a docID was found in the route but no global document with this tenant exists + // so we need to generate a redirect to the create route + redirectRoute = await generateCreateRedirect({ + collectionSlug, + payload, + tenantID: tenant, + }) + } + } else if (view === 'list') { + // global tenant document list view + if (globalTenantDocID) { + // tenant document exists, redirect from list view to the document edit view + redirectRoute = `/collections/${collectionSlug}/${globalTenantDocID}` + } else { + // no matching document was found for the current tenant + // so we need to generate a redirect to the create route + redirectRoute = await generateCreateRedirect({ + collectionSlug, + payload, + tenantID: tenant, + }) + } } + } catch (e: unknown) { + const prefix = `${e && typeof e === 'object' && 'message' in e && typeof e.message === 'string' ? `${e.message} - ` : ''}` + payload.logger.error(e, `${prefix}Multi Tenant Redirect Error`) } - } catch (e: unknown) { - payload.logger.error( - e, - `${typeof e === 'object' && e && 'message' in e ? `e?.message - ` : ''}Multi Tenant Redirect Error`, - ) + } else { + // no tenants were found, redirect to the admin view + return formatAdminURL({ + adminRoute: payload.config.routes.admin, + basePath, + path: '', + serverURL: payload.config.serverURL, + }) } if (redirectRoute) { @@ -114,5 +136,57 @@ export async function getGlobalViewRedirect({ }) } + // no redirect is needed + // the current route is valid return undefined } + +type GenerateCreateArgs = { + collectionSlug: string + payload: Payload + tenantID: number | string +} +/** + * Generate a redirect URL for creating a new document in a multi-tenant collection. + * + * If autosave is enabled on the collection, we need to create the document and then redirect to it. + * Otherwise we can redirect to the default create route. + */ +async function generateCreateRedirect({ + collectionSlug, + payload, + tenantID, +}: GenerateCreateArgs): Promise<`/${string}` | undefined> { + const collection = payload.collections[collectionSlug] + if ( + collection?.config.versions?.drafts && + typeof collection.config.versions.drafts === 'object' && + collection.config.versions.drafts.autosave + ) { + // Autosave is enabled, create a document first + try { + const doc = await payload.create({ + collection: collectionSlug, + data: { + tenant: tenantID, + }, + depth: 0, + draft: true, + select: { + id: true, + }, + }) + return `/collections/${collectionSlug}/${doc.id}` + } catch (error) { + payload.logger.error( + error, + `Error creating autosave global multi tenant document for ${collectionSlug}`, + ) + } + + return '/' + } + + // Autosave is not enabled, redirect to default create route + return `/collections/${collectionSlug}/create` +} diff --git a/packages/plugin-multi-tenant/src/utilities/getTenantOptions.ts b/packages/plugin-multi-tenant/src/utilities/getTenantOptions.ts index f3a2d13e7..17a220723 100644 --- a/packages/plugin-multi-tenant/src/utilities/getTenantOptions.ts +++ b/packages/plugin-multi-tenant/src/utilities/getTenantOptions.ts @@ -35,7 +35,7 @@ export const getTenantOptions = async ({ overrideAccess: false, select: { [useAsTitle]: true, - ...(isOrderable ? { _order: true } : {}), + ...(isOrderable && { _order: true }), }, sort: isOrderable ? '_order' : useAsTitle, user, diff --git a/test/helpers/e2e/selectInput.ts b/test/helpers/e2e/selectInput.ts index 67ee60f14..78c8b9c00 100644 --- a/test/helpers/e2e/selectInput.ts +++ b/test/helpers/e2e/selectInput.ts @@ -96,22 +96,26 @@ async function selectOption({ type GetSelectInputValueFunction = (args: { multiSelect: TMultiSelect selectLocator: Locator + valueLabelClass?: string }) => Promise export const getSelectInputValue: GetSelectInputValueFunction = async ({ selectLocator, multiSelect = false, + valueLabelClass, }) => { if (multiSelect) { // For multi-select, get all selected options const selectedOptions = await selectLocator - .locator('.multi-value-label__text') + .locator(valueLabelClass || '.multi-value-label__text') .allTextContents() return selectedOptions || [] } // For single-select, get the selected value - const singleValue = await selectLocator.locator('.react-select--single-value').textContent() + const singleValue = await selectLocator + .locator(valueLabelClass || '.react-select--single-value') + .textContent() return (singleValue ?? undefined) as any } diff --git a/test/plugin-multi-tenant/collections/AutosaveGlobal.ts b/test/plugin-multi-tenant/collections/AutosaveGlobal.ts new file mode 100644 index 000000000..451e6b97e --- /dev/null +++ b/test/plugin-multi-tenant/collections/AutosaveGlobal.ts @@ -0,0 +1,30 @@ +import type { CollectionConfig } from 'payload' + +import { autosaveGlobalSlug } from '../shared.js' + +export const AutosaveGlobal: CollectionConfig = { + slug: autosaveGlobalSlug, + admin: { + useAsTitle: 'title', + group: 'Tenant Globals', + }, + fields: [ + { + name: 'title', + label: 'Title', + type: 'text', + required: true, + }, + { + name: 'description', + type: 'textarea', + }, + ], + versions: { + drafts: { + autosave: { + interval: 100, + }, + }, + }, +} diff --git a/test/plugin-multi-tenant/config.ts b/test/plugin-multi-tenant/config.ts index 75a03a5b8..faba6468e 100644 --- a/test/plugin-multi-tenant/config.ts +++ b/test/plugin-multi-tenant/config.ts @@ -7,15 +7,16 @@ const dirname = path.dirname(filename) import type { Config as ConfigType } from './payload-types.js' import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' +import { AutosaveGlobal } from './collections/AutosaveGlobal.js' import { Menu } from './collections/Menu.js' import { MenuItems } from './collections/MenuItems.js' import { Tenants } from './collections/Tenants.js' import { Users } from './collections/Users/index.js' import { seed } from './seed/index.js' -import { menuItemsSlug, menuSlug } from './shared.js' +import { autosaveGlobalSlug, menuItemsSlug, menuSlug } from './shared.js' export default buildConfigWithDefaults({ - collections: [Tenants, Users, MenuItems, Menu], + collections: [Tenants, Users, MenuItems, Menu, AutosaveGlobal], admin: { autoLogin: false, importMap: { @@ -44,6 +45,9 @@ export default buildConfigWithDefaults({ [menuSlug]: { isGlobal: true, }, + [autosaveGlobalSlug]: { + isGlobal: true, + }, }, i18n: { translations: { diff --git a/test/plugin-multi-tenant/e2e.spec.ts b/test/plugin-multi-tenant/e2e.spec.ts index d30741996..86d9d7f03 100644 --- a/test/plugin-multi-tenant/e2e.spec.ts +++ b/test/plugin-multi-tenant/e2e.spec.ts @@ -28,7 +28,7 @@ import { reInitializeDB } from '../helpers/reInitializeDB.js' import { TEST_TIMEOUT_LONG } from '../playwright.config.js' import { credentials } from './credentials.js' import { seed } from './seed/index.js' -import { menuItemsSlug, menuSlug, tenantsSlug, usersSlug } from './shared.js' +import { autosaveGlobalSlug, menuItemsSlug, menuSlug, tenantsSlug, usersSlug } from './shared.js' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -37,6 +37,7 @@ test.describe('Multi Tenant', () => { let page: Page let serverURL: string let globalMenuURL: AdminUrlUtil + let autosaveGlobalURL: AdminUrlUtil let menuItemsURL: AdminUrlUtil let usersURL: AdminUrlUtil let tenantsURL: AdminUrlUtil @@ -50,6 +51,7 @@ test.describe('Multi Tenant', () => { menuItemsURL = new AdminUrlUtil(serverURL, menuItemsSlug) usersURL = new AdminUrlUtil(serverURL, usersSlug) tenantsURL = new AdminUrlUtil(serverURL, tenantsSlug) + autosaveGlobalURL = new AdminUrlUtil(serverURL, autosaveGlobalSlug) const context = await browser.newContext() page = await context.newPage() @@ -74,7 +76,7 @@ test.describe('Multi Tenant', () => { data: credentials.admin, }) - await clearTenant({ page }) + await clearGlobalTenant({ page }) await page.goto(tenantsURL.list) @@ -129,7 +131,7 @@ test.describe('Multi Tenant', () => { }) await page.goto(menuItemsURL.list) - await clearTenant({ page }) + await clearGlobalTenant({ page }) await expect( page.locator('.collection-list .table .cell-name', { @@ -174,7 +176,7 @@ test.describe('Multi Tenant', () => { }) await page.goto(menuItemsURL.list) - await clearTenant({ page }) + await clearGlobalTenant({ page }) await expect( page.locator('.collection-list .table .cell-name', { @@ -190,7 +192,7 @@ test.describe('Multi Tenant', () => { }) await page.goto(menuItemsURL.list) - await clearTenant({ page }) + await clearGlobalTenant({ page }) await expect( page.locator('.collection-list .table .cell-name', { @@ -209,7 +211,7 @@ test.describe('Multi Tenant', () => { }) await page.goto(usersURL.list) - await clearTenant({ page }) + await clearGlobalTenant({ page }) await expect( page.locator('.collection-list .table .cell-email', { @@ -269,7 +271,7 @@ test.describe('Multi Tenant', () => { }) await page.goto(menuItemsURL.list) - await clearTenant({ page }) + await clearGlobalTenant({ page }) await goToListDoc({ page, @@ -297,7 +299,7 @@ test.describe('Multi Tenant', () => { }) await page.goto(menuItemsURL.list) - await clearTenant({ page }) + await clearGlobalTenant({ page }) await goToListDoc({ page, @@ -306,7 +308,7 @@ test.describe('Multi Tenant', () => { urlUtil: menuItemsURL, }) - await selecteDocumentTenant({ + await selectDocumentTenant({ page, tenant: 'Steel Cat', }) @@ -324,7 +326,7 @@ test.describe('Multi Tenant', () => { data: credentials.admin, }) await page.goto(menuItemsURL.create) - await selecteDocumentTenant({ + await selectDocumentTenant({ page, tenant: 'Blue Dog', }) @@ -418,6 +420,24 @@ test.describe('Multi Tenant', () => { }) .toBe('Steel Cat') }) + + test('should navigate to globals with autosave enabled', async () => { + await loginClientSide({ + page, + serverURL, + data: credentials.admin, + }) + await page.goto(tenantsURL.list) + await clearGlobalTenant({ page }) + await page.goto(autosaveGlobalURL.list) + await expect(page.locator('.doc-header__title')).toBeVisible() + const globalTenant = await getGlobalTenant({ page }) + await expect + .poll(async () => { + return await getDocumentTenant({ page }) + }) + .toBe(globalTenant) + }) }) test.describe('Tenant Selector', () => { @@ -499,7 +519,7 @@ test.describe('Multi Tenant', () => { }) await page.goto(tenantsURL.list) - await clearTenant({ page }) + await clearGlobalTenant({ page }) await expect( page.locator('.collection-list .table .cell-name', { @@ -586,6 +606,24 @@ test.describe('Multi Tenant', () => { /** * Helper Functions */ + +async function getGlobalTenant({ page }: { page: Page }): Promise { + await openNav(page) + return await getSelectInputValue({ + selectLocator: page.locator('.tenant-selector'), + multiSelect: false, + }) +} + +async function getDocumentTenant({ page }: { page: Page }): Promise { + await openNav(page) + return await getSelectInputValue({ + selectLocator: page.locator('#field-tenant'), + multiSelect: false, + valueLabelClass: '.relationship--single-value', + }) +} + async function getTenantOptions({ page }: { page: Page }): Promise { await openNav(page) return await getSelectInputOptions({ @@ -593,7 +631,7 @@ async function getTenantOptions({ page }: { page: Page }): Promise { }) } -async function selecteDocumentTenant({ +async function selectDocumentTenant({ page, tenant, }: { @@ -617,7 +655,7 @@ async function selectTenant({ page, tenant }: { page: Page; tenant: string }): P }) } -async function clearTenant({ page }: { page: Page }): Promise { +async function clearGlobalTenant({ page }: { page: Page }): Promise { await openNav(page) return clearSelectInput({ selectLocator: page.locator('.tenant-selector'), diff --git a/test/plugin-multi-tenant/payload-types.ts b/test/plugin-multi-tenant/payload-types.ts index 165562286..912f45b0f 100644 --- a/test/plugin-multi-tenant/payload-types.ts +++ b/test/plugin-multi-tenant/payload-types.ts @@ -71,6 +71,7 @@ export interface Config { users: User; 'food-items': FoodItem; 'food-menu': FoodMenu; + 'autosave-global': AutosaveGlobal; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; @@ -85,6 +86,7 @@ export interface Config { users: UsersSelect | UsersSelect; 'food-items': FoodItemsSelect | FoodItemsSelect; 'food-menu': FoodMenuSelect | FoodMenuSelect; + 'autosave-global': AutosaveGlobalSelect | AutosaveGlobalSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; @@ -175,7 +177,7 @@ export interface User { */ export interface FoodItem { id: string; - tenant: string | Tenant; + tenant?: (string | null) | Tenant; name: string; content?: { root: { @@ -201,7 +203,7 @@ export interface FoodItem { */ export interface FoodMenu { id: string; - tenant: string | Tenant; + tenant?: (string | null) | Tenant; title: string; description?: string | null; menuItems?: @@ -217,6 +219,19 @@ export interface FoodMenu { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "autosave-global". + */ +export interface AutosaveGlobal { + id: string; + tenant?: (string | null) | Tenant; + title: string; + description?: string | null; + updatedAt: string; + createdAt: string; + _status?: ('draft' | 'published') | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents". @@ -239,6 +254,10 @@ export interface PayloadLockedDocument { | ({ relationTo: 'food-menu'; value: string | FoodMenu; + } | null) + | ({ + relationTo: 'autosave-global'; + value: string | AutosaveGlobal; } | null); globalSlug?: string | null; user: { @@ -352,6 +371,18 @@ export interface FoodMenuSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "autosave-global_select". + */ +export interface AutosaveGlobalSelect { + tenant?: T; + title?: T; + description?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-locked-documents_select". diff --git a/test/plugin-multi-tenant/shared.ts b/test/plugin-multi-tenant/shared.ts index e9ffba576..bcfdfa326 100644 --- a/test/plugin-multi-tenant/shared.ts +++ b/test/plugin-multi-tenant/shared.ts @@ -5,3 +5,5 @@ export const usersSlug = 'users' export const menuItemsSlug = 'food-items' export const menuSlug = 'food-menu' + +export const autosaveGlobalSlug = 'autosave-global'