From 2d3b80487f3de2070448c55b92316ea708665fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn?= Date: Mon, 1 Jun 2026 22:26:16 +0200 Subject: [PATCH] feat(spa): form-spec-driven change form + legacy-iframe fallback (#659) + 1.9.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The change form is now driven by the rest-api form-spec endpoint (GET ///form-spec/, django-admin-rest-api 1.4.0+ #59) instead of discovering fields client-side from the model serializer, so the SPA honours the ModelAdmin layer: request-aware get_form / get_fieldsets / get_readonly_fields, formfield_overrides, custom Form classes, and the admin relation widgets — resolved server-side and mapped through the closed widget.kind enum. - The original change-form querystring is forwarded, so a get_form that swaps the Form on ?variant=… renders the legacy admin's fields. - A change_form_template / add_form_template override returns renderer: "legacy-iframe"; the SPA embeds the legacy admin page in an iframe inside the SPA shell (closes part of #624) rather than silently dropping the customisation. - A spec-fetch failure (older backend) degrades gracefully to the previous detail-payload-driven form. Implementation reuses the battle-tested FieldInput/EditForm: a small adapter maps each FormSpecField onto the existing FieldDescriptor (the backend reuses the detail serializer for `initial`, so value shapes line up). New: FormSpecResponse/WidgetKind wire types, client.formSpec(), useFormSpec hook, adaptFormSpec, LegacyIframe, ChangeForm. i18n strings for es/fr/pt. 11 new tests (adapter mapping + ChangeForm branches: iframe, form-spec fields, custom-widget fallback, graceful degradation). Raises the django-admin-rest-api floor to ^1.4.0 (the endpoint) and the django-admin-mcp-api floor to >=1.2.0 (the parity tool, #70). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 23 ++++ README.md | 3 +- frontend/apps/web/src/pages/DetailPage.tsx | 15 +- .../web/src/pages/detail/ChangeForm.test.tsx | 130 ++++++++++++++++++ .../apps/web/src/pages/detail/ChangeForm.tsx | 77 +++++++++++ .../web/src/pages/detail/LegacyIframe.tsx | 50 +++++++ .../src/pages/detail/adaptFormSpec.test.ts | 96 +++++++++++++ .../web/src/pages/detail/adaptFormSpec.ts | 86 ++++++++++++ frontend/packages/api/src/client.ts | 23 ++++ frontend/packages/api/src/contract.ts | 95 +++++++++++++ .../packages/data/src/form-spec-context.tsx | 46 +++++++ frontend/packages/data/src/index.ts | 9 ++ frontend/packages/ui/src/i18n/es.json | 5 +- frontend/packages/ui/src/i18n/fr.json | 5 +- frontend/packages/ui/src/i18n/pt.json | 5 +- poetry.lock | 16 +-- pyproject.toml | 9 +- 17 files changed, 676 insertions(+), 17 deletions(-) create mode 100644 frontend/apps/web/src/pages/detail/ChangeForm.test.tsx create mode 100644 frontend/apps/web/src/pages/detail/ChangeForm.tsx create mode 100644 frontend/apps/web/src/pages/detail/LegacyIframe.tsx create mode 100644 frontend/apps/web/src/pages/detail/adaptFormSpec.test.ts create mode 100644 frontend/apps/web/src/pages/detail/adaptFormSpec.ts create mode 100644 frontend/packages/data/src/form-spec-context.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index cdcf783..a83fad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.9.0] — 2026-06-01 + +### Added + +- **Change-form parity via the rest-api form-spec endpoint (#659).** The + change form is now driven by `GET ///form-spec/` + (django-admin-rest-api 1.4.0+, #59) instead of discovering fields + client-side from the model serializer. The SPA now honours the + **ModelAdmin layer**: request-aware `get_form(request, obj)` / + `get_fieldsets(request, obj)` / `get_readonly_fields(request, obj)`, + `formfield_overrides`, custom `Form` classes, and the admin relation + widgets — resolved server-side and mapped through the closed + `widget.kind` enum. The original change-form querystring is forwarded, + so a `get_form` that swaps the `Form` on `?variant=…` renders the same + fields the legacy `/admin/` does. When the backend can't render the + form from JSON (a `change_form_template` override → `renderer: + "legacy-iframe"`), the SPA embeds the legacy admin page in an iframe + inside the SPA shell instead of silently dropping the customisation + (closes part of #624). A spec-fetch failure (older backend) degrades + gracefully to the previous detail-payload-driven form. New API client + method `formSpec()` + `useFormSpec` hook; the existing `FieldInput` + renders the adapted fields unchanged (one control set, no drift). + ### Changed - **Split DetailPage/ListPage into focused modules (no behavior change) diff --git a/README.md b/README.md index ffca9aa..1add217 100644 --- a/README.md +++ b/README.md @@ -646,7 +646,8 @@ issues link the work to close each gap. | Stock-Django hook | SPA behaviour | Tracked | |---|---|---| -| `change_form_template` / `change_list_template` / `add_form_template` / `change_password_template` / `object_history_template` overrides | Silently ignored — the SPA renders entirely from the JSON wire. | [#624](https://github.com/MartinCastroAlvarez/django-admin-react/issues/624) | +| `change_form_template` / `add_form_template` overrides | **Embedded in an iframe** (since 1.9.0, #659): the change/add form-spec endpoint returns a `legacy-iframe` pointer and the SPA embeds the legacy admin page inside the SPA shell (breadcrumb / sidebar / toolbar stay SPA-rendered). Port the form to documented ModelAdmin hooks at your own pace. | [#624](https://github.com/MartinCastroAlvarez/django-admin-react/issues/624) | +| `change_list_template` / `change_password_template` / `object_history_template` overrides | Silently ignored — those surfaces render entirely from the JSON wire. | [#624](https://github.com/MartinCastroAlvarez/django-admin-react/issues/624) | | `formfield_overrides = {Field: {"widget": CustomWidget}}` | Custom widget invisible — the SPA picks its own control from the field's `type`. No React-side widget-registration API yet. | [#625](https://github.com/MartinCastroAlvarez/django-admin-react/issues/625) | | `raw_id_fields` | Falls back to the autocomplete picker (same as `autocomplete_fields`). Defeats the purpose for FKs with 10M+ rows where autocomplete `get_search_results` is too expensive. | [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) | | `radio_fields = {"status": admin.HORIZONTAL}` | Renders a `