Frontend assets
MailDesk's UI is an Owl single-page client action loaded into the Odoo web backend. Basic ships the full desktop client; Pro extends it in place — almost entirely through Odoo's patch() mechanism plus a small set of new components, services, and a mobile-adaptive layer. This page maps the OWL components, services, hooks, registries, asset bundles, and the extension model.
MailDesk's UI is an Owl single-page client action loaded into the Odoo
web backend. Basic ships the full desktop client; Pro extends it in place — almost entirely
through Odoo's patch() mechanism plus a small set of new components, services, and a mobile-adaptive
layer. This page maps the OWL components, services, hooks, registries, asset bundles, and the
extension model.
All frontend files live under each module's static/src/ and carry the proprietary Metzler IT license
header.
1. Asset bundles
Both modules declare assets into the standard Odoo backend bundle (no custom bundle keys):
| Bundle | Basic | Pro |
|---|---|---|
web.assets_backend |
full client (SCSS + JS + XML templates) | extension SCSS/JS/XML, mobile layer, AI surface, patches |
web.assets_unit_tests |
Hoot tests (static/tests/*.test.js) |
Hoot tests |
Notes:
- Basic loads web/static/lib/jquery/jquery.js first, then scss/maildesk.scss, then services →
hooks → utils → registries → fields → the maildesk.esm.js action → composer → components/dialogs.
- Pro loads its SCSS first (theme tokens → shared panels → AI → adaptive → desktop dark), then its
services, hooks, mobile components, AI-surface components, and finally the patches/* (so the base
classes exist before they are patched). There is no separate dark-mode or mobile bundle — the
adaptive/dark assets ship unconditionally with Pro and are activated at runtime (see §6).
- Asset order within a bundle is load order; Pro's patches are listed last in the manifest for
exactly this reason.
Source: maildesk_mail_client/__manifest__.py and maildesk_mail_client_pro/__manifest__.py
assets keys.
2. Basic frontend (maildesk_mail_client/static/src)
2.1 Client action
js/maildesk.esm.js defines class MailDesk extends Component
(static template = "maildesk_mail_client.MailDesk") and registers it as the client action:
registry.category("actions").add("maildesk_mail_client.maildesk_action", MailDesk);
It is the shell: it owns initial data loading, message/thread loading and pagination, folder/account
selection, the composer launch handlers, optimistic delete with undo, and URL-hash routing. Its
static components = { FolderTree, MailList, MailDetail, UndoToast }. It wires action callbacks onto
the maildesk.store service so child components can trigger them, and starts/stops the
maildesk.sync bus listeners on mount/unmount.
2.2 OWL components (16)
| # | Component | Path (under static/src/) |
Role |
|---|---|---|---|
| 1 | FolderTree |
components/maildesk/folder_tree/ |
account + folder sidebar tree |
| 2 | MailList |
components/maildesk/mail_list/ |
message list / pagination |
| 3 | MailDetail |
components/maildesk/mail_detail/ |
reading pane + toolbar host |
| 4 | ThreadContainer |
components/maildesk/thread_container/ |
thread view container |
| 5 | ThreadMessage |
components/maildesk/thread_message/ |
one message in a thread (iframe body) |
| 6 | Toolbar |
components/maildesk/toolbar/ |
detail/list action toolbar |
| 7 | FilterMenu |
components/maildesk/filter_menu/ |
filter controls |
| 8 | UndoToast |
components/maildesk/undo_toast/ |
undoable trash toast |
| 9 | DebugStats |
components/maildesk/debug_stats/ |
developer diagnostics overlay |
| 10 | AssignTagsDialog |
components/dialogs/assign_tags/ |
tag-assignment dialog |
| 11 | ContactPickerDialog |
components/dialogs/contact_picker/ |
contact picker dialog |
| 12 | MoveToFolderDialog |
components/dialogs/move_to_folder/ |
move-to-folder dialog |
| 13 | PartnerCardPopover |
components/popovers/partner_card/ |
sender card popover |
| 14 | EmailInputField |
components/fields/email_input/ |
recipient input field widget |
| 15 | ComposeSystray |
components/composer/ |
systray "compose" launcher |
| 16 | Composer hub | core/composer/ |
ComposerContainer + window/input/bubble/record |
The composer is built from several files under core/composer/ (composer_container,
composer_window, composer_input, composer_bubble, composer_record, composer_shortcuts,
composer_attachment_list). ComposerContainer is mounted once via the main_components registry
(core/composer/main_components.esm.js), mirroring Odoo Mail's global-composer pattern.
2.3 Services (4)
Registered in registry.category("services"):
| Service name | File | Dependencies | Role |
|---|---|---|---|
maildesk.store |
js/services/maildesk_store.esm.js |
— | reactive client state (accounts, folders, messages, threads, selection, filters) |
maildesk.sync |
js/services/maildesk_sync.esm.js |
orm, bus_service, maildesk.store, maildesk.notification, multi_tab |
websocket bus listeners + optional background polling; account-scoped channels |
maildesk.notification |
js/services/maildesk_notification.esm.js |
— | in-app + native notification handling |
maildesk.composer |
core/composer/composer_service.esm.js |
— | global composer open/close (reply/replyAll/forward/draft/new) |
The MailDesk action consumes all four via useService(...); maildesk.sync is the bus client that
keeps the open UI fresh after a scheduled sync ingests a message (see
Realtime architecture §1.2).
2.4 Hooks (2)
| Hook | File | Role |
|---|---|---|
useIframeResize |
hooks/useIframeResize.esm.js |
size the email-body iframe to its content |
useMessageSelection |
hooks/useMessageSelection.esm.js |
list selection state/helpers |
(utils/maildesk_iframe.esm.js and utils/maildesk_dom.esm.js are plain helper modules — e.g.
normalizeMessage(...) — not OWL hooks.)
2.5 Registries — the public extension surface
registries/mail_detail_registry.esm.js defines four named registry categories that Pro and the
bridges extend without forking MailDetail:
| Category | Slot in the reading pane |
|---|---|
maildesk.detail.toolbar |
toolbar action contributors |
maildesk.detail.header_before_subject |
header (above the subject) contributors |
maildesk.detail.inline_cards |
inline cards rendered in the reading pane |
maildesk.detail.side_panels |
side-panel contributors |
Each has a register…Contributor(key, contributor, {sequence}) helper. A contributor is
{ key, Component, getProps?, isVisible?, sequence? }; resolveMailDetailRegistryEntries(category,
context) filters by isVisible(context) and returns ordered {key, Component, props} for the
component to render. This is the clean extension point (no monkey-patch needed) — see §4.
Components also register into Odoo core categories: actions (the client action),
main_components (the composer hub), and systray (the compose launcher).
2.6 SCSS + service worker
scss/maildesk.scssandcore/composer/composer.scss— Basic styling.service_worker/maildesk_service_worker.js— prepended onto Odoo's native service worker by thewebmanifest.pycontroller (mailbox-user-gated); handlesnotificationclickformaildesk.messagepayloads to open MailDesk deep links. This is the only "mobile-ish" Basic asset — Basic has no responsive/adaptive views.
3. Pro frontend (maildesk_mail_client_pro/static/src)
Pro adds bidirectional actions, the AI surface, signatures/linking, and the entire mobile-adaptive +
dark layer. Most of it bolts onto Basic via patch() (§4).
3.1 New OWL components
AI surface (components/ai_surface/): AiPanel, AiPanelContainer, AiComposer, AiMessage,
AiMarkdown (+ ai_markdown_parse), AiIcon, AiProviderIcon. AiPanelContainer is mounted once
via the main_components registry (same pattern as the Basic composer hub):
registry.category("main_components").add("maildesk.AiPanelContainer", { Component: AiPanelContainer });
so the unified AI surface persists across navigation. AI insight (components/ai_insight/):
AiThreadDigestCard — an inline thread-digest card.
Dialogs / fields (components/dialogs/, components/fields/): LinkToRecordDialog,
BindToRecordDialog, SignaturePickerDialog, TemplatePickerDialog, ComposeSignaturePickerField,
SelectedFollowerPickerField, plus the chatter SelectedFollowerPicker.
Mobile components (components/mobile/, each with its own SCSS + XML): MobileHeader,
SidebarDrawer, BottomNav, BottomSheet, KebabMenu, BulkActionBar, ComposeFab,
DetailOverlay, MobileModeToggle, ThemeToggle, MdtIcon.
3.2 Services (7)
| Service name | File | Dependencies | Role |
|---|---|---|---|
maildesk.pro_ai |
services/pro_ai_service.esm.js |
orm, notification |
AI RPC client (summarize, security check, reply) |
maildesk.ai_surface |
services/pro_ai_surface_service.esm.js |
(multiple) | drives the unified AI panel/surface |
maildesk.pro_linking |
services/pro_linking_service.esm.js |
orm |
email→Odoo-record linking RPC |
maildesk.email_navigation |
services/email_navigation_service.esm.js |
orm |
open/navigate to emails from records |
maildesk_pro.mobile_mode |
services/mobile_mode_service.esm.js |
— | detect/track mobile mode (touch/coarse-pointer or ≤480px; localStorage override maildesk_pro.mobileMode) |
maildesk_pro.drawer_state |
services/drawer_state_service.esm.js |
— | folder-drawer open/close state |
maildesk_pro.theme |
services/theme_service.esm.js |
— | dark/light theme state (mobile dark) |
3.3 Hooks (6)
| Hook | File | Role |
|---|---|---|
useMobileMode |
hooks/use_mobile_mode.esm.js |
reactive mobile-mode flag (wraps the service) |
useTheme |
hooks/use_theme.esm.js |
reactive theme state |
useLongPress |
hooks/use_long_press.esm.js |
long-press → multi-select |
useSwipe |
hooks/use_swipe.esm.js |
swipe gestures (swipe-to-archive) |
useFocusTrap |
hooks/use_focus_trap.esm.js |
focus trapping for overlays/sheets |
useIframeAutoHeight |
hooks/use_iframe_autoheight.esm.js |
full-height email-body iframe sizing (mobile detail + thread) |
3.4 SCSS layers
scss/theme/maildesk_theme_tokens.scss (the --mdt-* design tokens), scss/maildesk_shared_panels.scss,
scss/maildesk.scss, scss/maildesk_ai_insight.scss, scss/maildesk_ai_surface.scss,
scss/maildesk_pro_actions.scss, the scss/adaptive/* set (dialogs, composer, panels, chatter, mobile
dialogs), and scss/theme/desktop_dark.scss (desktop dark is a separate stylesheet from the
mobile dark path). Each mobile component also ships a co-located SCSS file.
4. Extension model — how Pro (and bridges) extend Basic
Two complementary mechanisms, both first-class Odoo/OWL patterns:
4.1 Registry contributors (preferred, no patch)
A module adds a contributor to one of the Basic mail_detail_* registry categories. Example — the AI
thread-digest card registers itself as an inline card, visible only when the message has a thread:
// components/ai_insight/ai_thread_digest_card.esm.js
import { registerMailDetailInlineCardContributor } from "@maildesk_mail_client/registries/mail_detail_registry.esm";
registerMailDetailInlineCardContributor(
/* key */ "...",
{ Component: AiThreadDigestCard, isVisible: ({ message }) => Boolean(message && message.thread_id) },
{ sequence: 15 },
);
MailDetail renders all visible contributors in sequence order — so Pro and the bridges add panels,
cards, header chips, and toolbar buttons without touching the Basic component.
4.2 patch() on prototypes (when behavior must change in place)
For deeper behavior changes Pro uses import { patch } from "@web/core/utils/patch" and patches the
prototype of a Basic OWL component (or an Odoo core component), calling super.setup() /
super.<method>() to chain. The patches/ directory holds all of these; the patched targets include:
| Patch file | Patches | Adds |
|---|---|---|
maildesk_patch.esm.js |
MailDesk.prototype |
Pro action behavior on the shell |
maildesk_shell_mobile_patch.esm.js |
shell | mobile shell wiring |
mail_detail_patch.esm.js |
MailDetail.prototype |
Ask AI, Link-to-Record, delete-confirm, mobile detail menu, iframe auto-height |
mail_list_patch.esm.js + mail_list_ai_patch.esm.js + mail_list_context_menu_patch.esm.js + mail_list_kebab_patch.esm.js + mail_list_touch_patch.esm.js |
MailList.prototype |
AI badges, context menu, kebab, touch/long-press/swipe |
thread_message_patch.esm.js |
ThreadMessage.prototype |
dark-mode iframe, auto-height |
composer_input_patch.esm.js |
ComposerInput.prototype |
signatures/templates |
folder_tree_ai_patch.esm.js |
FolderTree.prototype |
"Ask MailDesk AI" sidebar launcher (useService("maildesk.ai_surface").open()) |
store_selected_recipients_patch.esm.js |
Odoo Store.prototype |
selected-recipient state |
chatter_integration_patch.esm.js, chatter_selected_followers_patch.esm.js, chatter_message_navigation_patch.esm.js |
Odoo Thread/Chatter/Composer/Message |
chatter integration |
chatgpt_dialog_patch.esm.js |
Odoo ChatGPTDialog/ChatGPTPromptDialog |
route the editor AI dialog through MailDesk |
attachment_model_maildesk_patch.esm.js, message_model_maildesk_patch.esm.js |
Odoo Attachment/Message models |
MailDesk metadata |
Because patches mutate prototypes, load order matters — Pro lists patches/* last in
web.assets_backend so every base class is defined first (§1).
5. Runtime layering summary
graph TD
subgraph Basic [Basic — web.assets_backend]
A[MailDesk action] --> CMP[16 components]
A --> SVC[4 services: store / sync / notification / composer]
A --> HK[2 hooks]
REG[4 mail_detail registries]:::ext
SW[service worker]
end
subgraph Pro [Pro — same bundle]
PSVC[7 services]:::pro
PHK[6 hooks]:::pro
AI[AI surface + insight]:::pro
MOB[mobile components + dark theme]:::pro
PAT[patches/*]:::pro
end
PAT -->|patch prototype| CMP
AI -->|registry contributor| REG
PAT -->|registry contributor| REG
classDef pro fill:#efe6ff,stroke:#714697;
classDef ext fill:#fff3e0,stroke:#e07b00;
- Basic = the whole desktop client; non-adaptive; no AI; no dark mode.
- Pro = same bundle, extended via
patch()+ registry contributors; adds AI, two-way actions, signatures/linking, the mobile-adaptive layer, and the mobile dark theme. Mobile/dark are auto-detected at runtime, never configured.
6. Mobile / dark activation (Pro)
Pro's adaptive assets load unconditionally; they activate at runtime via body markers, not via a separate bundle:
maildesk_pro.mobile_modeservice detects touch/coarse-pointer or viewport ≤480px (override vialocalStorage["maildesk_pro.mobileMode"]) and toggles the.md-pro-mobilebody class.maildesk_pro.themeservice togglesbody.md-pro-dark(mobile dark; desktop dark is the separatescss/theme/desktop_dark.scss).- Stable Playwright selectors and breakpoints are listed in the discovery doc §7 and on the Mobile page.