From 303d179c1adb58aefcb5256eec0f2aeba2c3f0a2 Mon Sep 17 00:00:00 2001 From: git_admin Date: Mon, 27 Apr 2026 08:18:10 +0000 Subject: [PATCH] Tower: upload cetmix_tower_server 16.0.3.0.1 (via marketplace) --- .../ace_variables/autocomplete_popup.esm.js | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 addons/cetmix_tower_server/static/src/components/ace_variables/autocomplete_popup.esm.js diff --git a/addons/cetmix_tower_server/static/src/components/ace_variables/autocomplete_popup.esm.js b/addons/cetmix_tower_server/static/src/components/ace_variables/autocomplete_popup.esm.js new file mode 100644 index 0000000..136f91f --- /dev/null +++ b/addons/cetmix_tower_server/static/src/components/ace_variables/autocomplete_popup.esm.js @@ -0,0 +1,317 @@ +/** @odoo-module **/ + +import {Component, useEffect, useRef, useState} from "@odoo/owl"; + +class AutocompletePopup extends Component { + /** + * Component setup method that initializes refs, state, and effects + */ + setup() { + this.popupRef = useRef("popupRef"); + this.searchInput = useRef("searchInput"); + this.itemsContainer = useRef("itemsContainer"); + + // State for search functionality + this.state = useState({ + searchTerm: "", + }); + + useEffect( + () => { + this.scrollToSelected(); + }, + () => [this.props.selectedIndex] + ); + + // Auto-focus search input when popup opens + useEffect( + () => { + if (this.searchInput.el) { + // Use setTimeout to ensure DOM is ready + const timeoutId = setTimeout(() => { + this.searchInput.el.focus(); + }, 0); + return () => clearTimeout(timeoutId); + } + }, + () => [] + ); + + useEffect( + () => { + if (this.props.position) { + const timeoutId = setTimeout(() => { + if (this.popupRef.el) { + this.popupRef.el.style.left = `${this.props.position.left}px`; + this.popupRef.el.style.top = `${this.props.position.top}px`; + this.popupRef.el.style.position = "fixed"; + } + }, 0); + return () => clearTimeout(timeoutId); + } + }, + () => [this.props.position] + ); + + // Cleanup effect to clear search timeout + useEffect( + () => { + return () => { + if (this.searchTimeout) { + clearTimeout(this.searchTimeout); + } + }; + }, + () => [] + ); + } + + /** + * Updates search term from external keyboard input (from editor) + * @param {String} char - The character typed or 'Backspace' for deletion + */ + updateSearchFromEditor(char) { + if (char === "Backspace") { + this.state.searchTerm = this.state.searchTerm.slice(0, -1); + } else if (char.length === 1) { + this.state.searchTerm += char; + } + if (this.props.onSelectedIndexChange) { + this.props.onSelectedIndexChange(0); + } + } + + /** + * Filters commands based on search term with enhanced search capabilities + * @returns {Array} Filtered and sorted array of commands matching the search term + */ + get filteredCommands() { + if (!this.state.searchTerm.trim()) { + return this.props.commands; + } + + const searchTerm = this.state.searchTerm.toLowerCase(); + + // Filter and score commands based on search relevance + const scoredCommands = this.props.commands + .map((command) => { + const name = (command.name || "").toLowerCase(); + const reference = (command.reference || "").toLowerCase(); + + let score = 0; + + // Exact matches get highest priority + if (name === searchTerm || reference === searchTerm) { + score = 1000; + } + // Starts with search term gets high priority + else if ( + name.startsWith(searchTerm) || + reference.startsWith(searchTerm) + ) { + score = 100; + } + // Contains search term gets medium priority + else if (name.includes(searchTerm) || reference.includes(searchTerm)) { + score = 10; + } + // No match + else { + return null; + } + + // Boost score for name matches over reference matches + if (name.includes(searchTerm)) { + score += 5; + } + + // Boost score for shorter matches (more relevant) + score += Math.max(0, 50 - Math.min(name.length, reference.length)); + + return {command, score}; + }) + .filter((item) => item !== null) + .sort((a, b) => b.score - a.score) + .map((item) => item.command); + + return scoredCommands; + } + + /** + * Debounces the search filtering + * @param {String} searchTerm - The search term to set + */ + debouncedSearch(searchTerm) { + if (this.searchTimeout) { + clearTimeout(this.searchTimeout); + } + this.searchTimeout = setTimeout(() => { + this.state.searchTerm = searchTerm; + if (this.props.onSelectedIndexChange) { + this.props.onSelectedIndexChange(0); + } + }, 150); + } + + /** + * Handles search input changes + * @param {Event} ev - The input event + */ + onSearchInput(ev) { + ev.stopPropagation(); + this.debouncedSearch(ev.target.value); + } + + /** + * Common keyboard navigation logic + * @param {KeyboardEvent} ev - The keyboard event + */ + handleKeyboardNavigation(ev) { + if (ev.key === "ArrowDown") { + ev.preventDefault(); + const newIndex = Math.min( + (this.props.selectedIndex || 0) + 1, + this.filteredCommands.length - 1 + ); + if (this.props.onSelectedIndexChange) { + this.props.onSelectedIndexChange(newIndex); + } + this.scrollToSelected(); + } else if (ev.key === "ArrowUp") { + ev.preventDefault(); + const newIndex = Math.max((this.props.selectedIndex || 0) - 1, 0); + if (this.props.onSelectedIndexChange) { + this.props.onSelectedIndexChange(newIndex); + } + this.scrollToSelected(); + } else if (ev.key === "Enter") { + ev.preventDefault(); + const selectedCommand = + this.filteredCommands[this.props.selectedIndex || 0]; + if (selectedCommand) { + this.onItemClick(selectedCommand); + } + } else if (ev.key === "Escape") { + ev.preventDefault(); + this.props.onItemClick(null); + } + } + + /** + * Handles keydown events on search input + * @param {KeyboardEvent} ev - The keyboard event + */ + onSearchKeyDown(ev) { + ev.stopPropagation(); + this.handleKeyboardNavigation(ev); + } + + /** + * Handles focus events on search input + * @param {FocusEvent} ev - The focus event + */ + onSearchFocus(ev) { + ev.stopPropagation(); + } + + /** + * Handles blur events on search input + * @param {FocusEvent} ev - The blur event + */ + onSearchBlur(ev) { + ev.stopPropagation(); + } + + /** + * Handles click events on search input + * @param {MouseEvent} ev - The click event + */ + onSearchClick(ev) { + ev.stopPropagation(); + } + + /** + * Handles mousedown events on search input + * @param {MouseEvent} ev - The mousedown event + */ + onSearchMouseDown(ev) { + ev.stopPropagation(); + } + + /** + * Handles item click events + * @param {Object} command - The selected command object + */ + onItemClick(command) { + this.props.onItemClick(command); + } + + /** + * Handles close button click events + */ + onCloseClick() { + this.props.onItemClick(null); + } + + /** + * Handles global keydown events for the popup + * @param {KeyboardEvent} ev - The keyboard event + */ + onKeyDown(ev) { + // Handle search input from editor keyboard events + if (ev.key.length === 1 && ev.key.match(/[a-zA-Z0-9_]/)) { + // Add typed character to search + this.updateSearchFromEditor(ev.key); + } else if (ev.key === "Backspace") { + // Remove last character from search + this.updateSearchFromEditor("Backspace"); + } else { + // Use common keyboard navigation logic + this.handleKeyboardNavigation(ev); + } + } + + /** + * Scrolls the selected item into view + */ + scrollToSelected() { + const itemsContainer = this.itemsContainer.el; + if ( + itemsContainer && + this.props.selectedIndex !== undefined && + this.props.selectedIndex >= 0 && + this.props.selectedIndex < itemsContainer.children.length + ) { + const selectedItem = itemsContainer.children[this.props.selectedIndex]; + if (selectedItem) { + selectedItem.scrollIntoView({ + block: "nearest", + behavior: "smooth", + }); + } + } + } + + /** + * Returns CSS class for autocomplete item based on selection state + * @param {Number} index - The item index + * @returns {String} CSS class string + */ + getItemClass(index) { + return index === (this.props.selectedIndex || 0) + ? "ace-autocomplete-item ace-autocomplete-item-selected" + : "ace-autocomplete-item"; + } +} + +AutocompletePopup.template = "cetmix_tower_server.AutocompletePopup"; +AutocompletePopup.props = { + commands: {type: Array}, + onItemClick: {type: Function}, + position: {type: Object}, + selectedIndex: {type: Number, optional: true}, + onSelectedIndexChange: {type: Function, optional: true}, + type: {type: String, optional: true}, +}; + +export {AutocompletePopup};