Skip to content

Commit 5f4364e

Browse files
chore(release): v1.4.8 — standard ModelAdmin.actions on the detail page (#614)
Closes #603 revised. Patch — dep bump + client cleanup, no behaviour change beyond 'actions actually show up.'
1 parent 3ac3503 commit 5f4364e

5 files changed

Lines changed: 73 additions & 29 deletions

File tree

examples/fintech/admin.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,38 @@ class TransactionAdmin(admin.ModelAdmin):
2323
date_hierarchy = "posted_at"
2424
autocomplete_fields = ("account",)
2525
readonly_fields = ("reference",)
26+
# Stock Django admin actions (`@admin.action`). They surface on the
27+
# changelist (multi-pk runs) AND the SPA's detail page header
28+
# (single-pk runs) — no `django-object-actions` mixin, no
29+
# `change_actions = [...]` redeclaration. One source of truth for
30+
# everything the consumer wants to expose as an action.
31+
actions = ("mark_reconciled", "recompute_reference")
32+
33+
@admin.action(description="Mark as reconciled")
34+
def mark_reconciled(self, request, queryset):
35+
"""Tag every row in the queryset as reconciled.
36+
37+
Demo no-op: the model has no `reconciled` flag yet — this
38+
exists so the SPA's detail page has a stock-Django-admin
39+
action to render. A real ModelAdmin would update the row
40+
and message the user.
41+
"""
42+
count = queryset.count()
43+
self.message_user(request, f"Marked {count} transaction(s) as reconciled.")
44+
45+
@admin.action(description="Recompute reference")
46+
def recompute_reference(self, request, queryset):
47+
"""Roll the ``reference`` field on each row in the queryset.
48+
49+
Demo: bumps ``reference`` to ``TXN-RECOMP-<stamp>-<i>`` so the
50+
operator can see the action ran end-to-end on the SPA.
51+
"""
52+
from datetime import datetime, timezone as tz
53+
stamp = datetime.now(tz.utc).strftime("%Y%m%d%H%M%S")
54+
for i, row in enumerate(queryset):
55+
row.reference = f"TXN-RECOMP-{stamp}-{i:02d}"
56+
row.save(update_fields=["reference"])
57+
self.message_user(request, f"Recomputed reference on {queryset.count()} row(s).")
2658

2759

2860
@admin.register(Statement)

frontend/packages/api/src/client.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import type {
1818
HistoryResponse,
1919
ListResponse,
2020
LoginResponse,
21-
ObjectActionRunResponse,
2221
RecentActionsResponse,
2322
RegistryResponse,
2423
UpdatePayload,
@@ -336,25 +335,13 @@ export class ApiClient {
336335
);
337336
}
338337

339-
/**
340-
* Run one object-level change-page action (#236) against a single
341-
* object (`POST <app>/<model>/<pk>/action/<name>/`). The backend
342-
* re-resolves `name` through the admin's permitted `get_change_actions`
343-
* set — the SPA name is never trusted as a callable lookup. CSRF is
344-
* sent like other unsafe calls. Returns `{ok, message?, redirect?}`.
345-
*/
346-
runObjectAction(
347-
appLabel: string,
348-
modelName: string,
349-
pk: string | number,
350-
name: string,
351-
): Promise<ObjectActionRunResponse> {
352-
return this.request<ObjectActionRunResponse>(
353-
'POST',
354-
`${appLabel}/${modelName}/${pk}/action/${name}/`,
355-
{},
356-
);
357-
}
338+
// `runObjectAction` is intentionally not on the HTTP client anymore
339+
// (v1.4.8 / #603 revised). The dedicated `/<pk>/action/<name>/`
340+
// endpoint was removed in api 1.0.5 — detail-page actions now go
341+
// through `runAction(name, [pk])` against the existing changelist
342+
// runner. The legacy `runObjectAction` helper still exists in
343+
// `@dar/data`'s mutations.ts, but it delegates to `runAction`
344+
// internally rather than hitting a separate endpoint.
358345

359346
/**
360347
* Typeahead for a high-cardinality FK picker (contract §3.2). The

frontend/packages/data/src/mutations.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,32 @@ export function fetchDeletePreview(args: DeletePreviewArgs): Promise<DeletePrevi
6363
return args.client.deletePreview(args.appLabel, args.modelName, args.pk);
6464
}
6565

66-
/** Run one object-level change-page action (#236). */
67-
export function runObjectAction(args: RunObjectActionArgs): Promise<ObjectActionRunResponse> {
68-
return args.client.runObjectAction(args.appLabel, args.modelName, args.pk, args.name);
66+
/**
67+
* Run one detail-page action on a single object.
68+
*
69+
* Reuses the existing changelist actions runner (#603 revised, v1.4.8):
70+
* `data.object_actions` on the detail response is now sourced from the
71+
* standard `ModelAdmin.actions` — same descriptor builder as the list
72+
* response — so the SPA reuses the same `POST <app>/<model>/actions/
73+
* <name>/` runner with `pks=[<this pk>]`. No dedicated per-object
74+
* endpoint, no `django-object-actions` integration.
75+
*
76+
* Translates the changelist response shape (`{executed, messages,
77+
* redirect?}`) into the legacy `{ok, message?, redirect?}` callers
78+
* already consume — same external contract, simpler wiring.
79+
*/
80+
export async function runObjectAction(
81+
args: RunObjectActionArgs,
82+
): Promise<ObjectActionRunResponse> {
83+
const res = await args.client.runAction(args.appLabel, args.modelName, args.name, [args.pk]);
84+
// First `message_user` message becomes the legacy `message` field —
85+
// the SPA toasts it on success. Additional messages stay reachable
86+
// via the full ActionRunResponse if a caller wants to upgrade off
87+
// the legacy shape.
88+
const message = res.messages && res.messages.length > 0 ? res.messages[0]?.message : undefined;
89+
return {
90+
ok: res.executed,
91+
...(message !== undefined ? { message } : {}),
92+
...(res.redirect !== undefined ? { redirect: res.redirect } : {}),
93+
};
6994
}

poetry.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-admin-react"
3-
version = "1.4.7"
3+
version = "1.4.8"
44
description = "A drop-in React single-page admin for Django, driven entirely by ModelAdmin."
55
authors = ["django-admin-react contributors"]
66
license = "MIT"
@@ -49,7 +49,7 @@ django = ">=5.0,<7.0"
4949
# React SPA super-layer over `django-admin-rest-api`. The package's URLs
5050
# are included by `django_admin_react.urls`, and consumers add
5151
# `"django_admin_rest_api"` to `INSTALLED_APPS` alongside this package.
52-
django-admin-rest-api = "^1.0.4"
52+
django-admin-rest-api = "^1.0.5"
5353
# `django-admin-mcp-api` — MCP-protocol adapter over the same REST API
5454
# so agents reach the SAME `ModelAdmin`-driven surface. Wire-protocol-only
5555
# layer; adds NO new functionality, permissions, or validation.

0 commit comments

Comments
 (0)