5.3 KiB
forms
Wpina @payloadcms/plugin-form-builder (kolekcje forms + form-submissions) i
dostarcza submitForm — wywoływalną z frontu funkcję, która spina: weryfikację
Turnstile → zapis zgłoszenia → wysyłkę maili (naszym senderem).
Zależność
"dependencies": { "@payloadcms/plugin-form-builder": "3.84.1" }
Config (payload.config.ts)
ipalKit({
forms: {
redirectRelationships: ['pages'], // formularz może przekierować na Page
// fields: { text: true, textarea: true, email: true, ... }, // domyślnie włączone sensowne
},
})
Dodaje kolekcje Forms (edytor buduje formularze) i Form Submissions
(zgłoszenia). Wbudowany email form-buildera jest nieużywany — wysyłką zajmuje
się submitForm przez nasz sender (SMTP z panelu).
Front — submitForm (server)
Zwykle w Server Action wywoływanej przez formularz. HTML obu maili składasz na froncie (Twój wygląd):
import { submitForm } from 'ipal-kit/server'
const result = await submitForm({
payload,
formId, // z kolekcji Forms
data: { name, email, message },
turnstileToken, // opcjonalny — jeśli podany, weryfikowany
ip,
emails: {
// wiadomość do klienta/admina (np. "nowe zgłoszenie")
notification: {
to: '[email protected]',
subject: 'Nowe zgłoszenie',
html: renderAdminEmail(data), // Twój HTML
},
// potwierdzenie do wysyłającego
confirmation: {
to: email,
subject: 'Dziękujemy za wiadomość',
html: renderUserEmail(data), // Twój HTML
},
},
})
if (result.success) {
// result.submissionId
// result.emails.notification?.sent / result.emails.confirmation?.sent
} else {
// result.error — np. Turnstile / zapis
}
Flow i gwarancje
- Turnstile (jeśli token) → nieudany → odrzuć przed zapisem (brak spamu w bazie).
- Zapis submission (form-submissions).
- Maile — oba opcjonalne, HTML z frontu.
- Zwrot:
{ success, submissionId, emails: { notification?, confirmation? } }.
Zapisane zgłoszenie = sukces, nawet gdy mail padnie. Status wysyłki maili
jest osobno w result.emails, żeby dane zgłoszenia nie ginęły przez chwilową
awarię SMTP. Front może zareagować (ostrzec, ponowić).
Oba maile opcjonalne — możesz wysłać jeden, drugi, oba lub żaden.
turnstileToken opcjonalny — brak = pominięcie weryfikacji (decydujesz per
formularz).
Maile
Plugin nie składa maili formularzy. Po zapisie submission form-builder wysyła
wiadomości skonfigurowane przez edytora (Forms → formularz → Emails), przez
payload.sendEmail. Żeby wyszły, config musi mieć adapter:
email: panelSmtpAdapter(), // z 'ipal-kit'
Wtedy idą przez SMTP z Site Integrations. Edytor ustawia odbiorców, temat i
treść (placeholdery: {{pole}}, {{*}}, {{*:table}}) bez dotykania kodu.
submitForm odpowiada tylko za weryfikację Turnstile i zapis.
Własne pola w kolekcji formularzy
formOverrides przechodzi prosto do form-buildera — plugin nie ma opinii, czego
formularz potrzebuje poza swoimi polami:
ipalKit({
forms: {
redirectRelationships: ['pages'],
formOverrides: {
fields: ({ defaultFields }) => [
...defaultFields,
{ name: 'internalNote', type: 'textarea' },
],
admin: { group: 'Content' },
},
// formSubmissionOverrides: { ... } // to samo dla zgłoszeń
},
})
fields dostaje domyślne pola kolekcji i zwraca finalną listę — możesz dodawać,
usuwać, zmieniać kolejność. Poza fields przyjmuje dowolne ustawienia kolekcji
(admin, access, hooks).
Bezpieczeństwo i wynik zgłoszenia
submitForm przechodzi trzy bramki w kolejności rosnącej po koszcie: rate limit
(in-memory, per IP), Turnstile, walidacja względem schematu formularza. Dopiero
potem zapis. Flood ginie, zanim dotknie sieci czy bazy.
Walidacja jest istotna, bo server action to publiczny endpoint — da się go wołać
z pominięciem formularza. Plugin ładuje definicję formularza i: odrzuca nieznane
klucze, wymusza pola required, tnie długość (5000 znaków). Do bazy trafia tylko
to, co formularz definiuje.
Rate limit: 5/min/IP domyślnie, maxPerMinute zmienia, 0 wyłącza (gdy limiter
jest z przodu). In-memory — przy wielu instancjach licznik jest per-proces, więc
efektywny limit to per-instancja. Do formularza kontaktowego wystarcza.
Wynik to KOD, nie tekst
submitForm zwraca ustrukturyzowany błąd — plugin mówi CO się stało, projekt
decyduje JAK to pokazać (język, brzmienie, obsługa per pole):
type SubmitFormResult =
| { success: true; submissionId: string | number }
| { success: false; reason: 'rate_limited' }
| { success: false; reason: 'turnstile' }
| { success: false; reason: 'validation'; field?: string; kind?: 'required' | 'too_long' | 'unknown_fields' }
| { success: false; reason: 'not_found' }
| { success: false; reason: 'error' }
Front mapuje kody na własne komunikaty:
function errorMessage(r) {
switch (r.reason) {
case 'rate_limited': return 'Zbyt wiele zgłoszeń...'
case 'validation':
if (r.kind === 'required') return 'Uzupełnij wymagane pola.'
// r.field → podświetl konkretne pole
}
}
Ten sam wzorzec co consent: plugin nie zaszywa języka, oddaje dane.