mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 17:24:12 +00:00
feat: multi-tenant contact form refactoring
- Add forms + form-submissions to multiTenantPlugin with tenant scoping - Inject tenant field into forms via formOverrides - Reorder plugins: formBuilderPlugin before multiTenantPlugin (fixes warning) - Refactor ContactFormBlock: form relationship replaces hardcoded recipientEmail - Add setSubmissionTenant hook to auto-copy tenant from form to submission - Add tenant field (read-only) to FormSubmissionsOverrides - Migration: tenant_id on forms/form_submissions, form_id on contact block Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
130ab46ffb
commit
5e223cd7fb
7 changed files with 408 additions and 191 deletions
|
|
@ -7,6 +7,16 @@ export const ContactFormBlock: Block = {
|
||||||
plural: 'Kontaktformulare',
|
plural: 'Kontaktformulare',
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'form',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'forms',
|
||||||
|
required: true,
|
||||||
|
label: 'Formular',
|
||||||
|
admin: {
|
||||||
|
description: 'Wählen Sie ein Formular aus der Formulare-Sammlung',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'headline',
|
name: 'headline',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|
@ -21,28 +31,44 @@ export const ContactFormBlock: Block = {
|
||||||
localized: true,
|
localized: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'recipientEmail',
|
name: 'successMessage',
|
||||||
type: 'email',
|
type: 'textarea',
|
||||||
defaultValue: 'info@porwoll.de',
|
defaultValue: 'Vielen Dank! Wir melden uns schnellstmöglich.',
|
||||||
label: 'Empfänger E-Mail',
|
label: 'Erfolgsmeldung',
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showContactInfo',
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: true,
|
||||||
|
label: 'Kontaktinformationen anzeigen',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'showPhone',
|
name: 'showPhone',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
label: 'Telefon anzeigen',
|
label: 'Telefon anzeigen',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => siblingData?.showContactInfo,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'showAddress',
|
name: 'showAddress',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
label: 'Adresse anzeigen',
|
label: 'Adresse anzeigen',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => siblingData?.showContactInfo,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'showSocials',
|
name: 'showSocials',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
label: 'Social Media anzeigen',
|
label: 'Social Media anzeigen',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => siblingData?.showContactInfo,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ export const formSubmissionOverrides: Partial<CollectionConfig> = {
|
||||||
plural: 'Formular-Einsendungen',
|
plural: 'Formular-Einsendungen',
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
|
// Tenant (automatisch vom Formular übernommen via setSubmissionTenant Hook)
|
||||||
|
{
|
||||||
|
name: 'tenant',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'tenants',
|
||||||
|
admin: {
|
||||||
|
position: 'sidebar',
|
||||||
|
readOnly: true,
|
||||||
|
description: 'Wird automatisch vom Formular übernommen',
|
||||||
|
},
|
||||||
|
},
|
||||||
// Status-Workflow
|
// Status-Workflow
|
||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
|
|
|
||||||
39
src/hooks/setSubmissionTenant.ts
Normal file
39
src/hooks/setSubmissionTenant.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import type { CollectionBeforeChangeHook } from 'payload'
|
||||||
|
|
||||||
|
interface FormWithTenant {
|
||||||
|
id: number | string
|
||||||
|
tenant?: { id: number } | number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook: Kopiert den Tenant vom übergeordneten Formular auf die Einsendung.
|
||||||
|
* Wird als beforeChange auf form-submissions ausgeführt.
|
||||||
|
*/
|
||||||
|
export const setSubmissionTenant: CollectionBeforeChangeHook = async ({
|
||||||
|
data,
|
||||||
|
req,
|
||||||
|
operation,
|
||||||
|
}) => {
|
||||||
|
// Nur bei neuen Einsendungen den Tenant setzen
|
||||||
|
if (operation !== 'create') return data
|
||||||
|
|
||||||
|
const formId = data.form
|
||||||
|
if (!formId) return data
|
||||||
|
|
||||||
|
try {
|
||||||
|
const form = (await req.payload.findByID({
|
||||||
|
collection: 'forms',
|
||||||
|
id: formId,
|
||||||
|
depth: 0,
|
||||||
|
})) as unknown as FormWithTenant
|
||||||
|
|
||||||
|
if (form?.tenant) {
|
||||||
|
const tenantId = typeof form.tenant === 'object' ? form.tenant.id : form.tenant
|
||||||
|
return { ...data, tenant: tenantId }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Forms] Error reading tenant from form:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
81
src/migrations/20260217_120000_add_tenant_to_forms.ts
Normal file
81
src/migrations/20260217_120000_add_tenant_to_forms.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||||
|
|
||||||
|
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||||
|
// 1. Add tenant_id to forms
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "forms"
|
||||||
|
ADD COLUMN IF NOT EXISTS "tenant_id" integer REFERENCES tenants(id) ON DELETE SET NULL;
|
||||||
|
`)
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX IF NOT EXISTS "forms_tenant_idx" ON "forms" ("tenant_id");
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 2. Add tenant_id to form_submissions
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "form_submissions"
|
||||||
|
ADD COLUMN IF NOT EXISTS "tenant_id" integer REFERENCES tenants(id) ON DELETE SET NULL;
|
||||||
|
`)
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX IF NOT EXISTS "form_submissions_tenant_idx" ON "form_submissions" ("tenant_id");
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 3. ContactFormBlock: add form_id relationship column (replaces recipientEmail)
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "pages_blocks_contact_form_block"
|
||||||
|
ADD COLUMN IF NOT EXISTS "form_id" integer REFERENCES forms(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS "show_contact_info" boolean DEFAULT true;
|
||||||
|
`)
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX IF NOT EXISTS "pages_blocks_contact_form_block_form_idx"
|
||||||
|
ON "pages_blocks_contact_form_block" ("form_id");
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 4. ContactFormBlock locales: add successMessage
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "pages_blocks_contact_form_block_locales"
|
||||||
|
ADD COLUMN IF NOT EXISTS "success_message" varchar;
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 5. pages_rels: add forms_id column for the block relationship
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "pages_rels"
|
||||||
|
ADD COLUMN IF NOT EXISTS "forms_id" integer REFERENCES forms(id) ON DELETE CASCADE;
|
||||||
|
`)
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE INDEX IF NOT EXISTS "pages_rels_forms_idx" ON "pages_rels" ("forms_id");
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 6. Drop old recipientEmail column (data migrated: it was just a default 'info@porwoll.de')
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "pages_blocks_contact_form_block"
|
||||||
|
DROP COLUMN IF EXISTS "recipient_email";
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||||
|
// Restore recipientEmail
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "pages_blocks_contact_form_block"
|
||||||
|
ADD COLUMN IF NOT EXISTS "recipient_email" varchar DEFAULT 'info@porwoll.de';
|
||||||
|
`)
|
||||||
|
|
||||||
|
// Remove new columns
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "pages_blocks_contact_form_block"
|
||||||
|
DROP COLUMN IF EXISTS "form_id",
|
||||||
|
DROP COLUMN IF EXISTS "show_contact_info";
|
||||||
|
`)
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "pages_blocks_contact_form_block_locales"
|
||||||
|
DROP COLUMN IF EXISTS "success_message";
|
||||||
|
`)
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "pages_rels" DROP COLUMN IF EXISTS "forms_id";
|
||||||
|
`)
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "forms" DROP COLUMN IF EXISTS "tenant_id";
|
||||||
|
`)
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "form_submissions" DROP COLUMN IF EXISTS "tenant_id";
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ import * as migration_20260116_100000_add_token_notification_fields from './2026
|
||||||
import * as migration_20260116_120000_add_report_schedules from './20260116_120000_add_report_schedules';
|
import * as migration_20260116_120000_add_report_schedules from './20260116_120000_add_report_schedules';
|
||||||
import * as migration_20260215_120000_add_monitoring_collections from './20260215_120000_add_monitoring_collections';
|
import * as migration_20260215_120000_add_monitoring_collections from './20260215_120000_add_monitoring_collections';
|
||||||
import * as migration_20260216_150000_add_card_grid_icon_fields from './20260216_150000_add_card_grid_icon_fields';
|
import * as migration_20260216_150000_add_card_grid_icon_fields from './20260216_150000_add_card_grid_icon_fields';
|
||||||
|
import * as migration_20260217_120000_add_tenant_to_forms from './20260217_120000_add_tenant_to_forms';
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
|
|
@ -234,4 +235,9 @@ export const migrations = [
|
||||||
down: migration_20260216_150000_add_card_grid_icon_fields.down,
|
down: migration_20260216_150000_add_card_grid_icon_fields.down,
|
||||||
name: '20260216_150000_add_card_grid_icon_fields'
|
name: '20260216_150000_add_card_grid_icon_fields'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260217_120000_add_tenant_to_forms.up,
|
||||||
|
down: migration_20260217_120000_add_tenant_to_forms.down,
|
||||||
|
name: '20260217_120000_add_tenant_to_forms'
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -846,9 +846,14 @@ export interface Page {
|
||||||
blockType: 'cta-block';
|
blockType: 'cta-block';
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
/**
|
||||||
|
* Wählen Sie ein Formular aus der Formulare-Sammlung
|
||||||
|
*/
|
||||||
|
form: number | Form;
|
||||||
headline?: string | null;
|
headline?: string | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
recipientEmail?: string | null;
|
successMessage?: string | null;
|
||||||
|
showContactInfo?: boolean | null;
|
||||||
showPhone?: boolean | null;
|
showPhone?: boolean | null;
|
||||||
showAddress?: boolean | null;
|
showAddress?: boolean | null;
|
||||||
showSocials?: boolean | null;
|
showSocials?: boolean | null;
|
||||||
|
|
@ -3076,6 +3081,168 @@ export interface Page {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "forms".
|
||||||
|
*/
|
||||||
|
export interface Form {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
fields?:
|
||||||
|
| (
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
defaultValue?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'checkbox';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'email';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
message?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'message';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
defaultValue?: number | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'number';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
defaultValue?: string | null;
|
||||||
|
placeholder?: string | null;
|
||||||
|
options?:
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'select';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
defaultValue?: string | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'text';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name: string;
|
||||||
|
label?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
defaultValue?: string | null;
|
||||||
|
required?: boolean | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'textarea';
|
||||||
|
}
|
||||||
|
)[]
|
||||||
|
| null;
|
||||||
|
submitButtonLabel?: string | null;
|
||||||
|
/**
|
||||||
|
* Choose whether to display an on-page message or redirect to a different page after they submit the form.
|
||||||
|
*/
|
||||||
|
confirmationType?: ('message' | 'redirect') | null;
|
||||||
|
confirmationMessage?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
redirect?: {
|
||||||
|
type?: ('reference' | 'custom') | null;
|
||||||
|
reference?: {
|
||||||
|
relationTo: 'pages';
|
||||||
|
value: number | Page;
|
||||||
|
} | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Send custom emails when the form submits. Use comma separated lists to send the same email to multiple recipients. To reference a value from this form, wrap that field's name with double curly brackets, i.e. {{firstName}}. You can use a wildcard {{*}} to output all data and {{*:table}} to format it as an HTML table in the email.
|
||||||
|
*/
|
||||||
|
emails?:
|
||||||
|
| {
|
||||||
|
emailTo?: string | null;
|
||||||
|
cc?: string | null;
|
||||||
|
bcc?: string | null;
|
||||||
|
replyTo?: string | null;
|
||||||
|
emailFrom?: string | null;
|
||||||
|
subject: string;
|
||||||
|
/**
|
||||||
|
* Enter the message that should be sent in this email.
|
||||||
|
*/
|
||||||
|
message?: {
|
||||||
|
root: {
|
||||||
|
type: string;
|
||||||
|
children: {
|
||||||
|
type: any;
|
||||||
|
version: number;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}[];
|
||||||
|
direction: ('ltr' | 'rtl') | null;
|
||||||
|
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||||
|
indent: number;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
[k: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
tenant: number | Tenant;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Video-Bibliothek für YouTube/Vimeo Embeds und hochgeladene Videos
|
* Video-Bibliothek für YouTube/Vimeo Embeds und hochgeladene Videos
|
||||||
*
|
*
|
||||||
|
|
@ -4437,167 +4604,6 @@ export interface Job {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "forms".
|
|
||||||
*/
|
|
||||||
export interface Form {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
fields?:
|
|
||||||
| (
|
|
||||||
| {
|
|
||||||
name: string;
|
|
||||||
label?: string | null;
|
|
||||||
width?: number | null;
|
|
||||||
required?: boolean | null;
|
|
||||||
defaultValue?: boolean | null;
|
|
||||||
id?: string | null;
|
|
||||||
blockName?: string | null;
|
|
||||||
blockType: 'checkbox';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: string;
|
|
||||||
label?: string | null;
|
|
||||||
width?: number | null;
|
|
||||||
required?: boolean | null;
|
|
||||||
id?: string | null;
|
|
||||||
blockName?: string | null;
|
|
||||||
blockType: 'email';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
message?: {
|
|
||||||
root: {
|
|
||||||
type: string;
|
|
||||||
children: {
|
|
||||||
type: any;
|
|
||||||
version: number;
|
|
||||||
[k: string]: unknown;
|
|
||||||
}[];
|
|
||||||
direction: ('ltr' | 'rtl') | null;
|
|
||||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
|
||||||
indent: number;
|
|
||||||
version: number;
|
|
||||||
};
|
|
||||||
[k: string]: unknown;
|
|
||||||
} | null;
|
|
||||||
id?: string | null;
|
|
||||||
blockName?: string | null;
|
|
||||||
blockType: 'message';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: string;
|
|
||||||
label?: string | null;
|
|
||||||
width?: number | null;
|
|
||||||
defaultValue?: number | null;
|
|
||||||
required?: boolean | null;
|
|
||||||
id?: string | null;
|
|
||||||
blockName?: string | null;
|
|
||||||
blockType: 'number';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: string;
|
|
||||||
label?: string | null;
|
|
||||||
width?: number | null;
|
|
||||||
defaultValue?: string | null;
|
|
||||||
placeholder?: string | null;
|
|
||||||
options?:
|
|
||||||
| {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
id?: string | null;
|
|
||||||
}[]
|
|
||||||
| null;
|
|
||||||
required?: boolean | null;
|
|
||||||
id?: string | null;
|
|
||||||
blockName?: string | null;
|
|
||||||
blockType: 'select';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: string;
|
|
||||||
label?: string | null;
|
|
||||||
width?: number | null;
|
|
||||||
defaultValue?: string | null;
|
|
||||||
required?: boolean | null;
|
|
||||||
id?: string | null;
|
|
||||||
blockName?: string | null;
|
|
||||||
blockType: 'text';
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
name: string;
|
|
||||||
label?: string | null;
|
|
||||||
width?: number | null;
|
|
||||||
defaultValue?: string | null;
|
|
||||||
required?: boolean | null;
|
|
||||||
id?: string | null;
|
|
||||||
blockName?: string | null;
|
|
||||||
blockType: 'textarea';
|
|
||||||
}
|
|
||||||
)[]
|
|
||||||
| null;
|
|
||||||
submitButtonLabel?: string | null;
|
|
||||||
/**
|
|
||||||
* Choose whether to display an on-page message or redirect to a different page after they submit the form.
|
|
||||||
*/
|
|
||||||
confirmationType?: ('message' | 'redirect') | null;
|
|
||||||
confirmationMessage?: {
|
|
||||||
root: {
|
|
||||||
type: string;
|
|
||||||
children: {
|
|
||||||
type: any;
|
|
||||||
version: number;
|
|
||||||
[k: string]: unknown;
|
|
||||||
}[];
|
|
||||||
direction: ('ltr' | 'rtl') | null;
|
|
||||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
|
||||||
indent: number;
|
|
||||||
version: number;
|
|
||||||
};
|
|
||||||
[k: string]: unknown;
|
|
||||||
} | null;
|
|
||||||
redirect?: {
|
|
||||||
type?: ('reference' | 'custom') | null;
|
|
||||||
reference?: {
|
|
||||||
relationTo: 'pages';
|
|
||||||
value: number | Page;
|
|
||||||
} | null;
|
|
||||||
url?: string | null;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Send custom emails when the form submits. Use comma separated lists to send the same email to multiple recipients. To reference a value from this form, wrap that field's name with double curly brackets, i.e. {{firstName}}. You can use a wildcard {{*}} to output all data and {{*:table}} to format it as an HTML table in the email.
|
|
||||||
*/
|
|
||||||
emails?:
|
|
||||||
| {
|
|
||||||
emailTo?: string | null;
|
|
||||||
cc?: string | null;
|
|
||||||
bcc?: string | null;
|
|
||||||
replyTo?: string | null;
|
|
||||||
emailFrom?: string | null;
|
|
||||||
subject: string;
|
|
||||||
/**
|
|
||||||
* Enter the message that should be sent in this email.
|
|
||||||
*/
|
|
||||||
message?: {
|
|
||||||
root: {
|
|
||||||
type: string;
|
|
||||||
children: {
|
|
||||||
type: any;
|
|
||||||
version: number;
|
|
||||||
[k: string]: unknown;
|
|
||||||
}[];
|
|
||||||
direction: ('ltr' | 'rtl') | null;
|
|
||||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
|
||||||
indent: number;
|
|
||||||
version: number;
|
|
||||||
};
|
|
||||||
[k: string]: unknown;
|
|
||||||
} | null;
|
|
||||||
id?: string | null;
|
|
||||||
}[]
|
|
||||||
| null;
|
|
||||||
updatedAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Produkte und Artikel
|
* Produkte und Artikel
|
||||||
*
|
*
|
||||||
|
|
@ -7559,6 +7565,24 @@ export interface MonitoringSnapshot {
|
||||||
| number
|
| number
|
||||||
| boolean
|
| boolean
|
||||||
| null;
|
| null;
|
||||||
|
secrets?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
securityEvents?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Performance-Metriken
|
* Performance-Metriken
|
||||||
|
|
@ -7582,7 +7606,7 @@ export interface MonitoringSnapshot {
|
||||||
export interface MonitoringLog {
|
export interface MonitoringLog {
|
||||||
id: number;
|
id: number;
|
||||||
level: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
level: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||||
source: 'payload' | 'queue-worker' | 'cron' | 'email' | 'oauth' | 'sync';
|
source: 'payload' | 'queue-worker' | 'cron' | 'email' | 'oauth' | 'sync' | 'security';
|
||||||
message: string;
|
message: string;
|
||||||
/**
|
/**
|
||||||
* Strukturierte Metadaten
|
* Strukturierte Metadaten
|
||||||
|
|
@ -8597,9 +8621,11 @@ export interface PagesSelect<T extends boolean = true> {
|
||||||
'contact-form-block'?:
|
'contact-form-block'?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
|
form?: T;
|
||||||
headline?: T;
|
headline?: T;
|
||||||
description?: T;
|
description?: T;
|
||||||
recipientEmail?: T;
|
successMessage?: T;
|
||||||
|
showContactInfo?: T;
|
||||||
showPhone?: T;
|
showPhone?: T;
|
||||||
showAddress?: T;
|
showAddress?: T;
|
||||||
showSocials?: T;
|
showSocials?: T;
|
||||||
|
|
@ -12580,6 +12606,8 @@ export interface MonitoringSnapshotsSelect<T extends boolean = true> {
|
||||||
metaOAuth?: T;
|
metaOAuth?: T;
|
||||||
youtubeOAuth?: T;
|
youtubeOAuth?: T;
|
||||||
cronJobs?: T;
|
cronJobs?: T;
|
||||||
|
secrets?: T;
|
||||||
|
securityEvents?: T;
|
||||||
};
|
};
|
||||||
performance?:
|
performance?:
|
||||||
| T
|
| T
|
||||||
|
|
@ -12855,6 +12883,7 @@ export interface FormsSelect<T extends boolean = true> {
|
||||||
message?: T;
|
message?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
|
tenant?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ import { SEOSettings } from './globals/SEOSettings'
|
||||||
// Hooks
|
// Hooks
|
||||||
import { sendFormNotification } from './hooks/sendFormNotification'
|
import { sendFormNotification } from './hooks/sendFormNotification'
|
||||||
import { formSubmissionBeforeChange } from './hooks/formSubmissionHooks'
|
import { formSubmissionBeforeChange } from './hooks/formSubmissionHooks'
|
||||||
|
import { setSubmissionTenant } from './hooks/setSubmissionTenant'
|
||||||
|
|
||||||
// Form Submissions Overrides
|
// Form Submissions Overrides
|
||||||
import { formSubmissionOverrides } from './collections/FormSubmissionsOverrides'
|
import { formSubmissionOverrides } from './collections/FormSubmissionsOverrides'
|
||||||
|
|
@ -298,6 +299,50 @@ export default buildConfig({
|
||||||
// Sharp für Bildoptimierung
|
// Sharp für Bildoptimierung
|
||||||
sharp,
|
sharp,
|
||||||
plugins: [
|
plugins: [
|
||||||
|
// formBuilderPlugin MUSS vor multiTenantPlugin stehen, da es die forms/form-submissions
|
||||||
|
// Collections erstellt, die multiTenantPlugin dann mit Tenant-Scoping erweitert.
|
||||||
|
formBuilderPlugin({
|
||||||
|
fields: {
|
||||||
|
text: true,
|
||||||
|
textarea: true,
|
||||||
|
select: true,
|
||||||
|
email: true,
|
||||||
|
state: false,
|
||||||
|
country: false,
|
||||||
|
checkbox: true,
|
||||||
|
number: true,
|
||||||
|
message: true,
|
||||||
|
payment: false,
|
||||||
|
},
|
||||||
|
// Fix für TypeScript Types Generation - das Plugin braucht explizite relationTo Angaben
|
||||||
|
redirectRelationships: ['pages'],
|
||||||
|
formOverrides: {
|
||||||
|
admin: {
|
||||||
|
group: 'Formulare',
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
singular: 'Formular',
|
||||||
|
plural: 'Formulare',
|
||||||
|
},
|
||||||
|
fields: ({ defaultFields }) => [
|
||||||
|
...defaultFields,
|
||||||
|
{
|
||||||
|
name: 'tenant',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'tenants',
|
||||||
|
required: true,
|
||||||
|
admin: { position: 'sidebar' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
formSubmissionOverrides: {
|
||||||
|
...(formSubmissionOverrides as Record<string, unknown>),
|
||||||
|
hooks: {
|
||||||
|
beforeChange: [setSubmissionTenant, formSubmissionBeforeChange],
|
||||||
|
afterChange: [sendFormNotification],
|
||||||
|
},
|
||||||
|
} as Parameters<typeof formBuilderPlugin>[0]['formSubmissionOverrides'],
|
||||||
|
}),
|
||||||
multiTenantPlugin({
|
multiTenantPlugin({
|
||||||
tenantsSlug: 'tenants',
|
tenantsSlug: 'tenants',
|
||||||
collections: {
|
collections: {
|
||||||
|
|
@ -344,6 +389,9 @@ export default buildConfig({
|
||||||
series: {},
|
series: {},
|
||||||
// Debug: Minimal test collection - DISABLED
|
// Debug: Minimal test collection - DISABLED
|
||||||
// 'test-minimal': {},
|
// 'test-minimal': {},
|
||||||
|
// Form Builder Plugin Collections - customTenantField: true weil tenant via formOverrides injiziert wird
|
||||||
|
forms: { customTenantField: true },
|
||||||
|
'form-submissions': { customTenantField: true },
|
||||||
// Consent Management Collections - customTenantField: true weil sie bereits ein tenant-Feld haben
|
// Consent Management Collections - customTenantField: true weil sie bereits ein tenant-Feld haben
|
||||||
'cookie-configurations': { customTenantField: true },
|
'cookie-configurations': { customTenantField: true },
|
||||||
'cookie-inventory': { customTenantField: true },
|
'cookie-inventory': { customTenantField: true },
|
||||||
|
|
@ -386,29 +434,6 @@ export default buildConfig({
|
||||||
generateLabel: (_, doc) => doc.title as string,
|
generateLabel: (_, doc) => doc.title as string,
|
||||||
generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''),
|
generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''),
|
||||||
}),
|
}),
|
||||||
formBuilderPlugin({
|
|
||||||
fields: {
|
|
||||||
text: true,
|
|
||||||
textarea: true,
|
|
||||||
select: true,
|
|
||||||
email: true,
|
|
||||||
state: false,
|
|
||||||
country: false,
|
|
||||||
checkbox: true,
|
|
||||||
number: true,
|
|
||||||
message: true,
|
|
||||||
payment: false,
|
|
||||||
},
|
|
||||||
// Fix für TypeScript Types Generation - das Plugin braucht explizite relationTo Angaben
|
|
||||||
redirectRelationships: ['pages'],
|
|
||||||
formSubmissionOverrides: {
|
|
||||||
...(formSubmissionOverrides as Record<string, unknown>),
|
|
||||||
hooks: {
|
|
||||||
beforeChange: [formSubmissionBeforeChange],
|
|
||||||
afterChange: [sendFormNotification],
|
|
||||||
},
|
|
||||||
} as Parameters<typeof formBuilderPlugin>[0]['formSubmissionOverrides'],
|
|
||||||
}),
|
|
||||||
redirectsPlugin({
|
redirectsPlugin({
|
||||||
collections: ['pages'],
|
collections: ['pages'],
|
||||||
overrides: {
|
overrides: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue