From d589d6846dd6ead35de5dfa30663a7cbd07b2709 Mon Sep 17 00:00:00 2001 From: Bhavesh Heliconia Date: Fri, 27 Mar 2026 14:54:11 +0530 Subject: [PATCH] [ADD] mail_composer_template_search --- mail_composer_template_search/README.rst | 114 +++++++++++ mail_composer_template_search/__init__.py | 2 + mail_composer_template_search/__manifest__.py | 28 +++ mail_composer_template_search/pyproject.toml | 3 + .../readme/CONTRIBUTORS.md | 2 + .../readme/DESCRIPTION.md | 9 + mail_composer_template_search/readme/USAGE.md | 6 + .../static/description/index.html | 6 + .../js/mail_composer_template_selector.esm.js | 180 ++++++++++++++++++ .../scss/mail_composer_template_selector.scss | 9 + .../xml/mail_composer_template_selector.xml | 61 ++++++ 11 files changed, 420 insertions(+) create mode 100644 mail_composer_template_search/README.rst create mode 100644 mail_composer_template_search/__init__.py create mode 100644 mail_composer_template_search/__manifest__.py create mode 100644 mail_composer_template_search/pyproject.toml create mode 100644 mail_composer_template_search/readme/CONTRIBUTORS.md create mode 100644 mail_composer_template_search/readme/DESCRIPTION.md create mode 100644 mail_composer_template_search/readme/USAGE.md create mode 100644 mail_composer_template_search/static/description/index.html create mode 100644 mail_composer_template_search/static/src/js/mail_composer_template_selector.esm.js create mode 100644 mail_composer_template_search/static/src/scss/mail_composer_template_selector.scss create mode 100644 mail_composer_template_search/static/src/xml/mail_composer_template_selector.xml diff --git a/mail_composer_template_search/README.rst b/mail_composer_template_search/README.rst new file mode 100644 index 0000000000..0082347c15 --- /dev/null +++ b/mail_composer_template_search/README.rst @@ -0,0 +1,114 @@ +============================= +Mail Composer Template Search +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3ecf8791c31badd700b3c89946e2cd7786cd3dfdcd8bd2ae5c4cbde254478f6a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github + :target: https://github.com/OCA/social/tree/18.0/mail_composer_template_search + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-18-0/social-18-0-mail_composer_template_search + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module enhances the Odoo mail composer's template selector by +adding an inline search bar. This allows users to quickly filter and +select templates without having to click "Search More..." or scroll +through a long list. + +Key Features +~~~~~~~~~~~~ + +- **Inline Search**: Type directly in the template dropdown to filter. +- **Smart Filtering**: Case-insensitive search on template names. +- **Keyboard Navigation**: Use Arrow Up/Down and Enter to select + templates. +- **Debounced Search**: Optimized performance with a 300ms debounce. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Open the mail composer (e.g., from an Opportunity, Partner, or + Purchase Order). +2. Click on the template selector dropdown. +3. Start typing in the new search bar at the top of the dropdown. +4. The list of templates will filter automatically as you type. +5. Use **Arrow Keys** to navigate and **Enter** to select, or simply + click the desired template. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Heliconia Solutions Pvt. Ltd. + +Contributors +------------ + +- `Heliconia Solutions Pvt. Ltd. `__ + + - Bhavesh Heliconia + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-Bhavesh Heliconia| image:: https://github.com/Bhavesh Heliconia.png?size=40px + :target: https://github.com/Bhavesh Heliconia + :alt: Bhavesh Heliconia + +Current `maintainer `__: + +|maintainer-Bhavesh Heliconia| + +This module is part of the `OCA/social `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_composer_template_search/__init__.py b/mail_composer_template_search/__init__.py new file mode 100644 index 0000000000..a7017e1225 --- /dev/null +++ b/mail_composer_template_search/__init__.py @@ -0,0 +1,2 @@ +# Copyright 2026 Heliconia Solutions Pvt. Ltd. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). diff --git a/mail_composer_template_search/__manifest__.py b/mail_composer_template_search/__manifest__.py new file mode 100644 index 0000000000..2c23bdabe6 --- /dev/null +++ b/mail_composer_template_search/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2026 Heliconia Solutions Pvt. Ltd. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Mail Composer Template Search", + "version": "18.0.1.0.0", + "category": "Social", + "summary": "Adds an inline search bar to the mail composer template " + "selector dropdown", + "author": "Heliconia Solutions Pvt. Ltd., Odoo Community Association (OCA)", + "website": "https://github.com/OCA/social", + "license": "AGPL-3", + "maintainers": ["Bhavesh Heliconia"], + "development_status": "Alpha", + "depends": [ + "mail", + ], + "assets": { + "web.assets_backend": [ + "mail_composer_template_search/static/src/js/mail_composer_template_selector.esm.js", + "mail_composer_template_search/static/src/xml/mail_composer_template_selector.xml", + "mail_composer_template_search/static/src/scss/mail_composer_template_selector.scss", + ], + }, + "installable": True, + "application": False, + "auto_install": False, +} diff --git a/mail_composer_template_search/pyproject.toml b/mail_composer_template_search/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/mail_composer_template_search/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mail_composer_template_search/readme/CONTRIBUTORS.md b/mail_composer_template_search/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..2f6060845e --- /dev/null +++ b/mail_composer_template_search/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io) + - Bhavesh Heliconia diff --git a/mail_composer_template_search/readme/DESCRIPTION.md b/mail_composer_template_search/readme/DESCRIPTION.md new file mode 100644 index 0000000000..13be1fa4f5 --- /dev/null +++ b/mail_composer_template_search/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +This module enhances the Odoo mail composer's template selector by adding an +inline search bar. This allows users to quickly filter and select templates +without having to click "Search More..." or scroll through a long list. + +### Key Features +* **Inline Search**: Type directly in the template dropdown to filter. +* **Smart Filtering**: Case-insensitive search on template names. +* **Keyboard Navigation**: Use Arrow Up/Down and Enter to select templates. +* **Debounced Search**: Optimized performance with a 300ms debounce. diff --git a/mail_composer_template_search/readme/USAGE.md b/mail_composer_template_search/readme/USAGE.md new file mode 100644 index 0000000000..02a1ead94e --- /dev/null +++ b/mail_composer_template_search/readme/USAGE.md @@ -0,0 +1,6 @@ +1. Open the mail composer (e.g., from an Opportunity, Partner, or Purchase Order). +2. Click on the template selector dropdown. +3. Start typing in the new search bar at the top of the dropdown. +4. The list of templates will filter automatically as you type. +5. Use **Arrow Keys** to navigate and **Enter** to select, or simply click the + desired template. diff --git a/mail_composer_template_search/static/description/index.html b/mail_composer_template_search/static/description/index.html new file mode 100644 index 0000000000..84b82638f5 --- /dev/null +++ b/mail_composer_template_search/static/description/index.html @@ -0,0 +1,6 @@ +
+
+

Mail Composer Template Search

+

Search mail templates directly from the composer dropdown

+
+
diff --git a/mail_composer_template_search/static/src/js/mail_composer_template_selector.esm.js b/mail_composer_template_search/static/src/js/mail_composer_template_selector.esm.js new file mode 100644 index 0000000000..a8d29e71b4 --- /dev/null +++ b/mail_composer_template_search/static/src/js/mail_composer_template_selector.esm.js @@ -0,0 +1,180 @@ +/* Copyright 2026 Heliconia Solutions Pvt. Ltd. + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). */ + +/* global document, setTimeout, clearTimeout */ + +/** @odoo-module **/ + +import {onMounted, onWillUnmount} from "@odoo/owl"; +import {MailComposerTemplateSelector} from "@mail/core/web/mail_composer_template_selector"; +import {patch} from "@web/core/utils/patch"; + +let searchTimeout = null; + +patch(MailComposerTemplateSelector.prototype, { + setup() { + super.setup(...arguments); + this.state.searchQuery = ""; + this.state.searchTemplates = null; + this.state.isSearching = false; + this.state.highlightedIndex = -1; + + // Auto-focus the search input when the dropdown opens + this._handleDropdownOpen = (ev) => { + if (ev.target.closest(".mail-composer-template-dropdown-btn")) { + setTimeout(() => { + const input = document.querySelector(".mail-template-search-input"); + if (input) { + input.focus(); + } + }, 50); + } + }; + + onMounted(() => { + document.addEventListener("click", this._handleDropdownOpen, true); + }); + + onWillUnmount(() => { + document.removeEventListener("click", this._handleDropdownOpen, true); + }); + }, + + _resetSearchState() { + this.state.searchQuery = ""; + this.state.searchTemplates = null; + this.state.isSearching = false; + this.state.highlightedIndex = -1; + }, + + async onLoadTemplate(template) { + this._resetSearchState(); + await super.onLoadTemplate(template); + }, + + get displayedTemplates() { + if (this.state.searchQuery && this.state.searchTemplates !== null) { + return this.state.searchTemplates; + } + return this.state.templates || []; + }, + + get showSearchMore() { + if (this.state.searchQuery) { + return false; + } + return (this.state.templates || []).length >= this.limit; + }, + + _applyHighlight(el) { + const menu = el.closest(".mail-composer-template-dropdown"); + if (!menu) return; + // Get only the insert-template DropdownItems (skip Save/Delete sections) + const allItems = menu.querySelectorAll(":scope > .o-dropdown-item"); + const templates = this.displayedTemplates; + // The template items are the first N items in the dropdown + for (let i = 0; i < allItems.length && i < templates.length; i++) { + if (i === this.state.highlightedIndex) { + allItems[i].style.backgroundColor = "#017e84"; + allItems[i].style.color = "white"; + } else { + allItems[i].style.backgroundColor = ""; + allItems[i].style.color = ""; + } + } + }, + + _clearHighlight(el) { + const menu = el.closest(".mail-composer-template-dropdown"); + if (!menu) return; + const allItems = menu.querySelectorAll(":scope > .o-dropdown-item"); + allItems.forEach((item) => { + item.style.backgroundColor = ""; + item.style.color = ""; + }); + }, + + onSearchInput(ev) { + const rawValue = ev.target.value; + const query = rawValue.trim(); + this.state.searchQuery = rawValue; + this.state.highlightedIndex = -1; + this._clearHighlight(ev.target); + + if (searchTimeout) { + clearTimeout(searchTimeout); + } + + if (!query) { + this.state.searchTemplates = null; + this.state.isSearching = false; + return; + } + + this.state.isSearching = true; + + searchTimeout = setTimeout(async () => { + try { + const fields = ["display_name"]; + const domain = [ + ["model", "=", this.props.record.data.render_model], + ["name", "ilike", query], + ]; + const results = await this.orm.searchRead( + "mail.template", + domain, + fields, + {limit: 50} + ); + if (this.state.searchQuery.trim() === query) { + this.state.searchTemplates = results; + this.state.isSearching = false; + } + } catch { + this.state.isSearching = false; + } + }, 300); + }, + + onSearchKeydown(ev) { + // Stop ALL event propagation so the Dropdown component + // does not intercept Space, Arrow keys, Enter, etc. + ev.stopPropagation(); + + const templates = this.displayedTemplates; + + if (ev.key === "ArrowDown") { + ev.preventDefault(); + if (templates.length > 0) { + this.state.highlightedIndex = Math.min( + this.state.highlightedIndex + 1, + templates.length - 1 + ); + // Use setTimeout to apply after OWL re-render + setTimeout(() => this._applyHighlight(ev.target), 0); + } + } else if (ev.key === "ArrowUp") { + ev.preventDefault(); + if (templates.length > 0) { + this.state.highlightedIndex = Math.max( + this.state.highlightedIndex - 1, + 0 + ); + setTimeout(() => this._applyHighlight(ev.target), 0); + } + } else if (ev.key === "Enter") { + ev.preventDefault(); + const idx = this.state.highlightedIndex; + if (idx >= 0 && idx < templates.length) { + // Click the actual dropdown item to trigger both selection and close + const menu = ev.target.closest(".mail-composer-template-dropdown"); + if (menu) { + const items = menu.querySelectorAll(":scope > .o-dropdown-item"); + if (items[idx]) { + items[idx].click(); + } + } + } + } + }, +}); diff --git a/mail_composer_template_search/static/src/scss/mail_composer_template_selector.scss b/mail_composer_template_search/static/src/scss/mail_composer_template_selector.scss new file mode 100644 index 0000000000..32f403d093 --- /dev/null +++ b/mail_composer_template_search/static/src/scss/mail_composer_template_selector.scss @@ -0,0 +1,9 @@ +.mail-template-search-container { + .mail-template-search-input { + font-size: 0.85rem; + &::placeholder { + color: #999; + font-style: italic; + } + } +} diff --git a/mail_composer_template_search/static/src/xml/mail_composer_template_selector.xml b/mail_composer_template_search/static/src/xml/mail_composer_template_selector.xml new file mode 100644 index 0000000000..309477cad4 --- /dev/null +++ b/mail_composer_template_search/static/src/xml/mail_composer_template_selector.xml @@ -0,0 +1,61 @@ + + + + + +
+ +
+
+ + + + + + + Untitled + + + + + + +
+ Searching... +
+
+ No matching templates +
+
+ No saved templates +
+
+ + + + showSearchMore + +
+ +