refactor(cms): remove obsolete page fields
This commit is contained in:
50
docs/payload-field-inventory.md
Normal file
50
docs/payload-field-inventory.md
Normal 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.
|
||||
@@ -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: [
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
397
src/migrations/20260731_160000_focused_payload_cleanup.ts
Normal file
397
src/migrations/20260731_160000_focused_payload_cleanup.ts
Normal 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'")
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 –',
|
||||
|
||||
@@ -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: '✓',
|
||||
|
||||
@@ -34,7 +34,6 @@ type HomeHeroCms = {
|
||||
videoLabel?: string | null;
|
||||
};
|
||||
type HomeSectionCms = CmsRecord & {
|
||||
awardCaption?: string | null;
|
||||
awardImage?: unknown;
|
||||
awardImageAlt?: string | null;
|
||||
benefits?: unknown;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: 'Initiativbewerbung',
|
||||
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. 30–45 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',
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
}
|
||||
|
||||
133
tests/int/payload-field-inventory.int.spec.ts
Normal file
133
tests/int/payload-field-inventory.int.spec.ts
Normal 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user