Snippet insert — Single-click UX
Snippet insert — Single-click UX
Phần tiêu đề “Snippet insert — Single-click UX”Deep dive kỹ thuật cho Admin / Dev — hiểu cách sidebar chèn Jinja snippet vào OnlyOffice editor. Sales rep thường không cần đọc trang này.
Vấn đề
Phần tiêu đề “Vấn đề”OnlyOffice editor chạy trong iframe cross-origin (oo.parkone.vn) — Odoo sidebar chạy trên parkone.bsdinsights.com. Không thể trực tiếp gọi API OnlyOffice từ Odoo JS.
Giải pháp: postMessage IPC với OnlyOffice plugin sidecar.
Kiến trúc
Phần tiêu đề “Kiến trúc”┌─────────────────────────────────────────────┐│ Odoo Backend (parkone.bsdinsights.com) ││ ├── Sidebar OWL component ││ ├── Click field → postMessage() ││ └── target: OnlyOffice iframe.contentWindow │└──────────┬──────────────────────────────────┘ │ postMessage({type: 'insert', text: '...'}) ↓┌─────────────────────────────────────────────┐│ OnlyOffice iframe (oo.parkone.vn) ││ ├── Plugin sidecar loaded on startup ││ ├── window.addEventListener('message', ...)││ └── Receive message → Asc.plugin.callCommand│└──────────┬──────────────────────────────────┘ │ Asc.plugin.callCommand() ↓┌─────────────────────────────────────────────┐│ OnlyOffice Docs API ││ ├── GetRangeBySelect() — get cursor ││ ├── AddText(snippet) ││ └── (fallback chain if fail) │└─────────────────────────────────────────────┘Flow chi tiết
Phần tiêu đề “Flow chi tiết”1. Sidebar init
Phần tiêu đề “1. Sidebar init”Odoo OWL component mount → find OnlyOffice iframe → save reference oo_frame.
2. Plugin sidecar load
Phần tiêu đề “2. Plugin sidecar load”OnlyOffice startup → load plugin từ Odoo URL /parkone/doc/oo-plugin/index.html. Plugin đăng ký:
- postMessage receiver
Asc.plugin.initcallback
3. Sidebar broadcast hello
Phần tiêu đề “3. Sidebar broadcast hello”Sidebar send {type: 'parkone-hello'} tới:
window.top(nếu iframe nested)window.parentoo_frame.contentWindow
Multi-target vì OnlyOffice có thể có nested iframes.
4. Plugin ACK hello
Phần tiêu đề “4. Plugin ACK hello”Plugin nhận hello → send back {type: 'parkone-ack'} với plugin GUID.
Sidebar cache GUID → dùng cho subsequent messages.
5. User click field
Phần tiêu đề “5. User click field”Sidebar → insertCurated(snippet, label):
- Debounce 500ms (prevent double-click)
- postMessage
{type: 'insert', text: snippet, label: label}
6. Plugin receive + insert
Phần tiêu đề “6. Plugin receive + insert”Plugin onMessage handler:
- Verify sender is Odoo iframe
- Call
Asc.plugin.callCommand(function() { ... })
7. Insert chain (fallback)
Phần tiêu đề “7. Insert chain (fallback)”Callback tries 3 approaches sequentially:
// Approach 1: Selection rangevar range = Api.GetRangeBySelect();if (range) { range.AddText(snippet); return;}
// Approach 2: Last paragraphvar doc = Api.GetDocument();var lastPara = doc.GetElement(doc.GetElementsCount() - 1);if (lastPara) { lastPara.AddElement(Api.CreateRun(snippet)); return;}
// Approach 3: Push new paragraphdoc.Push(Api.CreateParagraph()).AddElement(Api.CreateRun(snippet));Fallback ensures insert always success dù cursor state khác nhau.
8. OnlyOffice update UI
Phần tiêu đề “8. OnlyOffice update UI”- Text
{{ record.customer_id.name }}xuất hiện tại vị trí cursor - Style highlighted (nếu template có Jinja syntax highlight)
Debouncing
Phần tiêu đề “Debouncing”Browser fires 2 events khi user double-click:
clickevent (index 1)clickevent (index 2)dblclickevent
Nếu handler bind cả 3 → chèn 3 lần. Fix:
onClickInsert(e, fn) { if (e.detail !== 1) return; // Only single click if (this._insertPending) return; this._insertPending = true; setTimeout(() => this._insertPending = false, 500); fn();}e.detail === 1 = single click (2 = double, 3 = triple).
Cache-buster URL
Phần tiêu đề “Cache-buster URL”Plugin JS URL có random suffix ?v=abc123 — force reload sau mỗi Odoo deploy:
const pluginUrl = `/parkone/doc/oo-plugin/code.js?v=${Math.random().toString(36).slice(2)}`;Không dùng suffix → browser cache stale JS → new feature không hoạt động cho user cũ.
Race condition — plugin chưa ready
Phần tiêu đề “Race condition — plugin chưa ready”Sidebar có thể send message trước khi plugin load xong. Fix:
- Sidebar send hello lặp lại 3 lần mỗi 500ms
- Plugin cache GUID sau nhận hello lần đầu
- Nếu 3 lần vẫn không ACK → sidebar show “Plugin loading…” indicator
Insert vào cell bảng — edge case
Phần tiêu đề “Insert vào cell bảng — edge case”OnlyOffice API Api.GetRangeBySelect() trong table cell không stable — có thể return null.
Fix: sidebar detect user đang trong cell → gợi ý user click ngoài cell trước, rồi cut+paste vào cell sau.
Ngăn frustration khi insert vào bảng không hoạt động.
Cursor jump after insert
Phần tiêu đề “Cursor jump after insert”v1 bug: sau insert, cursor nhảy về cuối doc → user không thấy chỗ vừa chèn.
v2 fix: simplify chain — chỉ GetRangeBySelect → AddText, không dùng Push. Cursor stay tại vị trí.
Cross-DB session
Phần tiêu đề “Cross-DB session”Nếu user có multi-workspace (multi-tenant), plugin phải include DB name trong hello để avoid cross-tenant leak:
postMessage({ type: 'parkone-hello', db: '<workspace_db_name>', partner: '<partner_id_of_current_record>'});Plugin verify DB match → refuse insert nếu mismatch.
Performance
Phần tiêu đề “Performance”- Hello → ACK: ~50ms
- Click → insert visible: ~200ms
- 100 fields sidebar render:
<1s(OWL fast)
User cảm giác instant.
Debug
Phần tiêu đề “Debug”Console logs
Phần tiêu đề “Console logs”Sidebar + Plugin đều log to console với prefix:
[Parkone] Sidebar hello sent to top/parent[Parkone] Plugin ACK received, GUID: xxx[Parkone] Insert command dispatched: <snippet>[OnlyOffice API] Insert via GetRangeBySelect (approach 1)
Filter console: [Parkone] để xem full flow.
Sidebar không thấy iframe
Phần tiêu đề “Sidebar không thấy iframe”- OnlyOffice iframe chưa mount xong → wait 2s rồi retry
- Iframe cross-origin block → verify JWT setup
Insert không hoạt động (silent fail)
Phần tiêu đề “Insert không hoạt động (silent fail)”- Plugin không nhận postMessage → check event listener registered
- callCommand fail → check OnlyOffice API version compatible
Advanced: custom insert handler
Phần tiêu đề “Advanced: custom insert handler”Nếu Đại muốn custom logic (vd chèn với formatting đặc biệt), extend plugin:
// Trong /parkone/doc/oo-plugin/code.jswindow.addEventListener('message', (ev) => { if (ev.data.type === 'insert-bold') { Asc.plugin.callCommand(function() { var range = Api.GetRangeBySelect(); var text = Api.CreateRun(ev.data.text); text.SetBold(true); range.AddElement(text); }); }});Sidebar send {type: 'insert-bold', text: '...'} → chèn text in bold.
Insert nhưng text không xuất hiện
Phần tiêu đề “Insert nhưng text không xuất hiện”- Verify plugin loaded:
Asc.pluginkhông null - Verify hello ACK: check console
- Verify GetRangeBySelect return non-null
Insert 2 lần liền
Phần tiêu đề “Insert 2 lần liền”- Debounce fail? Check
_insertPendingstate - Browser cache old JS — force reload (Ctrl+Shift+R)
Snippet có & hoặc < — render escape?
Phần tiêu đề “Snippet có & hoặc < — render escape?”OnlyOffice AddText treat text as plain — không HTML escape. Nếu snippet có & (Jinja HTML entity) → sẽ giữ nguyên → sai. Dùng Unicode char thẳng: & không phải &.