feat(10-03): open, edit and save a record with toast and 422 feedback

- The SPA loads the backend::lang bundle before /auth/me, sets the
  document language from meta.locale and renders plural messages with
  Intl.PluralRules; interpolate mirrors phrasebook for :name/:Name/:NAME
- Create and record routes; mapWinterUrl maps recordUrl and redirects
  onto the controller's list, create and record routes only
- List rows open their record; FormView loads the form schema and the
  record, shows context-allowed fields in the span grid, saves values
  keyed by field name and toasts the resolved saved message
- A 422 puts each message under its field (aria-invalid,
  aria-describedby), shows the plural banner, focuses the first invalid
  field in schema order and clears a field's error on change
- The D-05 registry maps text, textarea, number and dropdown; any other
  type renders the unsupported-field box with the type in DM Mono
- The admin OpenAPI document declares the write request bodies
  (AdminRecord, AdminIDsRequest) and the list filter query as a
  deepObject, so the typed client can send them
- New backend::lang form.load_failed key; boardwalk/dist rebuilt
This commit is contained in:
Jakub Zych
2026-09-27 16:43:47 +02:00
parent 453b8ae786
commit 126ca5b8ed
41 changed files with 1930 additions and 37 deletions

304
admin/tests/fixtures/lang.json vendored Normal file
View File

