feat(build): scaffold working admin controllers with model, AdminFS and delete (quick-261028-pq8)
- make:admin-controller edits the singular of its name (--model overrides):
ModelName, modelClass and the YAML under models/<model>/
- NewRecord returns &models.<Model>{} when the model exists; otherwise the
nil body stays and the command says to run make:model
- config_list.yaml offers create and delete with showCheckboxes
- registry.gen.go embeds controllers/*/*.yaml and models/*/*.yaml once they
exist and exposes generatedAdminFS(); scaffolded plugins implement
pact.AdminAssets through it, and a plugin without AdminFS gets a note
- blog example moves its YAML to models/post; docs and help text follow
This commit is contained in:
@@ -29,7 +29,7 @@ summer build
|
||||
| `summer make:plugin acme.blog` | `plugins/acme/blog` as its own Go module: `plugin.go`, `routes.go`, `registry.gen.go`, `go.mod`, `config/config.yaml`, `lang/en/lang.yaml`, `views/mail/welcome.htm` and a `doc.go` in `classes`, `console`, `controllers`, `jobs`, `middleware`, `models` and `updates` |
|
||||
| `summer make:model acme.blog Post` | `models/post.go` and `updates/<timestamp>_create_acme_blog_posts.go` |
|
||||
| `summer make:migration acme.blog AddPublishedAt` | `updates/<timestamp>_add_published_at.go` |
|
||||
| `summer make:admin-controller acme.blog Posts` | `controllers/posts.go`, `controllers/posts/config_form.yaml`, `controllers/posts/config_list.yaml`, `models/posts/fields.yaml` and `models/posts/columns.yaml` |
|
||||
| `summer make:admin-controller acme.blog Posts` | `controllers/posts.go`, `controllers/posts/config_form.yaml`, `controllers/posts/config_list.yaml`, `models/post/fields.yaml` and `models/post/columns.yaml` |
|
||||
| `summer make:command acme.blog Publish` | `console/publish.go` |
|
||||
| `summer plugin:add plugins/acme/blog` | The plugin in `summer.yaml`, a `require` and a local `replace` in the application's `go.mod`, and the directory in `go.work` |
|
||||
| `summer build` | `bin/acme`, the application binary with the plugin compiled in |
|
||||
@@ -149,9 +149,6 @@ var langFS embed.FS
|
||||
//go:embed views/mail
|
||||
var mailFS embed.FS
|
||||
|
||||
//go:embed controllers/*/*.yaml models/*/*.yaml
|
||||
var adminFS embed.FS
|
||||
|
||||
// Plugin is the acme.blog plugin, the Go form of Plugin.php.
|
||||
type Plugin struct {
|
||||
app *backpack.App
|
||||
@@ -190,9 +187,9 @@ func (p *Plugin) Commands() []bonfire.Command {
|
||||
return append(generatedCommands(p.app), console.PublishCommand(p.withDB))
|
||||
}
|
||||
|
||||
// AdminFS is the admin YAML the controllers read: controllers/posts and
|
||||
// models/posts.
|
||||
func (p *Plugin) AdminFS() fs.FS { return adminFS }
|
||||
// AdminFS is the admin YAML the controllers read, controllers/posts and
|
||||
// models/post, which registry.gen.go embeds.
|
||||
func (p *Plugin) AdminFS() fs.FS { return generatedAdminFS() }
|
||||
|
||||
// Permissions replaces registerPermissions().
|
||||
func (p *Plugin) Permissions() []pact.Permission {
|
||||
@@ -234,7 +231,7 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
Each `var _ pact.HasX = (*Plugin)(nil)` line is a compile-time check that the plugin really implements a capability; the application finds the capabilities by type assertion. `Boot` keeps the application, because the route handler and the command reach the database through it. `Models`, `Migrations`, `Jobs` and `AdminControllers` return `generatedModels()` and the other accessors from `registry.gen.go`, which the `make:` commands rewrite each time they add a model, a migration, a command, a job or an admin controller. `AdminFS`, `Permissions` and `Navigation` were added by hand for the backend, and `Commands` for the console command.
|
||||
Each `var _ pact.HasX = (*Plugin)(nil)` line is a compile-time check that the plugin really implements a capability; the application finds the capabilities by type assertion. `Boot` keeps the application, because the route handler and the command reach the database through it. `Models`, `Migrations`, `Jobs` and `AdminControllers` return `generatedModels()` and the other accessors from `registry.gen.go`, which the `make:` commands rewrite each time they add a model, a migration, a command, a job or an admin controller. `AdminFS` returns `generatedAdminFS()`, the admin YAML `registry.gen.go` embeds, and `Commands` returns `generatedCommands(p.app)`; both come with the scaffold. `Permissions` and `Navigation` were added by hand for the backend, and `Commands` appends the console command.
|
||||
|
||||
The plugin's defaults live in `config/config.yaml`, the equivalent of the plugin's `config/config.php`. They are merged under the plugin ID, so this value is read as `acme.blog.per_page`:
|
||||
|
||||
@@ -573,7 +570,7 @@ class Posts extends Controller
|
||||
}
|
||||
```
|
||||
|
||||
`summer make:admin-controller acme.blog Posts` writes `controllers/posts.go` and four YAML files. In SummerCMS the controller has no actions or views: the framework's generic admin API lists, shows, creates, updates and deletes records, and the admin SPA draws the screens from the YAML. The controller only says which model it serves, where its YAML is and which permission it needs:
|
||||
`summer make:admin-controller acme.blog Posts` writes `controllers/posts.go` and four YAML files for the model `Post`, the singular of the controller name (`--model` names another one). Because `make:model` already wrote `models.Post`, the generated `NewRecord` returns it. In SummerCMS the controller has no actions or views: the framework's generic admin API lists, shows, creates, updates and deletes records, and the admin SPA draws the screens from the YAML. The controller only says which model it serves, where its YAML is and which permission it needs:
|
||||
|
||||
```go src=docs/examples/blog/controllers/posts.go
|
||||
//summer:make admin-controller
|
||||
@@ -615,7 +612,7 @@ func PostsController() pact.AdminController { return postsAdmin{} }
|
||||
`RequiredPermissions` replaces `$requiredPermissions` and is checked before any schema or record is served. `NewRecord` gives the admin API the model to query, and its `Fillable` and `Rules` decide what a save may write. The YAML keeps its WinterCMS syntax; `modelClass` must equal `ModelName`:
|
||||
|
||||
```yaml src=docs/examples/blog/controllers/posts/config_list.yaml
|
||||
list: ~/plugins/acme/blog/models/posts/columns.yaml
|
||||
list: ~/plugins/acme/blog/models/post/columns.yaml
|
||||
modelClass: Post
|
||||
title: acme.blog::lang.posts.title
|
||||
recordUrl: acme/blog/posts/update/:id
|
||||
@@ -632,7 +629,7 @@ toolbar:
|
||||
|
||||
```yaml src=docs/examples/blog/controllers/posts/config_form.yaml
|
||||
name: acme.blog::lang.posts.post
|
||||
form: ~/plugins/acme/blog/models/posts/fields.yaml
|
||||
form: ~/plugins/acme/blog/models/post/fields.yaml
|
||||
modelClass: Post
|
||||
defaultRedirect: acme/blog/posts
|
||||
|
||||
@@ -645,9 +642,9 @@ update:
|
||||
redirectClose: acme/blog/posts
|
||||
```
|
||||
|
||||
The form and list fields sit under `models/posts/`, the controller's name, where `make:admin-controller` puts them (WinterCMS keeps them under `models/post/`, the model's name):
|
||||
The form and list fields sit under `models/post/`, the model's name, as in WinterCMS; `make:admin-controller` puts them there:
|
||||
|
||||
```yaml src=docs/examples/blog/models/posts/fields.yaml
|
||||
```yaml src=docs/examples/blog/models/post/fields.yaml
|
||||
fields:
|
||||
title:
|
||||
label: acme.blog::lang.posts.title_field
|
||||
@@ -665,7 +662,7 @@ fields:
|
||||
size: large
|
||||
```
|
||||
|
||||
```yaml src=docs/examples/blog/models/posts/columns.yaml
|
||||
```yaml src=docs/examples/blog/models/post/columns.yaml
|
||||
columns:
|
||||
title:
|
||||
label: acme.blog::lang.posts.title_field
|
||||
@@ -683,9 +680,9 @@ The WinterCMS form had a date picker for `published_at`. SummerCMS forms have no
|
||||
The plugin embeds the YAML through `pact.AdminAssets` and declares the permission and the menu entry, as `registerPermissions` and `registerNavigation` did:
|
||||
|
||||
```go src=docs/examples/blog/plugin.go#Plugin.AdminFS
|
||||
// AdminFS is the admin YAML the controllers read: controllers/posts and
|
||||
// models/posts.
|
||||
func (p *Plugin) AdminFS() fs.FS { return adminFS }
|
||||
// AdminFS is the admin YAML the controllers read, controllers/posts and
|
||||
// models/post, which registry.gen.go embeds.
|
||||
func (p *Plugin) AdminFS() fs.FS { return generatedAdminFS() }
|
||||
```
|
||||
|
||||
```go src=docs/examples/blog/plugin.go#Plugin.Permissions
|
||||
@@ -822,8 +819,9 @@ See [Writing commands](../console/writing-commands.md) for arguments, flags, pro
|
||||
The `make:` commands write files that compile, not a finished plugin. These are the steps this walkthrough had to do by hand, and the scaffolder behaviour behind them:
|
||||
|
||||
- **The `//summer:make` line stays.** Files the `make:` commands write for you to edit start with `//summer:make <kind>`, such as `//summer:make model`. Keep the line: the commands rebuild `registry.gen.go` from the files that carry it (or the older `// Code generated by summer make. DO NOT EDIT.` header), so a model, migration, command, job or admin controller whose line you delete disappears from the accessors the next time you run a `make:` command. Only `registry.gen.go` itself is marked as generated, so linters check the files you edit.
|
||||
- **The admin controller names the model after itself.** `make:admin-controller acme.blog Posts` returns `Posts` from `ModelName` and writes `modelClass: Posts`, and it puts `fields.yaml` and `columns.yaml` under `models/posts/`. Change `ModelName` and both `modelClass` values to the model, `Post`; the YAML can stay where it is, as in this example.
|
||||
- **The admin controller needs more than the scaffolder writes.** The generated controller implements only `pact.AdminController`. Add `NewRecord` (`pact.AdminRecordSource`) so the admin API has a model to query, and `RequiredPermissions` (`pact.AdminPermissioned`): without it, any signed-in administrator can open the controller. The model needs `Fillable` and `Rules` before the admin API can save it. The plugin needs `AdminFS` (`pact.AdminAssets`) to embed the YAML; without it the application refuses to start once the admin is enabled.
|
||||
- **Declare the admin permission.** The generated controller requires `acme.blog.access_posts`; declare it in the plugin's `Permissions()` (`pact.HasPermissions`), or the admin API refuses to start with an unknown-permission error.
|
||||
- **Give the model `Fillable` and `Rules`.** The admin API saves only the fields `Fillable` lists and validates them with `Rules`; until the model has them, a save writes nothing.
|
||||
- **Fill in `fields.yaml` and `columns.yaml`.** The scaffold lists only `id`; add the model's fields and columns. Run `make:model` before `make:admin-controller`, or `NewRecord` returns `nil` until you return the model yourself.
|
||||
- **A command with a narrower dependency is added by hand.** The function `make:command` writes receives the `*backpack.App` the plugin kept from `Boot`, and the generated accessor lists such functions. A command that takes something else, such as the `withDB` of `blog:publish`, is left out of the accessor; return it from `Commands` yourself.
|
||||
|
||||
## Checklist
|
||||
|
||||
Reference in New Issue
Block a user