Bỏ qua để đến nội dung

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.

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.

┌─────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────┘

Odoo OWL component mount → find OnlyOffice iframe → save reference oo_frame.

OnlyOffice startup → load plugin từ Odoo URL /parkone/doc/oo-plugin/index.html. Plugin đăng ký:

  • postMessage receiver
  • Asc.plugin.init callback

Sidebar send {type: 'parkone-hello'} tới:

  • window.top (nếu iframe nested)
  • window.parent
  • oo_frame.contentWindow

Multi-target vì OnlyOffice có thể có nested iframes.

Plugin nhận hello → send back {type: 'parkone-ack'} với plugin GUID.

Sidebar cache GUID → dùng cho subsequent messages.

Sidebar → insertCurated(snippet, label):

  • Debounce 500ms (prevent double-click)
  • postMessage {type: 'insert', text: snippet, label: label}

Plugin onMessage handler:

  • Verify sender is Odoo iframe
  • Call Asc.plugin.callCommand(function() { ... })

Callback tries 3 approaches sequentially:

// Approach 1: Selection range
var range = Api.GetRangeBySelect();
if (range) {
range.AddText(snippet);
return;
}
// Approach 2: Last paragraph
var doc = Api.GetDocument();
var lastPara = doc.GetElement(doc.GetElementsCount() - 1);
if (lastPara) {
lastPara.AddElement(Api.CreateRun(snippet));
return;
}
// Approach 3: Push new paragraph
doc.Push(Api.CreateParagraph()).AddElement(Api.CreateRun(snippet));

Fallback ensures insert always success dù cursor state khác nhau.

  • Text {{ record.customer_id.name }} xuất hiện tại vị trí cursor
  • Style highlighted (nếu template có Jinja syntax highlight)

Browser fires 2 events khi user double-click:

  1. click event (index 1)
  2. click event (index 2)
  3. dblclick event

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).

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ũ.

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

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.

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ỉ GetRangeBySelectAddText, không dùng Push. Cursor stay tại vị trí.

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.

  • Hello → ACK: ~50ms
  • Click → insert visible: ~200ms
  • 100 fields sidebar render: <1s (OWL fast)

User cảm giác instant.

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.

  • OnlyOffice iframe chưa mount xong → wait 2s rồi retry
  • Iframe cross-origin block → verify JWT setup
  • Plugin không nhận postMessage → check event listener registered
  • callCommand fail → check OnlyOffice API version compatible

Nếu Đại muốn custom logic (vd chèn với formatting đặc biệt), extend plugin:

// Trong /parkone/doc/oo-plugin/code.js
window.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.

  • Verify plugin loaded: Asc.plugin không null
  • Verify hello ACK: check console
  • Verify GetRangeBySelect return non-null
  • Debounce fail? Check _insertPending state
  • Browser cache old JS — force reload (Ctrl+Shift+R)

OnlyOffice AddText treat text as plain — không HTML escape. Nếu snippet có &amp; (Jinja HTML entity) → sẽ giữ nguyên → sai. Dùng Unicode char thẳng: & không phải &amp;.

Chia sẻ: