feat(15-02): add Categories, Tags, and journal settings admin
Register the remaining YAML admin screens and the settings singleton so a host can boot Posts, Categories, Tags, and Code journal. Posts update and delete refuse other authors without access_other_posts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
18
admin.go
18
admin.go
@@ -5,6 +5,7 @@ import (
|
||||
"io/fs"
|
||||
|
||||
"git.golem15.com/golem15/sm-journal-plugin/controllers"
|
||||
"git.golem15.com/golem15/sm-journal-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
@@ -13,6 +14,9 @@ import (
|
||||
// directory also holds Go sources that are not admin assets.
|
||||
//
|
||||
//go:embed controllers/posts/config_list.yaml controllers/posts/config_form.yaml controllers/posts/config_filter.yaml models/post/fields.yaml models/post/columns.yaml
|
||||
//go:embed controllers/categories/config_list.yaml controllers/categories/config_form.yaml models/category/fields.yaml models/category/columns.yaml
|
||||
//go:embed controllers/tags/config_list.yaml controllers/tags/config_form.yaml models/tag/fields.yaml models/tag/columns.yaml
|
||||
//go:embed models/settings/fields.yaml
|
||||
//go:embed assets/images/journal-icon.svg
|
||||
var adminFS embed.FS
|
||||
|
||||
@@ -29,9 +33,23 @@ func (p *Plugin) AdminControllers() []pact.AdminController {
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Plugin) Settings() []pact.SettingsItem {
|
||||
return []pact.SettingsItem{{
|
||||
Code: "journal",
|
||||
Label: "golem15.journal::lang.journal.menu_label",
|
||||
Description: "golem15.journal::lang.journal.settings_description",
|
||||
Category: "golem15.journal::lang.journal.menu_label",
|
||||
Icon: "icon-pencil",
|
||||
Permissions: []string{"golem15.journal.manage_settings"},
|
||||
Form: "models/settings/fields.yaml",
|
||||
NewModel: func() any { return &models.Settings{} },
|
||||
}}
|
||||
}
|
||||
|
||||
var (
|
||||
_ pact.AdminAssets = (*Plugin)(nil)
|
||||
_ pact.HasAdminControllers = (*Plugin)(nil)
|
||||
_ pact.HasPermissions = (*Plugin)(nil)
|
||||
_ pact.HasNavigation = (*Plugin)(nil)
|
||||
_ pact.HasSettings = (*Plugin)(nil)
|
||||
)
|
||||
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// Navigation is the plugin's Winter registerNavigation() entry: the main
|
||||
// item journal (order 300) with its side menu. Categories and tags side
|
||||
// items are added when those controllers register (Task 2): cabana boot
|
||||
// refuses a NavigationItem.Controller that is not an AdminController.
|
||||
// item journal (order 300) with its side menu.
|
||||
func (*Plugin) Navigation() []pact.NavigationItem {
|
||||
return []pact.NavigationItem{
|
||||
{
|
||||
@@ -33,6 +31,20 @@ func (*Plugin) Navigation() []pact.NavigationItem {
|
||||
Permissions: []string{controllers.PermissionAccessPosts},
|
||||
Controller: controllers.PostsControllerID,
|
||||
},
|
||||
{
|
||||
Code: "categories",
|
||||
Label: "golem15.journal::lang.journal.categories",
|
||||
Icon: "list",
|
||||
Permissions: []string{controllers.PermissionAccessCategories},
|
||||
Controller: controllers.CategoriesControllerID,
|
||||
},
|
||||
{
|
||||
Code: "tags",
|
||||
Label: "golem15.journal::lang.journal.tags",
|
||||
Icon: "tag",
|
||||
Permissions: []string{controllers.PermissionAccessTags},
|
||||
Controller: controllers.TagsControllerID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
func AdminControllers(app func() *backpack.App) []pact.AdminController {
|
||||
return []pact.AdminController{
|
||||
postsAdminController{app: app},
|
||||
categoriesAdminController{app: app},
|
||||
tagsAdminController{app: app},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +27,15 @@ func init() {
|
||||
var _ pact.FilterOptions = postsAdminController{}
|
||||
var _ pact.FormBeforeCreate = postsAdminController{}
|
||||
var _ pact.FormBeforeUpdate = postsAdminController{}
|
||||
var _ pact.FormBeforeDelete = postsAdminController{}
|
||||
var _ cabana.FieldRelationProvider = postsAdminController{}
|
||||
|
||||
var _ pact.AdminController = categoriesAdminController{}
|
||||
var _ pact.AdminPermissioned = categoriesAdminController{}
|
||||
var _ pact.AdminRecordSource = categoriesAdminController{}
|
||||
var _ cabana.FieldRelationProvider = categoriesAdminController{}
|
||||
|
||||
var _ pact.AdminController = tagsAdminController{}
|
||||
var _ pact.AdminPermissioned = tagsAdminController{}
|
||||
var _ pact.AdminRecordSource = tagsAdminController{}
|
||||
}
|
||||
|
||||
48
controllers/categories.go
Normal file
48
controllers/categories.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/sm-journal-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
const (
|
||||
// CategoriesControllerID is the cabana controller id of the Categories screen.
|
||||
CategoriesControllerID = "golem15.journal.categories"
|
||||
// PermissionAccessCategories is Categories::$requiredPermissions.
|
||||
PermissionAccessCategories = "golem15.journal.access_categories"
|
||||
)
|
||||
|
||||
type categoriesAdminController struct {
|
||||
app func() *backpack.App
|
||||
}
|
||||
|
||||
func (categoriesAdminController) ID() string { return CategoriesControllerID }
|
||||
|
||||
func (categoriesAdminController) ModelName() string { return `Golem15\Journal\Models\Category` }
|
||||
|
||||
func (categoriesAdminController) ConfigDir() string { return "controllers/categories" }
|
||||
|
||||
func (categoriesAdminController) RequiredPermissions() []string {
|
||||
return []string{PermissionAccessCategories}
|
||||
}
|
||||
|
||||
func (categoriesAdminController) NewRecord() any { return &models.Category{} }
|
||||
|
||||
func (categoriesAdminController) AdminFieldRelations() []cabana.FieldRelationContract {
|
||||
return []cabana.FieldRelationContract{{
|
||||
Field: "parent",
|
||||
Kind: "belongsTo",
|
||||
NewRelated: func() any { return &models.Category{} },
|
||||
ForeignKey: "parent_id",
|
||||
LabelColumn: "name",
|
||||
}}
|
||||
}
|
||||
|
||||
var (
|
||||
_ pact.AdminController = categoriesAdminController{}
|
||||
_ pact.AdminPermissioned = categoriesAdminController{}
|
||||
_ pact.AdminRecordSource = categoriesAdminController{}
|
||||
_ cabana.FieldRelationProvider = categoriesAdminController{}
|
||||
)
|
||||
19
controllers/categories/config_form.yaml
Normal file
19
controllers/categories/config_form.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
name: golem15.journal::lang.journal.create_category
|
||||
form: ~/plugins/golem15/journal/models/category/fields.yaml
|
||||
modelClass: Golem15\Journal\Models\Category
|
||||
defaultRedirect: golem15/journal/categories
|
||||
|
||||
create:
|
||||
redirect: golem15/journal/categories/update/:id
|
||||
redirectClose: golem15/journal/categories
|
||||
|
||||
update:
|
||||
redirect: golem15/journal/categories
|
||||
redirectClose: golem15/journal/categories
|
||||
|
||||
messages:
|
||||
create: golem15.journal::lang.journal.create_category
|
||||
update: backend::lang.messages.form.update
|
||||
saved: backend::lang.messages.form.saved
|
||||
deleteConfirm: golem15.journal::lang.category.delete_confirm
|
||||
deleted: backend::lang.messages.form.deleted
|
||||
19
controllers/categories/config_list.yaml
Normal file
19
controllers/categories/config_list.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
list: ~/plugins/golem15/journal/models/category/columns.yaml
|
||||
modelClass: Golem15\Journal\Models\Category
|
||||
title: golem15.journal::lang.category.label_plural
|
||||
recordUrl: golem15/journal/categories/update/:id
|
||||
noRecordsMessage: backend::lang.list.no_records
|
||||
recordsPerPage: 20
|
||||
showCheckboxes: true
|
||||
showSetup: true
|
||||
|
||||
toolbar:
|
||||
buttons: [create, delete]
|
||||
search:
|
||||
prompt: backend::lang.list.search_prompt
|
||||
|
||||
messages:
|
||||
create: golem15.journal::lang.journal.create_category
|
||||
empty: backend::lang.list.no_records
|
||||
deleteConfirm: golem15.journal::lang.category.delete_confirm
|
||||
deleted: backend::lang.messages.list.deleted
|
||||
@@ -174,9 +174,28 @@ func (c postsAdminController) FormBeforeUpdate(ctx context.Context, model any) e
|
||||
if !ok || post == nil {
|
||||
return errors.New("posts admin: form model is not a post")
|
||||
}
|
||||
if err := refuseOtherAuthor(ctx, post); err != nil {
|
||||
return err
|
||||
}
|
||||
return preparePostWrite(ctx, post)
|
||||
}
|
||||
|
||||
func (c postsAdminController) FormBeforeDelete(ctx context.Context, model any) error {
|
||||
post, ok := model.(*models.Post)
|
||||
if !ok || post == nil {
|
||||
return errors.New("posts admin: form model is not a post")
|
||||
}
|
||||
return refuseOtherAuthor(ctx, post)
|
||||
}
|
||||
|
||||
func refuseOtherAuthor(ctx context.Context, post *models.Post) error {
|
||||
principal, _ := bouncer.User(ctx)
|
||||
if post.CanEdit(principal) {
|
||||
return nil
|
||||
}
|
||||
return &cabana.ForbiddenError{Message: "golem15.journal::lang.journal.access_other_posts"}
|
||||
}
|
||||
|
||||
func preparePostWrite(ctx context.Context, post *models.Post) error {
|
||||
if err := refuseUnpublishedWrite(ctx, post); err != nil {
|
||||
return err
|
||||
|
||||
36
controllers/tags.go
Normal file
36
controllers/tags.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/sm-journal-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
const (
|
||||
// TagsControllerID is the cabana controller id of the Tags screen.
|
||||
TagsControllerID = "golem15.journal.tags"
|
||||
// PermissionAccessTags is Tags::$requiredPermissions.
|
||||
PermissionAccessTags = "golem15.journal.access_tags"
|
||||
)
|
||||
|
||||
type tagsAdminController struct {
|
||||
app func() *backpack.App
|
||||
}
|
||||
|
||||
func (tagsAdminController) ID() string { return TagsControllerID }
|
||||
|
||||
func (tagsAdminController) ModelName() string { return `Golem15\Journal\Models\Tag` }
|
||||
|
||||
func (tagsAdminController) ConfigDir() string { return "controllers/tags" }
|
||||
|
||||
func (tagsAdminController) RequiredPermissions() []string {
|
||||
return []string{PermissionAccessTags}
|
||||
}
|
||||
|
||||
func (tagsAdminController) NewRecord() any { return &models.Tag{} }
|
||||
|
||||
var (
|
||||
_ pact.AdminController = tagsAdminController{}
|
||||
_ pact.AdminPermissioned = tagsAdminController{}
|
||||
_ pact.AdminRecordSource = tagsAdminController{}
|
||||
)
|
||||
19
controllers/tags/config_form.yaml
Normal file
19
controllers/tags/config_form.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
name: golem15.journal::lang.journal.create_tag
|
||||
form: ~/plugins/golem15/journal/models/tag/fields.yaml
|
||||
modelClass: Golem15\Journal\Models\Tag
|
||||
defaultRedirect: golem15/journal/tags
|
||||
|
||||
create:
|
||||
redirect: golem15/journal/tags/update/:id
|
||||
redirectClose: golem15/journal/tags
|
||||
|
||||
update:
|
||||
redirect: golem15/journal/tags
|
||||
redirectClose: golem15/journal/tags
|
||||
|
||||
messages:
|
||||
create: golem15.journal::lang.journal.create_tag
|
||||
update: backend::lang.messages.form.update
|
||||
saved: backend::lang.messages.form.saved
|
||||
deleteConfirm: golem15.journal::lang.tag.delete_confirm
|
||||
deleted: backend::lang.messages.form.deleted
|
||||
19
controllers/tags/config_list.yaml
Normal file
19
controllers/tags/config_list.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
list: ~/plugins/golem15/journal/models/tag/columns.yaml
|
||||
modelClass: Golem15\Journal\Models\Tag
|
||||
title: golem15.journal::lang.tag.label_plural
|
||||
recordUrl: golem15/journal/tags/update/:id
|
||||
noRecordsMessage: backend::lang.list.no_records
|
||||
recordsPerPage: 20
|
||||
showCheckboxes: true
|
||||
showSetup: true
|
||||
|
||||
toolbar:
|
||||
buttons: [create, delete]
|
||||
search:
|
||||
prompt: backend::lang.list.search_prompt
|
||||
|
||||
messages:
|
||||
create: golem15.journal::lang.journal.create_tag
|
||||
empty: backend::lang.list.no_records
|
||||
deleteConfirm: golem15.journal::lang.tag.delete_confirm
|
||||
deleted: backend::lang.messages.list.deleted
|
||||
@@ -31,6 +31,13 @@ func (Category) Fillable() []string {
|
||||
return []string{"name", "slug", "code", "description", "parent_id"}
|
||||
}
|
||||
|
||||
func (Category) Rules() map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required",
|
||||
"slug": "required",
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(Category{})
|
||||
}
|
||||
|
||||
7
models/category/columns.yaml
Normal file
7
models/category/columns.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
columns:
|
||||
name:
|
||||
label: golem15.journal::lang.category.name
|
||||
searchable: true
|
||||
slug:
|
||||
label: golem15.journal::lang.category.slug
|
||||
searchable: true
|
||||
21
models/category/fields.yaml
Normal file
21
models/category/fields.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
fields:
|
||||
name:
|
||||
label: golem15.journal::lang.category.name
|
||||
type: mltext
|
||||
span: left
|
||||
required: true
|
||||
slug:
|
||||
label: golem15.journal::lang.category.slug
|
||||
type: mltext
|
||||
span: right
|
||||
required: true
|
||||
parent:
|
||||
label: Parent
|
||||
type: relation
|
||||
nameFrom: name
|
||||
span: full
|
||||
description:
|
||||
label: golem15.journal::lang.category.description
|
||||
type: mltext
|
||||
size: large
|
||||
span: full
|
||||
96
models/settings/fields.yaml
Normal file
96
models/settings/fields.yaml
Normal file
@@ -0,0 +1,96 @@
|
||||
fields:
|
||||
show_all_posts:
|
||||
span: left
|
||||
label: golem15.journal::lang.journal.show_all_posts_label
|
||||
comment: golem15.journal::lang.journal.show_all_posts_comment
|
||||
type: switch
|
||||
default: true
|
||||
tab: golem15.journal::lang.journal.tab_general
|
||||
use_rich_editor:
|
||||
span: left
|
||||
label: golem15.journal::lang.journal.use_rich_editor_label
|
||||
comment: golem15.journal::lang.journal.use_rich_editor_comment
|
||||
type: switch
|
||||
default: false
|
||||
tab: golem15.journal::lang.journal.tab_general
|
||||
search_use_typesense:
|
||||
span: left
|
||||
label: golem15.journal::lang.journal.search_use_typesense_label
|
||||
comment: golem15.journal::lang.journal.search_use_typesense_comment
|
||||
type: switch
|
||||
default: false
|
||||
tab: golem15.journal::lang.journal.tab_search
|
||||
search_title_weight:
|
||||
span: left
|
||||
label: golem15.journal::lang.journal.search_title_weight_label
|
||||
comment: golem15.journal::lang.journal.search_title_weight_comment
|
||||
type: number
|
||||
default: 5
|
||||
tab: golem15.journal::lang.journal.tab_search
|
||||
search_excerpt_weight:
|
||||
span: left
|
||||
label: golem15.journal::lang.journal.search_excerpt_weight_label
|
||||
comment: golem15.journal::lang.journal.search_excerpt_weight_comment
|
||||
type: number
|
||||
default: 3
|
||||
tab: golem15.journal::lang.journal.tab_search
|
||||
search_content_weight:
|
||||
span: left
|
||||
label: golem15.journal::lang.journal.search_content_weight_label
|
||||
comment: golem15.journal::lang.journal.search_content_weight_comment
|
||||
type: number
|
||||
default: 1
|
||||
tab: golem15.journal::lang.journal.tab_search
|
||||
rss_enabled:
|
||||
span: left
|
||||
label: golem15.journal::lang.journal.rss_enabled_label
|
||||
comment: golem15.journal::lang.journal.rss_enabled_comment
|
||||
type: switch
|
||||
default: true
|
||||
tab: golem15.journal::lang.journal.tab_rss
|
||||
rss_include_content:
|
||||
span: right
|
||||
label: golem15.journal::lang.journal.rss_include_content_label
|
||||
comment: golem15.journal::lang.journal.rss_include_content_comment
|
||||
type: switch
|
||||
default: false
|
||||
tab: golem15.journal::lang.journal.tab_rss
|
||||
rss_title:
|
||||
span: left
|
||||
label: golem15.journal::lang.journal.rss_title_label
|
||||
comment: golem15.journal::lang.journal.rss_title_comment
|
||||
type: text
|
||||
tab: golem15.journal::lang.journal.tab_rss
|
||||
rss_description:
|
||||
span: right
|
||||
label: golem15.journal::lang.journal.rss_description_label
|
||||
comment: golem15.journal::lang.journal.rss_description_comment
|
||||
type: textarea
|
||||
size: small
|
||||
tab: golem15.journal::lang.journal.tab_rss
|
||||
rss_language:
|
||||
span: left
|
||||
label: golem15.journal::lang.journal.rss_language_label
|
||||
comment: golem15.journal::lang.journal.rss_language_comment
|
||||
type: text
|
||||
default: en-us
|
||||
tab: golem15.journal::lang.journal.tab_rss
|
||||
rss_copyright:
|
||||
span: right
|
||||
label: golem15.journal::lang.journal.rss_copyright_label
|
||||
comment: golem15.journal::lang.journal.rss_copyright_comment
|
||||
type: text
|
||||
tab: golem15.journal::lang.journal.tab_rss
|
||||
rss_image_url:
|
||||
span: left
|
||||
label: golem15.journal::lang.journal.rss_image_url_label
|
||||
comment: golem15.journal::lang.journal.rss_image_url_comment
|
||||
type: text
|
||||
tab: golem15.journal::lang.journal.tab_rss
|
||||
rss_posts_per_feed:
|
||||
span: right
|
||||
label: golem15.journal::lang.journal.rss_posts_per_feed_label
|
||||
comment: golem15.journal::lang.journal.rss_posts_per_feed_comment
|
||||
type: number
|
||||
default: 20
|
||||
tab: golem15.journal::lang.journal.tab_rss
|
||||
@@ -21,6 +21,13 @@ func (Tag) Fillable() []string {
|
||||
return []string{"name", "slug", "description"}
|
||||
}
|
||||
|
||||
func (Tag) Rules() map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required",
|
||||
"slug": "required|between:3,64",
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(Tag{})
|
||||
}
|
||||
|
||||
7
models/tag/columns.yaml
Normal file
7
models/tag/columns.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
columns:
|
||||
name:
|
||||
label: golem15.journal::lang.tag.name
|
||||
searchable: true
|
||||
slug:
|
||||
label: golem15.journal::lang.tag.slug
|
||||
searchable: true
|
||||
19
models/tag/fields.yaml
Normal file
19
models/tag/fields.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
fields:
|
||||
name:
|
||||
label: golem15.journal::lang.tag.name
|
||||
type: text
|
||||
span: left
|
||||
required: true
|
||||
slug:
|
||||
label: golem15.journal::lang.tag.slug
|
||||
type: text
|
||||
span: right
|
||||
required: true
|
||||
preset:
|
||||
field: name
|
||||
type: slug
|
||||
description:
|
||||
label: golem15.journal::lang.tag.description
|
||||
type: textarea
|
||||
size: large
|
||||
span: full
|
||||
Reference in New Issue
Block a user