feat(spa): render raw_id_fields and radio_fields hints (#626)#642
Merged
Conversation
The wire contract already declared `WidgetHint = 'radio' | 'raw_id'
| 'password'`, but only `'password'` had a render path; the other
two were silently no-ops — a ModelAdmin declaring `raw_id_fields =
('owner',)` got the autocomplete picker anyway (the very thing the
consumer was OPTING OUT of), and `radio_fields = {"status":
admin.HORIZONTAL}` rendered as a `<select>`.
This adds the two render branches in `FieldInput.tsx`:
- **`widget === 'raw_id'` on a `foreignkey`** → plain pk text input
(no autocomplete request, no popup picker) + a small "Lookup ↗"
link that opens the FK target's changelist in a new tab so the
operator can find the pk by sight. Matches Django HTML admin's
approach for `raw_id_fields` on FKs with too many rows to
autocomplete. The link is omitted when `field.to` is absent (FK
target not admin-registered).
- **`widget === 'radio'` on a `choice` with `choices`** → inline
radio bank under a `<div role="radiogroup">`. Emits the original
choice value (not the stringified key) so number/bool choices
round-trip.
M2M + `raw_id` is intentionally not handled here (legacy admin
renders a CSV pk list textarea — separate follow-up); the guard
`field.type === 'foreignkey'` lets M2M fall through to the existing
M2M branch.
InlineEditor needs no change: its `InlineCellInput` already
text-inputs anything non-password/boolean/numeric, which IS the
`raw_id` rendering for a table-shaped inline cell.
Locks: 6 new vitests in `FieldInput.test.tsx` cover the rendering,
the lookup-link presence/absence, the onChange pk emission, the
clear-to-null path, and the radio group's checked + onChange
behaviour.
Closes #626.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MartinCastroAlvarez
added a commit
that referenced
this pull request
May 31, 2026
….4.13 (#643) A ModelAdmin action that returns ``HttpResponseRedirect(some_url)`` was looking silently no-op'd to the operator: the click ran, the toast didn't appear, and nothing visible happened. The diagnosis in the issue blamed the API for swallowing the response, but the API correctly extracts ``response["Location"]`` into the JSON envelope's ``redirect`` field (``api/views/actions.py:256``). The actual bug was on the SPA: ``DetailPage`` piped the redirect URL straight into React Router's ``navigate`` — which is scoped to the SPA's ``BrowserRouter`` ``basename``, so any URL outside the SPA mount silently no-op'd: - legacy admin paths (``/admin/<app>/<model>/<pk>/change/``) - hijack / impersonate URLs (``/hijack/release-user/?next=…``) - cross-origin downloads (signed S3 URLs) New ``followActionRedirect`` helper (`apps/web/src/action-redirect.ts`) picks the right primitive per URL: ``navigate`` for same-origin paths inside the SPA mount (no full reload), ``window.location.assign`` for everything else. Returns a stripped basename-relative path to the navigate call so BrowserRouter doesn't double-prefix. The helper is dependency-injected (``currentOrigin``, ``assignLocation``) so the test suite can lock the routing logic without touching jsdom's non-configurable ``window.location``. Locks: 6 new vitests in `action-redirect.test.ts` cover the SPA- internal path, search + hash preservation, the legacy-admin path, cross-origin URLs, the hijack pattern, and a malformed-URL fallback. Release 1.4.13 bundles this with the unreleased changes since 1.4.12 (all already merged on main): - #631 / PR #641 — ``PRIMARY_COLOR`` reads ``site_primary_color`` off the configured ``AdminSite`` before falling back to the setting + default. - #626 / PR #642 — ``raw_id_fields`` and ``radio_fields`` now render their intended widgets (plain-pk text input + lookup link, inline radio bank) instead of falling through to autocomplete / ``<select>``. - #623 / #624 / #633 / #634 / #635 — README "Stock-Django hooks that do NOT carry through" / "Writing safe ``list_display`` callables" / "Hardening" / "Mounting the API on a different origin" sections (PR #640). - PR #638 — ``release.yml`` → ``publish.yml`` rename so PyPI's Trusted Publisher config matches the workflow filename. Closes #620. Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Closes #626.
What
The wire contract already declared
WidgetHint = 'radio' | 'raw_id' | 'password', but only'password'had a render path. The other two hints were silently no-ops: a ModelAdmin declaringraw_id_fields = ('owner',)got the autocomplete picker anyway (the very thing the consumer was opting out of), andradio_fields = {"status": admin.HORIZONTAL}rendered as a<select>.Now
widget === 'raw_id'on aforeignkey→ plain pk text input (no autocomplete request, no popup picker) + a small "Lookup ↗" link that opens the FK target's changelist in a new tab. Matches Django HTML admin's approach forraw_id_fieldson FKs too large to autocomplete. Link is omitted whenfield.tois absent (FK target not admin-registered).widget === 'radio'on achoicewithchoices→ inline radio bank under a<div role="radiogroup">. Emits the original choice value (not the stringified key) so number/bool choices round-trip.What's NOT in this PR
raw_idrendering as a CSV pk-list textarea (separate follow-up — guarded out behindfield.type === 'foreignkey').raw_idlookup (new-tab is the v1 affordance; can promote to an in-SPA modal later).InlineEditor needs no change — its
InlineCellInputalready text-inputs anything non-password/boolean/numeric, which IS theraw_idrendering for table-shaped inline cells.Verification
FieldInput.test.tsxcover the rendering, lookup-link presence/absence, onChange pk emission, clear-to-null, and the radio group checked + onChange behaviour.pnpm test— 181 / 181 ✓ (up from 174 / 174; +7)pnpm -r typecheck✓pnpm lint✓🤖 Generated with Claude Code