fix(plugin-multi-tenant): autosave global documents not rendering (#13552)
Fixes https://github.com/payloadcms/payload/issues/13507 When enabling autosave on global multi-tenant documents, the page would not always render - more noticeably with smaller autosave intervals.
This commit is contained in:
@@ -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`
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export const getTenantOptions = async ({
|
||||
overrideAccess: false,
|
||||
select: {
|
||||
[useAsTitle]: true,
|
||||
...(isOrderable ? { _order: true } : {}),
|
||||
...(isOrderable && { _order: true }),
|
||||
},
|
||||
sort: isOrderable ? '_order' : useAsTitle,
|
||||
user,
|
||||
|
||||
@@ -96,22 +96,26 @@ async function selectOption({
|
||||
type GetSelectInputValueFunction = <TMultiSelect = true>(args: {
|
||||
multiSelect: TMultiSelect
|
||||
selectLocator: Locator
|
||||
valueLabelClass?: string
|
||||
}) => Promise<TMultiSelect extends true ? string[] : string | undefined>
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
30
test/plugin-multi-tenant/collections/AutosaveGlobal.ts
Normal file
30
test/plugin-multi-tenant/collections/AutosaveGlobal.ts
Normal file
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<string | undefined> {
|
||||
await openNav(page)
|
||||
return await getSelectInputValue<false>({
|
||||
selectLocator: page.locator('.tenant-selector'),
|
||||
multiSelect: false,
|
||||
})
|
||||
}
|
||||
|
||||
async function getDocumentTenant({ page }: { page: Page }): Promise<string | undefined> {
|
||||
await openNav(page)
|
||||
return await getSelectInputValue<false>({
|
||||
selectLocator: page.locator('#field-tenant'),
|
||||
multiSelect: false,
|
||||
valueLabelClass: '.relationship--single-value',
|
||||
})
|
||||
}
|
||||
|
||||
async function getTenantOptions({ page }: { page: Page }): Promise<string[]> {
|
||||
await openNav(page)
|
||||
return await getSelectInputOptions({
|
||||
@@ -593,7 +631,7 @@ async function getTenantOptions({ page }: { page: Page }): Promise<string[]> {
|
||||
})
|
||||
}
|
||||
|
||||
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<void> {
|
||||
async function clearGlobalTenant({ page }: { page: Page }): Promise<void> {
|
||||
await openNav(page)
|
||||
return clearSelectInput({
|
||||
selectLocator: page.locator('.tenant-selector'),
|
||||
|
||||
@@ -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<false> | UsersSelect<true>;
|
||||
'food-items': FoodItemsSelect<false> | FoodItemsSelect<true>;
|
||||
'food-menu': FoodMenuSelect<false> | FoodMenuSelect<true>;
|
||||
'autosave-global': AutosaveGlobalSelect<false> | AutosaveGlobalSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
@@ -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<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "autosave-global_select".
|
||||
*/
|
||||
export interface AutosaveGlobalSelect<T extends boolean = true> {
|
||||
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".
|
||||
|
||||
@@ -5,3 +5,5 @@ export const usersSlug = 'users'
|
||||
export const menuItemsSlug = 'food-items'
|
||||
|
||||
export const menuSlug = 'food-menu'
|
||||
|
||||
export const autosaveGlobalSlug = 'autosave-global'
|
||||
|
||||
Reference in New Issue
Block a user