feat(10.1-01): run registered widget actions through a cabana-owned route
- pact: AdminClientAssets, AdminAction, AdminActionInput, AdminActionResult,
HasAdminActions and AdminPartialData contracts
- fields.yaml type: widget with widget, action and fill keys; boot checks the
plugin tag prefix, the registered action and writable scalar fill fields
- POST .../widgets/{field} behind requireAjax, controller and action
permissions, scoped non-locking record read and a server-side fill filter
- typed OpenAPI operation, inventories and an acme conformance case
This commit is contained in:
@@ -14,6 +14,7 @@ Capability interfaces that compiled plugins implement to contribute routes, conf
|
||||
- HTTP contracts: the `pact.Router` group builder (implemented by surf), the `pact.Middleware` type, and named, parameterized (`name:param`) and house-envelope middleware through `pact.HasMiddleware`, `pact.HasMiddlewareFactories` and `pact.HasHouseMiddleware`.
|
||||
- Backend registration data: `pact.Permission`, `pact.NavigationItem` and `pact.SettingsItem`, exposed through `pact.HasPermissions`, `pact.HasNavigation` and `pact.HasSettings`.
|
||||
- Admin controller contracts: `pact.AdminController`, `pact.HasAdminControllers`, `pact.AdminAssets` (embedded Winter-shaped admin YAML), `pact.AdminPermissioned` and `pact.AdminRecordSource`.
|
||||
- Admin extension contracts, so a plugin extends the compiled admin SPA without a Node build: `pact.AdminClientAssets` (per-controller JS and CSS from the plugin's embedded `assets/` tree, Winter's `addJs`/`addCss`), `pact.HasAdminActions` with `pact.AdminAction`, `pact.AdminActionInput` and `pact.AdminActionResult` (named toolbar and widget actions whose routes, CSRF check, permissions and record scoping the framework owns), and `pact.AdminPartialData` (the curated view model a partial template renders).
|
||||
- Optional admin hooks a controller or model can implement: list and form query scoping (`pact.ListExtendQuery`, `pact.FormExtendQuery`), create, update and delete hooks (`pact.FormBeforeCreate`, `pact.FormAfterUpdate`, `pact.FormBeforeDelete` and their siblings), relation hooks (`pact.RelationExtendManageQuery`, `pact.RelationExtendOptionsQuery`, `pact.RelationBeforeLink`), filter scopes (`pact.FilterScope`, `pact.FilterOptions`) and dropdown options (`pact.DropdownOptionsProvider`).
|
||||
- A background job contract (`pact.Job`, `pact.JobArgs`) that does not depend on any queue library.
|
||||
- `pact.OptionalMessage`, a service an optional plugin can publish so others integrate with it without importing its package.
|
||||
@@ -83,6 +84,12 @@ func showPost(w http.ResponseWriter, r *http.Request) {}
|
||||
| `pact.AdminController` | Admin controller identity: ID, model name and YAML config directory. |
|
||||
| `pact.AdminAssets` | Embedded tree of the plugin's admin YAML. |
|
||||
| `pact.AdminRecordSource` | Supplies a new model record for the generic admin handlers. |
|
||||
| `pact.AdminClientAssets` | Declares a controller's admin JS (`AdminJS`) and CSS (`AdminCSS`) files, paths under the plugin's `assets/` directory. |
|
||||
| `pact.AdminAction` | One named controller action: name, label, extra permissions and the Go `Run` function. |
|
||||
| `pact.AdminActionInput` | What an action receives: widget field, optional record id and scoped record, and the fill snapshot. |
|
||||
| `pact.AdminActionResult` | What an action returns: a message for the toast and the fill write-back values. |
|
||||
| `pact.HasAdminActions` | Registers a controller's actions for `toolbar.buttons` and `type: widget` fields. |
|
||||
| `pact.AdminPartialData` | Supplies the view model a controller partial template renders; never the GORM model. |
|
||||
| `pact.FilterScope` | Model scopes a list filter may call, limited to an exact allow list. |
|
||||
| `pact.Option` | One dropdown choice (value and label). |
|
||||
|
||||
|
||||
@@ -129,6 +129,70 @@ type AdminRecordSource interface {
|
||||
NewRecord() any
|
||||
}
|
||||
|
||||
// AdminClientAssets is Winter's addJs/addCss for one admin controller. The
|
||||
// paths are relative to the owning plugin's AdminFS and must live under
|
||||
// assets/ (for example assets/js/lookup.js). The admin SPA loads them when the
|
||||
// controller's list or form opens; files are always served from the embedded
|
||||
// tree, never from disk. It is separate from AdminAssets, which is the YAML
|
||||
// tree itself.
|
||||
type AdminClientAssets interface {
|
||||
AdminJS() []string
|
||||
AdminCSS() []string
|
||||
}
|
||||
|
||||
// AdminAction is one controller action that a list toolbar button or a form
|
||||
// widget runs. The admin framework owns the HTTP route, the CSRF check,
|
||||
// authentication and record scoping; Run only carries the business logic.
|
||||
// Name is an identifier unique within the controller; create and delete are
|
||||
// reserved for the built-in toolbar actions. Label is a phrase key or literal
|
||||
// text used as the button caption. Permissions are checked in addition to the
|
||||
// controller's RequiredPermissions.
|
||||
type AdminAction struct {
|
||||
Name string
|
||||
Label string
|
||||
Permissions []string
|
||||
Run func(ctx context.Context, in AdminActionInput) (AdminActionResult, error) `json:"-"`
|
||||
}
|
||||
|
||||
// AdminActionInput is what the framework hands an AdminAction. Field is the
|
||||
// widget field name and is empty for a toolbar action. RecordID is nil on the
|
||||
// create form and always nil for a toolbar action: toolbar actions carry no
|
||||
// record ids, so an id list can never become an unscoped lookup. Record is the
|
||||
// record the framework loaded through the controller's FormExtendQuery scope,
|
||||
// nil when RecordID is nil. Values is the widget's snapshot of its fill
|
||||
// fields, already reduced to the field's declared fill keys and to scalars.
|
||||
type AdminActionInput struct {
|
||||
Field string
|
||||
RecordID *uint64
|
||||
Record any
|
||||
Values map[string]any
|
||||
}
|
||||
|
||||
// AdminActionResult is an action's answer. Message is a phrase key or text,
|
||||
// localized by the framework and shown as a toast. Fill is the widget
|
||||
// write-back; keys outside the field's declared fill keys and non-scalar
|
||||
// values are dropped before the response is written.
|
||||
type AdminActionResult struct {
|
||||
Message string
|
||||
Fill map[string]any
|
||||
}
|
||||
|
||||
// HasAdminActions is implemented by an admin controller that registers named
|
||||
// actions for its toolbar.buttons list and its `type: widget` form fields.
|
||||
type HasAdminActions interface {
|
||||
AdminActions() []AdminAction
|
||||
}
|
||||
|
||||
// AdminPartialData supplies the view model a controller partial template
|
||||
// renders (config_list.yaml headerPartial, fields.yaml `type: partial`). name
|
||||
// is the partial name; record is the scoped record for a form partial on an
|
||||
// existing record, else nil. The result must be a curated view model built
|
||||
// for the template, never the GORM model itself: the framework refuses a
|
||||
// value of the controller's model type.
|
||||
type AdminPartialData interface {
|
||||
PartialData(ctx context.Context, name string, record any) (any, error)
|
||||
}
|
||||
|
||||
// Permission is one registerPermissions() entry.
|
||||
type Permission struct {
|
||||
Code string
|
||||
|
||||
Reference in New Issue
Block a user