fix(17): WR-01 keep a menu entry's subtree and parent in its menu

- 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.
This commit is contained in:
2026-10-12 21:30:36 +02:00
parent 43267de323
commit 7255d34ae0
6 changed files with 69 additions and 6 deletions

View File

@@ -24,6 +24,7 @@ const (
errTypeUnknown = "golem15.menu::lang.errors.type_unknown"
errMenuRequired = "golem15.menu::lang.errors.menu_required"
errParentCycle = "golem15.menu::lang.errors.parent_cycle"
errParentMenu = "golem15.menu::lang.errors.parent_menu"
labelTypeURL = "golem15.menu::lang.models.menuentry.type_url"
labelTypeDivide = "golem15.menu::lang.models.menuentry.type_divider"
)
@@ -183,11 +184,33 @@ func (menuEntriesAdminController) FormAfterCreate(ctx context.Context, model any
}
// FormAfterUpdate also refuses a parent that is the entry itself or one of
// its descendants.
// its descendants, then moves the entry's descendants into its menu, so a
// subtree follows its root to another menu instead of staying behind with
// parents in a menu it is not in.
func (menuEntriesAdminController) FormAfterUpdate(ctx context.Context, model any) error {
return checkPlacement(ctx, model)
if err := checkPlacement(ctx, model); err != nil {
return err
}
entry, ok := model.(*models.MenuEntry)
if !ok || entry == nil || entry.ID == 0 {
return nil
}
tx, ok := cabana.TxFromContext(ctx)
if !ok {
return errors.New("golem15.menu: the entry check needs the save's transaction")
}
return tx.Session(&gorm.Session{NewDB: true}).WithContext(ctx).Exec(`WITH RECURSIVE sub(id) AS (
SELECT id FROM golem15_menu_menu_entries WHERE parent_id = ? AND id <> ?
UNION
SELECT e.id FROM golem15_menu_menu_entries e JOIN sub ON e.parent_id = sub.id
)
UPDATE golem15_menu_menu_entries SET menu_id = ? WHERE id IN (SELECT id FROM sub) AND id <> ? AND menu_id <> ?`,
entry.ID, entry.ID, entry.MenuID, entry.ID, entry.MenuID).Error
}
// checkPlacement refuses an entry in no menu, a parent from another menu
// than the one the entry names (BeforeSave would otherwise move the entry to
// the parent's menu without a word) and a parent cycle.
func checkPlacement(ctx context.Context, model any) error {
entry, ok := model.(*models.MenuEntry)
if !ok || entry == nil {
@@ -196,6 +219,9 @@ func checkPlacement(ctx context.Context, model any) error {
if entry.MenuID == 0 {
return &cabana.ValidationError{Details: map[string]any{"menu": []string{errMenuRequired}}}
}
if entry.RequestedMenuID() != 0 {
return &cabana.ValidationError{Details: map[string]any{"parent": []string{errParentMenu}}}
}
if entry.ParentID == nil {
return nil
}