Skip to content

Commit d9d586d

Browse files
perf(spa): lazy-load login/create routes + show-all windowing; README parity fixes (#670, #668)
#670: LoginPage and CreatePage are now React.lazy-loaded at the route boundary (out of the first-paint main chunk), wrapped in Suspense; App.tsx's "Page not found." now goes through t(). The "Show all N" (?all) list path uses native content-visibility row windowing (Table.virtualizeRows, wired in ListPage). #668: README parity table corrected — raw_id_fields / radio_fields / filter_horizontal flip to ✅ (they ship today), stale "does NOT carry through" entries removed, and a new section documents empty_value_display (hard-coded —), custom each_context, and list_select_related. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e669511 commit d9d586d

2 files changed

Lines changed: 43 additions & 13 deletions

File tree

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -618,8 +618,9 @@ all share the v1 wire contract. Per-feature live status below.
618618
| `list_editable` + bulk PATCH ||
619619
| `actions` — batch + detail (signature-classified) ||
620620
| `autocomplete_fields` ||
621-
| `raw_id_fields` (pk text input + lookup popup) | 🟡 [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) (API emits the hint; SPA still renders autocomplete) |
622-
| `radio_fields` (inline radio buttons vs `<select>`) | 🟡 [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) (API emits the hint; SPA still renders dropdown) |
621+
| `raw_id_fields` (pk text input + lookup popup) ||
622+
| `radio_fields` (inline radio buttons vs `<select>`) ||
623+
| `filter_horizontal` / `filter_vertical` (M2M shuttle) ||
623624
| `ManyToManyField` read + write ||
624625
| `inlines` (TabularInline / StackedInline) — read + write ||
625626
| `FileField` / `ImageField` — read ||
@@ -648,14 +649,13 @@ issues link the work to close each gap.
648649
|---|---|---|
649650
| `change_form_template` / `add_form_template` overrides | **Embedded in an iframe** (since 1.9.0, #659): the change/add form-spec endpoint returns a `legacy-iframe` pointer and the SPA embeds the legacy admin page inside the SPA shell (breadcrumb / sidebar / toolbar stay SPA-rendered). Port the form to documented ModelAdmin hooks at your own pace. | [#624](https://github.com/MartinCastroAlvarez/django-admin-react/issues/624) |
650651
| `change_list_template` / `change_password_template` / `object_history_template` overrides | Silently ignored — those surfaces render entirely from the JSON wire. | [#624](https://github.com/MartinCastroAlvarez/django-admin-react/issues/624) |
651-
| `formfield_overrides = {Field: {"widget": CustomWidget}}` | Custom widget invisible — the SPA picks its own control from the field's `type`. No React-side widget-registration API yet. | [#625](https://github.com/MartinCastroAlvarez/django-admin-react/issues/625) |
652-
| `raw_id_fields` | Falls back to the autocomplete picker (same as `autocomplete_fields`). Defeats the purpose for FKs with 10M+ rows where autocomplete `get_search_results` is too expensive. | [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) |
653-
| `radio_fields = {"status": admin.HORIZONTAL}` | Renders a `<select>` (default choice control) instead of inline radio buttons. | [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) |
654-
| `filter_horizontal` / `filter_vertical` (M2M shuttle widget) | Renders the generic multi-select checkbox list, not Django's two-pane shuttle. Switch the field to `autocomplete_fields` for a workable SPA UX. | [#627](https://github.com/MartinCastroAlvarez/django-admin-react/issues/627) |
652+
| `formfield_overrides = {Field: {"widget": CustomWidget}}` | Custom widget rendered via the React widget-registration API (`registerFieldWidget`, #625) when the consumer registers a renderer for the widget class; otherwise falls back to the default control + an operator-visible "not registered" note. | [#625](https://github.com/MartinCastroAlvarez/django-admin-react/issues/625) |
653+
| `empty_value_display` | **Hard-coded to ``.** A per-`ModelAdmin` / per-field `empty_value_display` override is **not** surfaced — the SPA renders the literal em-dash for every empty value, regardless of the consumer's chosen placeholder. | [#629](https://github.com/MartinCastroAlvarez/django-admin-react/issues/629) |
654+
| Custom `AdminSite.each_context(request)` extra keys | Not surfaced. Only a fixed set of site attributes (`site_header` / `site_title` / `site_logo` / `site_primary_color`) reaches the SPA; any extra keys a consumer adds in a custom `each_context` are dropped. | [#629](https://github.com/MartinCastroAlvarez/django-admin-react/issues/629) |
655+
| `list_select_related` | A backend query-optimisation concern, applied server-side by the REST API's queryset; it changes query efficiency, **not** the wire shape, so it is intentionally invisible to the SPA (no client-visible effect to surface). | [#629](https://github.com/MartinCastroAlvarez/django-admin-react/issues/629) |
655656
| `GenericForeignKey` / `GenericInlineModelAdmin` | Support gap — verify per-model before relying on the SPA. | [#628](https://github.com/MartinCastroAlvarez/django-admin-react/issues/628) |
656-
| `LANGUAGE_CODE` / `gettext` / `Accept-Language` | The SPA chrome stays English; translated `verbose_name` / `help_text` / `@admin.action(description=_("..."))` are not surfaced per-request. | [#630](https://github.com/MartinCastroAlvarez/django-admin-react/issues/630) |
657+
| `LANGUAGE_CODE` / `gettext` / `Accept-Language` | SPA chrome strings translate via the bundled catalogs (es / fr / pt; #630); translated `verbose_name` / `help_text` / `@admin.action(description=_("..."))` flow through when `LocaleMiddleware` is installed. | [#630](https://github.com/MartinCastroAlvarez/django-admin-react/issues/630) |
657658
| `ModelAdmin.get_urls()` custom views | Opens as a popout (`<a target="_blank">`) into the Django-rendered HTML page — no SPA chrome, no breadcrumb. The link IS surfaced; the UX is just outside the SPA. | [#623](https://github.com/MartinCastroAlvarez/django-admin-react/issues/623) |
658-
| Django 4.2 LTS support | Not yet — the package pins `django >= 5.0,<7.0`. | [#622](https://github.com/MartinCastroAlvarez/django-admin-react/issues/622) |
659659

660660
If your admin relies on any "silently ignored" hook above, the
661661
typical workaround is to keep that model on the legacy

frontend/apps/web/src/App.tsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
1+
import { Suspense, lazy } from 'react';
12
import { Route, Routes, useParams } from 'react-router-dom';
23

34
import { ApiError, useRegistry } from '@dar/data';
5+
import { t } from '@dar/ui';
46

57
import { ErrorBoundary } from './ErrorBoundary';
68
import { Layout } from './Layout';
79
import { HomePage } from './pages/HomePage';
810
import { ListPage } from './pages/ListPage';
911
import { DetailPage } from './pages/DetailPage';
10-
import { LoginPage } from './pages/LoginPage';
11-
import { CreatePage } from './pages/CreatePage';
1212
import { ToastProvider } from './toast';
1313

14+
// Route-level code-splitting (#670): the login + create pages aren't on the
15+
// first authenticated paint — login only renders when the session is dead,
16+
// and create only after the operator clicks "+ Add". Lazy-loading them keeps
17+
// their code out of the main chunk. (Home / List / Detail stay eager: one of
18+
// them is the very first paint on every load, so splitting them would only
19+
// add a Suspense flash.)
20+
const LoginPage = lazy(() =>
21+
import('./pages/LoginPage').then((m) => ({ default: m.LoginPage })),
22+
);
23+
const CreatePage = lazy(() =>
24+
import('./pages/CreatePage').then((m) => ({ default: m.CreatePage })),
25+
);
26+
27+
// Shared fallback for a lazy route chunk still in flight — a small, layout-
28+
// neutral hint rather than a blank frame.
29+
function RouteFallback() {
30+
return <div className="p-6 text-sm text-gray-500">{t('Loading…')}</div>;
31+
}
32+
1433
// Remount ListPage when the model changes so per-model state (selection,
1534
// retained "keep previous data" rows) resets cleanly on a model switch —
1635
// while filter / page / search changes within a model keep the same
@@ -34,7 +53,11 @@ export function App() {
3453
// registry can't keep a dead session looking alive.
3554
const { error, refresh } = registry;
3655
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
37-
return <LoginPage onSuccess={refresh} />;
56+
return (
57+
<Suspense fallback={<RouteFallback />}>
58+
<LoginPage onSuccess={refresh} />
59+
</Suspense>
60+
);
3861
}
3962

4063
return (
@@ -49,7 +72,14 @@ export function App() {
4972
{/* Literal `add` is ranked above the `:pk` route by React
5073
Router, so /app/model/add opens the create form, not a
5174
detail with pk="add". */}
52-
<Route path=":appLabel/:modelName/add" element={<CreatePage />} />
75+
<Route
76+
path=":appLabel/:modelName/add"
77+
element={
78+
<Suspense fallback={<RouteFallback />}>
79+
<CreatePage />
80+
</Suspense>
81+
}
82+
/>
5383
<Route path=":appLabel/:modelName/:pk" element={<DetailPage />} />
5484
{/* Django-admin URL aliases (#601). When the SPA is mounted
5585
at the legacy admin's prefix (after a /admin/ ↔ /admin-old/
@@ -75,7 +105,7 @@ export function App() {
75105
<Route path=":appLabel/:modelName/:pk/delete" element={<DetailPage />} />
76106
<Route
77107
path="*"
78-
element={<div className="p-6 text-sm text-gray-500">Page not found.</div>}
108+
element={<div className="p-6 text-sm text-gray-500">{t('Page not found.')}</div>}
79109
/>
80110
</Routes>
81111
</ErrorBoundary>

0 commit comments

Comments
 (0)