Tower: upload cetmix_tower_server 16.0.3.0.1 (via marketplace)

This commit is contained in:
2026-04-27 08:18:10 +00:00
parent b309b6f244
commit 303d179c1a

View File

@@ -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};