220 lines
7.3 KiB
Markdown
220 lines
7.3 KiB
Markdown
# 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ść
|
|
|
|
```json
|
|
"dependencies": { "@payloadcms/plugin-form-builder": "3.84.1" }
|
|
```
|
|
|
|
## Config (payload.config.ts)
|
|
|
|
```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):
|
|
|
|
```ts
|
|
import { submitForm } from '@intecion/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
|
|
|
|
1. **Turnstile** (jeśli token) → nieudany → odrzuć **przed** zapisem (brak
|
|
spamu w bazie).
|
|
2. **Zapis** submission (form-submissions).
|
|
3. **Maile** — oba opcjonalne, HTML z frontu.
|
|
4. 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:
|
|
|
|
```ts
|
|
email: panelSmtpAdapter(), // z '@intecion/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:
|
|
|
|
```ts
|
|
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):
|
|
|
|
```ts
|
|
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: 'consent'; field?: string }
|
|
| { success: false; reason: 'error' }
|
|
```
|
|
|
|
Front mapuje kody na własne komunikaty:
|
|
|
|
```tsx
|
|
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.
|
|
|
|
|
|
## Zgoda RODO (consent field)
|
|
|
|
Pole zgody RODO to checkbox o nazwie `consent` (konfigurowalna przez
|
|
`FormsOption.consentFieldName`). Plugin WYMUSZA jego zaznaczenie SERVER-SIDE —
|
|
niezależnie od tego, jak redaktor ustawił pole w panelu.
|
|
|
|
### Dlaczego server-side
|
|
|
|
Zgoda egzekwowana jest w `validateSubmission`, nie flagą w panelu. To jedyne
|
|
miejsce, którego redaktor nie osłabi (zapominając `required`) ani nie naruszy
|
|
(ustawiając `defaultValue: true` — pre-zaznaczenie, którego RODO zabrania), a
|
|
front nie obejdzie. Jeśli formularz ma pole `consent`, MUSI być zaznaczone,
|
|
inaczej `submitForm` zwraca `reason: 'consent'`.
|
|
|
|
### Jak użyć
|
|
|
|
1. Redaktor dodaje w panelu checkbox o nazwie `consent`, label „Akceptuję
|
|
politykę prywatności [link]" (link do polityki wpisuje w label — treść zgody
|
|
należy do panelu).
|
|
2. Plugin wymusza zaznaczenie. Niezaznaczony → `reason: 'consent'`.
|
|
3. Komunikat z modułu notifications (`notifications.form.consent`, per język).
|
|
|
|
Zmiana nazwy pola:
|
|
|
|
```ts
|
|
ipalKit({ forms: { consentFieldName: 'zgoda' } })
|
|
```
|
|
|
|
## Komunikaty — resolveFormMessage (zalecane)
|
|
|
|
Zamiast ręcznego switcha po `reason`, użyj `resolveFormMessage` z modułu
|
|
notifications — mapuje kod na tekst z panelu, per język, z interpolacją `{field}`:
|
|
|
|
```tsx
|
|
import { resolveFormMessage, getNotificationTexts } from '@intecion/ipal-kit'
|
|
|
|
const notifications = await getNotificationTexts({ payload, locale })
|
|
// w FormRenderer:
|
|
if (!result.success) {
|
|
setError(resolveFormMessage(result, notifications.form))
|
|
}
|
|
```
|
|
|
|
To obsługuje WSZYSTKIE kody (w tym `consent`, `rate_limited`, `validation` z
|
|
`{field}`) tekstami z panelu. Ręczny switch (wyżej) zostaw tylko, jeśli nie
|
|
używasz modułu notifications. Szczegóły: [notifications.md](./notifications.md).
|
|
|
|
## PUŁAPKA — pola captchy
|
|
|
|
Turnstile wstrzykuje ukryte pole `cf-turnstile-response`. Plugin je toleruje
|
|
(nie odrzuca jako `unknown_fields`) — bo sam obsługuje Turnstile. Nie musisz go
|
|
filtrować w kliencie. |