feat(spa): in-shell html-fragment rendering for custom change_form_template (drop iframe) + dep ^1.7.0 + 1.12.0#681
Merged
Conversation
…re (#679) The examples/jobs JobAdmin already ships the custom-template Path B (?run_custom=1 dual-listbox change_view + run_custom.html + the run_with_custom_steps action). Update its docstrings/comments from the old legacy-iframe contract to the server-rendered html-fragment contract (rest-api 1.7.0+, #75), and add JobHtmlFragmentApiTests covering the SPA's actual API path end-to-end against the example backend: - GET .../form-spec/?run_custom=1 -> renderer "html-fragment" (dual-listbox markup + inline <script> + csrf_token + submit_url preserved); - POST .../change/?run_custom=1 with no selection -> re-rendered html-fragment carrying the error message (PRG-to-self); - POST with a selection -> renderer "redirect", target mapped onto the SPA prefix, success message carried for toasting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mplate, drop iframe (#679) Replace the legacy-iframe escape hatch with the server-rendered html-fragment renderer (rest-api 1.7.0+, #75). A custom change_form_template / change_view admin now renders INSIDE the SPA shell — no iframe, so no X-Frame-Options / SameSite / cross-origin cookie failure mode ever again. ChangeForm: renderer "html-fragment" -> new HtmlFragment component injects the content HTML into the SPA shell (breadcrumb / sidebar / title / toolbar stay React-rendered). Because dangerouslySetInnerHTML leaves parsed <script> elements inert, HtmlFragment clones each injected <script> into a fresh element and re-inserts it so it executes (required for the dual-listbox JS). The injected <form>'s submit is wired to ApiClient.submitChangeFragment, which POSTs the FormData to the round-trip route with credentials + X-CSRFToken from the fragment. On response: another html-fragment re-injects in place (validation errors, no SPA route change); { renderer: "redirect", to } triggers an SPA navigate(to) (never window.location); messages[] surface as toasts via the shared toastMessages adapter. The backend HTML is trusted (the integrator's own admin template behind the same auth) and injected verbatim — the trust boundary is documented in HtmlFragment.tsx. Removed the iframe path entirely: LegacyIframe(.test), legacy-url(.test) and the #673 framing-refusal workaround. contract.ts drops LegacyIframeResponse / "legacy-iframe" and adds HtmlFragmentResponse + RedirectResponse (+ the ChangePostPayload union + FragmentMessage); @dar/data re-exports updated. No regression for ModelAdmins using only documented hooks (form / fieldsets / formfield_overrides / get_form) — those keep rendering via the JSON field-map path. Bump django-admin-rest-api dep to ^1.7.0; version 1.11.2 -> 1.12.0; CHANGELOG [1.12.0] section; README "Embedding the legacy admin in an iframe" section rewritten as "Custom change_form_template admins — rendered in-shell". Closes #679 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| SPA to embed THIS legacy page in an iframe. The contract that must hold: | ||
| overridden change_view renders a non-standard template), renders it | ||
| server-side, strips the admin chrome, and hands the SPA the content-block | ||
| HTML — including the inline <script>/<style> below — to inject INSIDE its own |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Custom
change_form_templateadmins now render inside the SPA shell via a server-rendered html-fragment — no iframe. This drops the legacy-iframe escape hatch (and itsX-Frame-Options/SameSite/ broken-image failure modes) entirely.Contract (rest-api 1.7.0+, #75)
GET
…/form-spec/returns, for a custom-template admin:{ "renderer": "html-fragment", "html": "<form …>…</form>", "csrf_token": "…", "submit_url": "/admin/<app>/<model>/<pk>/change/?<qs>", "method": "POST", "messages": [{ "level": "success", "text": "…" }] }The SPA POSTs the injected form back to
POST <app>/<model>/<pk>/change/?<qs>, which returns anotherhtml-fragment(validation errors / PRG-to-self re-render) or{ "renderer": "redirect", "to": "/admin2/…", "messages": […] }(success).What the SPA does
htmlinto the content area; breadcrumb / sidebar / title / toolbar stay React-rendered.<script>:dangerouslySetInnerHTMLleaves parsed<script>inert, soHtmlFragmentclones each into a fresh<script>element and re-inserts it (required for the dual-listbox JS).<style>applies for free.fetch(…, { method, credentials: "include", body: FormData, headers: { "X-CSRFToken": csrf_token } })viaApiClient.submitChangeFragment. Response handling: html-fragment → re-inject in place; redirect → SPAnavigate(to)(neverwindow.location);messages[]→ toasts via the sharedtoastMessagesadapter.HtmlFragment.tsx): the fragment is the integrator's own admin template, rendered behind the same auth as/admin/over the same-origin API — injected verbatim, deliberately not sanitised.form/fieldsets/formfield_overrides/get_form) — those keep rendering via the JSON field-map path.Removed (iframe path)
LegacyIframe(.test),legacy-url(.test)+ thesafeLegacyUrlvalidator, and the #673 framing-refusal workaround.contract.tsdropsLegacyIframeResponse/renderer: "legacy-iframe"and addsHtmlFragmentResponse+RedirectResponse(+ChangePostPayload,FragmentMessage).Reproduction (examples/jobs)
/admin2/jobs/job/<pk>/change/?run_custom=1renders the dual-listbox custom template in-shell. Verified end-to-end against the example backend: form-spec →html-fragment; POST empty → re-renderedhtml-fragment+ error message; POST with selection →redirectmapped onto the SPA prefix + success message. NewJobHtmlFragmentApiTestslock this in.Verification
pnpm test247 passed (35 files),pnpm typecheck,pnpm lint:js,pnpm lint:css,pnpm build— all green. NewHtmlFragment.test.tsxcovers script execution, POST → validation re-inject → redirect navigate, and message toasts.ruff check,ruff format --check,mypy(no issues),bandit(0 issues),pytest -q75 passed. examples/jobs 8 passed.django-admin-rest-api^1.6.0→^1.7.0;poetry lockresolves 1.7.0. Version1.11.2→1.12.0+ CHANGELOG[1.12.0]. README iframe section rewritten.Closes #679