- The admin refuses a parent from another menu than the one the form names (errors.parent_menu) instead of moving the entry to the parent's menu without a word; BeforeSave records the named menu (MenuEntry.RequestedMenuID). - An entry moved to another menu moves its descendants in the same transaction, so the public tree no longer drops them.
186 lines
5.6 KiB
Go
186 lines
5.6 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"`
|
|
|
|
// requestedMenuID is the menu a save asked for when BeforeSave replaced
|
|
// it with the parent's menu (see RequestedMenuID).
|
|
requestedMenuID uint
|
|
}
|
|
|
|
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.
|
|
// When the entry named another menu than its parent's, RequestedMenuID
|
|
// reports the menu it named; the admin refuses such a save.
|
|
func (e *MenuEntry) BeforeSave(tx *gorm.DB) error {
|
|
e.requestedMenuID = 0
|
|
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 {
|
|
if e.MenuID != 0 && e.MenuID != menuIDs[0] {
|
|
e.requestedMenuID = e.MenuID
|
|
}
|
|
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
|
|
}
|
|
|
|
// RequestedMenuID is the menu the last save of e named when BeforeSave
|
|
// replaced it with the parent's menu: the parent belongs to another menu.
|
|
// It is 0 when the entry named no menu or its parent's.
|
|
func (e *MenuEntry) RequestedMenuID() uint { return e.requestedMenuID }
|
|
|
|
// 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
|
|
}
|