Skip to content

feat(spa): form-spec-driven change form + legacy-iframe fallback (#659) + 1.9.0#663

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/form-spec-change-form-659
Jun 1, 2026
Merged

feat(spa): form-spec-driven change form + legacy-iframe fallback (#659) + 1.9.0#663
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/form-spec-change-form-659

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

Closes #659. Completes the three-repo form-spec feature (rest-api #59 → mcp #70 → this).

What

The change form is now driven by the rest-api form-spec endpoint (GET <app>/<model>/<pk>/form-spec/, django-admin-rest-api 1.4.0+) 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, admin relation widgets
  • each field's resolved widget 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.

Legacy-iframe escape hatch

When the admin overrides change_form_template / add_form_template, the backend returns {renderer: "legacy-iframe", legacy_url} and the SPA embeds the legacy admin page in an iframe inside the SPA shell (breadcrumb / sidebar / toolbar stay SPA-rendered) — closing part of #624 instead of silently dropping the customisation.

Graceful degradation

A spec-fetch failure (older backend without the endpoint) falls back to the previous detail-payload-driven form, so editing never breaks.

Design (per the issue's principle)

No SPA-specific customisation API. The SPA renders whatever the backend resolves from documented ModelAdmin hooks — and reuses the battle-tested FieldInput/EditForm via a small adapter that maps each FormSpecField onto the existing FieldDescriptor (the backend reuses the detail serializer for initial, so value shapes line up exactly — one control set, no drift).

Acceptance criteria (#659)

  • change form driven by the form-spec endpoint (no client-side field discovery)
  • fieldsets / readonly / formfield_overrides / get_form / get_fieldsets render from the resolved spec
  • closed widget-kind enum; custom widgets surface as kind: "custom" (+ widget_class, dispatched via the [audit] formfield_overrides custom widgets are ignored — no React extension point for non-stock controls #625 registry, textarea fallback when unregistered)
  • legacy-iframe fallback for change_form_template overrides
  • tests cover: fieldset collapse, request-aware get_form variant, readonly, custom→fallback, change_form_template→iframe

Changes

New wire types (FormSpecResponse/WidgetKind/…), client.formSpec(), useFormSpec hook, adaptFormSpec, LegacyIframe, ChangeForm; i18n strings (es/fr/pt). Raises the rest-api floor to ^1.4.0 and the mcp-api floor to >=1.2.0.

Tests / gate

11 new tests (adapter + ChangeForm branches). Full frontend suite green (233), typecheck + eslint + stylelint + dark-mode all clean, pnpm build produces the bundle; backend pytest green (66).

🤖 Generated with Claude Code

@MartinCastroAlvarez MartinCastroAlvarez force-pushed the feat/form-spec-change-form-659 branch from ccc81db to da74a14 Compare June 1, 2026 20:31
… + 1.9.0

The change form is now driven by the rest-api form-spec endpoint
(GET <app>/<model>/<pk>/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) <noreply@anthropic.com>
@MartinCastroAlvarez MartinCastroAlvarez force-pushed the feat/form-spec-change-form-659 branch from da74a14 to 2d3b804 Compare June 1, 2026 20:36
@MartinCastroAlvarez MartinCastroAlvarez merged commit 76277ab into main Jun 1, 2026
6 checks passed
@MartinCastroAlvarez MartinCastroAlvarez deleted the feat/form-spec-change-form-659 branch June 1, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detail-page form parity: honour ModelAdmin form / fieldsets / formfield_overrides / get_form (via rest-api form-spec)

1 participant