diff --git a/__tests__/server/collectionFilters.server.test.ts b/__tests__/server/collectionFilters.server.test.ts new file mode 100644 index 00000000..6e3412c0 --- /dev/null +++ b/__tests__/server/collectionFilters.server.test.ts @@ -0,0 +1,61 @@ +import { getTenantAndPublishedFilter } from '@/utilities/collectionFilters' + +const baseProps: Parameters[0] = { + data: {}, + req: { + headers: new Headers(), + }, + relationTo: 'pages', +} + +describe('getTenantAndPublishedFilter', () => { + it('adds the published status filter for page references', () => { + expect( + getTenantAndPublishedFilter({ + ...baseProps, + relationTo: 'pages', + }), + ).toEqual({ + _status: { + equals: 'published', + }, + }) + }) + + it('preserves tenant scoping when filtering page references', () => { + expect( + getTenantAndPublishedFilter({ + ...baseProps, + data: { tenant: 7 }, + relationTo: 'posts', + }), + ).toEqual({ + and: [ + { + tenant: { + equals: 7, + }, + }, + { + _status: { + equals: 'published', + }, + }, + ], + }) + }) + + it('does not add a draft status filter for built-in page references', () => { + expect( + getTenantAndPublishedFilter({ + ...baseProps, + data: { tenant: 7 }, + relationTo: 'builtInPages', + }), + ).toEqual({ + tenant: { + equals: 7, + }, + }) + }) +}) diff --git a/__tests__/server/headerUtils.server.test.ts b/__tests__/server/headerUtils.server.test.ts index 9b3bbd90..0d74ab58 100644 --- a/__tests__/server/headerUtils.server.test.ts +++ b/__tests__/server/headerUtils.server.test.ts @@ -1,4 +1,5 @@ import { + convertToNavLink, extractAllInternalUrls, findNavigationItemBySlug, getCanonicalUrlsFromNavigation, @@ -733,4 +734,50 @@ describe('Header Utilities', () => { expect(topLevelNavItem({ tab: undefined, label: 'Weather' })).toEqual([]) }) }) + + describe('convertToNavLink', () => { + it('does not render draft page references', () => { + const result = convertToNavLink({ + type: 'internal', + reference: { + relationTo: 'pages', + value: { + id: 1, + slug: 'who-we-are', + title: 'Who We Are', + tenant: 1, + layout: [], + updatedAt: '2026-07-01T00:00:00.000Z', + createdAt: '2026-07-01T00:00:00.000Z', + _status: 'draft', + }, + }, + }) + + expect(result).toBeUndefined() + }) + + it('renders built-in page references without draft filtering', () => { + const result = convertToNavLink({ + type: 'internal', + reference: { + relationTo: 'builtInPages', + value: { + id: 1, + title: 'Forecasts', + url: '/forecasts/avalanche', + tenant: 1, + updatedAt: '2026-07-01T00:00:00.000Z', + createdAt: '2026-07-01T00:00:00.000Z', + }, + }, + }) + + expect(result).toEqual({ + type: 'internal', + label: 'Forecasts', + url: '/forecasts/avalanche', + }) + }) + }) }) diff --git a/src/fields/linkField.ts b/src/fields/linkField.ts index 627669c7..ab640af2 100644 --- a/src/fields/linkField.ts +++ b/src/fields/linkField.ts @@ -1,5 +1,5 @@ import { clearIrrelevantLinkValues } from '@/utilities/clearIrrelevantLinkValues' -import { getTenantFilter } from '@/utilities/collectionFilters' +import { getTenantAndPublishedFilter } from '@/utilities/collectionFilters' import { validateExternalUrl } from '@/utilities/validateUrl' import { Field, NamedGroupField, TextFieldSingleValidation } from 'payload' import { text } from 'payload/shared' @@ -65,7 +65,7 @@ const buildLinkFields = ({ label: 'Select page or post', relationTo: ['pages', 'builtInPages', 'posts'], required: true, - filterOptions: getTenantFilter, + filterOptions: getTenantAndPublishedFilter, } const urlField: Field = { diff --git a/src/utilities/collectionFilters.ts b/src/utilities/collectionFilters.ts index 8a8f1534..c245605a 100644 --- a/src/utilities/collectionFilters.ts +++ b/src/utilities/collectionFilters.ts @@ -1,11 +1,32 @@ import { FilterOptionsProps, Where } from 'payload' import { getTenantSlugFromCookie } from './tenancy/getTenantFromCookie' +const draftableLinkCollections = new Set(['pages', 'posts']) + +type TenantFilterProps = { + data: FilterOptionsProps['data'] + req: Pick +} + +type TenantPublishedFilterProps = TenantFilterProps & Pick +type TenantIdFilterProps = TenantFilterProps & Pick + +const combineFilters = (...filters: Where[]): Where => { + const activeFilters = filters.filter((filter) => Object.keys(filter).length > 0) + + if (activeFilters.length === 0) return {} + if (activeFilters.length === 1) return activeFilters[0] + + return { + and: activeFilters, + } +} + export const getImageTypeFilter = () => ({ mimeType: { contains: 'image' }, }) -export const getTenantFilter = ({ data, req }: FilterOptionsProps): Where => { +export const getTenantFilter = ({ data, req }: TenantFilterProps): Where => { // If the document already has a tenant relationship set, use that if (data.tenant) { return { @@ -28,7 +49,19 @@ export const getTenantFilter = ({ data, req }: FilterOptionsProps): Where => { return {} } -export const getTenantAndIdFilter = ({ id, data, req }: FilterOptionsProps): Where => { +export const getTenantAndPublishedFilter = (props: TenantPublishedFilterProps): Where => { + const tenantFilter = getTenantFilter(props) + + if (!draftableLinkCollections.has(props.relationTo)) return tenantFilter + + return combineFilters(tenantFilter, { + _status: { + equals: 'published', + }, + }) +} + +export const getTenantAndIdFilter = ({ id, data, req }: TenantIdFilterProps): Where => { const idFilter = { id: { not_in: [id] } } // If the document already has a tenant relationship set, use that