Skip to content

feat(spa): render raw_id_fields and radio_fields hints (#626)#642

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/raw-id-radio-render-branches
May 31, 2026
Merged

feat(spa): render raw_id_fields and radio_fields hints (#626)#642
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/raw-id-radio-render-branches

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

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 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>.

Now

  • 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. Matches Django HTML admin's approach for raw_id_fields on FKs too large to autocomplete. 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.

What's NOT in this PR

  • M2M + raw_id rendering as a CSV pk-list textarea (separate follow-up — guarded out behind field.type === 'foreignkey').
  • A popup picker for the raw_id lookup (new-tab is the v1 affordance; can promote to an in-SPA modal later).

InlineEditor needs no change — its InlineCellInput already text-inputs anything non-password/boolean/numeric, which IS the raw_id rendering for table-shaped inline cells.

Verification

  • 6 new vitests in FieldInput.test.tsx cover 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

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 MartinCastroAlvarez merged commit 568ee32 into main May 31, 2026
5 checks passed
@MartinCastroAlvarez MartinCastroAlvarez deleted the feat/raw-id-radio-render-branches branch May 31, 2026 10:32
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>
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.

[audit] raw_id_fields & radio_fields widget hints declared in the wire contract are not rendered by the SPA

2 participants