refactor(cms): remove obsolete page fields

This commit is contained in:
syntaxbullet
2026-07-31 14:51:11 +02:00
parent 8dc17c0fea
commit ab47571139
22 changed files with 613 additions and 1424 deletions

View File

@@ -0,0 +1,50 @@
# Payload field-to-consumer inventory
This inventory records the frontend or platform owner for every retained `Pages` field group and relevant Global. Nested scalar, upload, relationship, and array fields belong to the consumer named for their parent group unless an exception is listed.
## Shared `Pages` fields
| Field path | Consumer or contract |
| --- | --- |
| `title`, `slug`, `spaPath`, `publishedAt`, `_status` | Route lookup, preview URLs, publication access, sitemap generation, and generic-page headings in `src/app/(frontend)/[[...slug]]/page.tsx`, the sitemap routes, and `src/spa/pages/CmsGenericPage.tsx`. |
| `hero`, `layout` | Non-SPA pages through `src/heros/RenderHero.tsx`, `src/blocks/RenderBlocks.tsx`, and `src/spa/pages/CmsGenericPage.tsx`. |
| `meta.*` | `@payloadcms/plugin-seo`, route metadata generation, and generic-page title fallback. |
## Page-specific fields
| Page field path | Retained consumer |
| --- | --- |
| `home.hero.*` | `src/spa/pages/Home.tsx`; partner relationship records remain authoritative and the names array is presentation fallback only. |
| `home.winners.*` | `src/spa/components/HomeWinnersSection.tsx` and `src/spa/homeWinnerSelection.ts`; also reused by `/der-bmp`. |
| `home.quickCheck.*`, `home.intro.*`, `home.benefits.*` | `src/spa/pages/Home.tsx`. |
| `home.testimonials.*` | `src/spa/pages/Home.tsx` through `src/spa/components/TestimonialsSection.tsx`. |
| `home.videoModal.*` | Homepage video action and modal in `src/spa/pages/Home.tsx`. |
| `participation.hero.*`, `participation.eligibility.*`, `participation.applicationWays.*`, `participation.datesBanner.*` | `src/spa/pages/Participation.tsx`. |
| `participation.process.*` | `src/spa/pages/Participation.tsx` through `src/spa/components/WegZurAuszeichnungSection.tsx`. |
| `participation.awardsGrid.*` | `src/spa/pages/Participation.tsx` through `src/spa/components/AwardsGridSection.tsx`. |
| `about.hero.*`, `about.prize.*`, `about.mittelstand.*`, `about.history.*`, `about.goals.*`, `about.values.*`, `about.cta.*` | `src/spa/pages/About.tsx`. |
| `about.testimonials.*` | `src/spa/pages/About.tsx` through `src/spa/components/TestimonialsSection.tsx`. |
| `contact.*` | `src/spa/pages/Contact.tsx` and `src/spa/components/forms/KontaktForm.tsx`. |
| `datenschutz.*`, `impressum.*` | `src/spa/components/legal/LegalPage.tsx` via the respective route component. |
| `formUpload.*` | `src/spa/pages/FormularUpload.tsx`. |
| `mitgliedWerden.*` | `src/spa/pages/MitgliedWerden.tsx` and `src/spa/components/membership/MembershipWizard.tsx`. |
| `preistraegerIndex.*` | `src/spa/pages/Preistraeger.tsx`; winner records remain collection-owned. |
| `pressIndex.hero.*`, `pressIndex.events.*`, `pressIndex.news.*`, `pressIndex.downloads.*` | `src/spa/pages/Press.tsx`; event/article records remain collection-owned, while missing-field labels and images are presentation fallbacks only. |
| `netzwerk.hero.*`, `netzwerk.patronage.*`, `netzwerk.juryIntro.*`, `netzwerk.expertise.*`, `netzwerk.sponsoring.*` | `src/spa/pages/Netzwerk.tsx`. |
| `netzwerk.jury.{eyebrow,heading,description,chairBadge,members}` | Jury heading and authoritative rendered relationship selection in `src/spa/pages/Netzwerk.tsx`. |
| `netzwerk.partners.{eyebrow,heading,description,detailLabel,tiers,modal}` | Partner section, tier labels, and modal copy in `src/spa/pages/Netzwerk.tsx`; partner records remain collection-owned. |
| `netzwerk.sponsoringForm.*` | The two visible form steps, validation, success state, and footer copy in `src/spa/components/forms/SponsoringForm.tsx`. |
## Global ownership
| Global | Consumer or contract |
| --- | --- |
| `application-form` | Single source for the application section and form on both `/` and `/teilnahme`, hydrated by the frontend route and consumed by `ApplicationFormSection` / `BewerbungsForm`. |
| `application-phase` | Shared phase state and phase-specific site copy used by Home, shared layout CTAs, the application/newsletter form switch, and sponsoring submissions. |
| `newsletter-form` | Evaluation/completed newsletter variants shared by `/` and `/teilnahme`. |
| `header`, `footer` | Site-wide navigation, branding, links, social entries, and footer partner marks rendered by `src/spa/components/layout/Layout.tsx`. |
| `site-settings` | Shared skyline, Preisträger placeholder, and generic-page fallback copy. |
## Task 4 removals
The audit found no frontend, metadata, SEO, seed-only contract, or explicit Global purpose for these fields: `home.application`, `home.form`, `participation.applicationForm`, `participation.form`, the hidden participation CTA controls (`eligibility.secondaryCta`, `eligibility.nudgeCta`, `applicationWays.recommendedLabel`, `applicationWays.fallbackCta`, `applicationWays.ways[].featured`, and `applicationWays.ways[].cta`), `about.highlights`, the five `pressIndex.downloads.kit*` controls, `pressIndex.accreditation`, the old event/news CTA labels, `netzwerk.jury.note`, `netzwerk.partners.premiumLabel`, `netzwerk.partners.defaultLogoColor`, the four non-rendered `netzwerk.sponsoring` footnote/form-eyebrow controls, and the obsolete sponsoring package step (`packages`, `step1Eyebrow`, `step1Heading`, `requiredPackageError`). They are intentionally absent from the retained inventory.

View File

