feat(17-04): menu plugin admin with registry types, URL checks and tree reorder
- golem15.menu plugin, tables golem15_menu_menus and golem15_menu_menu_entries with the PHP columns plus a nullable url_pattern (D-02) - MenuEntry BeforeSave ports PHP beforeSave: nest columns 0, menu from the parent, sort_order after the menu's last - entry type dropdown: url, divider and the signpost registry types; reference depends on type through signpost.ReferenceOptions - URL scheme allowlist (errors.url_scheme), target _self or _blank, menu and parent cycle checks - menu filter, toolbar reorder/create/delete and config_reorder.yaml partitioned by menu - navigation Menus with Menus and Menu entries, permission golem15.menu.manage_menu, en and pl phrasebooks - TestMenuAdminSmoke
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
go.work.sum
|
||||
45
admin.go
Normal file
45
admin.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
|
||||
"git.golem15.com/golem15/sm-menu-plugin/controllers"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// adminFS holds the admin YAML file by file: the controllers directory also
|
||||
// holds Go sources.
|
||||
//
|
||||
//go:embed controllers/menus/config_form.yaml controllers/menus/config_list.yaml models/menu/fields.yaml models/menu/columns.yaml
|
||||
//go:embed controllers/menuentries/config_form.yaml controllers/menuentries/config_list.yaml controllers/menuentries/config_filter.yaml controllers/menuentries/config_reorder.yaml
|
||||
//go:embed models/menuentry/fields.yaml models/menuentry/columns.yaml
|
||||
var adminFS embed.FS
|
||||
|
||||
//go:embed lang
|
||||
var langFS embed.FS
|
||||
|
||||
// AdminFS serves the admin YAML.
|
||||
func (p *Plugin) AdminFS() fs.FS { return adminFS }
|
||||
|
||||
// LangFS is the embedded English and Polish phrasebooks.
|
||||
func (p *Plugin) LangFS() fs.FS { return langFS }
|
||||
|
||||
// AdminControllers hands the controllers a lazy accessor of the booted app,
|
||||
// so they read the database and the menu item type registry at request
|
||||
// time.
|
||||
func (p *Plugin) AdminControllers() []pact.AdminController {
|
||||
return controllers.AdminControllers(func() *backpack.App {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p.app
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
_ pact.AdminAssets = (*Plugin)(nil)
|
||||
_ pact.HasAdminControllers = (*Plugin)(nil)
|
||||
_ pact.HasLang = (*Plugin)(nil)
|
||||
)
|
||||
38
admin_navigation.go
Normal file
38
admin_navigation.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/sm-menu-plugin/controllers"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// Navigation is a top-level Menus item with the side items Menus and Menu
|
||||
// entries. PHP hung both screens on another plugin's side menu
|
||||
// (SiteManager), which is not ported; the port registers its own.
|
||||
func (*Plugin) Navigation() []pact.NavigationItem {
|
||||
return []pact.NavigationItem{{
|
||||
Code: "menu",
|
||||
Label: "golem15.menu::lang.plugin.name",
|
||||
Icon: "list",
|
||||
Permissions: []string{"golem15.menu.*"},
|
||||
Order: 510,
|
||||
Controller: controllers.MenusControllerID,
|
||||
SideMenu: []pact.NavigationItem{
|
||||
{
|
||||
Code: "menus",
|
||||
Label: "golem15.menu::lang.models.menu.label_plural",
|
||||
Icon: "list",
|
||||
Permissions: []string{controllers.PermissionManageMenu},
|
||||
Controller: controllers.MenusControllerID,
|
||||
},
|
||||
{
|
||||
Code: "menuentries",
|
||||
Label: "golem15.menu::lang.models.menuentry.label_plural",
|
||||
Icon: "list-ordered",
|
||||
Permissions: []string{controllers.PermissionManageMenu},
|
||||
Controller: controllers.MenuEntriesControllerID,
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
var _ pact.HasNavigation = (*Plugin)(nil)
|
||||
19
admin_permissions.go
Normal file
19
admin_permissions.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/sm-menu-plugin/controllers"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// Permissions is the plugin's Winter registerPermissions() catalog: the PHP
|
||||
// code golem15.menu.manage_menu on the developer and publisher roles.
|
||||
func (*Plugin) Permissions() []pact.Permission {
|
||||
return []pact.Permission{{
|
||||
Code: controllers.PermissionManageMenu,
|
||||
Tab: "golem15.menu::lang.plugin.name",
|
||||
Label: "golem15.menu::lang.permissions.manage_menu",
|
||||
Roles: []string{"developer", "publisher"},
|
||||
}}
|
||||
}
|
||||
|
||||
var _ pact.HasPermissions = (*Plugin)(nil)
|
||||
46
classes/links.go
Normal file
46
classes/links.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Package classes holds the menu plugin's link rules and the tree the public
|
||||
// API serves.
|
||||
package classes
|
||||
|
||||
import "strings"
|
||||
|
||||
// AllowedURL reports whether an entry URL may be stored and served: empty, a
|
||||
// path starting with "/", a fragment starting with "#", or an absolute
|
||||
// http, https, mailto or tel URL (case-insensitive). Anything else, such as
|
||||
// javascript: or data:, is refused, and so is a URL with surrounding
|
||||
// whitespace or control characters, which browsers strip before reading the
|
||||
// scheme (RESEARCH Pitfall 15).
|
||||
func AllowedURL(url string) bool {
|
||||
if url == "" {
|
||||
return true
|
||||
}
|
||||
for _, r := range url {
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(url) != url {
|
||||
return false
|
||||
}
|
||||
if url[0] == '/' || url[0] == '#' {
|
||||
return true
|
||||
}
|
||||
lower := strings.ToLower(url)
|
||||
for _, prefix := range []string{"http://", "https://", "mailto:", "tel:"} {
|
||||
if strings.HasPrefix(lower, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// The link targets an entry may open in.
|
||||
const (
|
||||
TargetSelf = "_self"
|
||||
TargetBlank = "_blank"
|
||||
)
|
||||
|
||||
// AllowedTarget reports whether target is _self, _blank or empty.
|
||||
func AllowedTarget(target string) bool {
|
||||
return target == "" || target == TargetSelf || target == TargetBlank
|
||||
}
|
||||
21
controllers/admin_registry.go
Normal file
21
controllers/admin_registry.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Package controllers holds the menu plugin's admin controllers.
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// PermissionManageMenu is the PHP permission golem15.menu.manage_menu; both
|
||||
// screens require it.
|
||||
const PermissionManageMenu = "golem15.menu.manage_menu"
|
||||
|
||||
// AdminControllers is the plugin's admin controller list. app returns the
|
||||
// booted application; it is asked on every call, so a controller never keeps
|
||||
// a database handle or registry of its own.
|
||||
func AdminControllers(app func() *backpack.App) []pact.AdminController {
|
||||
return []pact.AdminController{
|
||||
menusAdminController{},
|
||||
menuEntriesAdminController{app: app},
|
||||
}
|
||||
}
|
||||
5
controllers/helpers.go
Normal file
5
controllers/helpers.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package controllers
|
||||
|
||||
import "strconv"
|
||||
|
||||
func uintText(id uint) string { return strconv.FormatUint(uint64(id), 10) }
|
||||
238
controllers/menuentries.go
Normal file
238
controllers/menuentries.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.golem15.com/golem15/sm-menu-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-menu-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/signpost"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MenuEntriesControllerID is the cabana controller id of the Menu entries
|
||||
// screen.
|
||||
const MenuEntriesControllerID = "golem15.menu.menuentries"
|
||||
|
||||
// The phrase keys of the entry form's refusals and built-in type labels.
|
||||
const (
|
||||
errURLScheme = "golem15.menu::lang.errors.url_scheme"
|
||||
errTarget = "golem15.menu::lang.errors.target"
|
||||
errTypeUnknown = "golem15.menu::lang.errors.type_unknown"
|
||||
errMenuRequired = "golem15.menu::lang.errors.menu_required"
|
||||
errParentCycle = "golem15.menu::lang.errors.parent_cycle"
|
||||
labelTypeURL = "golem15.menu::lang.models.menuentry.type_url"
|
||||
labelTypeDivide = "golem15.menu::lang.models.menuentry.type_divider"
|
||||
)
|
||||
|
||||
// menuEntriesAdminController lists and edits menu entries. Its type dropdown
|
||||
// offers the built-ins url and divider and every menu item type of the
|
||||
// registry; its reference dropdown depends on the type through
|
||||
// signpost.ReferenceOptions; config_reorder.yaml gives the tree reorder page
|
||||
// scoped to one menu.
|
||||
type menuEntriesAdminController struct {
|
||||
app func() *backpack.App
|
||||
}
|
||||
|
||||
func (menuEntriesAdminController) ID() string { return MenuEntriesControllerID }
|
||||
|
||||
func (menuEntriesAdminController) ModelName() string { return `Golem15\Menu\Models\MenuEntry` }
|
||||
|
||||
func (menuEntriesAdminController) ConfigDir() string { return "controllers/menuentries" }
|
||||
|
||||
func (menuEntriesAdminController) RequiredPermissions() []string {
|
||||
return []string{PermissionManageMenu}
|
||||
}
|
||||
|
||||
func (menuEntriesAdminController) NewRecord() any { return &models.MenuEntry{} }
|
||||
|
||||
// AdminFieldRelations binds the menu field (also the reorder partition) and
|
||||
// the parent field.
|
||||
func (menuEntriesAdminController) AdminFieldRelations() []cabana.FieldRelationContract {
|
||||
return []cabana.FieldRelationContract{
|
||||
{Field: "menu", Kind: "belongsTo", NewRelated: func() any { return &models.Menu{} }, ForeignKey: "menu_id"},
|
||||
{Field: "parent", Kind: "belongsTo", NewRelated: func() any { return &models.MenuEntry{} }, ForeignKey: "parent_id"},
|
||||
}
|
||||
}
|
||||
|
||||
// RelationExtendOptionsQuery orders the menu choices (and the reorder
|
||||
// partitions) by name and the parent choices by title.
|
||||
func (menuEntriesAdminController) RelationExtendOptionsQuery(_ context.Context, field string, db *gorm.DB) *gorm.DB {
|
||||
switch field {
|
||||
case "menu":
|
||||
return db.Order("name ASC").Order("id ASC")
|
||||
case "parent":
|
||||
return db.Order("title ASC").Order("id ASC")
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func (c menuEntriesAdminController) registry() *signpost.Registry {
|
||||
if c.app == nil {
|
||||
return nil
|
||||
}
|
||||
app := c.app()
|
||||
if app == nil {
|
||||
return nil
|
||||
}
|
||||
reg, _ := app.Lookup[*signpost.Registry]()
|
||||
return reg
|
||||
}
|
||||
|
||||
func (c menuEntriesAdminController) pool() *gorm.DB {
|
||||
if c.app == nil {
|
||||
return nil
|
||||
}
|
||||
app := c.app()
|
||||
if app == nil {
|
||||
return nil
|
||||
}
|
||||
db, _ := app.Lookup[*gorm.DB]()
|
||||
return db
|
||||
}
|
||||
|
||||
// DropdownOptions serves the type dropdown: url (Link) and divider
|
||||
// (Divider), then every registry type in registry order with its owning
|
||||
// plugin's label (D-05).
|
||||
func (c menuEntriesAdminController) DropdownOptions(string) []pact.Option {
|
||||
out := []pact.Option{
|
||||
{Value: models.TypeURL, Label: labelTypeURL},
|
||||
{Value: models.TypeDivider, Label: labelTypeDivide},
|
||||
}
|
||||
for _, t := range c.registry().Types() {
|
||||
out = append(out, pact.Option{Value: t.Key, Label: t.Label})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DependentOptions serves the reference dropdown: url, divider and an
|
||||
// unknown type take no reference (Applies false); a registry type offers its
|
||||
// references through signpost.ReferenceOptions (D-08).
|
||||
func (c menuEntriesAdminController) DependentOptions(ctx context.Context, _ string, values map[string]any) (pact.DependentOptionSet, error) {
|
||||
typ, _ := values["type"].(string)
|
||||
if typ == "" || models.Builtin(typ) {
|
||||
return pact.DependentOptionSet{}, nil
|
||||
}
|
||||
return signpost.ReferenceOptions(ctx, c.registry(), typ)
|
||||
}
|
||||
|
||||
// FilterOptions serves the menu filter's choices: every menu by name.
|
||||
func (c menuEntriesAdminController) FilterOptions(scope string) []pact.Option {
|
||||
if scope != models.FilterMenu {
|
||||
return nil
|
||||
}
|
||||
db := c.pool()
|
||||
if db == nil {
|
||||
return nil
|
||||
}
|
||||
var menus []models.Menu
|
||||
if err := db.Session(&gorm.Session{NewDB: true}).Order("name ASC, id ASC").Find(&menus).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]pact.Option, 0, len(menus))
|
||||
for _, m := range menus {
|
||||
out = append(out, pact.Option{Value: uintText(m.ID), Label: m.Name})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FormBeforeCreate checks the type, the URL scheme and the target.
|
||||
func (c menuEntriesAdminController) FormBeforeCreate(_ context.Context, model any) error {
|
||||
return c.checkEntry(model)
|
||||
}
|
||||
|
||||
// FormBeforeUpdate applies the create checks to an update.
|
||||
func (c menuEntriesAdminController) FormBeforeUpdate(_ context.Context, model any) error {
|
||||
return c.checkEntry(model)
|
||||
}
|
||||
|
||||
// checkEntry refuses an unknown type, a URL outside the scheme allowlist
|
||||
// (errors.url_scheme, Pitfall 15) and a target other than _self or _blank.
|
||||
func (c menuEntriesAdminController) checkEntry(model any) error {
|
||||
entry, ok := model.(*models.MenuEntry)
|
||||
if !ok || entry == nil {
|
||||
return nil
|
||||
}
|
||||
details := map[string]any{}
|
||||
if entry.Type != "" && !models.Builtin(entry.Type) {
|
||||
if _, known := c.registry().Type(entry.Type); !known {
|
||||
details["type"] = []string{errTypeUnknown}
|
||||
}
|
||||
}
|
||||
if !classes.AllowedURL(models.Text(entry.URL)) {
|
||||
details["url"] = []string{errURLScheme}
|
||||
}
|
||||
if !classes.AllowedTarget(models.Text(entry.Target)) {
|
||||
details["target"] = []string{errTarget}
|
||||
}
|
||||
if len(details) > 0 {
|
||||
return &cabana.ValidationError{Details: details}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormAfterCreate refuses an entry that ends up in no menu (no menu chosen
|
||||
// and no parent to take it from). cabana writes the menu and parent from the
|
||||
// relation fields after the model rules ran, so the check runs once the row
|
||||
// is written, inside the save's transaction.
|
||||
func (menuEntriesAdminController) FormAfterCreate(ctx context.Context, model any) error {
|
||||
return checkPlacement(ctx, model)
|
||||
}
|
||||
|
||||
// FormAfterUpdate also refuses a parent that is the entry itself or one of
|
||||
// its descendants.
|
||||
func (menuEntriesAdminController) FormAfterUpdate(ctx context.Context, model any) error {
|
||||
return checkPlacement(ctx, model)
|
||||
}
|
||||
|
||||
func checkPlacement(ctx context.Context, model any) error {
|
||||
entry, ok := model.(*models.MenuEntry)
|
||||
if !ok || entry == nil {
|
||||
return nil
|
||||
}
|
||||
if entry.MenuID == 0 {
|
||||
return &cabana.ValidationError{Details: map[string]any{"menu": []string{errMenuRequired}}}
|
||||
}
|
||||
if entry.ParentID == nil {
|
||||
return nil
|
||||
}
|
||||
tx, ok := cabana.TxFromContext(ctx)
|
||||
if !ok {
|
||||
return errors.New("golem15.menu: the entry check needs the save's transaction")
|
||||
}
|
||||
db := tx.Session(&gorm.Session{NewDB: true}).WithContext(ctx)
|
||||
seen := map[uint]bool{}
|
||||
for id := *entry.ParentID; id != 0; {
|
||||
if id == entry.ID || seen[id] {
|
||||
return &cabana.ValidationError{Details: map[string]any{"parent": []string{errParentCycle}}}
|
||||
}
|
||||
seen[id] = true
|
||||
var rows []models.MenuEntry
|
||||
if err := db.Select("id", "parent_id").Where("id = ?", id).Limit(1).Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rows) == 0 || rows[0].ParentID == nil {
|
||||
break
|
||||
}
|
||||
id = *rows[0].ParentID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ pact.AdminController = menuEntriesAdminController{}
|
||||
_ pact.AdminPermissioned = menuEntriesAdminController{}
|
||||
_ pact.AdminRecordSource = menuEntriesAdminController{}
|
||||
_ pact.DropdownOptionsProvider = menuEntriesAdminController{}
|
||||
_ pact.DependentOptionsProvider = menuEntriesAdminController{}
|
||||
_ pact.FilterOptions = menuEntriesAdminController{}
|
||||
_ pact.FormBeforeCreate = menuEntriesAdminController{}
|
||||
_ pact.FormBeforeUpdate = menuEntriesAdminController{}
|
||||
_ pact.FormAfterCreate = menuEntriesAdminController{}
|
||||
_ pact.FormAfterUpdate = menuEntriesAdminController{}
|
||||
_ pact.RelationExtendOptionsQuery = menuEntriesAdminController{}
|
||||
_ cabana.FieldRelationProvider = menuEntriesAdminController{}
|
||||
)
|
||||
14
controllers/menuentries/config_filter.yaml
Normal file
14
controllers/menuentries/config_filter.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
# ===================================
|
||||
# Filter Scope Definitions
|
||||
# ===================================
|
||||
#
|
||||
# The menu filter replaces the PHP toolbar's menu select. Its name is the
|
||||
# reorder partition column, so the list's Reorder button opens the filtered
|
||||
# menu.
|
||||
|
||||
scopes:
|
||||
menu_id:
|
||||
label: golem15.menu::lang.models.menuentry.menu
|
||||
modelClass: Golem15\Menu\Models\Menu
|
||||
nameFrom: name
|
||||
scope: FilterMenu
|
||||
25
controllers/menuentries/config_form.yaml
Normal file
25
controllers/menuentries/config_form.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
# ===================================
|
||||
# Form Behavior Config
|
||||
# ===================================
|
||||
|
||||
# Record name
|
||||
name: golem15.menu::lang.models.menuentry.label
|
||||
|
||||
# Model Form Field configuration
|
||||
form: ~/plugins/golem15/menu/models/menuentry/fields.yaml
|
||||
|
||||
# Model Class name
|
||||
modelClass: Golem15\Menu\Models\MenuEntry
|
||||
|
||||
# Default redirect location
|
||||
defaultRedirect: golem15/menu/menuentries
|
||||
|
||||
# Create page
|
||||
create:
|
||||
redirect: golem15/menu/menuentries/update/:id
|
||||
redirectClose: golem15/menu/menuentries
|
||||
|
||||
# Update page
|
||||
update:
|
||||
redirect: golem15/menu/menuentries
|
||||
redirectClose: golem15/menu/menuentries
|
||||
46
controllers/menuentries/config_list.yaml
Normal file
46
controllers/menuentries/config_list.yaml
Normal file
@@ -0,0 +1,46 @@
|
||||
# ===================================
|
||||
# List Behavior Config
|
||||
# ===================================
|
||||
#
|
||||
# The PHP list showed the top-level entries of one menu picked in a toolbar
|
||||
# select; here every entry is listed, the menu filter picks the menu, and
|
||||
# the reorder page edits the tree.
|
||||
|
||||
# Model List Column configuration
|
||||
list: ~/plugins/golem15/menu/models/menuentry/columns.yaml
|
||||
|
||||
# Model Class name
|
||||
modelClass: Golem15\Menu\Models\MenuEntry
|
||||
|
||||
# List Title
|
||||
title: golem15.menu::lang.models.menuentry.label_plural
|
||||
|
||||
# Link URL for each record
|
||||
recordUrl: golem15/menu/menuentries/update/:id
|
||||
|
||||
# Message to display if the list is empty
|
||||
noRecordsMessage: backend::lang.list.no_records
|
||||
|
||||
# Records to display per page
|
||||
recordsPerPage: 20
|
||||
|
||||
# Options to provide the user when selecting how many records to display per page
|
||||
perPageOptions: [20, 40, 80, 100, 120]
|
||||
|
||||
# Displays the list column set up button
|
||||
showSetup: true
|
||||
|
||||
# Displays the sorting link on each column
|
||||
showSorting: true
|
||||
|
||||
# Display checkboxes next to each record
|
||||
showCheckboxes: true
|
||||
|
||||
# The menu filter (config_filter.yaml)
|
||||
filter: config_filter.yaml
|
||||
|
||||
# Toolbar: reorder (config_reorder.yaml), create and delete selected
|
||||
toolbar:
|
||||
buttons: [reorder, create, delete]
|
||||
search:
|
||||
prompt: backend::lang.list.search_prompt
|
||||
27
controllers/menuentries/config_reorder.yaml
Normal file
27
controllers/menuentries/config_reorder.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
# ===================================
|
||||
# Reorder Behavior Config
|
||||
# ===================================
|
||||
#
|
||||
# The PHP reorder_config.yaml, scoped to one menu through the partition: the
|
||||
# page lists the entries of one menu as a tree and saves parent_id and
|
||||
# sort_order. The PHP toolbar partial is not supported.
|
||||
|
||||
# Reorder Title
|
||||
title: golem15.menu::lang.models.menuentry.reorder_title
|
||||
|
||||
# Attribute name
|
||||
nameFrom: title
|
||||
|
||||
# Model Class name
|
||||
modelClass: Golem15\Menu\Models\MenuEntry
|
||||
|
||||
# Tree columns
|
||||
parentColumn: parent_id
|
||||
sortColumn: sort_order
|
||||
|
||||
# One menu at a time
|
||||
partition:
|
||||
column: menu_id
|
||||
relation: menu
|
||||
nameFrom: name
|
||||
label: golem15.menu::lang.models.menuentry.menu
|
||||
31
controllers/menus.go
Normal file
31
controllers/menus.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/sm-menu-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
)
|
||||
|
||||
// MenusControllerID is the cabana controller id of the Menus screen.
|
||||
const MenusControllerID = "golem15.menu.menus"
|
||||
|
||||
// menusAdminController lists and edits menus: name, code, CSS class and the
|
||||
// active switch.
|
||||
type menusAdminController struct{}
|
||||
|
||||
func (menusAdminController) ID() string { return MenusControllerID }
|
||||
|
||||
func (menusAdminController) ModelName() string { return `Golem15\Menu\Models\Menu` }
|
||||
|
||||
func (menusAdminController) ConfigDir() string { return "controllers/menus" }
|
||||
|
||||
func (menusAdminController) RequiredPermissions() []string {
|
||||
return []string{PermissionManageMenu}
|
||||
}
|
||||
|
||||
func (menusAdminController) NewRecord() any { return &models.Menu{} }
|
||||
|
||||
var (
|
||||
_ pact.AdminController = menusAdminController{}
|
||||
_ pact.AdminPermissioned = menusAdminController{}
|
||||
_ pact.AdminRecordSource = menusAdminController{}
|
||||
)
|
||||
25
controllers/menus/config_form.yaml
Normal file
25
controllers/menus/config_form.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
# ===================================
|
||||
# Form Behavior Config
|
||||
# ===================================
|
||||
|
||||
# Record name
|
||||
name: golem15.menu::lang.models.menu.label
|
||||
|
||||
# Model Form Field configuration
|
||||
form: ~/plugins/golem15/menu/models/menu/fields.yaml
|
||||
|
||||
# Model Class name
|
||||
modelClass: Golem15\Menu\Models\Menu
|
||||
|
||||
# Default redirect location
|
||||
defaultRedirect: golem15/menu/menus
|
||||
|
||||
# Create page
|
||||
create:
|
||||
redirect: golem15/menu/menus/update/:id
|
||||
redirectClose: golem15/menu/menus
|
||||
|
||||
# Update page
|
||||
update:
|
||||
redirect: golem15/menu/menus
|
||||
redirectClose: golem15/menu/menus
|
||||
39
controllers/menus/config_list.yaml
Normal file
39
controllers/menus/config_list.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
# ===================================
|
||||
# List Behavior Config
|
||||
# ===================================
|
||||
|
||||
# Model List Column configuration
|
||||
list: ~/plugins/golem15/menu/models/menu/columns.yaml
|
||||
|
||||
# Model Class name
|
||||
modelClass: Golem15\Menu\Models\Menu
|
||||
|
||||
# List Title
|
||||
title: golem15.menu::lang.models.menu.label_plural
|
||||
|
||||
# Link URL for each record
|
||||
recordUrl: golem15/menu/menus/update/:id
|
||||
|
||||
# Message to display if the list is empty
|
||||
noRecordsMessage: backend::lang.list.no_records
|
||||
|
||||
# Records to display per page
|
||||
recordsPerPage: 20
|
||||
|
||||
# Options to provide the user when selecting how many records to display per page
|
||||
perPageOptions: [20, 40, 80, 100, 120]
|
||||
|
||||
# Displays the list column set up button
|
||||
showSetup: true
|
||||
|
||||
# Displays the sorting link on each column
|
||||
showSorting: true
|
||||
|
||||
# Display checkboxes next to each record
|
||||
showCheckboxes: true
|
||||
|
||||
# Toolbar: the PHP list_toolbar partial held create and delete selected
|
||||
toolbar:
|
||||
buttons: [create, delete]
|
||||
search:
|
||||
prompt: backend::lang.list.search_prompt
|
||||
80
go.mod
Normal file
80
go.mod
Normal file
@@ -0,0 +1,80 @@
|
||||
module git.golem15.com/golem15/sm-menu-plugin
|
||||
|
||||
go 1.27.0
|
||||
|
||||
require (
|
||||
git.golem15.com/golem15/summercms v0.1.23
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.7
|
||||
gorm.io/gorm v1.31.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.4 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.19.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.10.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||
github.com/knadh/koanf/parsers/yaml v1.1.1 // indirect
|
||||
github.com/knadh/koanf/providers/confmap v1.0.1 // indirect
|
||||
github.com/knadh/koanf/providers/env/v2 v2.0.1 // indirect
|
||||
github.com/knadh/koanf/providers/file v1.2.1 // indirect
|
||||
github.com/knadh/koanf/v2 v2.3.6 // indirect
|
||||
github.com/leodido/go-urn v1.5.0 // indirect
|
||||
github.com/lib/pq v1.12.3 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 // indirect
|
||||
github.com/riverqueue/river v0.47.0 // indirect
|
||||
github.com/riverqueue/river/riverdriver v0.47.0 // indirect
|
||||
github.com/riverqueue/river/riverdriver/riverdatabasesql v0.47.0 // indirect
|
||||
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.47.0 // indirect
|
||||
github.com/riverqueue/river/rivershared v0.47.0 // indirect
|
||||
github.com/riverqueue/river/rivertype v0.47.0 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/wneessen/go-mail v0.8.1 // indirect
|
||||
github.com/yuin/goldmark v1.8.6 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
gocloud.dev v0.46.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/image v0.46.0 // indirect
|
||||
golang.org/x/net v0.58.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/term v0.46.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/api v0.272.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gorm.io/driver/postgres v1.6.3 // indirect
|
||||
)
|
||||
|
||||
replace git.golem15.com/golem15/summercms => ../summercms.go
|
||||
346
go.sum
Normal file
346
go.sum
Normal file
@@ -0,0 +1,346 @@
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=
|
||||
cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24grGuuA5saDJTj2IIjLb9au668Fo5I=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11/go.mod h1:dnakxebH6UwFvcvujL0LVggYQ8nEvBGjU4G/V79Nv94=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.20 h1:8VMDnWc/kEzxsI/1ngGM9mG81a8IGmIHD8KLcYGwagc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.20/go.mod h1:PuwEpciweIXGULWeOeSTXtSbH4CW9mWdWrhdCKQI1sM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19 h1:yuFzSV1U0aRNYCQGVaTY2zW2M/L93pYHnXnrJUphYhU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19/go.mod h1:7y63L1kGzeoDlJaQ3Z578KrnmfBut96JjvJUzGwR+YE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 h1:0w6dCiO8iez+YKwRhRBlL1CH/E3GTfdkuzrwj1by8vo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25/go.mod h1:9FDWUothyr5RCRAHc45XOiVCzUR8n/IhCYX+uVqw6vk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3 h1:w5OoDiMN6x53ROmiIImGzmVcxXv2q1GXY+aKV4WAJYM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.3/go.mod h1:dAhgYp776bX3LuWvnSCFwQEjNs6fuFg7YXIy5PXcP3Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 h1:Uii3frf9ztec/ABM2/FSH9/z7PLzxfpG8h4RpkUFflQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25/go.mod h1:G6kntsA2GorAxDPbap6xgB2F+amSLUF8GJTi7PUoX44=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 h1:r1+/l6m+WaUJF9HISEsNOLHSNj5EXYQxK8VX6Cz9NlA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25/go.mod h1:cKf+D+NMDK1LndD7BowHbBZPgR9V0/5HubH0PFWvA+c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 h1:A1PmWU2zfkIm9EyFlJncFXL4W4phML+h8KjltUsCvNQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26/go.mod h1:dY4MRzXEizrD4hqtpKvWVGPX7QleSGGVY+EBolo1RmM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 h1:d5/908OJ4bXg8lyjeMPvXetEKqoDoLi5Owy1zNue3yg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10/go.mod h1:a57l7Hwh+FWI+we50g5NPJHYUKeJKfXbc4w8SyXu8Ig=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 h1:W/EyPFl9A5rXrtoilfwHYEvzHER+K4SpBPtMXi24Mos=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18/go.mod h1:UG50K+pvd/uy6xExbobg0rjqFBFZe6I3l75EPDZw4tg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 h1:dD3dhHNglpd98gs72my22Ndqi1hqQGllFFg1F+twfxg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25/go.mod h1:0yAbjPfd64gG7mj85RW+fMEYdfBgCRZw8g/oWcL1pjc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 h1:2pQEbwf+/6EDbiit/GcBE2K4IUpMZymaA0kOz3xK978=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25/go.mod h1:KvT6NCcQ0EZ+ZkVRrlBMt04Po3ok23YELEp7WimhLhM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 h1:ie4ElCmUKS26pzrZcIk/lmt4yWjAqLLcawstyQCh298=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2/go.mod h1:zjsomFeX5duj+4PlMB+o4JoWTIx+G0XMyzjYrUbQkN0=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 h1:1VwbP3qMNfxUDEXWki4rCE5iA+44VA1lokTz9HasGzw=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1/go.mod h1:vUtyoSj0OPji3kjIVSc/GlKuWEiL33f/WFxl6dmpy/A=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 h1:N6pIsdFOW1Kd9S4KyFKXdGRBojPPxkP32+uHFWLv4Hc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19/go.mod h1:3gt5WJArFooNmyLONS+h/R4J+o86II8du38IgCwj9dE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 h1:hc+lBYiiTr8Zk4MTzIsQ92MeDWCIDvWGmzKUWOaBcOg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2/go.mod h1:hU6fqB3OJA6/ePheD47LQnxvjYk6br6PtQxs+Q9ojvk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3SJXQNEgksORW3Js=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8=
|
||||
github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s=
|
||||
github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
|
||||
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
|
||||
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
|
||||
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.7 h1:PdT4jVPbRb4R+0Ey2R0yJOdctVf4Whiq1Qi4necaZdg=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.7/go.mod h1:3ouXglTuPrKF5+7cQyVGfvAXTU4vLMaYh9+EPl03uog=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.4 h1:9Rcod2ZPO6mOEG6b4GqyoHE/H6//Ze0RuhOo1hT1x0w=
|
||||
github.com/go-playground/validator/v10 v10.30.4/go.mod h1:numpT+RPLE91R9oYWMY/R9zRgJBewr3IXHko4OISPpk=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||
github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
|
||||
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
|
||||
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/parsers/yaml v1.1.1 h1:u70vV5IyaM0HvONh8HoqBC97oTgO33KcpZbTLiKVinU=
|
||||
github.com/knadh/koanf/parsers/yaml v1.1.1/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg=
|
||||
github.com/knadh/koanf/providers/confmap v1.0.1 h1:L15hbvMqlvhwUuCtL9BkL+rqiMAjk6cZc8O9XoDtE3A=
|
||||
github.com/knadh/koanf/providers/confmap v1.0.1/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
|
||||
github.com/knadh/koanf/providers/env/v2 v2.0.1 h1:a3KagndPqhcWHQv6Pz4OZmwkI/yMeTjkiZye6ZCkyW0=
|
||||
github.com/knadh/koanf/providers/env/v2 v2.0.1/go.mod h1:1g01PE+Ve1gBfWNNw2wmULRP0tc8RJrjn5p2N/jNCIc=
|
||||
github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM=
|
||||
github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
|
||||
github.com/knadh/koanf/v2 v2.3.6 h1:JoQPSJmvS4aP0xNc8xMDr5tcrkSEInL23/Il7pITAKo=
|
||||
github.com/knadh/koanf/v2 v2.3.6/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
|
||||
github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0=
|
||||
github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
|
||||
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
|
||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/riverqueue/river v0.47.0 h1:j8HOEyiOE8gRRhVS2wllKams372TH1WWiH6xCakXHqc=
|
||||
github.com/riverqueue/river v0.47.0/go.mod h1:Wgmwx475ZBd8lQnNrJgyG2DWH7BfyNiSAtv0rC9bJBQ=
|
||||
github.com/riverqueue/river/riverdriver v0.47.0 h1:qU8VkjdMl9plqeRg57SxsDUM/i/eECaSYejZ7HynC60=
|
||||
github.com/riverqueue/river/riverdriver v0.47.0/go.mod h1:NOXl0fUiF1AT/TaQOjdx2A/c0Davn+SKbW8nAXWjfC4=
|
||||
github.com/riverqueue/river/riverdriver/riverdatabasesql v0.47.0 h1:4wckhMlLN08F76/GWxGF6vlrbs5HCMuV01dpUnvOGI4=
|
||||
github.com/riverqueue/river/riverdriver/riverdatabasesql v0.47.0/go.mod h1:EME1EGr7RU/vMUf61mpf9YXhsZJ+GlFk4AHrIHT+YdE=
|
||||
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.47.0 h1:5N9nvemhQwbUElMxASw4oEaYJ/v6hiS5Y9VcOQfdC5g=
|
||||
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.47.0/go.mod h1:ZboiXXZKC4+fTkxBxGRVmAsCuUu0NPYliqbWYuQAZyw=
|
||||
github.com/riverqueue/river/rivershared v0.47.0 h1:jdtFsBexCvLqTXf8wnDnGXvB/eeOtPKQZAmThkjFpLs=
|
||||
github.com/riverqueue/river/rivershared v0.47.0/go.mod h1:w8Pi1T+6ypyko5/hs9Mv7IIIKo4fAL9eXYnkVV/Y418=
|
||||
github.com/riverqueue/river/rivertype v0.47.0 h1:SzNavtLGR4nMT1QkrEYQ7n96OMatYsn/z3aJWhewmv0=
|
||||
github.com/riverqueue/river/rivertype v0.47.0/go.mod h1:XKkcRQR6zm8RR/JQa1Q2ywpj8uXQu21quPa4Lpw1Xhw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI=
|
||||
github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE=
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0 h1:8fdv/9y3JMxjQ+ULAcOG8RtgeNu5t9XF9LolSXDuTwM=
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0/go.mod h1:CFr2LncGYokw+OKjXcr8ARCKG1SaC2UEnGxFBovE86g=
|
||||
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||
github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
|
||||
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
|
||||
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
|
||||
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
gocloud.dev v0.46.0 h1:niIuZwSjMtBx8K+ITB2s5kZullB13PGOS2ZoQPZxQ4Q=
|
||||
gocloud.dev v0.46.0/go.mod h1:ACQe+2qO+hEO+pdcvvsM+RB63r8TyGD1W3ESCLFyzvM=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
|
||||
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
|
||||
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
|
||||
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.6.3 h1:bAn6O2pUa8LtpWEvL5NFU4+52Tfx8Ut7IVaIacCLcI0=
|
||||
gorm.io/driver/postgres v1.6.3/go.mod h1:0c4fQA44XhOklXDkgtuKqysHCycTa5i9e3EIpDGCwXk=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
||||
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
5
go.work
Normal file
5
go.work
Normal file
@@ -0,0 +1,5 @@
|
||||
go 1.27.0
|
||||
|
||||
use .
|
||||
|
||||
replace git.golem15.com/golem15/summercms => ../summercms.go
|
||||
47
lang/en/lang.yaml
Normal file
47
lang/en/lang.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
plugin:
|
||||
name: Menus
|
||||
description: Site menus with links, dividers and content entries.
|
||||
permissions:
|
||||
manage_menu: Manage site menus
|
||||
models:
|
||||
general:
|
||||
id: ID
|
||||
created_at: Created At
|
||||
updated_at: Updated At
|
||||
menu:
|
||||
label: Menu
|
||||
label_plural: Menus
|
||||
name: Name
|
||||
code: Code
|
||||
css_class: CSS class
|
||||
is_active: Active
|
||||
menuentry:
|
||||
label: Menu entry
|
||||
label_plural: Menu entries
|
||||
menu: Menu
|
||||
menu_comment: Entries with a parent belong to the parent's menu.
|
||||
parent: Parent
|
||||
type: Type
|
||||
type_url: Link
|
||||
type_divider: Divider
|
||||
title: Title
|
||||
url: URL
|
||||
url_comment: Used by Link entries. Start with http://, https://, mailto:, tel:, / or #.
|
||||
reference: Reference
|
||||
reference_comment: The record this entry points to, for types that need one.
|
||||
url_pattern: URL pattern
|
||||
url_pattern_comment: Use :placeholder syntax, e.g. /blog/:slug
|
||||
code: Code
|
||||
icon: Icon
|
||||
css_class: CSS class
|
||||
target: Opens in
|
||||
target_self: Same tab
|
||||
target_blank: New tab
|
||||
is_published: Published
|
||||
reorder_title: Reorder menu entries
|
||||
errors:
|
||||
url_scheme: Use a link that starts with http://, https://, mailto:, tel:, / or #.
|
||||
target: Choose Same tab or New tab.
|
||||
type_unknown: Choose one of the listed types.
|
||||
menu_required: Choose a menu or a parent entry.
|
||||
parent_cycle: An entry cannot be placed under itself or one of its own entries.
|
||||
47
lang/pl/lang.yaml
Normal file
47
lang/pl/lang.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
plugin:
|
||||
name: Menu
|
||||
description: Menu strony z linkami, separatorami i pozycjami treści.
|
||||
permissions:
|
||||
manage_menu: Zarządzanie menu strony
|
||||
models:
|
||||
general:
|
||||
id: ID
|
||||
created_at: Utworzono
|
||||
updated_at: Zaktualizowano
|
||||
menu:
|
||||
label: Menu
|
||||
label_plural: Menu
|
||||
name: Nazwa
|
||||
code: Kod
|
||||
css_class: Klasa CSS
|
||||
is_active: Aktywne
|
||||
menuentry:
|
||||
label: Pozycja menu
|
||||
label_plural: Pozycje menu
|
||||
menu: Menu
|
||||
menu_comment: Pozycje z pozycją nadrzędną należą do jej menu.
|
||||
parent: Pozycja nadrzędna
|
||||
type: Typ
|
||||
type_url: Link
|
||||
type_divider: Separator
|
||||
title: Tytuł
|
||||
url: URL
|
||||
url_comment: Dla pozycji typu Link. Zacznij od http://, https://, mailto:, tel:, / lub #.
|
||||
reference: Odnośnik
|
||||
reference_comment: Rekord, do którego prowadzi pozycja, dla typów, które go wymagają.
|
||||
url_pattern: Wzorzec URL
|
||||
url_pattern_comment: Użyj składni :placeholder, np. /blog/:slug
|
||||
code: Kod
|
||||
icon: Ikona
|
||||
css_class: Klasa CSS
|
||||
target: Otwieraj w
|
||||
target_self: Tej samej karcie
|
||||
target_blank: Nowej karcie
|
||||
is_published: Opublikowane
|
||||
reorder_title: Zmiana kolejności pozycji menu
|
||||
errors:
|
||||
url_scheme: Podaj link zaczynający się od http://, https://, mailto:, tel:, / lub #.
|
||||
target: Wybierz Tej samej karcie lub Nowej karcie.
|
||||
type_unknown: Wybierz jeden z typów z listy.
|
||||
menu_required: Wybierz menu albo pozycję nadrzędną.
|
||||
parent_cycle: Pozycji nie można umieścić pod nią samą ani pod jej własnymi pozycjami.
|
||||
374
menu_smoke_test.go
Normal file
374
menu_smoke_test.go
Normal file
@@ -0,0 +1,374 @@
|
||||
package menu_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "git.golem15.com/golem15/sm-menu-plugin"
|
||||
"git.golem15.com/golem15/sm-menu-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/cabana"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
"git.golem15.com/golem15/summercms/modules/surf"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// testTypesPluginID is the fake content plugin that registers menu item
|
||||
// types for the smoke tests, so the menu plugin needs no content plugin.
|
||||
const testTypesPluginID = "acme.menutypes"
|
||||
|
||||
const permManage = "golem15.menu.manage_menu"
|
||||
|
||||
// acmeTypes registers acme-page (references 1 Home with 2 About under it)
|
||||
// and acme-list (a list type without references).
|
||||
type acmeTypes struct{}
|
||||
|
||||
func (acmeTypes) ID() string { return testTypesPluginID }
|
||||
func (acmeTypes) Requires() []string { return nil }
|
||||
func (acmeTypes) Register(*backpack.App) error { return nil }
|
||||
func (acmeTypes) Boot(*backpack.App) error { return nil }
|
||||
func (acmeTypes) MenuItemTypes() []pact.MenuItemType {
|
||||
return []pact.MenuItemType{
|
||||
{
|
||||
Key: "acme-page",
|
||||
Label: "Acme page",
|
||||
DefaultPattern: "/pages/:slug",
|
||||
References: func(context.Context) ([]pact.MenuItemReference, error) {
|
||||
return []pact.MenuItemReference{{Value: "1", Label: "Home", Items: []pact.MenuItemReference{{Value: "2", Label: "About"}}}}, nil
|
||||
},
|
||||
Resolve: resolvePage,
|
||||
},
|
||||
{
|
||||
Key: "acme-list",
|
||||
Label: "Acme pages",
|
||||
DefaultPattern: "/pages/:slug",
|
||||
PatternFrom: "acme-page",
|
||||
Nesting: true,
|
||||
DynamicItems: true,
|
||||
Resolve: func(_ context.Context, in pact.MenuItemResolve) (*pact.ResolvedMenuItem, error) {
|
||||
var items []pact.ResolvedMenuItem
|
||||
for _, slug := range []string{"home", "about"} {
|
||||
url := strings.ReplaceAll(in.Pattern, ":slug", slug)
|
||||
items = append(items, pact.ResolvedMenuItem{
|
||||
Title: slug, URL: url,
|
||||
Alternates: []pact.MenuItemAlternate{{Locale: "pl", URL: url, Title: slug}, {Locale: "en", URL: "/en" + url, Title: slug + " (en)"}},
|
||||
})
|
||||
}
|
||||
return &pact.ResolvedMenuItem{Items: items}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// resolvePage resolves references 1 (home) and 2 (about) with the layered
|
||||
// pattern; any other reference no longer exists.
|
||||
func resolvePage(_ context.Context, in pact.MenuItemResolve) (*pact.ResolvedMenuItem, error) {
|
||||
slugs := map[string]string{"1": "home", "2": "about"}
|
||||
slug, ok := slugs[in.Reference]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
url := strings.ReplaceAll(in.Pattern, ":slug", slug)
|
||||
return &pact.ResolvedMenuItem{
|
||||
Title: slug,
|
||||
URL: url,
|
||||
Alternates: []pact.MenuItemAlternate{
|
||||
{Locale: "pl", URL: url, Title: slug},
|
||||
{Locale: "en", URL: "/en" + url, Title: slug + " (en)"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ pact.HasMenuItemTypes = acmeTypes{}
|
||||
|
||||
func init() {
|
||||
party.Register(acmeTypes{})
|
||||
}
|
||||
|
||||
// menuEnv is a booted application with the menu plugin and the fake types.
|
||||
type menuEnv struct {
|
||||
h http.Handler
|
||||
app *backpack.App
|
||||
gdb *gorm.DB
|
||||
}
|
||||
|
||||
func testConfig(t *testing.T) *compass.Config {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: menu-smoke\nlocale: en\nfallback_locale: en\nurl: https://api.example.test\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := compass.Open(compass.Options{
|
||||
Dir: dir,
|
||||
Environ: []string{"SUMMER_ENV=development", "SUMMER_ADMIN__JWT__SECRET=menu-smoke-test-only-hs256-secret-key-0001"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for key, value := range map[string]any{
|
||||
"http.body_limits.default_bytes": 1048576,
|
||||
"http.body_limits.upload_bytes": 8388608,
|
||||
"mail.driver": "log",
|
||||
"mail.from": "noreply@example.test",
|
||||
} {
|
||||
if err := cfg.Set(key, value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func bootMenu(t *testing.T) *menuEnv {
|
||||
t.Helper()
|
||||
sqlDB := freshDB(t)
|
||||
gdb, err := lagoon.Use(t.Context(), sqlDB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app := backpack.New(testConfig(t))
|
||||
if err := lagoon.Publish(app, sqlDB, gdb); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plugins, err := party.Activate(app, []string{testTypesPluginID, "golem15.menu"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := lagoon.Migrate(gdb, plugins); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h, err := surf.Assemble(app, plugins)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &menuEnv{h: h, app: app, gdb: gdb}
|
||||
}
|
||||
|
||||
var adminSeq atomic.Uint64
|
||||
|
||||
// call sends an admin API request with an optional bearer token and JSON
|
||||
// body.
|
||||
func (e *menuEnv) call(t *testing.T, method, path, token string, body any) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
raw, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reader = bytes.NewReader(raw)
|
||||
}
|
||||
req := httptest.NewRequest(method, cabana.DefaultAdminPrefix+"/api/v1"+path, reader)
|
||||
req.Host = "api.example.test"
|
||||
req.RemoteAddr = fmt.Sprintf("198.51.100.%d:4000", adminSeq.Add(1)%250+1)
|
||||
req.Header.Set("Accept-Language", "en")
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
e.h.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
// admin signs in a new administrator whose role grants permissions.
|
||||
func (e *menuEnv) admin(t *testing.T, permissions ...string) string {
|
||||
t.Helper()
|
||||
stamp := fmt.Sprintf("%d%d", time.Now().UnixNano()%1_000_000_000, adminSeq.Add(1))
|
||||
login, password := "admin-"+stamp, "admin-secret-"+stamp
|
||||
hash, err := bouncer.HashPassword(10, password)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
grants := map[string]int{}
|
||||
for _, code := range permissions {
|
||||
grants[code] = 1
|
||||
}
|
||||
raw, _ := json.Marshal(grants)
|
||||
var roleID uint
|
||||
if err := e.gdb.Raw(`INSERT INTO backend_user_roles (name, code, permissions, is_system, created_at, updated_at) VALUES (?, ?, ?, FALSE, NOW(), NOW()) RETURNING id`,
|
||||
"Role "+stamp, "role-"+stamp, string(raw)).Scan(&roleID).Error; err != nil || roleID == 0 {
|
||||
t.Fatalf("backend role: id=%d err=%v", roleID, err)
|
||||
}
|
||||
if err := e.gdb.Exec(`INSERT INTO backend_users (login, email, password, is_activated, is_superuser, role_id, created_at, updated_at) VALUES (?, ?, ?, TRUE, FALSE, ?, NOW(), NOW())`,
|
||||
login, login+"@example.test", hash, roleID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := e.call(t, http.MethodPost, "/auth/login", "", map[string]string{"login": login, "password": password})
|
||||
var body struct {
|
||||
Data struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if rec.Code != http.StatusOK || json.Unmarshal(rec.Body.Bytes(), &body) != nil || body.Data.AccessToken == "" {
|
||||
t.Fatalf("admin login: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
return body.Data.AccessToken
|
||||
}
|
||||
|
||||
// create posts a record and returns its id; it fails the test unless the
|
||||
// answer is 201.
|
||||
func (e *menuEnv) create(t *testing.T, token, path string, body map[string]any) uint {
|
||||
t.Helper()
|
||||
rec := e.call(t, http.MethodPost, path, token, body)
|
||||
var out struct {
|
||||
Data struct {
|
||||
ID uint `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if rec.Code != http.StatusCreated || json.Unmarshal(rec.Body.Bytes(), &out) != nil || out.Data.ID == 0 {
|
||||
t.Fatalf("POST %s %v: %d %s", path, body, rec.Code, rec.Body.String())
|
||||
}
|
||||
return out.Data.ID
|
||||
}
|
||||
|
||||
func (e *menuEnv) entry(t *testing.T, id uint) models.MenuEntry {
|
||||
t.Helper()
|
||||
var entry models.MenuEntry
|
||||
if err := e.gdb.First(&entry, id).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func intOf(p *int) int {
|
||||
if p == nil {
|
||||
return -1
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
// expect422 posts or puts body and checks for a 422 naming field and,
|
||||
// when key is set, the phrase key.
|
||||
func (e *menuEnv) expect422(t *testing.T, token, method, path string, body map[string]any, field, key string) {
|
||||
t.Helper()
|
||||
rec := e.call(t, method, path, token, body)
|
||||
if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), `"`+field+`":`) || (key != "" && !strings.Contains(rec.Body.String(), key)) {
|
||||
t.Fatalf("%s %s %v: %d %s, want 422 on %s %s", method, path, body, rec.Code, rec.Body.String(), field, key)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
menusPath = "/golem15/menu/menus"
|
||||
entriesPath = "/golem15/menu/menuentries"
|
||||
)
|
||||
|
||||
// TestMenuAdminSmoke drives the menus and menu entries screens: the PHP save
|
||||
// rules, the D-05 type set, the reference recheck, the URL scheme and target
|
||||
// refusals, the parent cycle refusal and the tree reorder page scoped to one
|
||||
// menu.
|
||||
func TestMenuAdminSmoke(t *testing.T) {
|
||||
e := bootMenu(t)
|
||||
token := e.admin(t, permManage)
|
||||
|
||||
mainID := e.create(t, token, menusPath, map[string]any{"name": "Main", "code": "main", "is_active": true})
|
||||
e.expect422(t, token, http.MethodPost, menusPath, map[string]any{"name": "Again", "code": "main", "is_active": true}, "code", "")
|
||||
|
||||
// The form offers the built-ins and the registry types; the reference
|
||||
// depends on the type.
|
||||
rec := e.call(t, http.MethodGet, entriesPath+"/schema/form", token, nil)
|
||||
for _, want := range []string{`"value":"url"`, `"value":"divider"`, `"value":"acme-page"`, `"value":"acme-list"`, `"dependsOn":"type"`} {
|
||||
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), want) {
|
||||
t.Fatalf("form schema lacks %s: %d %s", want, rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
rec = e.call(t, http.MethodGet, entriesPath+"/fields/reference/dependent-options?values%5Btype%5D=acme-page", token, nil)
|
||||
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"value":"2"`) || !strings.Contains(rec.Body.String(), `"applies":true`) {
|
||||
t.Fatalf("dependent options acme-page: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
rec = e.call(t, http.MethodGet, entriesPath+"/fields/reference/dependent-options?values%5Btype%5D=url", token, nil)
|
||||
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"applies":false`) {
|
||||
t.Fatalf("dependent options url: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// The first entry; the second has no sort_order and goes after it with
|
||||
// zero nest columns.
|
||||
home := e.create(t, token, entriesPath, map[string]any{"menu": mainID, "type": "url", "title": "Home", "url": "/", "target": "_self"})
|
||||
divider := e.create(t, token, entriesPath, map[string]any{"menu": mainID, "type": "divider"})
|
||||
d := e.entry(t, divider)
|
||||
if intOf(d.SortOrder) != intOf(e.entry(t, home).SortOrder)+1 || intOf(d.NestLeft) != 0 || intOf(d.NestRight) != 0 || intOf(d.NestDepth) != 0 || models.Text(d.Title) != "" || d.IsPublished == nil || !*d.IsPublished {
|
||||
t.Fatalf("divider = %+v", d)
|
||||
}
|
||||
page := e.create(t, token, entriesPath, map[string]any{"menu": mainID, "type": "acme-page", "title": "About", "reference": "2", "url_pattern": "/p/:slug"})
|
||||
|
||||
// A child without a menu takes its parent's menu.
|
||||
child := e.create(t, token, entriesPath, map[string]any{"parent": home, "type": "url", "title": "Contact", "url": "mailto:hello@example.test", "target": "_blank"})
|
||||
if c := e.entry(t, child); c.MenuID != mainID || c.ParentID == nil || *c.ParentID != home {
|
||||
t.Fatalf("child = %+v", c)
|
||||
}
|
||||
|
||||
// Refusals: a reference the type does not offer, a javascript: URL, an
|
||||
// unknown target, an unknown type, no menu at all, a parent cycle.
|
||||
e.expect422(t, token, http.MethodPost, entriesPath, map[string]any{"menu": mainID, "type": "acme-page", "title": "Gone", "reference": "99"}, "reference", "")
|
||||
e.expect422(t, token, http.MethodPost, entriesPath, map[string]any{"menu": mainID, "type": "url", "title": "Evil", "url": "javascript:alert(1)"}, "url", "golem15.menu::lang.errors.url_scheme")
|
||||
e.expect422(t, token, http.MethodPost, entriesPath, map[string]any{"menu": mainID, "type": "url", "title": "Top", "url": "/", "target": "_top"}, "target", "golem15.menu::lang.errors.target")
|
||||
e.expect422(t, token, http.MethodPost, entriesPath, map[string]any{"menu": mainID, "type": "page", "title": "Old"}, "type", "")
|
||||
e.expect422(t, token, http.MethodPost, entriesPath, map[string]any{"type": "url", "title": "Nowhere", "url": "/"}, "menu", "golem15.menu::lang.errors.menu_required")
|
||||
e.expect422(t, token, http.MethodPut, fmt.Sprintf("%s/%d", entriesPath, home), map[string]any{"parent": child}, "parent", "golem15.menu::lang.errors.parent_cycle")
|
||||
var n int64
|
||||
e.gdb.Model(&models.MenuEntry{}).Count(&n)
|
||||
if n != 4 {
|
||||
t.Fatalf("refusals left %d entries, want 4", n)
|
||||
}
|
||||
|
||||
// The list filtered on the menu, and the reorder tree of that menu.
|
||||
footerID := e.create(t, token, menusPath, map[string]any{"name": "Footer", "code": "footer", "is_active": true})
|
||||
foreign := e.create(t, token, entriesPath, map[string]any{"menu": footerID, "type": "url", "title": "Imprint", "url": "/imprint"})
|
||||
rec = e.call(t, http.MethodGet, fmt.Sprintf("%s?filter[menu_id]=%d", entriesPath, mainID), token, nil)
|
||||
if rec.Code != http.StatusOK || strings.Contains(rec.Body.String(), "Imprint") || !strings.Contains(rec.Body.String(), "Contact") {
|
||||
t.Fatalf("filtered list: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
rec = e.call(t, http.MethodGet, fmt.Sprintf("%s/reorder?partition=%d", entriesPath, mainID), token, nil)
|
||||
if rec.Code != http.StatusOK || strings.Contains(rec.Body.String(), "Imprint") || !strings.Contains(rec.Body.String(), `"title":"Reorder menu entries"`) {
|
||||
t.Fatalf("reorder tree: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// Nest the acme page under Home after Contact; the divider moves up.
|
||||
nodes := []map[string]any{
|
||||
{"id": home, "parent_id": nil, "sort_order": 1},
|
||||
{"id": child, "parent_id": home, "sort_order": 1},
|
||||
{"id": page, "parent_id": home, "sort_order": 2},
|
||||
{"id": divider, "parent_id": nil, "sort_order": 2},
|
||||
}
|
||||
rec = e.call(t, http.MethodPost, entriesPath+"/reorder", token, map[string]any{"partition": mainID, "nodes": nodes})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("reorder: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if p := e.entry(t, page); p.ParentID == nil || *p.ParentID != home || intOf(p.SortOrder) != 2 {
|
||||
t.Fatalf("reordered page = %+v", p)
|
||||
}
|
||||
if dv := e.entry(t, divider); dv.ParentID != nil || intOf(dv.SortOrder) != 2 {
|
||||
t.Fatalf("reordered divider = %+v", dv)
|
||||
}
|
||||
|
||||
// A reorder naming an entry of another menu changes nothing.
|
||||
nodes = append(nodes, map[string]any{"id": foreign, "parent_id": nil, "sort_order": 3})
|
||||
rec = e.call(t, http.MethodPost, entriesPath+"/reorder", token, map[string]any{"partition": mainID, "nodes": nodes})
|
||||
if rec.Code != http.StatusConflict {
|
||||
t.Fatalf("reorder with a foreign entry: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// Without the permission the screens answer 403.
|
||||
other := e.admin(t)
|
||||
for _, path := range []string{menusPath, entriesPath} {
|
||||
if rec := e.call(t, http.MethodGet, path, other, nil); rec.Code != http.StatusForbidden {
|
||||
t.Errorf("GET %s without permission: %d", path, rec.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
38
models/menu.go
Normal file
38
models/menu.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Menu is a named menu stored on golem15_menu_menus, the PHP
|
||||
// Golem15\Menu\Models\Menu. Sites address a menu by its unique code.
|
||||
type Menu struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Name string `gorm:"column:name"`
|
||||
Code string `gorm:"column:code"`
|
||||
IsActive bool `gorm:"column:is_active"`
|
||||
CSSClass *string `gorm:"column:css_class"`
|
||||
Entries []MenuEntry `gorm:"foreignKey:MenuID" json:"-"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt *time.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
func (Menu) TableName() string { return "golem15_menu_menus" }
|
||||
|
||||
// MorphName is the PHP class string of the model.
|
||||
func (Menu) MorphName() string { return `Golem15\Menu\Models\Menu` }
|
||||
|
||||
// Fillable is the list an admin save may set.
|
||||
func (Menu) Fillable() []string {
|
||||
return []string{"name", "code", "is_active", "css_class"}
|
||||
}
|
||||
|
||||
// Rules require a name and a code that no other menu uses.
|
||||
func (Menu) Rules() map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required",
|
||||
"code": "required|unique:golem15_menu_menus",
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(Menu{})
|
||||
}
|
||||
26
models/menu/columns.yaml
Normal file
26
models/menu/columns.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
# ===================================
|
||||
# List Column Definitions
|
||||
# ===================================
|
||||
|
||||
columns:
|
||||
id:
|
||||
label: golem15.menu::lang.models.general.id
|
||||
searchable: true
|
||||
name:
|
||||
label: golem15.menu::lang.models.menu.name
|
||||
searchable: true
|
||||
code:
|
||||
label: golem15.menu::lang.models.menu.code
|
||||
searchable: true
|
||||
is_active:
|
||||
label: golem15.menu::lang.models.menu.is_active
|
||||
type: switch
|
||||
created_at:
|
||||
label: golem15.menu::lang.models.general.created_at
|
||||
type: datetime
|
||||
sortable: true
|
||||
invisible: true
|
||||
updated_at:
|
||||
label: golem15.menu::lang.models.general.updated_at
|
||||
type: datetime
|
||||
sortable: true
|
||||
25
models/menu/fields.yaml
Normal file
25
models/menu/fields.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
# ===================================
|
||||
# Form Field Definitions
|
||||
# ===================================
|
||||
|
||||
fields:
|
||||
name:
|
||||
label: golem15.menu::lang.models.menu.name
|
||||
type: text
|
||||
span: left
|
||||
required: true
|
||||
code:
|
||||
label: golem15.menu::lang.models.menu.code
|
||||
type: text
|
||||
span: right
|
||||
required: true
|
||||
preset: name
|
||||
css_class:
|
||||
label: golem15.menu::lang.models.menu.css_class
|
||||
type: text
|
||||
span: left
|
||||
is_active:
|
||||
label: golem15.menu::lang.models.menu.is_active
|
||||
type: switch
|
||||
span: right
|
||||
default: true
|
||||
170
models/menu_entry.go
Normal file
170
models/menu_entry.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// The built-in entry types (D-05). Every other type is a menu item type of
|
||||
// the framework's registry (signpost). PHP stored the link type as "link";
|
||||
// the Winter import maps it to TypeURL.
|
||||
const (
|
||||
// TypeURL is a link to the entry's stored URL.
|
||||
TypeURL = "url"
|
||||
// TypeDivider is a separator with no URL.
|
||||
TypeDivider = "divider"
|
||||
)
|
||||
|
||||
// Builtin reports whether typ is a built-in entry type.
|
||||
func Builtin(typ string) bool { return typ == TypeURL || typ == TypeDivider }
|
||||
|
||||
// MenuEntry is one entry of a menu, stored on golem15_menu_menu_entries: the
|
||||
// PHP Golem15\Menu\Models\MenuEntry. Entries form a tree per menu through
|
||||
// parent_id and are ordered by sort_order within their parent. The nest
|
||||
// columns are kept for the PHP schema; nothing reads them.
|
||||
type MenuEntry struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
MenuID uint `gorm:"column:menu_id"`
|
||||
Menu *Menu `gorm:"foreignKey:MenuID" json:"-"`
|
||||
ParentID *uint `gorm:"column:parent_id"`
|
||||
Parent *MenuEntry `gorm:"foreignKey:ParentID" json:"-"`
|
||||
NestLeft *int `gorm:"column:nest_left"`
|
||||
NestRight *int `gorm:"column:nest_right"`
|
||||
NestDepth *int `gorm:"column:nest_depth"`
|
||||
SortOrder *int `gorm:"column:sort_order"`
|
||||
Type string `gorm:"column:type"`
|
||||
Title *string `gorm:"column:title"`
|
||||
URL *string `gorm:"column:url"`
|
||||
URLPattern *string `gorm:"column:url_pattern"`
|
||||
Reference *string `gorm:"column:reference"`
|
||||
Code *string `gorm:"column:code"`
|
||||
Icon *string `gorm:"column:icon"`
|
||||
CSSClass *string `gorm:"column:css_class"`
|
||||
Target *string `gorm:"column:target"`
|
||||
IsPublished *bool `gorm:"column:is_published"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt *time.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
func (MenuEntry) TableName() string { return "golem15_menu_menu_entries" }
|
||||
|
||||
// MorphName is the PHP class string of the model.
|
||||
func (MenuEntry) MorphName() string { return `Golem15\Menu\Models\MenuEntry` }
|
||||
|
||||
// Fillable is the list an admin save may set. The nest columns and
|
||||
// sort_order are not: the save hook and the reorder page set them.
|
||||
func (MenuEntry) Fillable() []string {
|
||||
return []string{"menu_id", "parent_id", "type", "title", "url", "url_pattern", "reference", "code", "icon", "css_class", "target", "is_published"}
|
||||
}
|
||||
|
||||
// Rules require a type; the admin controller checks the type, URL, target,
|
||||
// menu and parent beyond that.
|
||||
func (MenuEntry) Rules() map[string]string {
|
||||
return map[string]string{"type": "required"}
|
||||
}
|
||||
|
||||
// BeforeSave ports the PHP beforeSave: the nest columns become 0 when unset,
|
||||
// an entry with a parent takes the parent's menu, and an entry without a
|
||||
// sort_order goes after the menu's last one. A missing title is stored empty
|
||||
// (dividers have none) and a missing published flag is the PHP default true.
|
||||
func (e *MenuEntry) BeforeSave(tx *gorm.DB) error {
|
||||
zero := func(p **int) {
|
||||
if *p == nil {
|
||||
v := 0
|
||||
*p = &v
|
||||
}
|
||||
}
|
||||
zero(&e.NestLeft)
|
||||
zero(&e.NestRight)
|
||||
zero(&e.NestDepth)
|
||||
if e.Title == nil {
|
||||
empty := ""
|
||||
e.Title = &empty
|
||||
}
|
||||
if e.IsPublished == nil {
|
||||
on := true
|
||||
e.IsPublished = &on
|
||||
}
|
||||
db := tx.Session(&gorm.Session{NewDB: true})
|
||||
if e.ParentID != nil && *e.ParentID != 0 {
|
||||
var menuIDs []uint
|
||||
if err := db.Model(&MenuEntry{}).Where("id = ?", *e.ParentID).Limit(1).Pluck("menu_id", &menuIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(menuIDs) > 0 && menuIDs[0] != 0 {
|
||||
e.MenuID = menuIDs[0]
|
||||
}
|
||||
}
|
||||
if e.SortOrder == nil {
|
||||
var max *int
|
||||
if err := db.Model(&MenuEntry{}).Where("menu_id = ?", e.MenuID).Select("MAX(sort_order)").Scan(&max).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
next := 1
|
||||
if max != nil {
|
||||
next = *max + 1
|
||||
}
|
||||
e.SortOrder = &next
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Text is the value of an optional text column, "" when null.
|
||||
func Text(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(MenuEntry{})
|
||||
}
|
||||
|
||||
// FilterMenu is the config_filter.yaml scope of the menu filter.
|
||||
const FilterMenu = "FilterMenu"
|
||||
|
||||
// FilterScopes lists the scopes the list filters may name.
|
||||
func (MenuEntry) FilterScopes() []string { return []string{FilterMenu} }
|
||||
|
||||
// FilterScope narrows the list to the entries of the chosen menus.
|
||||
func (MenuEntry) FilterScope(name string, db *gorm.DB, value any) *gorm.DB {
|
||||
if db == nil || name != FilterMenu {
|
||||
return db
|
||||
}
|
||||
ids := filterIDs(value)
|
||||
if len(ids) == 0 {
|
||||
return db
|
||||
}
|
||||
return db.Where("menu_id IN ?", ids)
|
||||
}
|
||||
|
||||
// filterIDs reads one id or a list of ids from a filter value.
|
||||
func filterIDs(value any) []uint64 {
|
||||
var raw []any
|
||||
switch v := value.(type) {
|
||||
case []any:
|
||||
raw = v
|
||||
case []string:
|
||||
for _, s := range v {
|
||||
raw = append(raw, s)
|
||||
}
|
||||
default:
|
||||
raw = []any{v}
|
||||
}
|
||||
var out []uint64
|
||||
for _, item := range raw {
|
||||
text := strings.TrimSpace(fmt.Sprint(item))
|
||||
if item == nil || text == "" {
|
||||
continue
|
||||
}
|
||||
if id, err := strconv.ParseUint(text, 10, 32); err == nil && id > 0 {
|
||||
out = append(out, id)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
34
models/menuentry/columns.yaml
Normal file
34
models/menuentry/columns.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
# ===================================
|
||||
# List Column Definitions
|
||||
# ===================================
|
||||
|
||||
columns:
|
||||
id:
|
||||
label: golem15.menu::lang.models.general.id
|
||||
searchable: true
|
||||
menu:
|
||||
label: golem15.menu::lang.models.menuentry.menu
|
||||
searchable: true
|
||||
relation: menu
|
||||
select: name
|
||||
title:
|
||||
label: golem15.menu::lang.models.menuentry.title
|
||||
searchable: true
|
||||
type:
|
||||
label: golem15.menu::lang.models.menuentry.type
|
||||
searchable: true
|
||||
url:
|
||||
label: golem15.menu::lang.models.menuentry.url
|
||||
searchable: true
|
||||
is_published:
|
||||
label: golem15.menu::lang.models.menuentry.is_published
|
||||
type: switch
|
||||
created_at:
|
||||
label: golem15.menu::lang.models.general.created_at
|
||||
type: datetime
|
||||
sortable: true
|
||||
invisible: true
|
||||
updated_at:
|
||||
label: golem15.menu::lang.models.general.updated_at
|
||||
type: datetime
|
||||
sortable: true
|
||||
77
models/menuentry/fields.yaml
Normal file
77
models/menuentry/fields.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
# ===================================
|
||||
# Form Field Definitions
|
||||
# ===================================
|
||||
#
|
||||
# Adapted from the WinterCMS form: the "Menu Entry Options" tab is flattened,
|
||||
# the "Children" tab and partial are gone (the reorder page replaces them),
|
||||
# menu is a relation field (it is also the reorder partition), the type
|
||||
# options are the built-ins plus the registry types, and reference,
|
||||
# url_pattern and target are new.
|
||||
|
||||
fields:
|
||||
menu:
|
||||
label: golem15.menu::lang.models.menuentry.menu
|
||||
comment: golem15.menu::lang.models.menuentry.menu_comment
|
||||
type: relation
|
||||
nameFrom: name
|
||||
span: full
|
||||
parent:
|
||||
label: golem15.menu::lang.models.menuentry.parent
|
||||
type: relation
|
||||
nameFrom: title
|
||||
emptyOption: backend::lang.form.none
|
||||
span: left
|
||||
type:
|
||||
label: golem15.menu::lang.models.menuentry.type
|
||||
type: dropdown
|
||||
options: getTypeOptions
|
||||
default: url
|
||||
required: true
|
||||
span: right
|
||||
title:
|
||||
label: golem15.menu::lang.models.menuentry.title
|
||||
type: text
|
||||
span: left
|
||||
url:
|
||||
label: golem15.menu::lang.models.menuentry.url
|
||||
comment: golem15.menu::lang.models.menuentry.url_comment
|
||||
type: text
|
||||
span: right
|
||||
reference:
|
||||
label: golem15.menu::lang.models.menuentry.reference
|
||||
comment: golem15.menu::lang.models.menuentry.reference_comment
|
||||
type: dropdown
|
||||
dependsOn: type
|
||||
emptyOption: backend::lang.form.none
|
||||
span: left
|
||||
url_pattern:
|
||||
label: golem15.menu::lang.models.menuentry.url_pattern
|
||||
comment: golem15.menu::lang.models.menuentry.url_pattern_comment
|
||||
type: text
|
||||
span: right
|
||||
code:
|
||||
label: golem15.menu::lang.models.menuentry.code
|
||||
type: text
|
||||
span: left
|
||||
icon:
|
||||
label: golem15.menu::lang.models.menuentry.icon
|
||||
type: text
|
||||
span: right
|
||||
css_class:
|
||||
label: golem15.menu::lang.models.menuentry.css_class
|
||||
type: text
|
||||
span: left
|
||||
target:
|
||||
label: golem15.menu::lang.models.menuentry.target
|
||||
type: dropdown
|
||||
default: _self
|
||||
span: right
|
||||
options:
|
||||
_self: golem15.menu::lang.models.menuentry.target_self
|
||||
_blank: golem15.menu::lang.models.menuentry.target_blank
|
||||
is_published:
|
||||
label: golem15.menu::lang.models.menuentry.is_published
|
||||
type: switch
|
||||
default: true
|
||||
context: update
|
||||
span: left
|
||||
15
models/registry.go
Normal file
15
models/registry.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Package models holds the menu plugin's GORM models.
|
||||
package models
|
||||
|
||||
var all []any
|
||||
|
||||
// Register appends GORM models so later files add models without editing
|
||||
// plugin.go.
|
||||
func Register(models ...any) {
|
||||
all = append(all, models...)
|
||||
}
|
||||
|
||||
// All returns every model registered by this package.
|
||||
func All() []any {
|
||||
return all
|
||||
}
|
||||
49
plugin.go
Normal file
49
plugin.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Package menu is the golem15.menu plugin: site menus whose entries are
|
||||
// links, dividers and items of the framework's menu item type registry,
|
||||
// edited in the admin as a tree per menu. A port of the WinterCMS
|
||||
// Golem15.Menu plugin.
|
||||
package menu
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/sm-menu-plugin/models"
|
||||
"git.golem15.com/golem15/sm-menu-plugin/updates"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
_ party.Plugin = (*Plugin)(nil)
|
||||
_ pact.HasMigrations = (*Plugin)(nil)
|
||||
_ pact.HasModels = (*Plugin)(nil)
|
||||
)
|
||||
|
||||
// Plugin is the golem15.menu plugin.
|
||||
type Plugin struct {
|
||||
app *backpack.App
|
||||
}
|
||||
|
||||
func (p *Plugin) ID() string { return "golem15.menu" }
|
||||
|
||||
// Requires is empty: content types come through the framework's menu item
|
||||
// type registry, not from a named plugin.
|
||||
func (p *Plugin) Requires() []string { return nil }
|
||||
|
||||
func (p *Plugin) Register(app *backpack.App) error {
|
||||
p.app = app
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Boot(app *backpack.App) error {
|
||||
p.app = app
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Migrations() []*gormigrate.Migration { return updates.All() }
|
||||
|
||||
func (p *Plugin) Models() []any { return models.All() }
|
||||
|
||||
func init() {
|
||||
party.Register(&Plugin{})
|
||||
}
|
||||
115
testmain_test.go
Normal file
115
testmain_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package menu_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
)
|
||||
|
||||
var (
|
||||
pgContainer *postgres.PostgresContainer
|
||||
pgAdmin *sql.DB
|
||||
pgDSN string
|
||||
pgErr error
|
||||
)
|
||||
|
||||
// TestMain starts one Postgres container for the smoke tests unless the run
|
||||
// is -short. go test passes -test.short=true, so the flag is parsed and read
|
||||
// through testing.Short, never by scanning os.Args.
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
if !testing.Short() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
pgErr = startPostgres(ctx)
|
||||
cancel()
|
||||
if pgErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "golem15.menu: testcontainers postgres: %v\n", pgErr)
|
||||
}
|
||||
}
|
||||
code := m.Run()
|
||||
stopPostgres()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func startPostgres(ctx context.Context) error {
|
||||
ctr, err := postgres.Run(ctx,
|
||||
"postgres:16-alpine",
|
||||
postgres.WithDatabase("golem15_menu"),
|
||||
postgres.WithUsername("golem15_menu"),
|
||||
postgres.WithPassword("golem15_menu"),
|
||||
postgres.BasicWaitStrategies(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pgContainer = ctr
|
||||
dsn, err := ctr.ConnectionString(ctx, "sslmode=disable")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
_ = db.Close()
|
||||
return err
|
||||
}
|
||||
pgAdmin, pgDSN = db, dsn
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopPostgres() {
|
||||
if pgAdmin != nil {
|
||||
_ = pgAdmin.Close()
|
||||
}
|
||||
if pgContainer != nil {
|
||||
_ = testcontainers.TerminateContainer(pgContainer)
|
||||
}
|
||||
}
|
||||
|
||||
// freshDB creates a database of its own for one test and drops it after.
|
||||
func freshDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
if testing.Short() {
|
||||
t.Skip("requires testcontainers postgres")
|
||||
}
|
||||
if pgErr != nil {
|
||||
t.Fatalf("postgres unavailable: %v", pgErr)
|
||||
}
|
||||
if pgAdmin == nil {
|
||||
t.Fatal("postgres unavailable: container was not started")
|
||||
}
|
||||
name := fmt.Sprintf("menu_%d", time.Now().UnixNano())
|
||||
quoted := `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
|
||||
if _, err := pgAdmin.ExecContext(t.Context(), `CREATE DATABASE `+quoted+` TEMPLATE template0 ENCODING 'UTF8'`); err != nil {
|
||||
t.Fatalf("create %s: %v", name, err)
|
||||
}
|
||||
u, err := url.Parse(pgDSN)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
u.Path = "/" + name
|
||||
db, err := sql.Open("pgx", u.String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = db.Close()
|
||||
_, _ = pgAdmin.ExecContext(context.Background(), `DROP DATABASE IF EXISTS `+quoted+` WITH (FORCE)`)
|
||||
})
|
||||
if err := db.PingContext(t.Context()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
33
updates/202610110001_create_golem15_menu_menus.go
Normal file
33
updates/202610110001_create_golem15_menu_menus.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// createMenus creates golem15_menu_menus with the PHP columns (v1.0.2
|
||||
// create_menus_table): a unique code, the active flag (default true) and an
|
||||
// optional CSS class.
|
||||
var createMenus = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610110001_create_golem15_menu_menus",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`CREATE TABLE golem15_menu_menus (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
code VARCHAR(255) NOT NULL UNIQUE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
css_class VARCHAR(255) NULL,
|
||||
created_at TIMESTAMPTZ NULL,
|
||||
updated_at TIMESTAMPTZ NULL
|
||||
)`).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`DROP TABLE IF EXISTS golem15_menu_menus`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(createMenus...)
|
||||
}
|
||||
53
updates/202610110002_create_golem15_menu_menu_entries.go
Normal file
53
updates/202610110002_create_golem15_menu_menu_entries.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// createMenuEntries creates golem15_menu_menu_entries with the PHP columns
|
||||
// (v1.0.1 create_menu_entries_table): the menu, the tree columns (parent_id,
|
||||
// the NestedTree nest columns and sort_order), the entry's type, title, URL
|
||||
// and reference, and its display attributes.
|
||||
var createMenuEntries = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610110002_create_golem15_menu_menu_entries",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
for _, stmt := range []string{
|
||||
`CREATE TABLE golem15_menu_menu_entries (
|
||||
id SERIAL PRIMARY KEY,
|
||||
menu_id INTEGER NOT NULL,
|
||||
parent_id INTEGER NULL,
|
||||
nest_left INTEGER NOT NULL,
|
||||
nest_right INTEGER NOT NULL,
|
||||
nest_depth INTEGER NOT NULL,
|
||||
sort_order INTEGER NULL,
|
||||
type VARCHAR(255) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
url VARCHAR(255) NULL,
|
||||
reference VARCHAR(255) NULL,
|
||||
code VARCHAR(255) NULL,
|
||||
icon VARCHAR(255) NULL,
|
||||
css_class VARCHAR(255) NULL,
|
||||
target VARCHAR(255) NULL,
|
||||
is_published BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMPTZ NULL,
|
||||
updated_at TIMESTAMPTZ NULL
|
||||
)`,
|
||||
`CREATE INDEX golem15_menu_menu_entries_menu_parent_sort_index ON golem15_menu_menu_entries (menu_id, parent_id, sort_order)`,
|
||||
} {
|
||||
if err := tx.Exec(stmt).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`DROP TABLE IF EXISTS golem15_menu_menu_entries`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(createMenuEntries...)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// addURLPattern adds the one column the PHP table lacks: a nullable
|
||||
// url_pattern, the entry's own URL pattern override for registry types
|
||||
// (D-02). Rows copied from PHP keep NULL there.
|
||||
var addURLPattern = []*gormigrate.Migration{
|
||||
{
|
||||
ID: "202610110003_add_url_pattern_to_golem15_menu_menu_entries",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`ALTER TABLE golem15_menu_menu_entries ADD COLUMN url_pattern VARCHAR(255) NULL`).Error
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Exec(`ALTER TABLE golem15_menu_menu_entries DROP COLUMN IF EXISTS url_pattern`).Error
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(addURLPattern...)
|
||||
}
|
||||
17
updates/registry.go
Normal file
17
updates/registry.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Package updates holds the menu plugin's migrations.
|
||||
package updates
|
||||
|
||||
import "github.com/go-gormigrate/gormigrate/v2"
|
||||
|
||||
var all []*gormigrate.Migration
|
||||
|
||||
// Register appends gormigrate steps so later files add migrations without
|
||||
// editing plugin.go.
|
||||
func Register(ms ...*gormigrate.Migration) {
|
||||
all = append(all, ms...)
|
||||
}
|
||||
|
||||
// All returns every migration registered by this package.
|
||||
func All() []*gormigrate.Migration {
|
||||
return all
|
||||
}
|
||||
Reference in New Issue
Block a user