diff --git a/__tests__/e2e/admin/tenant-selector/dashboard.e2e.spec.ts b/__tests__/e2e/admin/tenant-selector/dashboard.e2e.spec.ts new file mode 100644 index 000000000..1de1ce610 --- /dev/null +++ b/__tests__/e2e/admin/tenant-selector/dashboard.e2e.spec.ts @@ -0,0 +1,73 @@ +import { expect, tenantSelectorTest as test } from '../../fixtures/tenant-selector.fixture' +import { AdminUrlUtil, CollectionSlugs } from '../../helpers' + +/** + * Dashboard Tests + * + * Tests that the tenant selector is visible and functional on the admin dashboard. + * Previously, the selector was hidden on the dashboard because viewType was never + * set to 'dashboard'. Fixed by detecting dashboard from URL params instead. + * + * Related: https://github.com/NWACus/web/issues/691 + */ + +test.describe.configure({ timeout: 90000 }) + +test.describe('Tenant selector on dashboard', () => { + test('should be visible on the dashboard for super admin', async ({ + loginAs, + isTenantSelectorVisible, + getTenantOptions, + }) => { + const page = await loginAs('superAdmin') + const url = new AdminUrlUtil('http://localhost:3000', CollectionSlugs.pages) + + await page.goto(url.dashboard) + await page.waitForLoadState('networkidle') + + const isVisible = await isTenantSelectorVisible(page) + expect(isVisible).toBe(true) + + const options = await getTenantOptions(page) + expect(options.length).toBeGreaterThanOrEqual(4) + + await page.context().close() + }) + + test('should be visible on the dashboard for multi-tenant admin', async ({ + loginAs, + isTenantSelectorVisible, + getTenantOptions, + }) => { + const page = await loginAs('multiTenantAdmin') + const url = new AdminUrlUtil('http://localhost:3000', CollectionSlugs.pages) + + await page.goto(url.dashboard) + await page.waitForLoadState('networkidle') + + const isVisible = await isTenantSelectorVisible(page) + expect(isVisible).toBe(true) + + const options = await getTenantOptions(page) + expect(options.length).toBe(2) // NWAC and SNFAC + + await page.context().close() + }) + + test('should be hidden on dashboard for single-tenant users', async ({ + loginAs, + isTenantSelectorVisible, + }) => { + const page = await loginAs('singleTenantAdmin') + const url = new AdminUrlUtil('http://localhost:3000', CollectionSlugs.pages) + + await page.goto(url.dashboard) + await page.waitForLoadState('networkidle') + + // Single-tenant users have only 1 option, so selector is hidden + const isVisible = await isTenantSelectorVisible(page) + expect(isVisible).toBe(false) + + await page.context().close() + }) +}) diff --git a/__tests__/e2e/admin/tenant-selector/sync-on-save.e2e.spec.ts b/__tests__/e2e/admin/tenant-selector/sync-on-save.e2e.spec.ts new file mode 100644 index 000000000..5d8126e19 --- /dev/null +++ b/__tests__/e2e/admin/tenant-selector/sync-on-save.e2e.spec.ts @@ -0,0 +1,106 @@ +import { openNav } from '../../fixtures/nav.fixture' +import { expect, tenantSelectorTest as test } from '../../fixtures/tenant-selector.fixture' +import { AdminUrlUtil, CollectionSlugs, saveDocAndAssert, waitForFormReady } from '../../helpers' + +/** + * Sync on Save Tests + * + * Tests that the tenant selector dropdown refreshes automatically when a + * tenant is saved (created or updated), without requiring a page reload. + * + * The SyncTenantsOnSave component watches useDocumentInfo().lastUpdateTime + * and calls syncTenants() when it changes. + */ + +const TEMP_TENANT_SLUG_CREATE = 'e2e-sync-create' +const TEMP_TENANT_NAME_CREATE = 'E2E Sync Create Center' + +const TEMP_TENANT_SLUG_EDIT = 'e2e-sync-edit' +const TEMP_TENANT_NAME_EDIT = 'E2E Sync Edit Center' +const UPDATED_TENANT_NAME = 'E2E Sync Edit Renamed' + +test.describe.configure({ timeout: 90000 }) + +test.describe('Tenant selector syncs on save', () => { + test('should show new tenant in selector after creation', async ({ + loginAs, + getTenantOptions, + }) => { + const page = await loginAs('superAdmin') + const tenantsUrl = new AdminUrlUtil('http://localhost:3000', CollectionSlugs.tenants) + + try { + // Navigate to create a new tenant + await page.goto(tenantsUrl.create) + await page.waitForLoadState('networkidle') + await waitForFormReady(page) + + await page.locator('#field-name').fill(TEMP_TENANT_NAME_CREATE) + await page.locator('#field-slug').fill(TEMP_TENANT_SLUG_CREATE) + await saveDocAndAssert(page) + + // Navigate to a tenant-scoped collection via client-side nav link (NOT page.goto) + await openNav(page) + await Promise.all([ + page.waitForURL('**/admin/collections/pages'), + page.locator('nav a[href="/admin/collections/pages"]').click(), + ]) + await page.waitForLoadState('networkidle') + + // The tenant selector should show the new tenant without a full page reload + const options = await getTenantOptions(page) + expect(options).toContain(TEMP_TENANT_NAME_CREATE) + } finally { + // Clean up: delete the temporary tenant + const response = await page.request.get( + `http://localhost:3000/api/tenants?where[slug][equals]=${TEMP_TENANT_SLUG_CREATE}&limit=1`, + ) + const data = await response.json() + if (data.docs?.[0]) { + await page.request.delete(`http://localhost:3000/api/tenants/${data.docs[0].id}`) + } + await page.context().close() + } + }) + + test('should update tenant name in selector after editing', async ({ + loginAs, + getTenantOptions, + }) => { + const page = await loginAs('superAdmin') + const tenantsUrl = new AdminUrlUtil('http://localhost:3000', CollectionSlugs.tenants) + + // Create a temporary tenant via API + const createResponse = await page.request.post('http://localhost:3000/api/tenants', { + data: { name: TEMP_TENANT_NAME_EDIT, slug: TEMP_TENANT_SLUG_EDIT }, + }) + const { doc: tenant } = await createResponse.json() + + try { + // Navigate to the tenant edit page + await page.goto(tenantsUrl.edit(tenant.id)) + await page.waitForLoadState('networkidle') + await waitForFormReady(page) + + // Rename the tenant + await page.locator('#field-name').fill(UPDATED_TENANT_NAME) + await saveDocAndAssert(page) + + // Navigate to a tenant-scoped collection via client-side nav link (NOT page.goto) + await openNav(page) + await Promise.all([ + page.waitForURL('**/admin/collections/pages'), + page.locator('nav a[href="/admin/collections/pages"]').click(), + ]) + await page.waitForLoadState('networkidle') + + const options = await getTenantOptions(page) + expect(options).toContain(UPDATED_TENANT_NAME) + expect(options).not.toContain(TEMP_TENANT_NAME_EDIT) + } finally { + // Clean up: delete the temporary tenant + await page.request.delete(`http://localhost:3000/api/tenants/${tenant.id}`) + await page.context().close() + } + }) +}) diff --git a/docs/onboarding.md b/docs/onboarding.md index 5908f8ab0..d81449b3a 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -27,15 +27,6 @@ This outlines steps required when a new center (tenant) comes on board. This doc Manual Troubleshoot:
In Vercel, go to the project → Storage → production Edge Config and edit file - - New tenant option shown in TenantSelectionProvider - Manual - Refresh page - - - Future automation - Use syncTenants - Create documents for Global Collections Manual diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 9f3e21543..0469a98be 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -35,6 +35,7 @@ import { InviteUser as InviteUser_6042b6804e11048cd4fbe6206cbc2b0f } from '@/col import { ResendInviteButton as ResendInviteButton_e262b7912e5bdc08a1a83eb2731de735 } from '@/collections/Users/components/ResendInviteButton' import { CollectionsField as CollectionsField_49c0311020325b59204cc21d2f536b8d } from '@/collections/Roles/components/CollectionsField' import { RulesCell as RulesCell_649699f5b285e7a5429592dc58fd6f0c } from '@/collections/Roles/components/RulesCell' +import { SyncTenantsOnSave as SyncTenantsOnSave_7025498606b767f7843bf544e6535ee1 } from '@/collections/Tenants/components/SyncTenantsOnSave' import { LinkLabelDescription as LinkLabelDescription_cc2cf53f1598892c0c926f3cb616a721 } from '@/fields/navLink/components/LinkLabelDescription' import { AvalancheCenterName as AvalancheCenterName_acb7f1a03857e27efe1942bb65ab80ad } from '@/collections/Settings/components/AvalancheCenterName' import { USFSLogoDescription as USFSLogoDescription_d2eea91290575f9a545768dce25713f4 } from '@/collections/Settings/components/USFSLogoDescription' @@ -93,6 +94,7 @@ export const importMap = { "@/collections/Users/components/ResendInviteButton#ResendInviteButton": ResendInviteButton_e262b7912e5bdc08a1a83eb2731de735, "@/collections/Roles/components/CollectionsField#CollectionsField": CollectionsField_49c0311020325b59204cc21d2f536b8d, "@/collections/Roles/components/RulesCell#RulesCell": RulesCell_649699f5b285e7a5429592dc58fd6f0c, + "@/collections/Tenants/components/SyncTenantsOnSave#SyncTenantsOnSave": SyncTenantsOnSave_7025498606b767f7843bf544e6535ee1, "@/fields/navLink/components/LinkLabelDescription#LinkLabelDescription": LinkLabelDescription_cc2cf53f1598892c0c926f3cb616a721, "@/collections/Settings/components/AvalancheCenterName#AvalancheCenterName": AvalancheCenterName_acb7f1a03857e27efe1942bb65ab80ad, "@/collections/Settings/components/USFSLogoDescription#USFSLogoDescription": USFSLogoDescription_d2eea91290575f9a545768dce25713f4, diff --git a/src/collections/Tenants/components/SyncTenantsOnSave.tsx b/src/collections/Tenants/components/SyncTenantsOnSave.tsx new file mode 100644 index 000000000..987157a0d --- /dev/null +++ b/src/collections/Tenants/components/SyncTenantsOnSave.tsx @@ -0,0 +1,25 @@ +'use client' + +import { useDocumentInfo } from '@payloadcms/ui' +import { useEffect, useRef } from 'react' + +import { useTenantSelection } from '@/providers/TenantSelectionProvider/index.client' + +/** + * Invisible component placed on the Tenants edit view that syncs the tenant + * selector dropdown whenever a tenant document is saved (created or updated). + */ +export const SyncTenantsOnSave = () => { + const { lastUpdateTime } = useDocumentInfo() + const { syncTenants } = useTenantSelection() + const prevUpdateTime = useRef(lastUpdateTime) + + useEffect(() => { + if (lastUpdateTime !== prevUpdateTime.current) { + prevUpdateTime.current = lastUpdateTime + syncTenants() + } + }, [lastUpdateTime, syncTenants]) + + return null +} diff --git a/src/collections/Tenants/index.ts b/src/collections/Tenants/index.ts index 1ff075756..04513c9a8 100644 --- a/src/collections/Tenants/index.ts +++ b/src/collections/Tenants/index.ts @@ -19,6 +19,13 @@ export const Tenants: CollectionConfig = { useAsTitle: 'name', group: 'Permissions', hidden: ({ user }) => hasReadOnlyAccess(user, 'tenants'), + components: { + edit: { + beforeDocumentControls: [ + '@/collections/Tenants/components/SyncTenantsOnSave#SyncTenantsOnSave', + ], + }, + }, }, labels: { plural: 'Avalanche Centers',