@@ -123,52 +123,9 @@ export const aboutFields: Field[] = [
},
],
},
{
name: 'highlights',
label: '04 · Preisträger highlights',
type: 'group',
admin: {
...sectionAdmin('Legacy data retained for compatibility. The rendered section now reuses the homepage Preisträger card grid.'),
hidden: true,
},
fields: [
uploadField('fallbackImage', 'Fallback winner image', `Current frontend fallback image: /images/${aboutContent.highlights.fallbackImageFilename}`),
text('eyebrow', 'Eyebrow', aboutContent.highlights.eyebrow),
textarea('heading', 'Heading', aboutContent.highlights.heading),
ctaGroup('cta', 'CTA', aboutContent.highlights.cta.label, aboutContent.highlights.cta.url),
text('hoverLabel', 'Card hover label', aboutContent.highlights.hoverLabel),
{
name: 'fallbackWinner',
label: 'Fallback winner text',
type: 'group',
fields: [
text('name', 'Name fallback', aboutContent.highlights.fallbackWinner.name),
text('category', 'Category fallback', aboutContent.highlights.fallbackWinner.category),
text('awardType', 'Award type fallback', aboutContent.highlights.fallbackWinner.awardType),
],
},
{
name: 'year',
label: 'Fallback winner year',
type: 'number',
defaultValue: aboutContent.highlights.year,
},
{
name: 'selectedWinners',
label: 'Selected winners',
type: 'relationship',
relationTo: 'preistraeger',
hasMany: true,
maxRows: 8,
admin: {
description: 'Choose up to 8 Preisträger to show. If empty, the frontend falls back to winners from the selected year.',
},
},
],
},
{
name: 'testimonials',
label: '05 · Testimonials',
label: '04 · Testimonials',
type: 'group',
admin: sectionAdmin('Testimonial heading and slides. Upload images when available, or keep the migrated URL fallback.'),
fields: [
@@ -199,7 +156,7 @@ export const aboutFields: Field[] = [
},
{
name: 'history',
label: '06 · Geschichte & Meilensteine',
label: '05 · Geschichte & Meilensteine',
type: 'group',
admin: sectionAdmin('Navy timeline section.'),
fields: [
@@ -219,7 +176,7 @@ export const aboutFields: Field[] = [
},
{
name: 'goals',
label: '07 · Zielsetzung',
label: '06 · Zielsetzung',
type: 'group',
admin: sectionAdmin('Cream values list next to the history timeline.'),
fields: [
@@ -237,7 +194,7 @@ export const aboutFields: Field[] = [
},
{
name: 'values',
label: '08 · Vorteile & Werte',
label: '07 · Vorteile & Werte',
type: 'group',
admin: sectionAdmin('Cream editorial rows before the final CTA.'),
fields: [
@@ -256,7 +213,7 @@ export const aboutFields: Field[] = [
},
{
name: 'cta',
label: '09 · Final CTA',
label: '08 · Final CTA',
type: 'group',
admin: sectionAdmin('Bottom navy CTA section.'),
fields: [

View File

@@ -33,33 +33,6 @@ const sectionAdmin = (description: string) => ({
initCollapsed: true,
})
const iconOptions = [
{ label: 'Alert circle', value: 'alertCircle' },
{ label: 'Arrow right', value: 'arrowRight' },
{ label: 'Building', value: 'building2' },
{ label: 'Check', value: 'check' },
{ label: 'Check circle', value: 'checkCircle2' },
{ label: 'File text', value: 'fileText' },
{ label: 'Map pin', value: 'mapPin' },
{ label: 'Search', value: 'search' },
{ label: 'Send', value: 'send' },
{ label: 'Star', value: 'star' },
{ label: 'Trending up', value: 'trendingUp' },
{ label: 'Trophy', value: 'trophy' },
{ label: 'User', value: 'user' },
{ label: 'User check', value: 'userCheck' },
{ label: 'User plus', value: 'userPlus' },
{ label: 'Users', value: 'users' },
]
const iconField = (defaultValue = 'star'): Field => ({
name: 'icon',
label: 'Icon',
type: 'select',
defaultValue,
options: iconOptions,
})
export const homeFields: Field[] = [
{
name: 'home',
@@ -219,144 +192,9 @@ export const homeFields: Field[] = [
},
],
},
{
name: 'application',
label: '07 · Application form section',
type: 'group',
admin: { ...sectionAdmin('Legacy page-level form section. The frontend now uses Formulare > Bewerbungsformular.'), hidden: true },
fields: [
text('eyebrow', 'Eyebrow', homeContent.application.eyebrow),
textarea('heading', 'Heading', homeContent.application.heading),
textarea('body', 'Body copy', homeContent.application.body),
{ name: 'benefits', label: 'Benefit rows', type: 'array', defaultValue: homeContent.application.benefits, fields: [text('num', 'Number'), text('label', 'Label'), text('desc', 'Description')] },
uploadField('awardImage', 'Award image', `Current frontend image: /images/${homeContent.application.awardImageFilename}`),
text('awardImageAlt', 'Award image alt text', homeContent.application.awardImageAlt),
text('awardCaption', 'Award image caption', homeContent.application.awardCaption),
text('formEyebrow', 'Form eyebrow', homeContent.application.formEyebrow),
text('formTitle', 'Form title', homeContent.application.formTitle),
text('footnote', 'Footnote normal text', homeContent.application.footnote),
text('footnoteStrong', 'Footnote highlighted text', homeContent.application.footnoteStrong),
],
},
{
name: 'form',
label: '08 · Embedded application form copy and logic labels',
type: 'group',
admin: { ...sectionAdmin('Legacy page-level form fields. The frontend now uses Formulare > Bewerbungsformular.'), hidden: true },
fields: [
text('stepCompleteLabel', 'Completed step label', homeContent.form.stepCompleteLabel),
text('backLabel', 'Back button label', homeContent.form.backLabel),
text('nextLabel', 'Next button label', homeContent.form.nextLabel),
text('submitLabel', 'Submit button label', homeContent.form.submitLabel),
text('yesLabel', 'Yes option label', homeContent.form.yesLabel),
text('noLabel', 'No option label', homeContent.form.noLabel),
text('requiredSuffix', 'Required field suffix', homeContent.form.requiredSuffix),
{
name: 'validation',
label: 'Validation messages',
type: 'group',
fields: [
text('choose', 'Choose message', homeContent.form.validation.choose),
text('required', 'Required message', homeContent.form.validation.required),
text('invalidEmail', 'Invalid email message', homeContent.form.validation.invalidEmail),
],
},
{
name: 'typeStep',
label: 'Step 1 · Submission type',
type: 'group',
fields: [
text('eyebrow', 'Eyebrow', homeContent.form.typeStep.eyebrow),
text('heading', 'Heading', homeContent.form.typeStep.heading),
text('selfTitle', 'Self-application title', homeContent.form.typeStep.selfTitle),
textarea('selfDesc', 'Self-application description', homeContent.form.typeStep.selfDesc),
text('nominationTitle', 'Nomination title', homeContent.form.typeStep.nominationTitle),
textarea('nominationDesc', 'Nomination description', homeContent.form.typeStep.nominationDesc),
],
},
{
name: 'selfSteps',
label: 'Self-application step labels',
type: 'array',
dbName: 'home_self_steps',
defaultValue: homeContent.form.selfSteps,
fields: [text('label', 'Label'), iconField('trophy')],
},
{
name: 'nominationSteps',
label: 'Nomination step labels',
type: 'array',
dbName: 'home_nom_steps',
defaultValue: homeContent.form.nominationSteps,
fields: [text('label', 'Label'), iconField('trophy')],
},
{
name: 'employeeOptions',
label: 'Employee options',
type: 'array',
dbName: 'home_emp_opts',
defaultValue: homeContent.form.employeeOptions,
fields: [text('value', 'Value'), text('label', 'Label')],
},
{
name: 'eligibility',
label: 'Self-application eligibility step',
type: 'group',
fields: [
text('allowedEmployeeValues', 'Allowed employee option values, comma-separated', homeContent.form.eligibility.allowedEmployeeValues),
text('requiredBayernValue', 'Required Bavaria answer value', homeContent.form.eligibility.requiredBayernValue),
text('eyebrow', 'Eyebrow', homeContent.form.eligibility.eyebrow),
text('heading', 'Heading', homeContent.form.eligibility.heading),
text('employeesLabel', 'Employees field label', homeContent.form.eligibility.employeesLabel),
text('bayernLabel', 'Bavaria field label', homeContent.form.eligibility.bayernLabel),
text('ownerLabel', 'Owner field label', homeContent.form.eligibility.ownerLabel),
text('ineligibleHeading', 'Ineligible heading', homeContent.form.eligibility.ineligibleHeading),
textarea('ineligibleBody', 'Ineligible body', homeContent.form.eligibility.ineligibleBody),
text('ineligibleContactPrefix', 'Ineligible contact prefix', homeContent.form.eligibility.ineligibleContactPrefix),
text('ineligibleContactEmail', 'Ineligible contact email', homeContent.form.eligibility.ineligibleContactEmail),
],
},
{
name: 'selfContact',
label: 'Self-application contact step',
type: 'group',
fields: Object.entries(homeContent.form.selfContact).map(([name, value]) => text(name, name, value)),
},
{
name: 'selfSummary',
label: 'Self-application summary step',
type: 'group',
fields: Object.entries(homeContent.form.selfSummary).map(([name, value]) => text(name, name, value)),
},
{
name: 'nominationCompany',
label: 'Nomination company step',
type: 'group',
fields: Object.entries(homeContent.form.nominationCompany).map(([name, value]) => text(name, name, value)),
},
{
name: 'nominationContact',
label: 'Nomination contact step',
type: 'group',
fields: Object.entries(homeContent.form.nominationContact).map(([name, value]) => text(name, name, value)),
},
{
name: 'nominationSummary',
label: 'Nomination summary step',
type: 'group',
fields: Object.entries(homeContent.form.nominationSummary).map(([name, value]) => text(name, name, value)),
},
{
name: 'success',
label: 'Success state copy',
type: 'group',
fields: Object.entries(homeContent.form.success).map(([name, value]) => text(name, name, value)),
},
],
},
{
name: 'videoModal',
label: '10 · Video modal',
label: '07 · Video modal',
type: 'group',
admin: sectionAdmin('Video and fallback copy shown when the hero video button is clicked.'),
fields: [

View File

@@ -51,7 +51,6 @@ const personFields: Field[] = [
const rowFields: Field[] = [text('num', 'Number'), text('label', 'Label'), textarea('body', 'Body')]
const benefitFields: Field[] = [text('num', 'Number'), text('label', 'Label'), text('sub', 'Subline')]
const packageFields: Field[] = [text('value', 'Value'), text('title', 'Title'), textarea('desc', 'Description')]
const formStepFields: Field[] = [text('label', 'Label')]
export const netzwerkFields: Field[] = [
@@ -134,13 +133,12 @@ export const netzwerkFields: Field[] = [
name: 'jury',
label: '04 · Jury members',
type: 'group',
admin: sectionAdmin('Jury section heading, chair badge, members, and note.'),
admin: sectionAdmin('Jury section heading, chair badge, and rendered member selection.'),
fields: [
text('eyebrow', 'Eyebrow', netzwerkContent.jury.eyebrow),
textarea('heading', 'Heading', netzwerkContent.jury.heading),
textarea('description', 'Description', netzwerkContent.jury.description),
text('chairBadge', 'Chair badge', netzwerkContent.jury.chairBadge),
textarea('note', 'Note', netzwerkContent.jury.note),
{
name: 'members',
label: 'Members',
@@ -183,8 +181,6 @@ export const netzwerkFields: Field[] = [
textarea('heading', 'Heading', netzwerkContent.partners.heading),
textarea('description', 'Description', netzwerkContent.partners.description),
text('detailLabel', 'Detail label', netzwerkContent.partners.detailLabel),
text('premiumLabel', 'Premium label', netzwerkContent.partners.premiumLabel),
text('defaultLogoColor', 'Default logo color', netzwerkContent.partners.defaultLogoColor),
{
name: 'tiers',
label: 'Tiers',
@@ -214,7 +210,7 @@ export const netzwerkFields: Field[] = [
name: 'sponsoring',
label: '07 · Sponsoring section',
type: 'group',
admin: sectionAdmin('Sponsoring pitch, image, benefits, form eyebrow, and membership footnote.'),
admin: sectionAdmin('Sponsoring pitch, image, benefits, and form eyebrow.'),
fields: [
text('eyebrow', 'Eyebrow', netzwerkContent.sponsoring.eyebrow),
textarea('heading', 'Heading', netzwerkContent.sponsoring.heading),
@@ -222,10 +218,6 @@ export const netzwerkFields: Field[] = [
uploadField('image', 'Form panel image', `Current frontend image: /images/${netzwerkContent.sponsoring.imageFilename}`),
text('imageAlt', 'Image alt text', netzwerkContent.sponsoring.imageAlt),
text('imageLabel', 'Image label', netzwerkContent.sponsoring.imageLabel),
text('formEyebrow', 'Form eyebrow', netzwerkContent.sponsoring.formEyebrow),
text('footnotePrefix', 'Footnote prefix', netzwerkContent.sponsoring.footnotePrefix),
text('footnoteLabel', 'Footnote link label', netzwerkContent.sponsoring.footnoteLabel),
text('footnoteUrl', 'Footnote link URL', netzwerkContent.sponsoring.footnoteUrl),
{
name: 'benefits',
label: 'Benefits',
@@ -240,7 +232,7 @@ export const netzwerkFields: Field[] = [
name: 'sponsoringForm',
label: '08 · Sponsoring form',
type: 'group',
admin: sectionAdmin('Wizard labels, package options, validation messages, success copy, and footer note.'),
admin: sectionAdmin('Two-step wizard labels, field copy, validation messages, success copy, and footer note.'),
fields: [
{
name: 'steps',
@@ -248,26 +240,18 @@ export const netzwerkFields: Field[] = [
type: 'array',
dbName: 'network_form_steps',
defaultValue: netzwerkContent.sponsoringForm.steps,
minRows: 2,
maxRows: 2,
fields: formStepFields,
},
{
name: 'packages',
label: 'Package options',
type: 'array',
dbName: 'network_form_pkgs',
defaultValue: netzwerkContent.sponsoringForm.packages,
fields: packageFields,
},
text('step1Eyebrow', 'Step 1 eyebrow', netzwerkContent.sponsoringForm.step1Eyebrow),
text('step1Heading', 'Step 1 heading', netzwerkContent.sponsoringForm.step1Heading),
text('step2Eyebrow', 'Step 2 eyebrow', netzwerkContent.sponsoringForm.step2Eyebrow),
text('step2Heading', 'Step 2 heading', netzwerkContent.sponsoringForm.step2Heading),
text('step2Eyebrow', 'Step 1 eyebrow', netzwerkContent.sponsoringForm.step2Eyebrow),
text('step2Heading', 'Step 1 heading', netzwerkContent.sponsoringForm.step2Heading),
text('companyLabel', 'Company label', netzwerkContent.sponsoringForm.companyLabel),
text('companyPlaceholder', 'Company placeholder', netzwerkContent.sponsoringForm.companyPlaceholder),
text('industryLabel', 'Industry label', netzwerkContent.sponsoringForm.industryLabel),
text('industryPlaceholder', 'Industry placeholder', netzwerkContent.sponsoringForm.industryPlaceholder),
text('step3Eyebrow', 'Step 3 eyebrow', netzwerkContent.sponsoringForm.step3Eyebrow),
text('step3Heading', 'Step 3 heading', netzwerkContent.sponsoringForm.step3Heading),
text('step3Eyebrow', 'Step 2 eyebrow', netzwerkContent.sponsoringForm.step3Eyebrow),
text('step3Heading', 'Step 2 heading', netzwerkContent.sponsoringForm.step3Heading),
text('contactLabel', 'Contact label', netzwerkContent.sponsoringForm.contactLabel),
text('contactPlaceholder', 'Contact placeholder', netzwerkContent.sponsoringForm.contactPlaceholder),
text('emailLabel', 'Email label', netzwerkContent.sponsoringForm.emailLabel),
@@ -275,7 +259,6 @@ export const netzwerkFields: Field[] = [
text('backLabel', 'Back label', netzwerkContent.sponsoringForm.backLabel),
text('nextLabel', 'Next label', netzwerkContent.sponsoringForm.nextLabel),
text('submitLabel', 'Submit label', netzwerkContent.sponsoringForm.submitLabel),
text('requiredPackageError', 'Required package error', netzwerkContent.sponsoringForm.requiredPackageError),
text('requiredFieldError', 'Required field error', netzwerkContent.sponsoringForm.requiredFieldError),
text('invalidEmailError', 'Invalid email error', netzwerkContent.sponsoringForm.invalidEmailError),
text('doneLabel', 'Done step label', netzwerkContent.sponsoringForm.doneLabel),

View File

@@ -24,13 +24,6 @@ const textarea = (name: string, label: string, defaultValue?: string): Field =>
defaultValue,
})
const checkbox = (name: string, label: string, defaultValue = false): Field => ({
name,
type: 'checkbox',
label,
defaultValue,
})
const sectionAdmin = (description: string) => ({
description,
initCollapsed: true,
@@ -133,7 +126,6 @@ export const participationFields: Field[] = [
textarea('heading', 'Heading', participationContent.eligibility.heading),
textarea('description', 'Intro paragraph', participationContent.eligibility.description),
ctaGroup('primaryCta', 'Primary CTA', participationContent.eligibility.primaryCta.label, participationContent.eligibility.primaryCta.url),
ctaGroup('secondaryCta', 'Secondary CTA', participationContent.eligibility.secondaryCta.label, participationContent.eligibility.secondaryCta.url),
{
name: 'criteria',
label: 'Eligibility rows',
@@ -151,7 +143,6 @@ export const participationFields: Field[] = [
fields: [text('title', 'Title'), textarea('body', 'Body'), iconField('building2')],
},
text('nudgeText', 'Bottom nudge text', participationContent.eligibility.nudgeText),
ctaGroup('nudgeCta', 'Bottom CTA', participationContent.eligibility.nudgeCta.label, participationContent.eligibility.nudgeCta.url),
],
},
{
@@ -163,13 +154,6 @@ export const participationFields: Field[] = [
text('eyebrow', 'Eyebrow', participationContent.applicationWays.eyebrow),
textarea('heading', 'Heading', participationContent.applicationWays.heading),
textarea('description', 'Intro paragraph', participationContent.applicationWays.description),
text('recommendedLabel', 'Featured card label', participationContent.applicationWays.recommendedLabel),
ctaGroup(
'fallbackCta',
'Fallback CTA for path cards',
participationContent.applicationWays.fallbackCta.label,
participationContent.applicationWays.fallbackCta.url,
),
{
name: 'institutions',
label: 'Proposal institutions',
@@ -189,7 +173,6 @@ export const participationFields: Field[] = [
textarea('title', 'Title'),
textarea('desc', 'Description'),
iconField('send', 'ico'),
checkbox('featured', 'Featured card'),
{
name: 'listType',
label: 'Detail list type',
@@ -208,7 +191,6 @@ export const participationFields: Field[] = [
dbName: 'way_facts',
fields: factFields,
},
ctaGroup('cta', 'CTA', participationContent.applicationWays.fallbackCta.label, participationContent.applicationWays.fallbackCta.url),
],
},
],
@@ -268,149 +250,6 @@ export const participationFields: Field[] = [
},
],
},
{
name: 'applicationForm',
label: '07 · Application form wrapper',
type: 'group',
admin: { ...sectionAdmin('Legacy page-level form section. The frontend now uses Formulare > Bewerbungsformular.'), hidden: true },
fields: [
uploadField('image', 'Form image', `Current frontend image: /images/${participationContent.applicationForm.imageFilename}`),
text('imageAlt', 'Image alt text', participationContent.applicationForm.imageAlt),
text('imageCaption', 'Image caption', participationContent.applicationForm.imageCaption),
text('eyebrow', 'Eyebrow', participationContent.applicationForm.eyebrow),
textarea('heading', 'Heading', participationContent.applicationForm.heading),
textarea('description', 'Body copy', participationContent.applicationForm.description),
{
name: 'facts',
label: 'Fact rows',
type: 'array',
dbName: 'form_facts',
defaultValue: participationContent.applicationForm.facts,
fields: [text('num', 'Number'), text('label', 'Label'), textarea('desc', 'Description')],
},
text('formEyebrow', 'Form eyebrow', participationContent.applicationForm.formEyebrow),
text('formTitle', 'Form title', participationContent.applicationForm.formTitle),
ctaGroup('uploadCta', 'Top upload CTA', participationContent.applicationForm.uploadCta.label, participationContent.applicationForm.uploadCta.url),
textarea('uploadPrompt', 'Bottom upload prompt', participationContent.applicationForm.uploadPrompt),
ctaGroup('uploadFooterCta', 'Bottom upload CTA', participationContent.applicationForm.uploadFooterCta.label, participationContent.applicationForm.uploadFooterCta.url),
],
},
{
name: 'form',
label: '08 · Application form copy and logic labels',
type: 'group',
admin: { ...sectionAdmin('Legacy page-level form fields. The frontend now uses Formulare > Bewerbungsformular.'), hidden: true },
fields: [
text('stepCompleteLabel', 'Completed step label', participationContent.form.stepCompleteLabel),
text('backLabel', 'Back button label', participationContent.form.backLabel),
text('nextLabel', 'Next button label', participationContent.form.nextLabel),
text('submitLabel', 'Submit button label', participationContent.form.submitLabel),
text('yesLabel', 'Yes option label', participationContent.form.yesLabel),
text('noLabel', 'No option label', participationContent.form.noLabel),
text('requiredSuffix', 'Required field suffix', participationContent.form.requiredSuffix),
{
name: 'validation',
label: 'Validation messages',
type: 'group',
fields: [
text('choose', 'Choose message', participationContent.form.validation.choose),
text('required', 'Required message', participationContent.form.validation.required),
text('invalidEmail', 'Invalid email message', participationContent.form.validation.invalidEmail),
],
},
{
name: 'typeStep',
label: 'Step 1 · Submission type',
type: 'group',
fields: [
text('eyebrow', 'Eyebrow', participationContent.form.typeStep.eyebrow),
text('heading', 'Heading', participationContent.form.typeStep.heading),
text('selfTitle', 'Self-application title', participationContent.form.typeStep.selfTitle),
textarea('selfDesc', 'Self-application description', participationContent.form.typeStep.selfDesc),
text('nominationTitle', 'Nomination title', participationContent.form.typeStep.nominationTitle),
textarea('nominationDesc', 'Nomination description', participationContent.form.typeStep.nominationDesc),
],
},
{
name: 'selfSteps',
label: 'Self-application step labels',
type: 'array',
dbName: 'self_steps',
defaultValue: participationContent.form.selfSteps,
fields: [text('label', 'Label'), iconField('trophy')],
},
{
name: 'nominationSteps',
label: 'Nomination step labels',
type: 'array',
dbName: 'nom_steps',
defaultValue: participationContent.form.nominationSteps,
fields: [text('label', 'Label'), iconField('trophy')],
},
{
name: 'employeeOptions',
label: 'Employee options',
type: 'array',
dbName: 'emp_opts',
defaultValue: participationContent.form.employeeOptions,
fields: [text('value', 'Value'), text('label', 'Label')],
},
{
name: 'eligibility',
label: 'Self-application eligibility step',
type: 'group',
fields: [
text('allowedEmployeeValues', 'Allowed employee option values, comma-separated', participationContent.form.eligibility.allowedEmployeeValues),
text('requiredBayernValue', 'Required Bavaria answer value', participationContent.form.eligibility.requiredBayernValue),
text('eyebrow', 'Eyebrow', participationContent.form.eligibility.eyebrow),
text('heading', 'Heading', participationContent.form.eligibility.heading),
text('employeesLabel', 'Employees field label', participationContent.form.eligibility.employeesLabel),
text('bayernLabel', 'Bavaria field label', participationContent.form.eligibility.bayernLabel),
text('ownerLabel', 'Owner field label', participationContent.form.eligibility.ownerLabel),
text('ineligibleHeading', 'Ineligible heading', participationContent.form.eligibility.ineligibleHeading),
textarea('ineligibleBody', 'Ineligible body', participationContent.form.eligibility.ineligibleBody),
text('ineligibleContactPrefix', 'Ineligible contact prefix', participationContent.form.eligibility.ineligibleContactPrefix),
text('ineligibleContactEmail', 'Ineligible contact email', participationContent.form.eligibility.ineligibleContactEmail),
],
},
{
name: 'selfContact',
label: 'Self-application contact step',
type: 'group',
fields: Object.entries(participationContent.form.selfContact).map(([name, value]) => text(name, name, value)),
},
{
name: 'selfSummary',
label: 'Self-application summary step',
type: 'group',
fields: Object.entries(participationContent.form.selfSummary).map(([name, value]) => text(name, name, value)),
},
{
name: 'nominationCompany',
label: 'Nomination company step',
type: 'group',
fields: Object.entries(participationContent.form.nominationCompany).map(([name, value]) => text(name, name, value)),
},
{
name: 'nominationContact',
label: 'Nomination contact step',
type: 'group',
fields: Object.entries(participationContent.form.nominationContact).map(([name, value]) => text(name, name, value)),
},
{
name: 'nominationSummary',
label: 'Nomination summary step',
type: 'group',
fields: Object.entries(participationContent.form.nominationSummary).map(([name, value]) => text(name, name, value)),
},
{
name: 'success',
label: 'Success state copy',
type: 'group',
fields: Object.entries(participationContent.form.success).map(([name, value]) => text(name, name, value)),
},
],
},
],
},
]

View File

@@ -66,7 +66,6 @@ export const pressIndexFields: Field[] = [
text('eyebrow', 'Eyebrow', pressIndexContent.events.eyebrow),
textarea('heading', 'Heading', pressIndexContent.events.heading),
textarea('description', 'Description', pressIndexContent.events.description),
text('detailLabel', 'Detail link label', pressIndexContent.events.detailLabel),
text('defaultStatusLabel', 'Missing status fallback label', pressIndexContent.events.defaultStatusLabel),
text('openStatusLabel', 'Open status label', pressIndexContent.events.openStatusLabel),
text('missingTitleLabel', 'Missing event title label', pressIndexContent.events.missingTitleLabel),
@@ -92,7 +91,6 @@ export const pressIndexFields: Field[] = [
text('eyebrow', 'Eyebrow', pressIndexContent.news.eyebrow),
textarea('heading', 'Heading', pressIndexContent.news.heading),
textarea('description', 'Description', pressIndexContent.news.description),
text('readMoreLabel', 'Read more label', pressIndexContent.news.readMoreLabel),
text('missingTitleLabel', 'Missing news title label', pressIndexContent.news.missingTitleLabel),
text('missingCategoryLabel', 'Missing news category label', pressIndexContent.news.missingCategoryLabel),
uploadField('collectionFallbackImage', 'Missing news image fallback', `Current frontend image: /images/${pressIndexContent.news.collectionFallbackImageFilename}`),
@@ -102,38 +100,16 @@ export const pressIndexFields: Field[] = [
name: 'downloads',
label: '04 · Press material and contact',
type: 'group',
admin: sectionAdmin('Download button and press contact block.'),
admin: sectionAdmin('Press material introduction and contact block.'),
fields: [
text('eyebrow', 'Eyebrow', pressIndexContent.downloads.eyebrow),
textarea('heading', 'Heading', pressIndexContent.downloads.heading),
textarea('description', 'Description', pressIndexContent.downloads.description),
text('kitLabel', 'Kit label', pressIndexContent.downloads.kitLabel),
text('kitMeta', 'Kit metadata', pressIndexContent.downloads.kitMeta),
uploadField('kitFile', 'Press kit file', `Current frontend file: ${pressIndexContent.downloads.kitUrl}`),
text('kitUrl', 'Kit URL', pressIndexContent.downloads.kitUrl),
text('kitDownloadName', 'Kit download filename', pressIndexContent.downloads.kitDownloadName),
text('contactEyebrow', 'Contact eyebrow', pressIndexContent.downloads.contactEyebrow),
text('contactName', 'Contact name', pressIndexContent.downloads.contactName),
textarea('contactLines', 'Contact lines', pressIndexContent.downloads.contactLines),
],
},
{
name: 'accreditation',
label: '05 · Accreditation form copy',
type: 'group',
admin: sectionAdmin('Labels and success state for the press accreditation form.'),
fields: [
text('eyebrow', 'Eyebrow', pressIndexContent.accreditation.eyebrow),
textarea('heading', 'Heading', pressIndexContent.accreditation.heading),
textarea('description', 'Description', pressIndexContent.accreditation.description),
text('mediumLabel', 'Medium label', pressIndexContent.accreditation.mediumLabel),
text('nameLabel', 'Name label', pressIndexContent.accreditation.nameLabel),
text('emailLabel', 'Email label', pressIndexContent.accreditation.emailLabel),
text('submitLabel', 'Submit label', pressIndexContent.accreditation.submitLabel),
text('successHeading', 'Success heading', pressIndexContent.accreditation.successHeading),
textarea('successMessage', 'Success message', pressIndexContent.accreditation.successMessage),
],
},
],
},
]

View File

@@ -0,0 +1,397 @@
import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
import { homeContent } from '@/spa/homeContent'
type MigrationDB = MigrateUpArgs['db']
const ident = (value: string) => `\`${value.replace(/`/g, '``')}\``
const literal = (value: string) => `'${value.replace(/'/g, "''")}'`
const snake = (value: string) => value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
const obsoleteChildTables = [
'pages_home_application_benefits',
'_pages_v_version_home_application_benefits',
'home_self_steps',
'_home_self_steps_v',
'home_nom_steps',
'_home_nom_steps_v',
'home_emp_opts',
'_home_emp_opts_v',
'form_facts',
'_form_facts_v',
'self_steps',
'_self_steps_v',
'nom_steps',
'_nom_steps_v',
'emp_opts',
'_emp_opts_v',
'network_form_pkgs',
'_network_form_pkgs_v',
] as const
const livePrefixes = [
'home_application_',
'home_form_',
'participation_application_form_',
'participation_form_',
'about_highlights_',
'press_index_downloads_kit_',
'press_index_accreditation_',
] as const
const versionPrefixes = livePrefixes.map((prefix) => `version_${prefix}`)
const standaloneLiveColumns = [
'netzwerk_jury_note',
'netzwerk_partners_premium_label',
'netzwerk_partners_default_logo_color',
'netzwerk_sponsoring_footnote_prefix',
'netzwerk_sponsoring_footnote_label',
'netzwerk_sponsoring_footnote_url',
'netzwerk_sponsoring_form_eyebrow',
'netzwerk_sponsoring_form_step1_eyebrow',
'netzwerk_sponsoring_form_step1_heading',
'netzwerk_sponsoring_form_required_package_error',
'participation_eligibility_secondary_cta_label',
'participation_eligibility_secondary_cta_url',
'participation_eligibility_nudge_cta_label',
'participation_eligibility_nudge_cta_url',
'participation_application_ways_recommended_label',
'participation_application_ways_fallback_cta_label',
'participation_application_ways_fallback_cta_url',
'press_index_events_detail_label',
'press_index_news_read_more_label',
] as const
const obsoleteApplicationWayColumns = ['featured', 'cta_label', 'cta_url'] as const
const uploadIndexes = [
'pages_home_application_home_application_award_image_idx',
'_pages_v_version_home_application_version_home_applicati_idx',
'pages_participation_application_form_participation_appli_idx',
'_pages_v_version_participation_application_form_version__idx',
'pages_about_highlights_about_highlights_fallback_image_idx',
'_pages_v_version_about_highlights_version_about_highligh_idx',
'pages_press_index_downloads_press_index_downloads_kit_fi_idx',
'_pages_v_version_press_index_downloads_version_press_ind_idx',
] as const
function flattenScalars(value: Record<string, unknown>, prefix = ''): Array<[string, string]> {
return Object.entries(value).flatMap(([key, item]) => {
const path = prefix ? `${prefix}_${snake(key)}` : snake(key)
if (Array.isArray(item) || item === undefined || item === null) return []
if (typeof item === 'object') return flattenScalars(item as Record<string, unknown>, path)
return [[path, String(item)]]
})
}
const homeApplicationColumns = flattenScalars(homeContent.application as Record<string, unknown>)
.filter(([name]) => name !== 'award_image_filename')
.map(([name, value]) => [`home_application_${name}`, `text DEFAULT ${literal(value)}`] as const)
const sharedFormColumns = flattenScalars(homeContent.form as Record<string, unknown>)
const participationApplicationColumns = [
'image_alt',
'image_caption',
'eyebrow',
'heading',
'description',
'form_eyebrow',
'form_title',
'upload_cta_label',
'upload_cta_url',
'upload_prompt',
'upload_footer_cta_label',
'upload_footer_cta_url',
].map((suffix) => [`participation_application_form_${suffix}`, 'text'] as const)
const aboutHighlightColumns = [
'eyebrow',
'heading',
'cta_label',
'cta_url',
'hover_label',
'fallback_winner_name',
'fallback_winner_category',
'fallback_winner_award_type',
].map((suffix) => [`about_highlights_${suffix}`, 'text'] as const)
const pressColumns = [
'press_index_downloads_kit_label',
'press_index_downloads_kit_meta',
'press_index_downloads_kit_url',
'press_index_downloads_kit_download_name',
'press_index_accreditation_eyebrow',
'press_index_accreditation_heading',
'press_index_accreditation_description',
'press_index_accreditation_medium_label',
'press_index_accreditation_name_label',
'press_index_accreditation_email_label',
'press_index_accreditation_submit_label',
'press_index_accreditation_success_heading',
'press_index_accreditation_success_message',
].map((name) => [name, 'text'] as const)
const restoredLiveColumns = [
...homeApplicationColumns,
['home_application_award_caption', 'text'] as const,
['home_application_award_image_id', 'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null'] as const,
...sharedFormColumns.map(([name, value]) => [`home_form_${name}`, `text DEFAULT ${literal(value)}`] as const),
...participationApplicationColumns,
['participation_application_form_image_id', 'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null'] as const,
...sharedFormColumns.map(([name, value]) => [`participation_form_${name}`, `text DEFAULT ${literal(value)}`] as const),
...aboutHighlightColumns,
['about_highlights_fallback_image_id', 'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null'] as const,
['about_highlights_year', 'numeric'] as const,
...pressColumns,
['press_index_downloads_kit_file_id', 'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null'] as const,
...standaloneLiveColumns.map((name) => [name, 'text'] as const),
]
async function tableExists(db: MigrationDB, tableName: string) {
const rows = (await db.all(
sql.raw(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`),
)) as Array<{ name: string }>
return rows.length > 0
}
async function columnNames(db: MigrationDB, tableName: string) {
if (!(await tableExists(db, tableName))) return []
const columns = (await db.all(sql.raw(`PRAGMA table_info(${ident(tableName)})`))) as Array<{ name: string }>
return columns.map((column) => column.name)
}
async function columnExists(db: MigrationDB, tableName: string, columnName: string) {
return (await columnNames(db, tableName)).includes(columnName)
}
async function addColumn(db: MigrationDB, tableName: string, columnName: string, definition: string) {
if (await columnExists(db, tableName, columnName)) return
await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} ADD ${ident(columnName)} ${definition};`))
}
function splitDefinitions(value: string) {
const definitions: string[] = []
let current = ''
let depth = 0
let quote = ''
for (let index = 0; index < value.length; index += 1) {
const character = value[index]
const previous = value[index - 1]
if (quote) {
current += character
if (character === quote && previous !== '\\') quote = ''
continue
}
if (character === '`' || character === '"' || character === "'") {
quote = character
current += character
continue
}
if (character === '(') depth += 1
if (character === ')') depth -= 1
if (character === ',' && depth === 0) {
definitions.push(current.trim())
current = ''
continue
}
current += character
}
if (current.trim()) definitions.push(current.trim())
return definitions
}
function definitionColumnName(definition: string) {
const match = definition.trim().match(/^(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|([^\s]+))/)
return match?.[1] || match?.[2] || match?.[3] || match?.[4] || ''
}
function referencesColumn(definition: string, column: string) {
return [ident(column), `"${column}"`, `[${column}]`].some((value) => definition.includes(value))
}
async function rebuildTableWithoutColumns(
db: MigrationDB,
tableName: string,
removedColumns: readonly string[],
) {
if (removedColumns.length === 0) return
const schemaRows = (await db.all(
sql.raw(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`),
)) as Array<{ sql: string }>
const schema = schemaRows[0]?.sql
if (!schema) return
const open = schema.indexOf('(')
const close = schema.lastIndexOf(')')
if (open < 0 || close < open) throw new Error(`Could not parse schema for ${tableName}`)
const definitions = splitDefinitions(schema.slice(open + 1, close))
const keptDefinitions = definitions.filter((definition) => {
const columnName = definitionColumnName(definition)
const isConstraint = /^(CONSTRAINT|FOREIGN|PRIMARY|UNIQUE|CHECK)\b/i.test(columnName)
if (isConstraint) return !removedColumns.some((column) => referencesColumn(definition, column))
return !removedColumns.includes(columnName)
})
const keptColumns = (await columnNames(db, tableName)).filter((column) => !removedColumns.includes(column))
const indexRows = (await db.all(
sql.raw(`SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ${literal(tableName)} AND sql IS NOT NULL`),
)) as Array<{ sql: string }>
const keptIndexes = indexRows
.map((row) => row.sql)
.filter((indexSql) => !removedColumns.some((column) => referencesColumn(indexSql, column)))
const temporaryTable = `__task4_${tableName}`
const columnList = keptColumns.map(ident).join(', ')
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident(temporaryTable)};`))
await db.run(sql.raw(`CREATE TABLE ${ident(temporaryTable)} (${keptDefinitions.join(',\n')})${schema.slice(close + 1)};`))
await db.run(sql.raw(`INSERT INTO ${ident(temporaryTable)} (${columnList}) SELECT ${columnList} FROM ${ident(tableName)};`))
await db.run(sql.raw(`DROP TABLE ${ident(tableName)};`))
await db.run(sql.raw(`ALTER TABLE ${ident(temporaryTable)} RENAME TO ${ident(tableName)};`))
for (const indexSql of keptIndexes) await db.run(sql.raw(`${indexSql};`))
}
async function trimSponsoringSteps(db: MigrationDB, tableName: string) {
if (!(await tableExists(db, tableName))) return
await db.run(sql.raw(`
DELETE FROM ${ident(tableName)}
WHERE ${ident('id')} IN (
SELECT ${ident('id')}
FROM (
SELECT ${ident('id')}, ROW_NUMBER() OVER (
PARTITION BY ${ident('_parent_id')} ORDER BY ${ident('_order')} DESC
) AS row_number
FROM ${ident(tableName)}
)
WHERE row_number > 2
);
`))
await db.run(sql.raw(`
UPDATE ${ident(tableName)} AS current
SET ${ident('_order')} = (
SELECT COUNT(*)
FROM ${ident(tableName)} AS preceding
WHERE preceding.${ident('_parent_id')} = current.${ident('_parent_id')}
AND preceding.${ident('_order')} <= current.${ident('_order')}
);
`))
}
async function createArrayTable(
db: MigrationDB,
tableName: string,
parentTable: 'pages' | '_pages_v',
versioned: boolean,
fields: string,
) {
if (await tableExists(db, tableName)) return
await db.run(sql.raw(`
CREATE TABLE ${ident(tableName)} (
${ident('_order')} integer NOT NULL,
${ident('_parent_id')} integer NOT NULL,
${ident('id')} ${versioned ? 'integer' : 'text'} PRIMARY KEY NOT NULL,
${fields},
${versioned ? `${ident('_uuid')} text,` : ''}
FOREIGN KEY (${ident('_parent_id')}) REFERENCES ${ident(parentTable)}(${ident('id')}) ON UPDATE no action ON DELETE cascade
);
`))
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_order_idx`)} ON ${ident(tableName)} (${ident('_order')});`))
await db.run(sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(`${tableName}_parent_id_idx`)} ON ${ident(tableName)} (${ident('_parent_id')});`))
}
async function restoreChildTables(db: MigrationDB) {
const textRows = `${ident('num')} text, ${ident('label')} text, ${ident('desc')} text`
const stepRows = `${ident('label')} text, ${ident('icon')} text DEFAULT 'trophy'`
const optionRows = `${ident('value')} text, ${ident('label')} text`
const packageRows = `${ident('value')} text, ${ident('title')} text, ${ident('desc')} text`
await createArrayTable(db, 'pages_home_application_benefits', 'pages', false, textRows)
await createArrayTable(db, '_pages_v_version_home_application_benefits', '_pages_v', true, textRows)
await createArrayTable(db, 'home_self_steps', 'pages', false, stepRows)
await createArrayTable(db, '_home_self_steps_v', '_pages_v', true, stepRows)
await createArrayTable(db, 'home_nom_steps', 'pages', false, stepRows)
await createArrayTable(db, '_home_nom_steps_v', '_pages_v', true, stepRows)
await createArrayTable(db, 'home_emp_opts', 'pages', false, optionRows)
await createArrayTable(db, '_home_emp_opts_v', '_pages_v', true, optionRows)
await createArrayTable(db, 'form_facts', 'pages', false, textRows)
await createArrayTable(db, '_form_facts_v', '_pages_v', true, textRows)
await createArrayTable(db, 'self_steps', 'pages', false, stepRows)
await createArrayTable(db, '_self_steps_v', '_pages_v', true, stepRows)
await createArrayTable(db, 'nom_steps', 'pages', false, stepRows)
await createArrayTable(db, '_nom_steps_v', '_pages_v', true, stepRows)
await createArrayTable(db, 'emp_opts', 'pages', false, optionRows)
await createArrayTable(db, '_emp_opts_v', '_pages_v', true, optionRows)
await createArrayTable(db, 'network_form_pkgs', 'pages', false, packageRows)
await createArrayTable(db, '_network_form_pkgs_v', '_pages_v', true, packageRows)
}
export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise<void> {
await db.run(sql.raw('PRAGMA foreign_keys=OFF;'))
for (const indexName of uploadIndexes) {
await db.run(sql.raw(`DROP INDEX IF EXISTS ${ident(indexName)};`))
}
await db.run(sql.raw("DELETE FROM `pages_rels` WHERE `path` = 'about.highlights.selectedWinners';"))
await db.run(sql.raw("DELETE FROM `_pages_v_rels` WHERE `path` = 'version.about.highlights.selectedWinners';"))
for (const tableName of obsoleteChildTables) {
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident(tableName)};`))
}
const pageColumns = await columnNames(db, 'pages')
const removedPageColumns = pageColumns.filter(
(column) => standaloneLiveColumns.includes(column as (typeof standaloneLiveColumns)[number]) || livePrefixes.some((prefix) => column.startsWith(prefix)),
)
const versionColumns = await columnNames(db, '_pages_v')
const standaloneVersionColumns = standaloneLiveColumns.map((column) => `version_${column}`)
const removedVersionColumns = versionColumns.filter(
(column) => standaloneVersionColumns.includes(column) || versionPrefixes.some((prefix) => column.startsWith(prefix)),
)
await rebuildTableWithoutColumns(db, 'pages', removedPageColumns)
await rebuildTableWithoutColumns(db, '_pages_v', removedVersionColumns)
await rebuildTableWithoutColumns(db, 'app_ways', obsoleteApplicationWayColumns)
await rebuildTableWithoutColumns(db, '_app_ways_v', obsoleteApplicationWayColumns)
await trimSponsoringSteps(db, 'network_form_steps')
await trimSponsoringSteps(db, '_network_form_steps_v')
await db.run(sql.raw('PRAGMA foreign_keys=ON;'))
}
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
for (const [column, definition] of restoredLiveColumns) {
await addColumn(db, 'pages', column, definition)
await addColumn(db, '_pages_v', `version_${column}`, definition)
}
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `pages_home_application_home_application_award_image_idx` ON `pages` (`home_application_award_image_id`);'))
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `_pages_v_version_home_application_version_home_applicati_idx` ON `_pages_v` (`version_home_application_award_image_id`);'))
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `pages_participation_application_form_participation_appli_idx` ON `pages` (`participation_application_form_image_id`);'))
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `_pages_v_version_participation_application_form_version__idx` ON `_pages_v` (`version_participation_application_form_image_id`);'))
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `pages_about_highlights_about_highlights_fallback_image_idx` ON `pages` (`about_highlights_fallback_image_id`);'))
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `_pages_v_version_about_highlights_version_about_highligh_idx` ON `_pages_v` (`version_about_highlights_fallback_image_id`);'))
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `pages_press_index_downloads_press_index_downloads_kit_fi_idx` ON `pages` (`press_index_downloads_kit_file_id`);'))
await db.run(sql.raw('CREATE INDEX IF NOT EXISTS `_pages_v_version_press_index_downloads_version_press_ind_idx` ON `_pages_v` (`version_press_index_downloads_kit_file_id`);'))
await restoreChildTables(db)
await addColumn(db, 'app_ways', 'featured', 'integer DEFAULT false')
await addColumn(db, 'app_ways', 'cta_label', "text DEFAULT 'Zum Formular'")
await addColumn(db, 'app_ways', 'cta_url', "text DEFAULT '#bewerben'")
await addColumn(db, '_app_ways_v', 'featured', 'integer DEFAULT false')
await addColumn(db, '_app_ways_v', 'cta_label', "text DEFAULT 'Zum Formular'")
await addColumn(db, '_app_ways_v', 'cta_url', "text DEFAULT '#bewerben'")
}

View File

@@ -16,6 +16,7 @@ import * as migration_20260713_194500_contact_submissions from './20260713_19450
import * as migration_20260713_213000_netzwerk_jury_selection from './20260713_213000_netzwerk_jury_selection';
import * as migration_20260713_220000_preistraeger_detail_cleanup from './20260713_220000_preistraeger_detail_cleanup';
import * as migration_20260731_143000_remove_press_record_fallbacks from './20260731_143000_remove_press_record_fallbacks';
import * as migration_20260731_160000_focused_payload_cleanup from './20260731_160000_focused_payload_cleanup';
export const migrations = [
{
@@ -108,4 +109,9 @@ export const migrations = [
down: migration_20260731_143000_remove_press_record_fallbacks.down,
name: '20260731_143000_remove_press_record_fallbacks',
},
{
up: migration_20260731_160000_focused_payload_cleanup.up,
down: migration_20260731_160000_focused_payload_cleanup.down,
name: '20260731_160000_focused_payload_cleanup',
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -41,25 +41,15 @@ async function main() {
const page = pageResult.docs[0]
if (!page) throw new Error('About page not found. Expected spaPath=/der-bmp or slug=der-bmp/about.')
const [heroImage, prizeImage, mittelstandImage, fallbackWinnerImage, winnerResult] = await Promise.all([
const [heroImage, prizeImage, mittelstandImage] = await Promise.all([
mediaByFilename(payload, aboutContent.hero.backgroundImageFilename),
mediaByFilename(payload, aboutContent.prize.imageFilename),
mediaByFilename(payload, aboutContent.mittelstand.imageFilename),
mediaByFilename(payload, aboutContent.highlights.fallbackImageFilename),
payload.find({
collection: 'preistraeger',
limit: 8,
pagination: false,
draft: true,
overrideAccess: true,
where: { year: { equals: aboutContent.highlights.year } },
}),
])
const { backgroundImageFilename: _heroFilename, ...heroContent } = aboutContent.hero
const { imageFilename: _prizeFilename, ...prizeContent } = aboutContent.prize
const { imageFilename: _mittelstandFilename, ...mittelstandContent } = aboutContent.mittelstand
const { fallbackImageFilename: _highlightFallbackFilename, ...highlightsContent } = aboutContent.highlights
const testimonials = await testimonialsWithImages(payload)
await payload.update({
@@ -84,11 +74,6 @@ async function main() {
image: mittelstandImage,
body: aboutContent.mittelstand.body.map((text) => ({ text })),
},
highlights: {
...highlightsContent,
fallbackImage: fallbackWinnerImage,
selectedWinners: winnerResult.docs.map((winner) => winner.id),
},
testimonials,
},
} as never,

View File

@@ -41,12 +41,11 @@ async function main() {
const page = pageResult.docs[0]
if (!page) throw new Error('Home page not found. Expected spaPath=/ or slug=startseite/home.')
const [heroImage, quickImage, introImage, benefitsImage, awardImage] = await Promise.all([
const [heroImage, quickImage, introImage, benefitsImage] = await Promise.all([
mediaByFilename(payload, homeContent.hero.backgroundImageFilename),
mediaByFilename(payload, homeContent.quickCheck.imageFilename),
mediaByFilename(payload, homeContent.intro.imageFilename),
mediaByFilename(payload, homeContent.benefits.imageFilename),
mediaByFilename(payload, homeContent.application.awardImageFilename),
])
const fallbackWinnerImage = await mediaByFilename(payload, homeContent.winners.fallbackImageFilename)
const { backgroundImageFilename: _heroFilename, ...heroContent } = homeContent.hero
@@ -54,7 +53,6 @@ async function main() {
const { imageFilename: _introFilename, ...introContent } = homeContent.intro
const { fallbackImageFilename: _winnerFallbackFilename, ...winnersContent } = homeContent.winners
const { imageFilename: _benefitsFilename, ...benefitsContent } = homeContent.benefits
const { awardImageFilename: _awardFilename, ...applicationContent } = homeContent.application
const testimonials = await testimonialsWithImages(payload)
const winners = await payload.find({
@@ -94,11 +92,6 @@ async function main() {
...benefitsContent,
image: benefitsImage,
},
application: {
...applicationContent,
awardImage,
},
form: homeContent.form,
videoModal: homeContent.videoModal,
},
} as never,

View File

@@ -23,13 +23,11 @@ async function main() {
const page = pageResult.docs[0]
if (!page) throw new Error('Participation page not found. Expected spaPath=/teilnahme or slug=teilnahme/participation.')
const [heroImage, formImage, ...awardCardImages] = await Promise.all([
const [heroImage, ...awardCardImages] = await Promise.all([
mediaByFilename(payload, participationContent.hero.backgroundImageFilename),
mediaByFilename(payload, participationContent.applicationForm.imageFilename),
...participationContent.awardsGrid.cards.map((card) => mediaByFilename(payload, card.imageFilename)),
])
const { backgroundImageFilename: _heroFilename, ...heroContent } = participationContent.hero
const { imageFilename: _formFilename, ...applicationFormContent } = participationContent.applicationForm
const { cards: awardCards, ...awardsGridContent } = participationContent.awardsGrid
await payload.update({
@@ -54,11 +52,6 @@ async function main() {
image: awardCardImages[index],
})),
},
applicationForm: {
...applicationFormContent,
image: formImage,
},
form: participationContent.form,
},
} as never,
})

View File

@@ -23,11 +23,10 @@ async function main() {
const page = pageResult.docs[0]
if (!page) throw new Error('Press index page not found. Expected spaPath=/presse or slug=presse/press.')
const [heroImage, eventFallbackImage, newsFallbackImage, kitFile] = await Promise.all([
const [heroImage, eventFallbackImage, newsFallbackImage] = await Promise.all([
mediaByFilename(payload, pressIndexContent.hero.imageFilename),
mediaByFilename(payload, pressIndexContent.events.collectionFallbackImageFilename),
mediaByFilename(payload, pressIndexContent.news.collectionFallbackImageFilename),
mediaByFilename(payload, pressIndexContent.downloads.kitDownloadName),
])
const { imageFilename: _heroFilename, ...heroContent } = pressIndexContent.hero
const { collectionFallbackImageFilename: _eventFallbackFilename, ...eventsContent } = pressIndexContent.events
@@ -52,10 +51,7 @@ async function main() {
...newsContent,
collectionFallbackImage: newsFallbackImage,
},
downloads: {
...pressIndexContent.downloads,
kitFile,
},
downloads: pressIndexContent.downloads,
},
} as never,
})

View File

@@ -43,20 +43,6 @@ export const aboutContent = {
'In einer globalisierten Welt sind es diese Unternehmen, die für Stabilität, Innovation und soziale Verantwortung stehen. Der BMP macht sie sichtbar.',
],
},
highlights: {
fallbackImageFilename: 'gala-saal-overview.jpg',
eyebrow: 'Preisträger-Highlights',
heading: 'AUSGEZEICHNETE 2025',
cta: { label: 'Alle Preisträger', url: '/preistraeger' },
hoverLabel: 'Detail →',
year: 2025,
fallbackWinner: {
name: 'Preisträger',
category: 'Preisträger',
awardType: 'Auszeichnung',
},
selectedWinners: [],
},
testimonials: testimonialsContent,
history: {
eyebrow: 'Geschichte & Meilensteine',

View File

@@ -147,8 +147,7 @@ export default function SponsoringForm({ theme = 'dark', content }: { theme?: 'd
const isMobile = useIsMobile();
const applicationPhase = useApplicationPhase();
const copy = { ...netzwerkContent.sponsoringForm, ...(content || {}) };
const configuredSteps = fallbackArray<StepOption>(copy.steps, netzwerkContent.sponsoringForm.steps);
const steps = configuredSteps.slice(-2);
const steps = fallbackArray<StepOption>(copy.steps, netzwerkContent.sponsoringForm.steps);
const [step, setStep] = useState(0);
const [dir, setDir] = useState(1);
const [submitted, setSubmitted] = useState(false);

View File

@@ -106,7 +106,6 @@ export const homeContent = {
],
awardImageFilename: 'preisuebergabe.jpg',
awardImageAlt: 'Preisverleihung Bayerischer Mittelstandspreis',
awardCaption: 'Preisverleihung 2025',
formEyebrow: 'Direktbewerbung 2026',
formTitle: 'Kostenlos bewerben',
footnote: 'Kostenlos und unverbindlich ',

View File

@@ -61,7 +61,6 @@ export const netzwerkContent = {
description:
'Unabhängige Expertinnen und Experten aus Wirtschaft, Wissenschaft und Verbänden, für einen vollständig unabhängigen Bewertungsprozess.',
chairBadge: 'Jury-Vorsitz',
note: 'Hinweis: Einzelne Persönlichkeiten engagieren sich sowohl in der Jury als auch als Partner des BMP beide Rollen werden auf dieser Seite getrennt ausgewiesen.',
members: [
{
name: 'Prof. Dr. Klaus Bergmann',
@@ -123,8 +122,6 @@ export const netzwerkContent = {
heading: 'PARTNER & SPONSOREN',
description: 'Ein starkes Netzwerk aus Unternehmen und Medien begleitet den Bayerischen Mittelstandspreis. Klicken für Details.',
detailLabel: 'Details',
premiumLabel: '',
defaultLogoColor: '#111D55',
tiers: [
{ tier: 1, label: 'Große Darstellung', note: '' },
{ tier: 2, label: 'Mittlere Darstellung', note: '' },
@@ -287,10 +284,6 @@ export const netzwerkContent = {
imageFilename: 'networking-innenhof.jpg',
imageAlt: 'BMP Partnernetzwerk',
imageLabel: 'BMP Partnernetzwerk',
formEyebrow: 'Sponsoring 2026',
footnotePrefix: 'Oder:',
footnoteLabel: 'Vereinsmitglied werden',
footnoteUrl: '/mitglied-werden',
benefits: [
{ num: '01', label: 'Markenpräsenz', sub: 'Bühne, Drucksachen, digitale Kanäle' },
{ num: '02', label: 'Top-Entscheider', sub: 'Exklusives Netzwerk-Dinner für Partner' },
@@ -302,13 +295,6 @@ export const netzwerkContent = {
{ label: 'Unternehmen' },
{ label: 'Kontakt' },
],
packages: [
{ value: 'main', title: 'Haupt-Sponsoring', desc: 'Maximale Sichtbarkeit auf allen Kanälen Bühne, Digital, Print.' },
{ value: 'category', title: 'Kategorie-Partner', desc: 'Namensgebung einer Auszeichnungskategorie fokussiertes Branding.' },
{ value: 'event', title: 'Event-Partner', desc: 'Präsenz beim exklusiven Gala-Abend in München.' },
],
step1Eyebrow: 'Schritt 1 Paket wählen',
step1Heading: 'Interesse an',
step2Eyebrow: 'Schritt 2 Unternehmen',
step2Heading: 'Ihr Unternehmen',
companyLabel: 'Unternehmen *',
@@ -324,7 +310,6 @@ export const netzwerkContent = {
backLabel: 'Zurück',
nextLabel: 'Weiter',
submitLabel: 'Exposé anfordern',
requiredPackageError: 'Bitte wählen Sie ein Paket.',
requiredFieldError: 'Pflichtfeld',
invalidEmailError: 'Ungültige E-Mail',
doneLabel: '✓',

View File

@@ -34,7 +34,6 @@ type HomeHeroCms = {
videoLabel?: string | null;
};
type HomeSectionCms = CmsRecord & {
awardCaption?: string | null;
awardImage?: unknown;
awardImageAlt?: string | null;
benefits?: unknown;

View File

@@ -23,7 +23,7 @@ type PressIndexCms = Partial<typeof pressIndexContent> & {
hero?: Partial<typeof pressIndexContent.hero> & { image?: unknown }
events?: Partial<typeof pressIndexContent.events> & { collectionFallbackImage?: unknown }
news?: Partial<typeof pressIndexContent.news> & { collectionFallbackImage?: unknown }
downloads?: Partial<typeof pressIndexContent.downloads> & { kitFile?: unknown }
downloads?: Partial<typeof pressIndexContent.downloads>
}
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;

View File

@@ -51,7 +51,6 @@ export const participationContent = {
eyebrow: 'Berechtigung',
heading: 'WER KANN\nTEILNEHMEN?',
primaryCta: { label: 'Jetzt bewerben', url: '#bewerben' },
secondaryCta: { label: 'Mitglied werden', url: '/mitglied-werden' },
description:
'Vier klar definierte Kriterien: Erfüllen Sie alle, bewerben Sie sich kostenlos und ohne bürokratischen Aufwand.',
criteria: [
@@ -74,7 +73,6 @@ export const participationContent = {
},
],
nudgeText: 'Alle 4 Punkte erfüllt? Dann jetzt kostenlos bewerben.',
nudgeCta: { label: 'Zum Formular', url: '#bewerben' },
},
applicationWays: {
eyebrow: 'Bewerbung 2026',
@@ -90,15 +88,12 @@ export const participationContent = {
'Mitglieder des Wirtschaftsbeirates Bayern',
'Ehemalige Preisträger des Bayerischen Mittelstandspreises',
].map((text) => ({ text })),
fallbackCta: { label: 'Zum Formular', url: '#bewerben' },
ways: [
{
label: 'Weg 01',
title: 'Vorschlag\nunterbreiten',
desc: 'Durch ehemalige Preisträger, die HAM, Senatoren und den Preis unterstützende Institutionen.',
icon: 'send',
featured: false,
cta: { label: 'Vorschlag unterbreiten', url: '#bewerben' },
listType: 'institutions',
facts: [],
},
@@ -107,8 +102,6 @@ export const participationContent = {
title: 'Initiativ­bewerbung',
desc: 'Für Unternehmer, die selbst die Initiative ergreifen.',
icon: 'userCheck',
featured: false,
cta: { label: 'Jetzt bewerben', url: '#bewerben' },
listType: 'facts',
facts: [
{ label: '0 EUR', desc: 'Teilnahmegebühr' },
@@ -118,7 +111,6 @@ export const participationContent = {
],
},
],
recommendedLabel: 'Empfohlen',
},
datesBanner: {
heading: 'Wichtige Termine 2026',
@@ -135,25 +127,6 @@ export const participationContent = {
],
},
awardsGrid: participationAwardsGrid,
applicationForm: {
imageFilename: 'preistraeger-jubel.jpg',
imageAlt: 'Preisverleihung 2025',
imageCaption: 'Preisverleihung 2025',
eyebrow: 'Direktbewerbung',
heading: 'BEWERBEN ODER\nVORSCHLAGEN.',
description:
'Sie können sich selbst bewerben oder ein herausragendes Unternehmen vorschlagen. Firmenverbunde können sich ebenfalls bewerben. Die Teilnahme ist vollständig kostenfrei.',
facts: [
{ num: '01', label: '0 EUR', desc: 'Teilnahmegebühr' },
{ num: '02', label: 'Schnell', desc: 'Ca. 3045 Minuten' },
{ num: '03', label: 'Mehrstufig', desc: 'Transparenter Prozess' },
],
formEyebrow: 'Bewerbung 2026',
formTitle: 'Kostenlos bewerben',
uploadCta: { label: 'PDF hochladen', url: '/formular-hochladen' },
uploadPrompt: 'Sie haben Ihre Unterlagen bereits als PDF vorbereitet? Laden Sie sie direkt hoch, statt das Formular auszufüllen.',
uploadFooterCta: { label: 'Formular hochladen →', url: '/formular-hochladen' },
},
form: {
stepCompleteLabel: '✓',
backLabel: 'Zurück',

View File

@@ -10,7 +10,6 @@ export const pressIndexContent = {
eyebrow: 'Termine 2026',
heading: 'EVENTS RUND UM DEN PREIS.',
description: 'Von der Jurysitzung bis zur festlichen Gala alle Veranstaltungen auf einen Blick.',
detailLabel: 'Details',
defaultStatusLabel: 'Geplant',
openStatusLabel: 'Anmeldung offen',
missingTitleLabel: 'Event',
@@ -29,7 +28,6 @@ export const pressIndexContent = {
eyebrow: 'Newsroom',
heading: 'NACHRICHTEN & EINBLICKE.',
description: 'Berichte, Hintergründe und Einblicke rund um den bayerischen Mittelstand und den BMP.',
readMoreLabel: 'Weiterlesen',
missingTitleLabel: 'Presse',
missingCategoryLabel: 'Presse',
collectionFallbackImageFilename: 'gala-saal-overview.jpg',
@@ -38,23 +36,8 @@ export const pressIndexContent = {
eyebrow: 'Pressekontakt & Material',
heading: 'PRESSE-MATERIAL & KONTAKT.',
description: 'Laden Sie unser offizielles Material herunter oder kontaktieren Sie direkt unser Presseteam für individuelle Anfragen.',
kitLabel: 'Download Presse-Kit',
kitMeta: 'Print_BMP.zip 1,5 MB',
kitUrl: '/Print_BMP.zip',
kitDownloadName: 'Print_BMP.zip',
contactEyebrow: 'Pressekontakt',
contactName: 'Tanja Meier',
contactLines: 'presse@bmp-bayern.de\n+49 89 123 456 99',
},
accreditation: {
eyebrow: 'Akkreditierung',
heading: 'AKKREDITIERUNG ANFRAGEN.',
description: 'Melden Sie sich für unsere Presse-Verteiler an oder fordern Sie eine Akkreditierung für die Gala-Verleihung an.',
mediumLabel: 'Medium / Redaktion *',
nameLabel: 'Ihr Name *',
emailLabel: 'E-Mail-Adresse *',
submitLabel: 'Akkreditierung anfragen',
successHeading: 'Anfrage eingegangen',
successMessage: 'Vielen Dank für Ihre Akkreditierungsanfrage. Unser Presseteam meldet sich zeitnah bei Ihnen.',
},
}

View File

@@ -0,0 +1,133 @@
import type { Field } from 'payload'
import { describe, expect, it } from 'vitest'
import { ApplicationForm } from '@/ApplicationForm/config'
import { ApplicationPhase } from '@/ApplicationPhase/config'
import { Footer } from '@/Footer/config'
import { Header } from '@/Header/config'
import { NewsletterForm } from '@/NewsletterForm/config'
import { SiteSettings } from '@/SiteSettings/config'
import { aboutFields } from '@/collections/Pages/aboutFields'
import { homeFields } from '@/collections/Pages/homeFields'
import { netzwerkFields } from '@/collections/Pages/netzwerkFields'
import { participationFields } from '@/collections/Pages/participationFields'
import { pressIndexFields } from '@/collections/Pages/pressIndexFields'
function fieldPaths(fields: Field[], prefix = ''): string[] {
return fields.flatMap((field) => {
if (!('name' in field) || typeof field.name !== 'string') {
return 'fields' in field && Array.isArray(field.fields) ? fieldPaths(field.fields, prefix) : []
}
const path = prefix ? `${prefix}.${field.name}` : field.name
const children = 'fields' in field && Array.isArray(field.fields) ? fieldPaths(field.fields, path) : []
return [path, ...children]
})
}
function findField(fields: Field[], path: string): Field | undefined {
const [name, ...rest] = path.split('.')
const field = fields.find((candidate) => 'name' in candidate && candidate.name === name)
if (!field || rest.length === 0) return field
if (!('fields' in field) || !Array.isArray(field.fields)) return undefined
return findField(field.fields, rest.join('.'))
}
describe('Payload field inventory', () => {
it('keeps obsolete page controls out of the schema', () => {
const paths = [
...fieldPaths(homeFields),
...fieldPaths(participationFields),
...fieldPaths(aboutFields),
...fieldPaths(pressIndexFields),
...fieldPaths(netzwerkFields),
]
expect(paths).not.toEqual(
expect.arrayContaining([
'home.application',
'home.form',
'participation.applicationForm',
'participation.form',
'participation.eligibility.secondaryCta',
'participation.eligibility.nudgeCta',
'participation.applicationWays.recommendedLabel',
'participation.applicationWays.fallbackCta',
'participation.applicationWays.ways.featured',
'participation.applicationWays.ways.cta',
'about.highlights',
'pressIndex.events.detailLabel',
'pressIndex.news.readMoreLabel',
'pressIndex.downloads.kitLabel',
'pressIndex.downloads.kitMeta',
'pressIndex.downloads.kitFile',
'pressIndex.downloads.kitUrl',
'pressIndex.downloads.kitDownloadName',
'pressIndex.accreditation',
'netzwerk.jury.note',
'netzwerk.partners.premiumLabel',
'netzwerk.partners.defaultLogoColor',
'netzwerk.sponsoring.footnotePrefix',
'netzwerk.sponsoring.footnoteLabel',
'netzwerk.sponsoring.footnoteUrl',
'netzwerk.sponsoring.formEyebrow',
'netzwerk.sponsoringForm.packages',
'netzwerk.sponsoringForm.step1Eyebrow',
'netzwerk.sponsoringForm.step1Heading',
'netzwerk.sponsoringForm.requiredPackageError',
]),
)
})
it('retains rendered page controls and authoritative relationships', () => {
const paths = [
...fieldPaths(homeFields),
...fieldPaths(participationFields),
...fieldPaths(aboutFields),
...fieldPaths(pressIndexFields),
...fieldPaths(netzwerkFields),
]
expect(paths).toEqual(
expect.arrayContaining([
'home.hero',
'home.winners.featured',
'home.videoModal.video',
'participation.process.steps',
'participation.awardsGrid.cards',
'about.testimonials.items',
'pressIndex.events.statusLabels',
'pressIndex.downloads.contactName',
'netzwerk.jury.members',
'netzwerk.partners.tiers',
'netzwerk.partners.modal.websiteLabel',
'netzwerk.sponsoringForm.steps',
'netzwerk.sponsoringForm.requiredFieldError',
]),
)
})
it('keeps shared multi-page and site-wide content in Globals', () => {
const globals = [ApplicationForm, ApplicationPhase, NewsletterForm, Header, Footer, SiteSettings]
const slugs = globals.map((global) => global.slug)
expect(slugs).toEqual([
'application-form',
'application-phase',
'newsletter-form',
'header',
'footer',
'site-settings',
])
expect(fieldPaths(ApplicationForm.fields)).toEqual(expect.arrayContaining(['section', 'form']))
expect(fieldPaths(ApplicationPhase.fields)).toEqual(expect.arrayContaining(['activePhase', 'phases']))
expect(fieldPaths(NewsletterForm.fields)).toEqual(expect.arrayContaining(['evaluation', 'completed']))
})
it('exposes exactly two editable sponsoring step labels', () => {
expect(findField(netzwerkFields, 'netzwerk.sponsoringForm.steps')).toMatchObject({
minRows: 2,
maxRows: 2,
})
})
})