@@ -0,0 +1,304 @@
{
"data": {
"backend::lang.auth.invalid": {
"other": "Nieprawidłowe dane logowania"
},
"backend::lang.auth.login": {
"other": "Login lub e-mail"
},
"backend::lang.auth.logout": {
"other": "Wyloguj"
},
"backend::lang.auth.password": {
"other": "Hasło"
},
"backend::lang.auth.session_expired": {
"other": "Sesja wygasła. Zaloguj się ponownie."
},
"backend::lang.auth.submit": {
"other": "Zaloguj"
},
"backend::lang.auth.title": {
"other": "Witaj ponownie"
},
"backend::lang.form.add": {
"other": "Dodaj"
},
"backend::lang.form.add_item": {
"other": "Dodaj…"
},
"backend::lang.form.cancel": {
"other": "Anuluj"
},
"backend::lang.form.close": {
"other": "Zamknij"
},
"backend::lang.form.confirm": {
"other": "Potwierdź"
},
"backend::lang.form.create": {
"other": "Utwórz"
},
"backend::lang.form.delete": {
"other": "Usuń"
},
"backend::lang.form.deleting": {
"other": "Usuwanie…"
},
"backend::lang.form.error_fields": {
"one": "Popraw :count pole oznaczone poniżej.",
"few": "Popraw :count pola oznaczone poniżej.",
"many": "Popraw :count pól oznaczonych poniżej.",
"other": "Popraw :count pola oznaczonego poniżej."
},
"backend::lang.form.error_generic": {
"other": "Nie udało się zapisać zmian. Spróbuj ponownie."
},
"backend::lang.form.error_title": {
"other": "Nie udało się zapisać."
},
"backend::lang.form.load_failed": {
"other": "Nie udało się wczytać formularza."
},
"backend::lang.form.loading_options": {
"other": "Wczytywanie…"
},
"backend::lang.form.no_options": {
"other": "Brak wyników"
},
"backend::lang.form.none": {
"other": "— brak —"
},
"backend::lang.form.remove_item": {
"other": "Usuń: :name"
},
"backend::lang.form.return_to_list": {
"other": "Wróć do listy"
},
"backend::lang.form.save": {
"other": "Zapisz"
},
"backend::lang.form.save_and_close": {
"other": "Zapisz i zamknij"
},
"backend::lang.form.saving": {
"other": "Zapisywanie…"
},
"backend::lang.form.select_placeholder": {
"other": "Wybierz…"
},
"backend::lang.form.tab_errors": {
"one": ":count błąd",
"few": ":count błędy",
"many": ":count błędów",
"other": ":count błędu"
},
"backend::lang.form.unsaved_confirm": {
"other": "Masz niezapisane zmiany. Porzucić je?"
},
"backend::lang.form.unsupported_field": {
"other": "Nieobsługiwany typ pola: :type"
},
"backend::lang.form.update": {
"other": "Edytuj"
},
"backend::lang.list.clear_search": {
"other": "Wyczyść wyszukiwanie"
},
"backend::lang.list.column_created": {
"other": "Utworzono"
},
"backend::lang.list.column_switch_false": {
"other": "Nie"
},
"backend::lang.list.column_switch_true": {
"other": "Tak"
},
"backend::lang.list.column_updated": {
"other": "Zaktualizowano"
},
"backend::lang.list.delete_selected": {
"other": "Usuń zaznaczone"
},
"backend::lang.list.empty_value": {
"other": "—"
},
"backend::lang.list.load_failed": {
"other": "Nie udało się wczytać listy."
},
"backend::lang.list.loading": {
"other": "Wczytywanie…"
},
"backend::lang.list.next_page": {
"other": "Następna"
},
"backend::lang.list.no_records": {
"other": "Brak rekordów w tym widoku."
},
"backend::lang.list.no_results": {
"other": "0 wyników"
},
"backend::lang.list.pagination_range": {
"other": ":from–:to z :total"
},
"backend::lang.list.prev_page": {
"other": "Poprzednia"
},
"backend::lang.list.records_per_page": {
"other": "Na stronę"
},
"backend::lang.list.results": {
"one": ":count wynik",
"few": ":count wyniki",
"many": ":count wyników",
"other": ":count wyniku"
},
"backend::lang.list.search_prompt": {
"other": "Szukaj…"
},
"backend::lang.list.select_all": {
"other": "Zaznacz wszystkie na stronie"
},
"backend::lang.list.select_row": {
"other": "Zaznacz wiersz"
},
"backend::lang.list.sort_ascending": {
"other": "Sortuj rosnąco"
},
"backend::lang.list.sort_descending": {
"other": "Sortuj malejąco"
},
"backend::lang.messages.form.create": {
"other": "Nowy rekord"
},
"backend::lang.messages.form.delete_confirm": {
"other": "Usunąć ten rekord?"
},
"backend::lang.messages.form.deleted": {
"other": "Usunięto rekord"
},
"backend::lang.messages.form.saved": {
"other": "Zapisano"
},
"backend::lang.messages.form.update": {
"other": "Edycja rekordu"
},
"backend::lang.messages.list.create": {
"other": "Nowy rekord"
},
"backend::lang.messages.list.delete_confirm": {
"other": "Usunąć zaznaczone (:count)?"
},
"backend::lang.messages.list.deleted": {
"one": "Usunięto :count rekord",
"few": "Usunięto :count rekordy",
"many": "Usunięto :count rekordów",
"other": "Usunięto :count rekordu"
},
"backend::lang.messages.list.empty_search": {
"other": "Nic nie znaleziono"
},
"backend::lang.messages.list.empty_search_hint": {
"other": "Żaden rekord nie pasuje do frazy „:term”. Spróbuj innej frazy."
},
"backend::lang.messages.list.record_count": {
"one": ":count rekord",
"few": ":count rekordy",
"many": ":count rekordów",
"other": ":count rekordu"
},
"backend::lang.messages.list.selected": {
"other": "Zaznaczono :count"
},
"backend::lang.messages.relation.candidate_search": {
"other": "Szukaj…"
},
"backend::lang.messages.relation.empty": {
"other": "Brak powiązanych rekordów."
},
"backend::lang.messages.relation.link": {
"other": "Dodaj"
},
"backend::lang.messages.relation.link_hint": {
"other": "Wybierz rekordy, które chcesz dołączyć."
},
"backend::lang.messages.relation.linked": {
"one": "Dołączono :count rekord",
"few": "Dołączono :count rekordy",
"many": "Dołączono :count rekordów",
"other": "Dołączono :count rekordu"
},
"backend::lang.messages.relation.unlink_confirm": {
"other": "Odłączyć zaznaczone (:count)?"
},
"backend::lang.messages.relation.unlink_selected": {
"other": "Odłącz zaznaczone"
},
"backend::lang.messages.relation.unlinked": {
"one": "Odłączono :count rekord",
"few": "Odłączono :count rekordy",
"many": "Odłączono :count rekordów",
"other": "Odłączono :count rekordu"
},
"backend::lang.nav.breadcrumbs": {
"other": "Ścieżka nawigacji"
},
"backend::lang.nav.collapse": {
"other": "Zwiń menu"
},
"backend::lang.nav.empty": {
"other": "Brak dostępnych sekcji."
},
"backend::lang.nav.expand": {
"other": "Rozwiń menu"
},
"backend::lang.nav.plugins": {
"other": "Wtyczki"
},
"backend::lang.nav.sections": {
"other": "Sekcje"
},
"backend::lang.nav.settings": {
"other": "Ustawienia"
},
"backend::lang.nav.user_menu": {
"other": "Menu użytkownika"
},
"backend::lang.page.not_found": {
"other": "Nie znaleziono strony."
},
"backend::lang.relation.add": {
"other": "Dodaj"
},
"backend::lang.relation.add_count": {
"other": "Dodaj (:count)"
},
"backend::lang.relation.candidates_range": {
"other": ":from–:to z :total"
},
"backend::lang.relation.link": {
"other": "Dołącz"
},
"backend::lang.relation.remove": {
"other": "Usuń"
},
"backend::lang.relation.search": {
"other": "Szukaj…"
},
"backend::lang.relation.unlink": {
"other": "Odłącz"
},
"backend::lang.settings.empty": {
"other": "Brak dostępnych ustawień."
},
"backend::lang.settings.saved": {
"other": "Zapisano ustawienia"
},
"backend::lang.settings.title": {
"other": "Ustawienia"
}
},
"meta": {
"locale": "pl"
}
}