Files
goodie/pkg/server/action.go
Marc cc823d3310 Fix navbar: SVG logos, Settings menu, Logout handler
- Image handler now returns SVG placeholders for company logo and user
  avatars instead of broken 1x1 PNG (fixes yellow border in navbar)
- Supports both query-param (?model=&field=) and path-style URLs
  (/web/image/res.partner/2/avatar_128)
- Added Settings app menu with Users & Technical sub-menus
- Added actions for Settings (company form), Users list, Sequences
- Added /web/session/logout handler that clears session + cookie
- Added logout to middleware whitelist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 01:31:37 +02:00

300 lines
8.8 KiB
Go

package server
import (
"encoding/json"
"net/http"
)
// handleLoadBreadcrumbs returns breadcrumb data for the current navigation path.
// Mirrors: odoo/addons/web/controllers/action.py Action.load_breadcrumbs()
func (s *Server) handleLoadBreadcrumbs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req JSONRPCRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeJSONRPC(w, nil, nil, &RPCError{Code: -32700, Message: "Parse error"})
return
}
s.writeJSONRPC(w, req.ID, []interface{}{}, nil)
}
// handleActionLoad loads an action definition by ID.
// Mirrors: odoo/addons/web/controllers/action.py Action.load()
func (s *Server) handleActionLoad(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req JSONRPCRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeJSONRPC(w, nil, nil, &RPCError{Code: -32700, Message: "Parse error"})
return
}
var params struct {
ActionID interface{} `json:"action_id"`
Context interface{} `json:"context"`
}
json.Unmarshal(req.Params, &params)
// Parse action_id from params (can be float64 from JSON or string)
actionID := 0
switch v := params.ActionID.(type) {
case float64:
actionID = int(v)
case string:
// Try to parse numeric string
for _, c := range v {
if c >= '0' && c <= '9' {
actionID = actionID*10 + int(c-'0')
} else {
actionID = 0
break
}
}
}
// Action definitions by ID
actions := map[int]map[string]interface{}{
1: {
"id": 1,
"type": "ir.actions.act_window",
"name": "Contacts",
"res_model": "res.partner",
"view_mode": "list,kanban,form",
"views": [][]interface{}{{nil, "list"}, {nil, "kanban"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "contacts.action_contacts",
},
2: {
"id": 2,
"type": "ir.actions.act_window",
"name": "Invoices",
"res_model": "account.move",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": `[("move_type","in",["out_invoice","out_refund"])]`,
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "account.action_move_out_invoice_type",
},
3: {
"id": 3,
"type": "ir.actions.act_window",
"name": "Sale Orders",
"res_model": "sale.order",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "sale.action_quotations_with_onboarding",
},
4: {
"id": 4,
"type": "ir.actions.act_window",
"name": "CRM Pipeline",
"res_model": "crm.lead",
"view_mode": "kanban,list,form",
"views": [][]interface{}{{nil, "kanban"}, {nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "crm.crm_lead_all_pipeline",
},
5: {
"id": 5,
"type": "ir.actions.act_window",
"name": "Transfers",
"res_model": "stock.picking",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "stock.action_picking_tree_all",
},
6: {
"id": 6,
"type": "ir.actions.act_window",
"name": "Products",
"res_model": "product.template",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "stock.action_product_template",
},
7: {
"id": 7,
"type": "ir.actions.act_window",
"name": "Purchase Orders",
"res_model": "purchase.order",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "purchase.action_purchase_orders",
},
8: {
"id": 8,
"type": "ir.actions.act_window",
"name": "Employees",
"res_model": "hr.employee",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "hr.action_hr_employee",
},
9: {
"id": 9,
"type": "ir.actions.act_window",
"name": "Departments",
"res_model": "hr.department",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "hr.action_hr_department",
},
10: {
"id": 10,
"type": "ir.actions.act_window",
"name": "Projects",
"res_model": "project.project",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "project.action_project",
},
11: {
"id": 11,
"type": "ir.actions.act_window",
"name": "Tasks",
"res_model": "project.task",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "project.action_project_task",
},
12: {
"id": 12,
"type": "ir.actions.act_window",
"name": "Vehicles",
"res_model": "fleet.vehicle",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "fleet.action_fleet_vehicle",
},
100: {
"id": 100,
"type": "ir.actions.act_window",
"name": "Settings",
"res_model": "res.company",
"res_id": 1,
"view_mode": "form",
"views": [][]interface{}{{nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "base.action_res_company_form",
},
101: {
"id": 101,
"type": "ir.actions.act_window",
"name": "Users",
"res_model": "res.users",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "base.action_res_users",
},
102: {
"id": 102,
"type": "ir.actions.act_window",
"name": "Sequences",
"res_model": "ir.sequence",
"view_mode": "list,form",
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
"search_view_id": false,
"domain": "[]",
"context": "{}",
"target": "current",
"limit": 80,
"help": "",
"xml_id": "base.ir_sequence_form",
},
}
action, ok := actions[actionID]
if !ok {
// Default to Contacts if unknown action ID
action = actions[1]
}
s.writeJSONRPC(w, req.ID, action, nil)
}