Skip to content

feat(spa): read --dar-primary from AdminSite.site_primary_color (#631)#641

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/primary-color-adminsite-attr
May 31, 2026
Merged

feat(spa): read --dar-primary from AdminSite.site_primary_color (#631)#641
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/primary-color-adminsite-attr

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

Closes #631.

What

A consumer with a custom AdminSite subclass can now brand the whole admin (legacy + SPA) from one place by setting site_primary_color on the AdminSite — no need to add a separate DJANGO_ADMIN_REACT settings dict for the SPA accent.

Mirrors the existing site_header / site_logo fallback pattern already used by BRAND_TITLE / BRAND_LOGO_URL.

Resolution order

  1. DJANGO_ADMIN_REACT["PRIMARY_COLOR"] — explicit per-deployment override (highest precedence).
  2. admin_site.site_primary_color — structural default for shops with a custom AdminSite.
  3. DEFAULT_PRIMARY_COLOR (= #2563eb, unchanged).

Every layer runs through the existing hex-regex gate — CSS injection is impossible at any source. Same trust boundary as before.

Tests

Three new in tests/test_spa_index.py:

  • AdminSite attr is honoured when no setting is configured.
  • Explicit setting wins over the AdminSite attr.
  • Non-hex AdminSite attr can't inject CSS — falls through to default.

All 61 pytests pass; typecheck clean.

Side effect

DEFAULTS["PRIMARY_COLOR"] flipped from "#2563eb" to None so the resolver can distinguish "user set this" from "default in effect." Existing tests for the default-injection case still pass; the actual hex fallback now lives in the new DEFAULT_PRIMARY_COLOR module constant.

🤖 Generated with Claude Code

Mirrors how `BRAND_TITLE` / `BRAND_LOGO_URL` already fall through to
`AdminSite.site_header` / `site_logo`: a consumer with a custom
`AdminSite` subclass can now brand the whole admin (legacy + SPA)
from one place — set `site_primary_color` on the AdminSite, skip
the `settings.DJANGO_ADMIN_REACT["PRIMARY_COLOR"]` dance.

Resolution order (per-deployment override → structural default →
built-in fallback):

  1. `DJANGO_ADMIN_REACT["PRIMARY_COLOR"]` setting, when set.
  2. `admin_site.site_primary_color` attr.
  3. `DEFAULT_PRIMARY_COLOR` (= `#2563eb`, unchanged).

Every layer runs through the existing hex-regex gate, so CSS
injection is impossible at any source — same trust boundary as
before.

DEFAULTS["PRIMARY_COLOR"] flipped from `"#2563eb"` to `None` so the
resolver can distinguish "consumer set this" from "default is in
effect"; the actual fallback hex now lives in a re-exported
`DEFAULT_PRIMARY_COLOR` constant.

Locked by three new tests in `test_spa_index.py`:
  - AdminSite attr is honoured when no setting is configured.
  - Explicit setting wins over the AdminSite attr.
  - Non-hex AdminSite attr can't inject CSS — falls through to default.

Closes #631.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MartinCastroAlvarez MartinCastroAlvarez merged commit ff308bb into main May 31, 2026
5 checks passed
@MartinCastroAlvarez MartinCastroAlvarez deleted the feat/primary-color-adminsite-attr branch May 31, 2026 10:27
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] DJANGO_ADMIN_REACT settings dict is alien to stock Django admin — read consumer's AdminSite first, fall back to the dict

2 participants