Replaces all hardcoded Go maps with database-driven loading:
Menus (pkg/server/menus.go):
- Queries ir_ui_menu table joined with ir_model_data for XML IDs
- Builds parent/child tree from parent_id relationships
- Resolves appID by walking up to top-level ancestor
- Parses action references ("ir.actions.act_window,123" format)
Actions (pkg/server/action.go):
- Loads from ir_actions_act_window table by integer ID
- Supports XML ID resolution ("sale.action_quotations_with_onboarding")
via ir_model_data lookup
- Builds views array from view_mode string dynamically
- Handles nullable DB columns with helper functions
Seed data (pkg/service/db.go):
- seedActions(): 15 actions with XML IDs in ir_model_data
- seedMenus(): 25 menus with parent/child hierarchy and XML IDs
- Both called during database creation
This mirrors Python Odoo's architecture where menus and actions
are database records loaded from XML data files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
178 lines
4.9 KiB
Go
178 lines
4.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// menuRow holds a single row from the ir_ui_menu table joined with ir_model_data.
|
|
type menuRow struct {
|
|
ID int
|
|
Name string
|
|
ParentID *int // NULL for top-level menus
|
|
Sequence int
|
|
Action *string // e.g. "ir.actions.act_window,1"
|
|
WebIcon *string
|
|
XMLID string // e.g. "contacts.menu_contacts" from ir_model_data
|
|
}
|
|
|
|
// handleLoadMenus returns the menu tree for the webclient.
|
|
// Mirrors: odoo/addons/web/controllers/home.py Home.web_load_menus()
|
|
//
|
|
// Loads menus from the ir_ui_menu table, builds the parent/child tree,
|
|
// and returns the JSON map keyed by string IDs that the webclient expects.
|
|
func (s *Server) handleLoadMenus(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "public, max-age=3600")
|
|
|
|
ctx := r.Context()
|
|
menus, err := s.loadWebMenus(ctx)
|
|
if err != nil {
|
|
log.Printf("menus: error loading menus: %v", err)
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(menus)
|
|
}
|
|
|
|
// loadWebMenus queries ir_ui_menu and builds the JSON structure the webclient expects.
|
|
// Mirrors: odoo/addons/base/models/ir_ui_menu.py IrUiMenu.load_web_menus()
|
|
func (s *Server) loadWebMenus(ctx context.Context) (map[string]interface{}, error) {
|
|
// Query all active menus, joined with ir_model_data for the xmlid.
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT m.id, m.name, m.parent_id, m.sequence, m.action, m.web_icon,
|
|
COALESCE(d.module || '.' || d.name, '') AS xmlid
|
|
FROM ir_ui_menu m
|
|
LEFT JOIN ir_model_data d
|
|
ON d.model = 'ir.ui.menu' AND d.res_id = m.id
|
|
WHERE m.active = true
|
|
ORDER BY m.sequence, m.id`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query ir_ui_menu: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var allMenus []menuRow
|
|
for rows.Next() {
|
|
var mr menuRow
|
|
if err := rows.Scan(&mr.ID, &mr.Name, &mr.ParentID, &mr.Sequence, &mr.Action, &mr.WebIcon, &mr.XMLID); err != nil {
|
|
return nil, fmt.Errorf("scan menu row: %w", err)
|
|
}
|
|
allMenus = append(allMenus, mr)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate menu rows: %w", err)
|
|
}
|
|
|
|
// Build children map: parent_id → list of child IDs
|
|
childrenOf := make(map[int][]int)
|
|
for _, m := range allMenus {
|
|
if m.ParentID != nil {
|
|
childrenOf[*m.ParentID] = append(childrenOf[*m.ParentID], m.ID)
|
|
}
|
|
}
|
|
|
|
// Find the root app ID for each menu by walking up to the top-level ancestor.
|
|
// Top-level menus (parent_id IS NULL) are their own appID.
|
|
menuByID := make(map[int]*menuRow, len(allMenus))
|
|
for i := range allMenus {
|
|
menuByID[allMenus[i].ID] = &allMenus[i]
|
|
}
|
|
|
|
appIDOf := func(id int) int {
|
|
cur := id
|
|
for {
|
|
m, ok := menuByID[cur]
|
|
if !ok || m.ParentID == nil {
|
|
return cur
|
|
}
|
|
cur = *m.ParentID
|
|
}
|
|
}
|
|
|
|
// Build the result map
|
|
result := make(map[string]interface{}, len(allMenus)+1)
|
|
|
|
// Collect top-level menu IDs (parent_id IS NULL) for the root entry
|
|
var rootChildren []int
|
|
for _, m := range allMenus {
|
|
if m.ParentID == nil {
|
|
rootChildren = append(rootChildren, m.ID)
|
|
}
|
|
}
|
|
|
|
// "root" pseudo-entry
|
|
result["root"] = map[string]interface{}{
|
|
"id": "root",
|
|
"name": "root",
|
|
"children": rootChildren,
|
|
"appID": false,
|
|
"xmlid": "",
|
|
"actionID": false,
|
|
"actionModel": false,
|
|
"actionPath": false,
|
|
"webIcon": nil,
|
|
"webIconData": nil,
|
|
"webIconDataMimetype": nil,
|
|
"backgroundImage": nil,
|
|
}
|
|
|
|
// Each menu entry
|
|
for _, m := range allMenus {
|
|
children := childrenOf[m.ID]
|
|
if children == nil {
|
|
children = []int{}
|
|
}
|
|
|
|
actionID, actionModel := parseMenuAction(m.Action)
|
|
appID := appIDOf(m.ID)
|
|
|
|
var webIcon interface{}
|
|
if m.WebIcon != nil && *m.WebIcon != "" {
|
|
webIcon = *m.WebIcon
|
|
}
|
|
|
|
result[strconv.Itoa(m.ID)] = map[string]interface{}{
|
|
"id": m.ID,
|
|
"name": m.Name,
|
|
"children": children,
|
|
"appID": appID,
|
|
"xmlid": m.XMLID,
|
|
"actionID": actionID,
|
|
"actionModel": actionModel,
|
|
"actionPath": false,
|
|
"webIcon": webIcon,
|
|
"webIconData": nil,
|
|
"webIconDataMimetype": nil,
|
|
"backgroundImage": nil,
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// parseMenuAction parses the Odoo action reference format "ir.actions.act_window,123"
|
|
// into an action ID (int or false) and action model (string or false).
|
|
// Mirrors: odoo/addons/base/models/ir_ui_menu.py _compute_action()
|
|
func parseMenuAction(action *string) (interface{}, interface{}) {
|
|
if action == nil || *action == "" {
|
|
return false, false
|
|
}
|
|
parts := strings.SplitN(*action, ",", 2)
|
|
if len(parts) != 2 {
|
|
return false, false
|
|
}
|
|
model := parts[0]
|
|
id, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return false, false
|
|
}
|
|
return id, model
|
|
}
|