diff --git a/docs/client-feedback-task-list.md b/docs/client-feedback-task-list.md new file mode 100644 index 0000000..dfb723c --- /dev/null +++ b/docs/client-feedback-task-list.md @@ -0,0 +1,384 @@ +# Client feedback implementation task list + +This plan translates the client feedback from `Feedback.xlsx` into implementation tasks for the current Payload CMS and Next.js codebase. + +## Confirmed decisions + +- Unpublishing any draft-enabled CMS document must remove it from the anonymous public frontend. +- Authenticated draft/live preview may continue to show unpublished content intentionally. +- The CMS cleanup is focused: remove obsolete or non-rendered controls, keep page-specific content on its page, and keep content shared by multiple pages in Globals. +- `/netzwerk` receives its own CMS video upload instead of reusing the homepage video. +- Images and videos introduced by this work must be selectable through Payload's Media collection. +- Sponsor hierarchy needs a responsive design checkpoint before implementation. + +## Definition of done + +- An anonymous visitor cannot see an unpublished page, winner, jury member, partner, event, or press article in a list, relationship-driven section, direct detail route, sitemap, or fallback UI. +- An editor can still preview drafts through Payload preview/live preview. +- Payload exposes only controls that affect the current frontend or an explicitly retained global contract. +- New press and Netzwerk media can be selected and replaced in Payload without developer assistance. +- All visual feedback is verified at desktop, tablet, and mobile widths. +- Every schema change has a committed migration tested against a copy of the current SQLite database. + +## Recommended implementation order + +### 1. Establish the publication contract + +This is the highest-priority work because later CMS cleanup must not hide the underlying visibility problem. + +- [ ] Reproduce unpublishing from the Payload admin for every draft-enabled collection: + - [ ] `pages` + - [ ] `preistraeger` + - [ ] `jury-mitglieder` + - [ ] `partners` + - [ ] `events` + - [ ] `posts` +- [ ] For every reproduction, record: + - [ ] the document ID and `_status` before and after clicking **Unpublish** + - [ ] whether the admin request actually saves `_status = draft` + - [ ] behavior in an anonymous/private browser session + - [ ] behavior in Payload preview or a browser carrying a draft-mode cookie + - [ ] behavior on the collection/list page and direct detail URL +- [ ] Confirm that the reported problem is not only draft preview doing what it is designed to do. +- [ ] Add a table-driven integration test that queries each collection as an anonymous public request and proves that only `_status = published` documents are returned. + +Acceptance criteria: + +- [ ] Directly unpublishing a document changes its stored `_status` to `draft`. +- [ ] Anonymous Local API reads enforce access control with `overrideAccess: false` and `draft: false`. +- [ ] Draft preview is the only frontend context allowed to request unpublished documents. + +### 2. Fix public route hydration and relationship resolution + +The public route currently loads the primary document plus supporting collections in `src/app/(frontend)/[[...slug]]/page.tsx`. Make the public/draft boundary explicit rather than relying on implicit parameter combinations. + +- [ ] Split public and preview query options into a small, testable helper: + - [ ] public: `draft: false`, `overrideAccess: false` + - [ ] preview: drafts enabled only when Next draft mode is active +- [ ] Apply the same contract to: + - [ ] the route document + - [ ] support pages + - [ ] winners + - [ ] events + - [ ] press articles + - [ ] partners + - [ ] jury members +- [ ] Ensure relationship-populated objects cannot bypass the filtered public collection. +- [ ] Update `resolveCmsRelationship(...)` and/or the consuming selection helpers so a relationship ID missing from the published collection resolves to nothing, never to the embedded relationship object. +- [ ] Keep the existing jury page selection authoritative, but silently omit selected jury members that are unpublished. +- [ ] Add regression tests for: + - [ ] unpublished jury member selected on `/netzwerk` + - [ ] unpublished winner selected on the homepage or `/der-bmp` + - [ ] unpublished partner previously present in the homepage ticker + - [ ] unpublished event previously present on `/presse` + - [ ] unpublished press article previously present on `/presse` + +Acceptance criteria: + +- [ ] Unpublishing one related item removes only that item and preserves the remaining published selection order. +- [ ] Republishing the item restores it without requiring a page document edit. + +### 3. Remove collection fallbacks that resurrect unpublished content + +Fallbacks for a missing image or missing optional field may remain. Entire fallback records must not replace intentionally empty CMS collections. + +- [ ] Change `/presse` so an empty published `events` collection renders an intentional empty state or no event rows instead of `pressIndex.events.fallbackItems`. +- [ ] Change `/presse` so an empty published `posts` collection renders an intentional empty state or no news cards instead of `pressIndex.news.fallbackItems`. +- [ ] Remove the event and news fallback-record arrays from `pressIndexFields.ts` after the collection-backed render path is authoritative. +- [ ] Remove their preload/seed plumbing from `preload-press-index-page-cms.ts` and fresh-database setup. +- [ ] Audit winner, jury, and partner surfaces for the same pattern. +- [ ] Preserve only presentation fallbacks that cannot make a removed CMS record reappear, such as a placeholder image on an otherwise published document. + +Acceptance criteria: + +- [ ] Unpublishing the final event, article, or related record produces an honest empty state. +- [ ] No demo/default record appears merely because the CMS collection became empty. + +### 4. Complete the focused Payload CMS cleanup + +Create a field-to-renderer inventory before deleting schema. Every retained field must have a current frontend consumer or a documented global purpose. + +#### Startseite + +- [ ] Keep the page-specific hero, winners, quick check, intro, testimonials, benefits, and homepage video controls. +- [ ] Remove the hidden legacy `home.application` group now owned by the `application-form` Global. +- [ ] Remove the hidden legacy `home.form` group now owned by the `application-form` Global. + +#### Teilnahme + +- [ ] Keep hero, process, eligibility, application paths, dates, and awards. +- [ ] Remove the hidden legacy `participation.applicationForm` group. +- [ ] Remove the hidden legacy `participation.form` group. + +#### Der BMP + +- [ ] Remove the hidden legacy `about.highlights` group because the rendered winners section reuses the homepage source of truth. + +#### Presse + +- [ ] Remove obsolete press-kit controls that no longer render: + - [ ] `kitLabel` + - [ ] `kitMeta` + - [ ] `kitFile` + - [ ] `kitUrl` + - [ ] `kitDownloadName` +- [ ] Remove the complete non-rendered `pressIndex.accreditation` group. +- [ ] Remove the fallback-record arrays covered by Task 3. +- [ ] Keep the rendered section headings, descriptions, status labels, missing-image controls, and press-contact copy. + +#### Netzwerk + +- [ ] Remove confirmed non-rendered fields after the field-to-renderer audit, including current candidates: + - [ ] `jury.note` + - [ ] `partners.premiumLabel` + - [ ] `partners.defaultLogoColor` + - [ ] sponsoring membership-footnote fields +- [ ] Do not remove partner tier labels, partner modal copy, jury selection, or sponsoring form copy that still renders. + +#### Global ownership + +- [ ] Keep shared application content in **Formulare > Bewerbungsformular**. +- [ ] Keep phase state in the **Application Phase** Global. +- [ ] Keep phase-dependent newsletter content in the **Newsletter-Formular** Global. +- [ ] Keep Header, Footer, and site-wide settings in their existing Website Globals. +- [ ] Do not create a generic “Sections” content type unless a second real consumer justifies it. + +#### Schema completion + +- [ ] Generate a narrow migration that removes the obsolete live-table and version-table columns/child tables. +- [ ] Update `payload-types.ts`. +- [ ] Update preload, seed, and fresh-database scripts. +- [ ] Validate migration upgrade and rollback on a copied SQLite database. + +Acceptance criteria: + +- [ ] The Payload admin contains no hidden legacy groups or controls with no current frontend effect. +- [ ] Shared content is edited once in a Global and is not duplicated on individual pages. +- [ ] Page editors contain only page-specific content plus standard page metadata/SEO controls. + +### 5. Give `/netzwerk` its own Ilse Aigner video + +- [ ] Add a video upload field under the Netzwerk patronage/greeting section. +- [ ] Filter the upload relationship to video MIME types. +- [ ] Add page-specific video button label, title, description, and close-label fields if those strings need to differ from the homepage. +- [ ] Replace the `HomeVideoCms`/homepage lookup in `Netzwerk.tsx` with the Netzwerk page fields. +- [ ] Keep the existing homepage video and modal completely independent. +- [ ] Update preload/fresh-database code without copying the existing homepage media relationship into the new field unless the client explicitly wants a temporary placeholder. +- [ ] Let the client upload and select the supplied Netzwerk video after deployment. + +Acceptance criteria: + +- [ ] Changing the homepage video does not change `/netzwerk`. +- [ ] Changing the Netzwerk video does not change the homepage. +- [ ] Empty Netzwerk video selection hides the video action cleanly. + +### 6. Add the CMS-controlled press-contact image + +- [ ] Add `contactImage` and `contactImageAlt` to the rendered Presse contact/material section. +- [ ] Use a Payload upload relationship filtered to image MIME types. +- [ ] Restore the desktop contact section to a two-column composition with copy on the left and the selected image on the right. +- [ ] Stack image and copy in a deliberate order on mobile. +- [ ] Define a safe empty-image layout so the section still works before the client selects an asset. +- [ ] Update the preload script and migration. + +Acceptance criteria: + +- [ ] The client can select, replace, or clear the image in Payload. +- [ ] No code edit is required to change the press image. + +### 7. Repair the `/teilnahme` “Schritt für Schritt” timeline + +The current desktop implementation uses a `300vh` scroll container and produces overlapping/off-screen cards. + +- [ ] Replace the current per-card transform calculation with a single deterministic horizontal-track transform, or replace scroll-jacking with an explicit horizontal carousel/stepper. +- [ ] Ensure steps 1–4 are all reachable at normal wheel/trackpad speed. +- [ ] Remove the unused vertical space after the final step. +- [ ] Make the progress indicator derive from actual scroll/active-step state instead of fixed copy. +- [ ] Preserve the existing stacked mobile presentation unless testing finds a separate defect. +- [ ] Respect `prefers-reduced-motion` by providing a non-scroll-jacked interaction. +- [ ] Add a browser regression test that proves every step becomes visible and the next section follows without a large blank gap. + +Acceptance criteria: + +- [ ] Steps 1, 2, 3, and 4 can each be read in full. +- [ ] Cards never overlap at supported desktop/tablet widths. +- [ ] The eligibility section begins promptly after the timeline finishes. + +### 8. Design and approve a responsive sponsor hierarchy + +Recommended direction: preserve a hierarchy through tier bands and decreasing visual weight rather than forcing lower tiers into empty higher-tier slots. Mixing tiers within one row weakens the pyramid and produces inconsistent card widths. + +#### Proposed responsive behavior + +- [ ] Desktop (`>= 1200px`): + - [ ] Tier 1: up to 2 large cards per centered row + - [ ] Tier 2: up to 3 medium cards per centered row + - [ ] Tier 3: up to 4 compact cards per centered row + - [ ] Center incomplete rows rather than filling them with a different tier +- [ ] Tablet (`768–1199px`): + - [ ] Tier 1: 2 columns + - [ ] Tier 2: 2 columns + - [ ] Tier 3: 3 columns where space allows, otherwise 2 +- [ ] Mobile (`< 768px`): + - [ ] one clearly labeled section per tier + - [ ] Tier 1 rendered as full-width feature cards + - [ ] Tier 2 rendered as medium full-width cards + - [ ] Tier 3 rendered as compact cards, using two columns only when logos remain readable +- [ ] Use card size, logo area, spacing, and tier heading—not color alone—to communicate rank accessibly. +- [ ] Keep `partner.tier` and `partner.sortOrder` as the CMS source of truth. + +Approval gate: + +- [ ] Produce desktop, tablet, and mobile mockups using the current partner counts. +- [ ] Review the hierarchy with the client before implementing the final grid. +- [ ] After approval, implement the chosen breakpoints without changing the partner data model unless the mockup reveals a real need. + +Acceptance criteria: + +- [ ] Hierarchy is immediately understandable at all three breakpoint classes. +- [ ] Adding or removing a partner does not create broken gaps or stretch a card unpredictably. +- [ ] Logo aspect ratios remain intact. + +### 9. Make sponsor ordering explicit and manageable + +- [ ] Confirm that `sortOrder` remains authoritative within each tier. +- [ ] Show `tier`, `sortOrder`, publication status, and active state in useful Partners admin columns. +- [ ] Add validation preventing unsupported tier values outside 1–3. +- [ ] Document the current order for client review: + 1. RSM Ebner Stolz + 2. WWK + 3. Radio Gong 96.3 + 4. münchen.tv + 5. METZLER + 6. Dr. Wieselhuber & Partner + 7. Fristads + 8. New Edge + 9. Moodtalk + 10. Deutsche Bank + 11. Bionorica + 12. Primus +- [ ] Treat an order change as a CMS content operation, not a frontend code change. + +### 10. Slow the homepage sponsor ticker + +- [ ] Replace the fixed `22s` duration with a slower content-aware duration or an agreed slower fixed baseline. +- [ ] Keep perceived speed stable as the number of logos changes. +- [ ] Pause or substantially reduce motion on hover/focus and respect `prefers-reduced-motion`. +- [ ] Verify that duplicating the track still produces a seamless loop. + +Acceptance criteria: + +- [ ] The ticker feels calm enough to read individual logos. +- [ ] No jump is visible at the loop boundary. + +### 11. Correct the homepage phase-heading line break + +- [ ] Confirm the client copy belongs to the homepage status/phase module. +- [ ] Make phase headlines honor explicit CMS newlines. +- [ ] Store/render the target split as: + + ```text + Ein weiteres + erfolgreiches Jahr + ``` + +- [ ] Preserve responsive wrapping when translated or edited copy is longer. +- [ ] Do not hard-code this one sentence in the component. + +### 12. Adjust the homepage bottom application/newsletter split + +- [ ] Widen the blue left panel on the homepage so the boundary with the gold form panel feels more balanced. +- [ ] Scope the ratio change through the existing `source="homepage"` prop unless the client separately approves the same change on `/teilnahme`. +- [ ] Recheck text measure, benefit rows, form width, and the image area after changing the ratio. +- [ ] Verify both open-application and newsletter phase variants. + +### 13. Verify the shared application/newsletter background color + +The current component already uses dark blue `#24366A` instead of black. + +- [ ] Compare the deployed site with the current source. +- [ ] If production is still black, deploy the existing dark-blue implementation or identify the production content/style override. +- [ ] Make no additional palette change if production already matches the requested dark blue. + +### 14. Improve `/teilnahme` eligibility hierarchy + +- [ ] Keep the existing content and CTAs. +- [ ] Introduce clearer grouping within the white eligibility area through restrained section tinting, dividers, spacing, or row containers. +- [ ] Avoid turning every criterion into an unrelated floating card. +- [ ] Make the visual transition into “Wichtige Termine” clear without redesigning the rest of the page. +- [ ] Verify mobile scan order and contrast. + +### 15. Adjust `/der-bmp` “Relevanz & Zielsetzung” + +- [ ] Increase the desktop share of the left image so more of it is visible. +- [ ] Shift the right-hand copy farther into the viewport and give it a centered content measure within its panel. +- [ ] Tune the image `object-position` rather than cropping important subjects. +- [ ] Keep mobile image-first ordering and readable padding. + +### 16. Correct `/der-bmp` “Was ist der Preis?” facts and caption + +- [ ] Center the three key facts within the text panel instead of anchoring each value to the left edge. +- [ ] Keep separators and equal visual weight across all three facts. +- [ ] Improve the image-caption legibility with a controlled background/overlay or by moving it into a dedicated caption area. +- [ ] Verify long CMS labels do not collide. + +### 17. Fix sitemap route generation + +Sitemap support already exists, but current output needs correction. + +- [ ] Generate the homepage URL as `/` when the page slug is either `home` or `startseite`, or prefer the stored `spaPath` directly. +- [ ] Generate press article URLs under `/presse/blog/` instead of `/posts/`. +- [ ] Add event detail URLs under `/presse/events/`. +- [ ] Add winner detail URLs under `/preistraeger/`. +- [ ] Keep unpublished documents out by using public access and `_status = published`. +- [ ] Ensure generated sitemap and robots URLs use the production base URL during deployment, never committed localhost/example URLs. +- [ ] Add route-level tests for root, page, event, article, and winner entries. + +Acceptance criteria: + +- [ ] Every sitemap URL returns `200` on production. +- [ ] Unpublished entries disappear from the sitemap immediately or after the documented cache/revalidation window. + +### 18. Regression, migration, and deployment verification + +- [ ] Run focused unit/integration tests for publication filtering, relationship resolution, and sitemap paths. +- [ ] Run TypeScript without incremental cache. +- [ ] Run the relevant Vitest suite. +- [ ] Build against a migrated copy of `content.db`. +- [ ] Visually verify at approximately `1440px`, `1024px`, `768px`, and `390px` widths. +- [ ] Test anonymous public browsing separately from authenticated draft preview. +- [ ] Back up the production SQLite database. +- [ ] Deploy the schema-changing commit with explicit migrations enabled. +- [ ] Upload/select the client-supplied Netzwerk video and the selected press image in production Payload. +- [ ] Smoke-test all affected routes after deployment: + - [ ] `/` + - [ ] `/teilnahme` + - [ ] `/der-bmp` + - [ ] `/netzwerk` + - [ ] `/presse` + - [ ] representative event/article/winner detail routes + - [ ] sitemap and robots endpoints +- [ ] Perform a production unpublish/republish smoke test using disposable or approved test records. + +## Feedback traceability + +| Workbook feedback | Planned work | +| --- | --- | +| CMS unpublish button does not work | Tasks 1–3 and 18 | +| `/teilnahme` steps 2–4 do not open; excess space | Task 7 | +| Separate Ilse Aigner video from homepage | Task 5 | +| Remove press-kit CMS controls | Task 4 | +| Keep Pages focused and shared content in Globals | Task 4 | +| Remove accreditation form copy | Task 4 | +| Shared 2027-interest section should be dark blue | Task 13; current source already appears compliant | +| `/der-bmp` relevance text/image alignment | Task 15 | +| Partner tier hierarchy and responsive layout | Tasks 8–9 | +| Add image beside press contact | Task 6 | +| Add/fix sitemap in code | Task 17 | +| Sponsor ticker feels too fast | Task 10 | +| Homepage success heading line break | Task 11 | +| Homepage bottom blue panel should be wider | Task 12 | +| Participation eligibility area lacks structure | Task 14 | +| `/der-bmp` facts and caption are misaligned/hard to read | Task 16 | +| Confirm sponsor order | Task 9 | +| “Aktuelles” looks good | No change required | diff --git a/src/app/(frontend)/[[...slug]]/page.tsx b/src/app/(frontend)/[[...slug]]/page.tsx index bab3f3e..565ea9f 100644 --- a/src/app/(frontend)/[[...slug]]/page.tsx +++ b/src/app/(frontend)/[[...slug]]/page.tsx @@ -11,6 +11,7 @@ import { normalizeFooterData, normalizeHeaderData } from '@/spa/cmsNavigation' import type { CmsRouteCollection, CmsRouteDoc } from '@/spa/cmsRoute' import { normalizeNewsletterFormData, type SpaNewsletterFormData } from '@/spa/newsletterForm' import { normalizeSiteSettings, type SpaSiteSettingsData } from '@/spa/siteSettings' +import { getPublicationQueryOptions } from '@/utilities/publicationQuery' export const dynamic = 'force-dynamic' @@ -43,8 +44,7 @@ async function findRouteDoc(payload: Awaited>, pat const result = await payload.find({ collection: target.collection, depth: 1, - draft, - overrideAccess: draft, + ...getPublicationQueryOptions(draft), limit: 1, pagination: false, where: { @@ -63,8 +63,7 @@ async function findList(payload: Awaited>, collect const result = await payload.find({ collection, depth: 1, - draft, - overrideAccess: draft, + ...getPublicationQueryOptions(draft), limit: 300, pagination: false, ...(collection === 'partners' || collection === 'jury-mitglieder' ? { sort: 'sortOrder' } : {}), @@ -77,8 +76,7 @@ async function findSupportPages(payload: Awaited>, const result = await payload.find({ collection: 'pages', depth: 1, - draft, - overrideAccess: draft, + ...getPublicationQueryOptions(draft), limit: 10, pagination: false, where: { diff --git a/src/utilities/publicationQuery.ts b/src/utilities/publicationQuery.ts new file mode 100644 index 0000000..38c4407 --- /dev/null +++ b/src/utilities/publicationQuery.ts @@ -0,0 +1,22 @@ +export type PublicationQueryOptions = Readonly<{ + draft: boolean + overrideAccess: boolean +}> + +const publicPublicationQuery = Object.freeze({ + draft: false, + overrideAccess: false, +}) satisfies PublicationQueryOptions + +const previewPublicationQuery = Object.freeze({ + draft: true, + overrideAccess: true, +}) satisfies PublicationQueryOptions + +/** + * Keeps public Local API reads access-controlled while allowing the authenticated + * Next.js draft-mode route to intentionally load the latest draft versions. + */ +export function getPublicationQueryOptions(isPreview: boolean): PublicationQueryOptions { + return isPreview ? previewPublicationQuery : publicPublicationQuery +} diff --git a/tests/int/publication-contract.int.spec.ts b/tests/int/publication-contract.int.spec.ts new file mode 100644 index 0000000..f87181a --- /dev/null +++ b/tests/int/publication-contract.int.spec.ts @@ -0,0 +1,241 @@ +import config from '@/payload.config' +import { getPublicationQueryOptions } from '@/utilities/publicationQuery' +import type { Payload, RequiredDataFromCollectionSlug } from 'payload' +import { getPayload } from 'payload' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +const publicationCollections = [ + 'pages', + 'preistraeger', + 'jury-mitglieder', + 'partners', + 'events', + 'posts', +] as const + +type PublicationCollection = (typeof publicationCollections)[number] + +type PublicationCase = { + collection: TCollection + data: (label: string) => RequiredDataFromCollectionSlug + hasPublicDetailRoute: boolean +} + +const lexicalDocument = { + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal' as const, + style: '', + text: 'Publication contract fixture', + type: 'text', + version: 1, + }, + ], + direction: 'ltr' as const, + format: '' as const, + indent: 0, + textFormat: 0, + type: 'paragraph', + version: 1, + }, + ], + direction: 'ltr' as const, + format: '' as const, + indent: 0, + type: 'root', + version: 1, + }, +} + +let mediaID: number + +const publicationCases: PublicationCase[] = [ + { + collection: 'pages', + data: (label) => ({ + layout: [{ blockType: 'content', columns: [] }], + _status: 'published', + slug: `publication-${label}`, + spaPath: `/publication-${label}`, + title: `Publication ${label}`, + }), + hasPublicDetailRoute: true, + }, + { + collection: 'preistraeger', + data: (label) => ({ + _status: 'published', + slug: `publication-${label}`, + spaPath: `/preistraeger/publication-${label}`, + title: `Publication ${label}`, + }), + hasPublicDetailRoute: true, + }, + { + collection: 'jury-mitglieder', + data: (label) => ({ _status: 'published', name: `Publication ${label}` }), + hasPublicDetailRoute: false, + }, + { + collection: 'partners', + data: (label) => ({ + _status: 'published', + logo: mediaID, + name: `Publication ${label}`, + stableId: `publication-${label}`, + tier: 3, + }), + hasPublicDetailRoute: false, + }, + { + collection: 'events', + data: (label) => ({ + _status: 'published', + slug: `publication-${label}`, + spaPath: `/presse/events/publication-${label}`, + title: `Publication ${label}`, + }), + hasPublicDetailRoute: true, + }, + { + collection: 'posts', + data: (label) => ({ + _status: 'published', + content: lexicalDocument, + slug: `publication-${label}`, + spaPath: `/presse/blog/publication-${label}`, + title: `Publication ${label}`, + }), + hasPublicDetailRoute: true, + }, +] + +let payload: Payload + +describe('draft-enabled collection publication contract', () => { + beforeAll(async () => { + payload = await getPayload({ config }) + + const imageData = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', + 'base64', + ) + const media = await payload.create({ + collection: 'media', + data: { alt: 'Publication contract fixture' }, + file: { + data: imageData, + mimetype: 'image/png', + name: `publication-contract-${process.pid}.png`, + size: imageData.length, + }, + }) + mediaID = media.id + }) + + afterAll(async () => { + if (mediaID) { + await payload.delete({ collection: 'media', id: mediaID }) + } + }) + + it('keeps anonymous public and authenticated preview query modes explicit', () => { + expect(getPublicationQueryOptions(false)).toEqual({ + draft: false, + overrideAccess: false, + }) + expect(getPublicationQueryOptions(true)).toEqual({ + draft: true, + overrideAccess: true, + }) + }) + + it.each(publicationCases)( + 'returns only published $collection documents to anonymous public reads', + async ({ collection, data, hasPublicDetailRoute }) => { + const published = await payload.create({ + collection, + context: { disableRevalidate: true }, + data: data(`${collection}-published`), + draft: false, + }) + const unpublished = await payload.create({ + collection, + context: { disableRevalidate: true }, + data: data(`${collection}-unpublished`), + draft: false, + }) + + const storedDraft = await payload.update({ + collection, + context: { disableRevalidate: true }, + id: unpublished.id, + data: { _status: 'draft' }, + draft: false, + }) + + expect(published._status).toBe('published') + expect(storedDraft._status).toBe('draft') + + const publicResult = await payload.find({ + collection, + ...getPublicationQueryOptions(false), + pagination: false, + where: { + id: { + in: [published.id, unpublished.id], + }, + }, + }) + + expect(publicResult.docs.map((doc) => doc.id)).toEqual([published.id]) + expect(publicResult.docs.every((doc) => doc._status === 'published')).toBe(true) + + const previewResult = await payload.find({ + collection, + ...getPublicationQueryOptions(true), + pagination: false, + where: { + id: { + in: [published.id, unpublished.id], + }, + }, + }) + + expect(previewResult.docs.map((doc) => doc.id).sort()).toEqual( + [published.id, unpublished.id].sort(), + ) + + if (hasPublicDetailRoute) { + await expect( + payload.findByID({ + collection, + id: published.id, + ...getPublicationQueryOptions(false), + }), + ).resolves.toMatchObject({ id: published.id, _status: 'published' }) + + await expect( + payload.findByID({ + collection, + id: unpublished.id, + ...getPublicationQueryOptions(false), + }), + ).rejects.toThrow() + + await expect( + payload.findByID({ + collection, + id: unpublished.id, + ...getPublicationQueryOptions(true), + }), + ).resolves.toMatchObject({ id: unpublished.id, _status: 'draft' }) + } + }, + ) +})