feat(12.1-01): list row states from one controller call per page
- pact.ListRowStates with the fixed RowState set deleted, negative, disabled - list response meta.row_states keyed by row id; unknown values dropped - list messages rowStateDeleted, rowStateNegative, rowStateDisabled - update writes through the scope the load used, so a soft-deleted record a controller includes stays soft-deleted - DataTable row state badges and text styles - roster fixture, smoke tests, OpenAPI, TS types, dist, READMEs, docs
This commit is contained in:
@@ -15,7 +15,7 @@ Capability interfaces that compiled plugins implement to contribute routes, conf
|
||||
- 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), `pact.HasAdminBulkActions` with `pact.AdminBulkAction`, `pact.AdminBulkActionInput` and `pact.AdminBulkActionResult` (named actions on the rows selected in a list, which receive records the framework loaded through the list scope, never ids), `pact.HasAdminRecordActions` with `pact.AdminRecordAction`, `pact.AdminRecordActionInput` and `pact.AdminRecordActionResult` (named actions on one record, each with an `Applies` rule for the record's state), 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`), relation child hooks around creating, updating and deleting a related record (`pact.RelationBeforeCreate`, `pact.RelationAfterCreate`, `pact.RelationBeforeUpdate`, `pact.RelationAfterUpdate`, `pact.RelationBeforeDelete`, `pact.RelationAfterDelete`), filter scopes (`pact.FilterScope`, `pact.FilterOptions`) and dropdown options (`pact.DropdownOptionsProvider`).
|
||||
- Optional admin hooks a controller or model can implement: list and form query scoping (`pact.ListExtendQuery`, `pact.FormExtendQuery`), list row states (`pact.ListRowStates` with the fixed `pact.RowState` set `pact.RowStateDeleted`, `pact.RowStateNegative` and `pact.RowStateDisabled`), create, update and delete hooks (`pact.FormBeforeCreate`, `pact.FormAfterUpdate`, `pact.FormBeforeDelete` and their siblings), relation hooks (`pact.RelationExtendManageQuery`, `pact.RelationExtendOptionsQuery`, `pact.RelationBeforeLink`), relation child hooks around creating, updating and deleting a related record (`pact.RelationBeforeCreate`, `pact.RelationAfterCreate`, `pact.RelationBeforeUpdate`, `pact.RelationAfterUpdate`, `pact.RelationBeforeDelete`, `pact.RelationAfterDelete`), 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.
|
||||
- A schedule contract: `pact.HasSchedule` returns `pact.ScheduledCommand` entries (a registered command name, its arguments and a `pact.Cadence` built with `pact.Daily`, `pact.DailyAt` or `pact.Every`), the Go form of WinterCMS `registerSchedule`. It does not depend on any queue library either.
|
||||
- `pact.OptionalMessage`, a service an optional plugin can publish so others integrate with it without importing its package.
|
||||
@@ -118,6 +118,8 @@ func (p *Plugin) Schedule() []pact.ScheduledCommand {
|
||||
| `pact.AdminRecordActionResult` | What a record action returns: an optional message for the toast. |
|
||||
| `pact.HasAdminRecordActions` | Registers a controller's record actions for the `recordActions` list of `config_form.yaml`. |
|
||||
| `pact.AdminPartialData` | Supplies the view model a controller partial template renders; never the GORM model. |
|
||||
| `pact.ListRowStates` | Optional controller hook called once per list page; returns the states of the page's records, index-aligned. |
|
||||
| `pact.RowState` | One state of a list row: `pact.RowStateDeleted`, `pact.RowStateNegative` or `pact.RowStateDisabled`. |
|
||||
| `pact.FilterScope` | Model scopes a list filter may call, limited to an exact allow list. |
|
||||
| `pact.RelationBeforeLink` | Optional controller hook that checks or fills pivot columns before a relation link is written. |
|
||||
| `pact.RelationBeforeCreate` | Optional controller hook run in the write transaction before a relation manager creates a related record. |
|
||||
|
||||
@@ -420,6 +420,30 @@ type ListExtendQuery interface {
|
||||
ListExtendQuery(ctx context.Context, db *gorm.DB) *gorm.DB
|
||||
}
|
||||
|
||||
// RowState is one state an admin list row may carry. The set is fixed; the
|
||||
// admin shows each state as a text badge and a row text style.
|
||||
type RowState string
|
||||
|
||||
// The row states the admin understands.
|
||||
const (
|
||||
// RowStateDeleted marks a soft-deleted record.
|
||||
RowStateDeleted RowState = "deleted"
|
||||
// RowStateNegative marks a record in a blocked or otherwise negative state.
|
||||
RowStateNegative RowState = "negative"
|
||||
// RowStateDisabled marks an inactive record.
|
||||
RowStateDisabled RowState = "disabled"
|
||||
)
|
||||
|
||||
// ListRowStates optionally marks the rows of an admin list with states. The
|
||||
// framework calls it once per list page with the page's records, in page
|
||||
// order, and the list's database handle: a list does not run in a
|
||||
// transaction, so cabana.TxFromContext reports none here. The result is
|
||||
// index-aligned with records; a row may carry several states and a nil entry
|
||||
// means none. Values outside the three RowState constants are dropped.
|
||||
type ListRowStates interface {
|
||||
ListRowStates(ctx context.Context, db *gorm.DB, records []any) ([][]RowState, error)
|
||||
}
|
||||
|
||||
// ListRelationColumnMapper maps a source-schema relation column onto the
|
||||
// physical column exposed by the related Go model. It supports legacy admin
|
||||
// schemas whose public field names no longer match the database schema.
|
||||
|
||||
@@ -247,3 +247,15 @@ func TestRelationHookInterfaces(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRowStateValues pins the wire values of the fixed row-state set: the
|
||||
// admin SPA and plugin CSS match on these exact strings.
|
||||
func TestRowStateValues(t *testing.T) {
|
||||
got := []RowState{RowStateDeleted, RowStateNegative, RowStateDisabled}
|
||||
want := []string{"deleted", "negative", "disabled"}
|
||||
for i, state := range got {
|
||||
if string(state) != want[i] {
|
||||
t.Fatalf("row state %d = %q, want %q", i, state, want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user