feat(cabana): per-request options for plugin field types
- pact.FormFieldType gains RequestOptions, called with the application's database once per form schema request for each type the form uses - the form schema route merges the answer into a copy of each field's typeOptions; a fields.yaml key wins, the compiled schema never changes - an error logs one warning naming the type and serves the fields.yaml options only - TestFieldTypeRequestOptions covers every screen, the precedence, the call count, the error path, unchanged schemas, form extension fields and the authenticated handler; a runnable example backs the docs - the typeOptions description, the regenerated OpenAPI document and schema.d.ts, the field types page and the pact and cabana READMEs document it
This commit is contained in:
@@ -32,7 +32,7 @@ Use cases:
|
||||
- My account contributions: `pact.HasAccountContributions` with `pact.AccountContribution` and `pact.AccountAction`, so a plugin adds a section with a status line and buttons to the administrator's own account page, run for the signed-in administrator only.
|
||||
- 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; a `Detached` action runs outside the framework transaction), `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), `pact.HasAdminActionExtensions` with `pact.AdminActionExtension` (a plugin adds record actions to any admin controller by controller ID, one it does not own included; they are offered on that controller's preview screen after the owner's) and `pact.AdminFormExtension` (a plugin places fields and tabs in another controller's form, before or after the owner's, and loads and saves their values itself inside the record's save transaction), and `pact.AdminPartialData` (the curated view model a partial template renders).
|
||||
- Plugin field types: `pact.HasFormFieldTypes` returns `pact.FormFieldType` entries, admin form field types any controller's `fields.yaml` uses by a short alias (`type: notes`). A type names its custom element, its scripts and styles, its own option keys and its Go funcs: `Decode` turns a submitted `pact.FormFieldValue` into a `pact.FormFieldDecoded` (the value to store plus optional `pact.FormFieldNotice` messages for the administrator, such as what a sanitizer removed) or refuses it with a `pact.FormFieldError`; `Load` serves the stored value to the element, `ListValue` formats a list column, and `Upload` stores a file from a `pact.FormFieldUpload` and answers a `pact.FormFieldUploadResult`. The admin framework owns the routes, permissions, record scope, locale storage of a translatable type and the localization of notices.
|
||||
- Plugin field types: `pact.HasFormFieldTypes` returns `pact.FormFieldType` entries, admin form field types any controller's `fields.yaml` uses by a short alias (`type: notes`). A type names its custom element, its scripts and styles, its own option keys and its Go funcs: `Decode` turns a submitted `pact.FormFieldValue` into a `pact.FormFieldDecoded` (the value to store plus optional `pact.FormFieldNotice` messages for the administrator, such as what a sanitizer removed) or refuses it with a `pact.FormFieldError`; `Load` serves the stored value to the element, `ListValue` formats a list column, `Upload` stores a file from a `pact.FormFieldUpload` and answers a `pact.FormFieldUploadResult`, and `RequestOptions` adds options that change per request (a site setting the element follows) to the served form schema. The admin framework owns the routes, permissions, record scope, locale storage of a translatable type and the localization of notices.
|
||||
- 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`), values of unstored list columns filled once per page (`pact.ListExtendRecords`), the record title of the form and preview screens (`pact.FormRecordTitle`), create, update and delete hooks (`pact.FormBeforeCreate`, `pact.FormAfterUpdate`, `pact.FormBeforeDelete` and their siblings), a hook after a reorder save (`pact.ReorderAfterSave`), form-only fields that reach those hooks without being model columns (`pact.FormVirtualFields`), validation rules per operation for admin saves (`pact.FormRules`), 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`).
|
||||
- Dependent dropdown options: `pact.DependentOptionsProvider` answers a `pact.DependentOptionSet` (whether the sibling's value takes a choice at all, and the choices) for a dropdown with `dependsOn`, from the submitted values of its siblings and with a context, so the choices can come from the database; the admin framework calls it for the options route and again on every save. `pact.Option.Depth` indents a choice taken from a tree.
|
||||
- Menu item types: `pact.HasMenuItemTypes` returns `pact.MenuItemType` values (key, label, default URL pattern, `PatternFrom` for a list type, the `References` an item can point at as a `pact.MenuItemReference` tree, and `Resolve`), the typed successor of WinterCMS's `pages.menuitem.*` events. `Resolve` receives a `pact.MenuItemResolve` with the pattern already layered and answers a `pact.ResolvedMenuItem` with its alternates as an ordered `[]pact.MenuItemAlternate`. [signpost](../signpost/README.md) collects them at boot. A record type may set `Content` to a `pact.ContentDescriptor`, which describes its records for consumers that work on every content type at once (sitemaps, search indexers, metadata editors): the admin controller that edits them, their morph type, `Describe` (a `pact.ContentRecord` with per-locale `pact.ContentLocale` URL, title, description, image and word count), and the optional `Records` (paged through `pact.ContentList` and `pact.ContentPage`) and `Match` (a public path to a `pact.ContentMatchResult`).
|
||||
@@ -182,7 +182,7 @@ func (p *Plugin) Schedule() []pact.ScheduledCommand {
|
||||
| `pact.ReorderAfterSave` | Optional controller hook `ReorderAfterSave(ctx, partition)`: runs after a reorder save wrote the new order, inside the same transaction (`cabana.TxFromContext` works); an error rolls the reorder back. |
|
||||
| `pact.AdminPartialData` | Supplies the view model a controller partial template renders; never the GORM model. |
|
||||
| `pact.HasFormFieldTypes` | Implemented by a plugin (not a controller) that registers admin form field types. |
|
||||
| `pact.FormFieldType` | One field type: the alias `Name`, the custom-element tag `Element`, `Translatable`, the `Scripts` and `Styles` under `assets/`, the `Options` keys, and the funcs `CheckOptions` (optional), `Decode` (required), `Load`, `ListValue` (`func(ctx context.Context, value any) string`) and `Upload` (optional). |
|
||||
| `pact.FormFieldType` | One field type: the alias `Name`, the custom-element tag `Element`, `Translatable`, the `Scripts` and `Styles` under `assets/`, the `Options` keys, and the funcs `CheckOptions` (optional), `Decode` (required), `Load`, `ListValue` (`func(ctx context.Context, value any) string`), `Upload` and `RequestOptions` (`func(ctx context.Context, db *gorm.DB) (map[string]any, error)`, merged under the fields.yaml options of each form schema request) (optional). |
|
||||
| `pact.FormFieldValue` | What `Decode` and `Load` receive: the transaction, the record, the field, its options, the content locale (translatable types), whether the value lives in the host column, the value (always a string for a translatable type) and an exact reader of another attribute in the same locale. |
|
||||
| `pact.FormFieldDecoded` | `Decode`'s answer: the `Value` to store (a string for a translatable type) and optional `Notices`. |
|
||||
| `pact.FormFieldNotice` | A message about a saved value: a phrase key or text with params; a positive `Count` selects the plural form. |
|
||||
|
||||
@@ -686,6 +686,15 @@ type AdminPartialData interface {
|
||||
// set, turns a stored value into the value the element receives. ListValue,
|
||||
// when set, returns the text a list column of this type shows. Upload, when
|
||||
// set, stores a file the element uploads and returns its URL.
|
||||
//
|
||||
// RequestOptions, when set, adds options that change per request, such as a
|
||||
// site setting the element follows. The admin calls it once per form schema
|
||||
// request for each type the form uses, with the application's database (nil
|
||||
// when none is configured), and merges the answer into every field's
|
||||
// options: an option the field sets in fields.yaml wins. An error is logged
|
||||
// and the schema is served with the fields.yaml options only. The options
|
||||
// reach every administrator who may open the form, so they must hold no
|
||||
// secret.
|
||||
type FormFieldType struct {
|
||||
Name string
|
||||
Element string
|
||||
@@ -698,6 +707,9 @@ type FormFieldType struct {
|
||||
Load func(ctx context.Context, in FormFieldValue) (any, error) `json:"-"`
|
||||
ListValue func(ctx context.Context, value any) string `json:"-"`
|
||||
Upload func(ctx context.Context, in FormFieldUpload) (FormFieldUploadResult, error) `json:"-"`
|
||||
// RequestOptions answers options for one form schema request; nil when
|
||||
// the type has none.
|
||||
RequestOptions func(ctx context.Context, db *gorm.DB) (map[string]any, error) `json:"-"`
|
||||
}
|
||||
|
||||
// FormFieldValue is what the framework hands Decode and Load. DB is the save
|
||||
|
||||
Reference in New Issue
Block a user