Umut asked where a contact form's mail is sent from: it was the recipient address, `From` equal to `To`. That breaks as soon as the recipient is a mailbox at another provider, because most hosts only deliver mail that claims an address of their own domain, and the provider's SPF/DMARC then treats it as forged. Each form now has an optional `sender` setting, validated like the recipient; empty keeps the old behaviour, so stored forms need no migration. Delivery uses `sender ?: recipient`; the reply-to field is untouched. The administration shows the field under the recipient with a note on why it matters. The pilot launch config moves to port 8091 because 8087 was held by another session. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
200 lines
13 KiB
Twig
200 lines
13 KiB
Twig
{% extends 'admin/shell.html.twig' %}
|
||
{% set nav = 'forms' %}
|
||
{# Die Vorschau zeichnet den Absendeknopf mit dem Standardbutton der Website - dafuer braucht die
|
||
Seite dessen Stylesheet, genau wie die Designseite. #}
|
||
{% block styles %}<link rel="stylesheet" href="/assets/buttons.css?v={{ kant_version() }}"><link rel="stylesheet" href="/assets/color-control.css?v={{ kant_version() }}">{% endblock %}
|
||
{% block scripts %}<script type="module" src="/assets/form-editor.js?v={{ kant_version() }}"></script>{% endblock %}
|
||
{% block title %}Formulare{% endblock %}
|
||
{% block heading %}Formulare{% endblock %}
|
||
{% block lead %}Felder, Empfänger, Einwilligungstext und Aussehen werden hier gepflegt. Auf einer Seite steht nur ein Formularblock mit der Adresse – so kann ein gespeicherter Seiteninhalt niemals ändern, wohin eine Anfrage geht.{% endblock %}
|
||
{% block actions %}<a class="primary" href="#neues-formular">Neues Formular</a>{% endblock %}
|
||
|
||
{# Eine Feldkarte. Der Index steht in den Feldnamen; die Vorlage fuer neue Felder traegt an seiner
|
||
Stelle den Platzhalter __n__, den das Skript beim Einfuegen ersetzt. #}
|
||
{% macro field(f, n, row, types) %}
|
||
{% set id = 'f' ~ f ~ '-' ~ n %}
|
||
<li class="field-card{{ row.label == '' ? ' field-card-new' : '' }}" data-field>
|
||
<div class="field-card-head">
|
||
<span class="field-card-index" aria-hidden="true" data-field-index></span>
|
||
<div class="field-card-title">
|
||
<label for="{{ id }}-label">Beschriftung</label>
|
||
<input id="{{ id }}-label" name="fields[{{ n }}][label]" maxlength="120" value="{{ row.label }}" placeholder="{{ row.label == '' ? 'Neues Feld – Beschriftung eingeben' : '' }}" data-field-label>
|
||
</div>
|
||
<div class="field-card-type">
|
||
<label for="{{ id }}-type">Typ</label>
|
||
<select id="{{ id }}-type" name="fields[{{ n }}][type]" data-field-type>
|
||
{% for value, label in types %}<option value="{{ value }}"{{ row.type == value ? ' selected' : '' }}>{{ label }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
<button type="button" class="field-remove" data-remove-field hidden aria-label="Feld entfernen" title="Feld entfernen">{% include 'admin/icon.html.twig' with {name: 'remove'} only %}</button>
|
||
</div>
|
||
<div class="field-card-body">
|
||
<div>
|
||
<label for="{{ id }}-name">Technischer Name</label>
|
||
<input id="{{ id }}-name" name="fields[{{ n }}][name]" maxlength="32" value="{{ row.name }}" pattern="[a-z][a-z0-9_]*" placeholder="vorname" data-field-name>
|
||
<p class="field-help">Kleinbuchstaben, Ziffern, Unterstrich. Steht in der E-Mail vor der Antwort.</p>
|
||
</div>
|
||
<div>
|
||
<label for="{{ id }}-help">Hilfetext</label>
|
||
<input id="{{ id }}-help" name="fields[{{ n }}][help]" maxlength="200" value="{{ row.help }}" placeholder="Steht unter dem Feld" data-field-help>
|
||
</div>
|
||
<div data-field-options>
|
||
<label for="{{ id }}-options">Optionen der Auswahl</label>
|
||
<input id="{{ id }}-options" name="fields[{{ n }}][options]" maxlength="500" value="{{ row.options|join(', ') }}" placeholder="Beratung, Angebot, Sonstiges" data-field-options-input>
|
||
<p class="field-help">Durch Komma trennen, mindestens zwei.</p>
|
||
</div>
|
||
<div class="field-card-flags">
|
||
<label class="check-label" for="{{ id }}-required"><input id="{{ id }}-required" type="checkbox" name="fields[{{ n }}][required]" value="1"{{ row.required ? ' checked' : '' }} data-field-required> Pflichtfeld</label>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
{% endmacro %}
|
||
|
||
{% block content %}
|
||
{% import _self as karte %}
|
||
<div class="form-screens" data-form-schema="{{ schema|json_encode|e('html_attr') }}">
|
||
{% for form in forms|merge([null]) %}
|
||
{% set new = form is null %}
|
||
{% set f = loop.index %}
|
||
{% set rows = new ? [] : form.fields %}
|
||
{% set settings = new ? {} : form.settings %}
|
||
{% if new %}
|
||
<details class="card form-new" id="neues-formular"{{ forms is empty ? ' open' : '' }}>
|
||
<summary>Neues Formular anlegen</summary>
|
||
{% else %}
|
||
<section class="card">
|
||
{% endif %}
|
||
<form action="{{ path('admin_form_save', {slug: new ? 'neu' : form.slug}) }}" method="post" class="form-def" data-form-editor>
|
||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('forms') }}">
|
||
<input type="hidden" name="expectedVersion" value="{{ new ? 0 : form.version }}">
|
||
{% if new %}
|
||
{# Die Zusammenfassung des Details ist keine Ueberschrift; ohne diese springt die
|
||
Gliederung von der Seitenueberschrift direkt zu den Abschnittstiteln. #}
|
||
<h2 class="visually-hidden">Neues Formular</h2>
|
||
{% else %}
|
||
<div class="card-head">
|
||
<h2>{{ form.name }} <span class="count">{{ form.fields|length }} {{ form.fields|length == 1 ? 'Feld' : 'Felder' }}</span></h2>
|
||
<span class="muted">Im Editor als Formularblock mit der Adresse <code>{{ form.slug }}</code></span>
|
||
</div>
|
||
{% endif %}
|
||
<div class="card-body form-editor">
|
||
<div class="form-editor-main">
|
||
<h3 class="form-part">Zustellung</h3>
|
||
<div class="fields">
|
||
<div>
|
||
<label for="name-{{ f }}">Name</label>
|
||
<input id="name-{{ f }}" name="name" maxlength="120" required value="{{ new ? '' : form.name }}" placeholder="Kontakt">
|
||
</div>
|
||
{% if new %}
|
||
<div>
|
||
<label for="slug-new">Adresse</label>
|
||
<input id="slug-new" name="slug" maxlength="80" pattern="[a-z0-9]+(-[a-z0-9]+)*" required placeholder="kontakt">
|
||
<p class="field-help">Wird im Formularblock eingetragen. Kleinbuchstaben, Ziffern, Bindestrich.</p>
|
||
</div>
|
||
{% endif %}
|
||
<div>
|
||
<label for="recipient-{{ f }}">Empfänger</label>
|
||
<input id="recipient-{{ f }}" name="recipient" type="email" required value="{{ settings.recipient|default('') }}" placeholder="info@beispiel.de">
|
||
</div>
|
||
<div>
|
||
<label for="sender-{{ f }}">Absender</label>
|
||
<input id="sender-{{ f }}" name="sender" type="email" value="{{ settings.sender|default('') }}" placeholder="formular@beispiel.de">
|
||
<p class="field-help">Leer: die Empfängeradresse. Viele Anbieter stellen nur zu, was von einer Adresse der eigenen Domain kommt.</p>
|
||
</div>
|
||
<div>
|
||
<label for="subject-{{ f }}">Betreff der E-Mail</label>
|
||
<input id="subject-{{ f }}" name="subject" maxlength="160" value="{{ settings.subject|default('') }}" placeholder="Neue Formularnachricht">
|
||
</div>
|
||
<div>
|
||
<label for="reply-{{ f }}">Antwortadresse aus Feld</label>
|
||
<input id="reply-{{ f }}" name="replyToField" maxlength="32" value="{{ settings.replyToField|default('') }}" placeholder="email">
|
||
<p class="field-help">Technischer Name eines E-Mail-Feldes. Dann lässt sich direkt antworten.</p>
|
||
</div>
|
||
<div class="field-wide">
|
||
<label for="success-{{ f }}">Meldung nach dem Absenden</label>
|
||
<textarea id="success-{{ f }}" name="success" maxlength="300" rows="2" placeholder="Vielen Dank. Wir melden uns in Kürze bei dir.">{{ settings.success|default('') }}</textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-fields-head">
|
||
<h3 class="form-part">Felder</h3>
|
||
<button type="button" class="secondary" data-add-field hidden>{% include 'admin/icon.html.twig' with {name: 'plus'} only %}Feld hinzufügen</button>
|
||
</div>
|
||
<ol class="field-list" data-field-list>
|
||
{% for row in rows %}{{ karte.field(f, loop.index0, row, types) }}{% endfor %}
|
||
{{ karte.field(f, rows|length, {name: '', type: 'text', label: '', required: false, help: '', options: []}, types) }}
|
||
</ol>
|
||
<template data-field-template>{{ karte.field(f, '__n__', {name: '', type: 'text', label: '', required: false, help: '', options: []}, types) }}</template>
|
||
<p class="field-help" data-fields-help>Ein Feld ohne Beschriftung wird nicht gespeichert – so lässt sich ein Feld auch ohne Skript entfernen. Ein Einwilligungsfeld ist immer ein Pflichtfeld; der Zeitpunkt der Einwilligung wird mitgespeichert.</p>
|
||
|
||
<h3 class="form-part">Aussehen</h3>
|
||
<div class="fields form-look">
|
||
<div>
|
||
<label for="buttonLabel-{{ f }}">Beschriftung des Buttons</label>
|
||
<input id="buttonLabel-{{ f }}" name="buttonLabel" maxlength="40" value="{{ settings.buttonLabel|default('') }}" placeholder="Absenden" data-form-button-label>
|
||
</div>
|
||
{% for key, label in {fieldBorderColor: 'Rahmen der Felder', fieldBackgroundColor: 'Hintergrund der Felder', buttonColor: 'Button', buttonInk: 'Buttonschrift'} %}
|
||
<div>
|
||
<label for="{{ key }}-{{ f }}">{{ label }}</label>
|
||
<input id="{{ key }}-{{ f }}" name="{{ key }}" value="{{ settings[key]|default('') }}" placeholder="Wie die Website" data-form-color="{{ key }}">
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
<p class="field-help">Leer heißt: wie die Website. Sonst eine Farbe aus der Palette oder ein Wert wie <code>#1a2b3c</code>. Form und Größe des Buttons kommen aus dem Design.</p>
|
||
</div>
|
||
|
||
{# Die Vorschau: dieselben Felder, dieselben Farben, der Standardbutton der Website.
|
||
Ohne Skript steht sie mit den gespeicherten Feldern da; mit Skript folgt sie jeder
|
||
Eingabe. #}
|
||
<aside class="form-editor-preview" aria-label="Vorschau des Formulars">
|
||
<h3 class="form-part">Vorschau</h3>
|
||
<div class="form-preview" data-form-preview>
|
||
{% for row in rows %}
|
||
<div class="form-preview-field">
|
||
{% if row.type == 'consent' %}
|
||
<span class="form-preview-check"><span class="form-preview-box" aria-hidden="true"></span>{{ row.label }} *</span>
|
||
{% else %}
|
||
<span class="form-preview-label">{{ row.label }}{% if row.required %} *{% endif %}</span>
|
||
<span class="form-preview-input{{ row.type == 'textarea' ? ' form-preview-area' : '' }}{{ row.type == 'select' ? ' form-preview-select' : '' }}" aria-hidden="true">{{ row.type == 'select' ? '— bitte wählen —' : '' }}</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
<span class="cms-button b-desktop-shape-{{ schema.buttons.shape }} b-desktop-appearance-{{ schema.buttons.appearance }} b-desktop-size-{{ schema.buttons.size }}" data-preview-button>{{ settings.buttonLabel|default('Absenden') }}</span>
|
||
</div>
|
||
<p class="field-help">Schrift, Abstände und Buttonform kommen aus dem Design der Website.</p>
|
||
</aside>
|
||
</div>
|
||
<div class="card-foot form-editor-foot">
|
||
<button class="primary" type="submit">{{ new ? 'Formular anlegen' : 'Formular speichern' }}</button>
|
||
{% if not new %}<span class="muted">Änderungen gelten sofort auf jeder Seite, die dieses Formular zeigt.</span>{% endif %}
|
||
</div>
|
||
</form>
|
||
{% if new %}
|
||
</details>
|
||
{% else %}
|
||
</section>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<div class="section-head"><h2>Einsendungen <span class="count">{{ submissions.total }}</span></h2><span class="muted">Neueste zuerst.</span></div>
|
||
{% if submissions.items is empty %}
|
||
<div class="empty"><h3>Noch keine Einsendungen.</h3><p>Hier erscheinen eingegangene Nachrichten, auch wenn der E-Mail-Versand fehlschlägt.</p></div>
|
||
{% else %}
|
||
<div class="rows">
|
||
{% for entry in submissions.items %}
|
||
<article class="row row-top">
|
||
<div class="row-main">
|
||
<h3>{{ entry.form }}</h3>
|
||
<p>{{ entry.createdAt|date('d.m.Y H:i') }} UTC{% if entry.consentAt %} · Einwilligung {{ entry.consentAt|date('d.m.Y H:i') }}{% endif %}</p>
|
||
<dl class="submission-data">{% for key, value in entry.data %}<dt>{{ key }}</dt><dd>{{ value }}</dd>{% endfor %}</dl>
|
||
</div>
|
||
<span class="pill {{ entry.delivered ? 'pill-live' : 'pill-off' }}">{{ entry.delivered ? 'Versendet' : 'Nicht versendet' }}</span>
|
||
</article>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
{% include 'admin/pager.html.twig' with {result: submissions, route: 'admin_forms', label: 'Seiten der Einsendungen'} only %}
|
||
<p class="footnote">KantSites · Arbeitsstand</p>
|
||
{% endblock %}
|