- 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
171 lines
5.0 KiB
Go
171 lines
5.0 KiB
Go
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
|
|
}
|