เรียนรู้สาเหตุที่ Charts และ Number Cards ไม่แสดงใน Frappe Workspace และวิธีแก้ปัญหา Label Matching ที่นักพัฒนาหลายคนไม่รู้

ปัญหาที่นักพัฒนา Frappe หลายคนเจอคือ Charts และ Number Cards ไม่แสดงใน Workspace แม้ว่าจะตั้งค่าทุกอย่างถูกต้อง ข้อมูลมีใน Database และ API ก็ Return ค่าถูก บทความนี้จะเปิดเผยสาเหตุที่ซ่อนอยู่
Charts และ Number Cards ไม่ Render ใน Workspace แม้ว่า:
Frappe's Workspace Block Rendering Code (frappe/public/js/frappe/views/workspace/blocks/block.js) ใช้ Label Matching ในการหา Chart และ Number Card Data:
Key Insight: Code เปรียบเทียบ obj.label กับ block_name (ซึ่งมาจาก chart_name หรือ number_card_name ใน Content JSON) ถ้าไม่ตรงกัน Block จะ Return false และไม่ Render อะไรเลย - แบบ Silent
ใน Workspace Child Tables, ฟิลด์ label ต้องตรงกับ chart_name หรือ number_card_name เป๊ะ
"number_cards": [
{
"label": "Active Items",
"number_card_name": "App Active Items"
}
]ปัญหา: "Active Items" ≠ "App Active Items"
"number_cards": [
{
"label": "App Active Items",
"number_card_name": "App Active Items"
}
]Label และ number_card_name ตรงกัน
"charts": [
{
"chart_name": "Sales Trend",
"label": "Sales Trend" // ต้องตรงกับ chart_name
}
]"number_cards": [
{
"number_card_name": "Active Items",
"label": "Active Items" // ต้องตรงกับ number_card_name
}
]"shortcuts": [
{
"label": "Sales Order",
"link_to": "Sales Order",
"type": "DocType",
"color": "Blue",
"doc_view": "List"
}
]"links": [
{
"label": "Masters",
"type": "Card Break",
"link_count": 8
},
{
"label": "Item",
"link_to": "Item",
"link_type": "DocType",
"type": "Link",
"onboard": 1
}
]frappe.cache().flushall()
frappe.clear_cache()
frappe.clear_document_cache('Workspace', 'Your Workspace')
frappe.db.commit()หรือใช้ Bash:
bench --site your-site.local clear-cache
redis-cli FLUSHALLCmd+Shift+R (Mac) หรือ Ctrl+Shift+R (Windows)| ปัญหา | อาการ | วิธีแก้ |
|---|---|---|
| Label Mismatch | Charts/Cards ไม่ Render, ไม่มี Error | Match label กับ chart_name/number_card_name |
| Module Mismatch | บาง Elements หายไป | ใช้ Module เดียวกันสำหรับ Workspace, Charts, Cards |
| Permission Issues | Charts/Cards ว่างเปล่า | Set is_public: 1 บน Charts และ Cards |
| Cache Not Cleared | แสดงข้อมูลเก่า | Clear Cache ทั้ง Server และ Browser |
Frappe ถือว่า Workspaces เป็น User-customizable Data ไม่ใช่ Configuration ซึ่งหมายความว่า:
bench migrate ไม่ได้ Overwrite Workspace Records ที่มีอยู่bench update ไม่ได้ Apply การเปลี่ยนแปลงจาก JSONfrappe.reload_doc() ใช้ไม่ได้ กับ Workspacesวิธีเดียวที่เชื่อถือได้สำหรับอัพเดท Workspaces:
สร้าง Module สำหรับจัดการ Workspace:
# your_app/setup/workspace_manager.py
# ลงทะเบียนใน hooks.py:
after_migrate = [
"your_app.setup.workspace_manager.sync_workspaces"
]Field col ใช้ 12-column Grid System คำนวณจำนวน Cards ต่อ Row ด้วย 12 / col:
ค่า col | Cards ต่อ Row | Use Case |
|---|---|---|
2 | 6 ต่อ Row | Metrics ขนาดเล็กจำนวนมาก |
3 | 4 ต่อ Row | Dashboard มาตรฐาน |
4 | 3 ต่อ Row | Cards ขนาดกลาง |
6 | 2 ต่อ Row | Charts คู่ |
12 | 1 ต่อ Row | Headers, Spacers เต็มความกว้าง |
Number Cards ต้อง Match ข้าม 3 ตำแหน่ง:
Content JSON (number_card_name) → Child Table (label) → Number Card Document (name)
"Total Sales" == "Total Sales" == "Total Sales"
Backend get_number_cards() ยัง Translate Labels ผ่าน _(label) ถ้ามี Translation ภาษาไทยสำหรับข้อความ Label ค่าที่ JS ได้รับจะเป็นค่าที่แปลแล้ว ใช้ชื่อที่ไม่ต้องแปล (ใส่ Prefix เช่น "App_") หรือดูแลให้ Translation สอดคล้องกัน
ใน Developer Mode frappe.delete_doc('Workspace', name) จะ ลบ JSON File จาก Disk ด้วย เพื่อป้องกัน ให้ตั้ง in_migrate flag:
frappe.flags.in_migrate = True # ป้องกันการลบ JSON file
frappe.delete_doc('Workspace', 'Your Workspace',
force=True, ignore_permissions=True, delete_permanently=True)
frappe.db.commit()ถ้าไม่ตั้ง in_migrate = True JSON Source File จะถูกลบ และต้อง Recreate จาก Git
label = chart_name = number_card_nameApp_ สำหรับทุก Elementsin_migrate = True ก่อนลบ เพื่อรักษา JSON Fileปัญหา Charts และ Number Cards ไม่แสดงใน Frappe Workspace มักเกิดจาก Label Mismatch กฎง่ายๆ คือ:
Label ต้องตรงกับ chart_name/number_card_name เป๊ะ!
Frappe ไม่แสดง Error เมื่อหาไม่เจอ ทำให้ Debug ยาก แต่เมื่อรู้สาเหตุแล้ว การแก้ไขก็ตรงไปตรงมา
// block.js lines 8-14
make(block, block_name, widget_type = block) {
let block_data = this.config.page_data[block + "s"].items.find((obj) => {
return (
frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(__(block_name))
);
});
if (!block_data) return false; // Silent failure![
{"type": "header", "data": {"text": "<span class=\"h4\"><b>Dashboard</b></span>", "col": 12}},
{"type": "chart", "data": {"chart_name": "Sales Trend", "col": 6}},
{"type": "number_card", "data": {"number_card_name": "Active Items", "col": 2}}
]# ใน bench console
ws = frappe.get_doc('Workspace', 'Your Workspace')
print("Number Cards Label Check:")
for card in ws.number_cards:
match = card.label == card.number_card_name
status = "✓" if match else "✗ MISMATCH"
print(f" {status} label: '{card.label}' vs name: '{card.number_card_name}'")
print("\nCharts Label Check:")
for chart in ws.charts:
match = chart.label == chart.chart_name
status = "✓" if match else "✗ MISMATCH"
print(f" {status} label: '{chart.label}' vs name: '{chart.chart_name}'"){
"doctype": "Workspace",
"name": "Sales Dashboard",
"label": "Sales Dashboard",
"module": "Selling",
"icon": "chart-line",
"public": 1,
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<b>Sales Overview</b>\",\"col\":12}},{\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Sales\",\"col\":3}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Monthly Sales\",\"col\":6}}]",
"charts": [
{
"chart_name": "Monthly Sales",
"label": "Monthly Sales"
}
],
"number_cards": [
{
"number_card_name": "Total Sales",
"label": "Total Sales"
}
],
"shortcuts": [
{
"label": "Sales Order",
"link_to": "Sales Order",
"type": "DocType",
"doc_view": "List"
}
]
}# 1. ลบ Workspace และ Child Tables ที่มีอยู่
frappe.db.delete("Workspace", {"name": workspace_name})
frappe.db.delete("Workspace Link", {"parent": workspace_name})
frappe.db.delete("Workspace Shortcut", {"parent": workspace_name})
frappe.db.delete("Workspace Chart", {"parent": workspace_name})
frappe.db.delete("Workspace Number Card", {"parent": workspace_name})
# 2. Insert ใหม่จาก JSON
doc = frappe.get_doc(workspace_data)
doc.flags.ignore_permissions = True
doc.flags.ignore_links = True
doc.insert(ignore_permissions=True)
# 3. Commit และ Clear Cache
frappe.db.commit()
frappe.cache().flushall()