Wipe addons/: full reset for clean re-upload

This commit is contained in:
Tower Deploy
2026-04-27 11:20:53 +03:00
parent 2cf3b5185d
commit 9bb80002c8
363 changed files with 0 additions and 112641 deletions

View File

@@ -1,507 +0,0 @@
/** @odoo-module **/
import {AceField} from "@web/views/fields/ace/ace_field";
import {AutocompletePopup} from "./autocomplete_popup.esm";
import {registry} from "@web/core/registry";
import {useService} from "@web/core/utils/hooks";
import {useState} from "@odoo/owl";
const POPUP_FALLBACK_WIDTH = 500;
const POPUP_FALLBACK_HEIGHT = 300;
class AceCommandField extends AceField {
/**
* Initialize the component with required services and properties
*/
setup() {
super.setup();
this.orm = useService("orm");
this.inputListener = null;
this.clickOutsideListener = null;
this.inputTimeout = null;
this.variables = [];
this.secrets = [];
// Use reactive state for properties that affect rendering
this.state = useState({
showPopup: false,
popupItems: [],
popupPosition: {},
selectedIndex: 0,
// Add popup type to distinguish between variables and secrets
popupType: "variables",
});
this.updateSelectedIndex = this.updateSelectedIndex.bind(this);
}
/**
* Load variables from the backend using ORM service
* @returns {Promise<void>}
*/
async loadVariables() {
try {
this.variables = await this.orm.searchRead(
"cx.tower.variable",
[],
["name", "reference"]
);
console.log(`Loaded ${this.variables.length} variables for autocomplete`);
} catch (error) {
console.error("Failed to load variables for autocomplete:", error);
this.variables = [];
this.env.services.notification.add(
"Failed to load autocomplete variables",
{type: "warning"}
);
}
}
/**
* Load secrets from the backend using ORM service
* @returns {Promise<void>}
*/
async loadSecrets() {
try {
this.secrets = await this.orm.searchRead(
"cx.tower.key",
[["key_type", "=", "s"]],
["name", "reference"]
);
console.log(`Loaded ${this.secrets.length} secrets for autocomplete`);
} catch (error) {
console.error("Failed to load secrets for autocomplete:", error);
this.secrets = [];
this.env.services.notification.add("Failed to load autocomplete secrets", {
type: "warning",
});
}
}
/**
* Set up ACE editor with custom autocompletion
*/
setupAce() {
super.setupAce();
if (this.aceEditor) {
this.setupCustomAutocompletion();
}
}
/**
* Configure custom autocompletion commands and keyboard bindings for ACE editor
*/
setupCustomAutocompletion() {
// Remove any existing conflicting commands first
this.aceEditor.commands.removeCommand("startAutocomplete");
this.aceEditor.commands.removeCommand("expandSnippet");
// Only add the main autocomplete trigger command
this.aceEditor.commands.addCommand({
name: "customAutoComplete",
bindKey: {win: "Ctrl-Space", mac: null},
exec: (editor) => {
this.showCustomCompletions(editor);
return true;
},
});
// Set up input listener for {{ and #! triggers
this.inputListener = () => {
// Clear any existing timeout
if (this.inputTimeout) {
clearTimeout(this.inputTimeout);
}
// Use setTimeout to ensure the text is fully processed
this.inputTimeout = setTimeout(() => {
const cursor = this.aceEditor.getCursorPosition();
const session = this.aceEditor.getSession();
const line = session.getLine(cursor.row);
const textBeforeCursor = line.substring(0, cursor.column);
// Check for variables trigger {{
if (textBeforeCursor.endsWith("{{")) {
// Remove {{ symbols from editor
const startColumn = Math.max(0, cursor.column - 2);
const range = {
start: {row: cursor.row, column: startColumn},
end: {row: cursor.row, column: cursor.column},
};
session.replace(range, "");
// Update cursor position
const newCursor = {
row: cursor.row,
column: startColumn,
};
this.aceEditor.moveCursorToPosition(newCursor);
this.showCustomCompletions(this.aceEditor, "variables");
}
// Check for secrets trigger !#
else if (textBeforeCursor.endsWith("#!")) {
// Remove !# symbols from editor
const startColumn = Math.max(0, cursor.column - 2);
const range = {
start: {row: cursor.row, column: startColumn},
end: {row: cursor.row, column: cursor.column},
};
session.replace(range, "");
// Update cursor position
const newCursor = {
row: cursor.row,
column: startColumn,
};
this.aceEditor.moveCursorToPosition(newCursor);
this.showCustomCompletions(this.aceEditor, "secrets");
}
}, 10);
};
this.aceEditor.on("input", this.inputListener);
}
/**
* Show custom completions popup with available variables or secrets
* @param {Object} editor - ACE editor instance
* @param {String} type - Type of completion ('variables' or 'secrets')
* @returns {Promise<void>}
*/
async showCustomCompletions(editor, type = "variables") {
const cursor = editor.getCursorPosition();
const session = editor.getSession();
const line = session.getLine(cursor.row);
const textBeforeCursor = line.substring(0, cursor.column);
let items = [];
let triggerLength = 0;
if (type === "secrets") {
// Handle secrets
await this.loadSecrets();
if (!this.secrets.length) {
return;
}
items = this.secrets;
} else {
// Handle variables
await this.loadVariables();
if (!this.variables.length) {
return;
}
items = this.variables;
// Check if we're already in a variable context
const isInVariableContext = textBeforeCursor.endsWith("{{");
if (isInVariableContext) {
triggerLength = 2;
}
}
const position = this.calculatePopupPosition(editor, cursor);
// Set popup type in state
this.state.popupType = type;
await this.showAutocompletePopup(items, position, editor, triggerLength, type);
}
/**
* Calculate the optimal position for the autocomplete popup
* @param {Object} editor - ACE editor instance
* @param {Object} cursor - Cursor position object
* @returns {Object} Position object with left and top coordinates
*/
calculatePopupPosition(editor, cursor) {
const renderer = editor.renderer;
// Calculate cursor position within the editor
const cursorPixelPos = renderer.textToScreenCoordinates(
cursor.row,
cursor.column
);
// Get scroll position
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
// Calculate the cursor position relative to the viewport
const viewportLeft = cursorPixelPos.pageX - scrollLeft;
const viewportTop = cursorPixelPos.pageY - scrollTop;
// Position popup just below the cursor
const finalLeft = viewportLeft;
const finalTop = viewportTop + renderer.lineHeight;
// Ensure popup doesn't go outside viewport
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const popup = document.querySelector(".ace-autocomplete-popup");
const popupWidth = popup ? popup.offsetWidth : POPUP_FALLBACK_WIDTH;
const popupHeight = popup ? popup.offsetHeight : POPUP_FALLBACK_HEIGHT;
let adjustedLeft = finalLeft;
let adjustedTop = finalTop;
// Adjust if popup would go off-screen horizontally
if (finalLeft + popupWidth > viewportWidth) {
adjustedLeft = finalLeft - popupWidth;
}
// Adjust if popup would go off-screen vertically
if (finalTop + popupHeight > viewportHeight) {
adjustedTop = finalTop - popupHeight - renderer.lineHeight;
}
// Make sure popup is not positioned off-screen
adjustedLeft = Math.max(0, adjustedLeft);
adjustedTop = Math.max(0, adjustedTop);
return {
left: adjustedLeft,
top: adjustedTop,
};
}
/**
* Display the autocomplete popup with variables or secrets at the specified position
* @param {Array} items - Array of available variables or secrets
* @param {Object} position - Position object with left and top coordinates
* @param {Object} editor - ACE editor instance
* @param {Number} triggerLength - Length of trigger text that should be replaced
* @param {String} type - Type of completion ('variables' or 'secrets')
* @returns {Promise<void>}
*/
async showAutocompletePopup(
items,
position,
editor,
triggerLength,
type = "variables"
) {
this.hideAutocompletePopup();
this.state.popupItems = items;
this.state.popupPosition = position;
this.state.showPopup = true;
this.state.selectedIndex = 0;
this.state.popupType = type;
this.currentEditor = editor;
this.currentTriggerLength = triggerLength;
this.currentType = type;
// Add click outside listener
this.clickOutsideListener = (event) => {
// Check if click is outside the popup and ace editor
const popupElement = document.querySelector(".ace-autocomplete-popup");
const aceElement = this.aceEditor.container;
if (
popupElement &&
!popupElement.contains(event.target) &&
aceElement &&
!aceElement.contains(event.target)
) {
this.hideAutocompletePopup();
}
};
setTimeout(() => {
document.addEventListener("click", this.clickOutsideListener, true);
}, 0);
}
/**
* Hide the autocomplete popup and clean up event listeners
*/
hideAutocompletePopup() {
// Remove click outside listener
if (this.clickOutsideListener) {
document.removeEventListener("click", this.clickOutsideListener, true);
this.clickOutsideListener = null;
}
this.state.showPopup = false;
this.state.popupVariables = [];
this.currentEditor = null;
this.state.selectedIndex = 0;
// Return focus to the ACE editor
if (this.aceEditor) {
this.aceEditor.focus();
}
}
/**
* Update the selected index in the autocomplete popup
* @param {Number} index - New selected index
*/
updateSelectedIndex(index) {
if (this.state) {
this.state.selectedIndex = index;
}
}
/**
* Handle selection of a command from the autocomplete popup
* @param {Object} command - Selected command object
* @param {Object} editor - ACE editor instance
*/
handleCommandSelection(command, editor) {
if (!command || !command.reference) {
this.hideAutocompletePopup();
return;
}
const cursor = editor.getCursorPosition();
const session = editor.getSession();
const line = session.getLine(cursor.row);
const textBeforeCursor = line.substring(0, cursor.column);
// Get line length for validation
const lineLength = session.getLine(cursor.row).length;
const currentType = this.currentType || this.state.popupType;
let range = null;
let insertText = "";
if (currentType === "secrets") {
// Handle secrets insertion
// Check if we're inside a secret context (between #!cxtower.secret and !#)
const lastSecretStart = textBeforeCursor.lastIndexOf("#!cxtower.secret");
const lastSecretEnd = textBeforeCursor.lastIndexOf("!#");
// Count occurrences of start and end delimiters for more robust validation
const startCount = (textBeforeCursor.match(/#!cxtower\.secret/g) || [])
.length;
const endCount = (textBeforeCursor.match(/!#/g) || []).length;
const isInsideSecret =
startCount > endCount &&
lastSecretStart > lastSecretEnd &&
lastSecretStart !== -1;
if (isInsideSecret) {
// We're inside a secret context, replace from after #!cxtower to cursor
range = {
start: {row: cursor.row, column: lastSecretStart + 16},
end: {row: cursor.row, column: cursor.column},
};
// Clamp range to valid bounds
range.start.column = Math.max(
0,
Math.min(range.start.column, lineLength)
);
range.end.column = Math.max(
range.start.column,
Math.min(range.end.column, lineLength)
);
insertText = `${command.reference}!#`;
} else {
// We're not in a secret context, insert complete secret
const triggerLength = this.currentTriggerLength || 0;
range = {
start: {row: cursor.row, column: cursor.column - triggerLength},
end: {row: cursor.row, column: cursor.column},
};
// Clamp range to valid bounds
range.start.column = Math.max(
0,
Math.min(range.start.column, lineLength)
);
range.end.column = Math.max(
range.start.column,
Math.min(range.end.column, lineLength)
);
insertText = `#!cxtower.secret.${command.reference}!#`;
}
} else {
// Handle variables insertion (existing logic)
const lastOpenBrace = textBeforeCursor.lastIndexOf("{{");
const lastCloseBrace = textBeforeCursor.lastIndexOf("}}");
const isInsideVariable =
lastOpenBrace > lastCloseBrace && lastOpenBrace !== -1;
if (isInsideVariable) {
// We're inside a variable context, replace from after {{ to cursor
range = {
start: {row: cursor.row, column: lastOpenBrace + 2},
end: {row: cursor.row, column: cursor.column},
};
// Clamp range to valid bounds
range.start.column = Math.max(
0,
Math.min(range.start.column, lineLength)
);
range.end.column = Math.max(
range.start.column,
Math.min(range.end.column, lineLength)
);
insertText = ` ${command.reference} `;
} else {
// We're not in a variable context, insert complete variable
const triggerLength = this.currentTriggerLength || 0;
range = {
start: {row: cursor.row, column: cursor.column - triggerLength},
end: {row: cursor.row, column: cursor.column},
};
// Clamp range to valid bounds
range.start.column = Math.max(
0,
Math.min(range.start.column, lineLength)
);
range.end.column = Math.max(
range.start.column,
Math.min(range.end.column, lineLength)
);
insertText = `{{ ${command.reference} }}`;
}
}
// Replace the text
session.replace(range, insertText);
// Get the updated line length after replacement
const updatedLineLength = session.getLine(cursor.row).length;
// Position cursor after the inserted text
const newCursor = {
row: cursor.row,
column: range.start.column + insertText.length,
};
newCursor.column = Math.max(0, Math.min(newCursor.column, updatedLineLength));
editor.moveCursorToPosition(newCursor);
this.hideAutocompletePopup();
editor.focus();
}
/**
* Clean up resources when component is destroyed
*/
destroy() {
if (this.inputTimeout) {
clearTimeout(this.inputTimeout);
}
if (this.aceEditor && this.inputListener) {
this.aceEditor.off("input", this.inputListener);
}
this.hideAutocompletePopup();
super.destroy();
}
}
AceCommandField.template = "cetmix_tower_server.AceCommandField";
AceCommandField.components = {
AutocompletePopup,
};
registry.category("fields").add("ace_tower", AceCommandField);
export {AceCommandField};

View File

@@ -1,44 +0,0 @@
// Custom styles ONLY for AceCommandField - more specific selectors
.o_field_widget.o_field_ace_tower {
display: block !important;
}
.o_field_widget[data-field-name] .o_field_ace.ace-command-field {
min-height: 200px !important;
height: auto !important;
width: 100% !important;
display: block !important;
.ace_editor {
min-height: 200px !important;
height: auto !important;
width: 100% !important;
border: 1px solid #ced4da;
border-radius: 4px;
display: block !important;
}
.ace_content {
min-height: 200px !important;
width: 100% !important;
}
.ace_scroller {
width: 100% !important;
// Remove any scroll restrictions that might affect standard ACE
overflow: auto !important;
}
}
// Custom autocomplete popup styles
.ace-autocomplete-popup {
.ace-autocomplete-item {
&:hover {
background-color: #e6f3ff !important;
}
&:last-child {
border-bottom: none !important;
}
}
}

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="cetmix_tower_server.AceCommandField" owl="1">
<t t-call="web.AceField" />
<t t-if="state.showPopup">
<AutocompletePopup
commands="state.popupItems"
position="state.popupPosition"
selectedIndex="state.selectedIndex"
type="state.popupType"
onSelectedIndexChange="updateSelectedIndex"
onItemClick="(command) => this.handleCommandSelection(command, this.currentEditor)"
/>
</t>
</t>
</templates>

View File

@@ -1,317 +0,0 @@
/** @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};

View File

@@ -1,190 +0,0 @@
// Define z-index variable for better management
$z-index-autocomplete: 1050; // Above dropdowns but below modals
.ace-autocomplete-popup {
position: absolute; // Keep original positioning for cursor placement
background: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: $z-index-autocomplete;
min-width: 300px;
max-width: 500px;
max-height: 300px;
overflow: hidden;
font-family: monospace;
font-size: 14px;
// Mobile adaptations
@media (max-width: 768px) {
width: 90%;
min-width: unset;
max-width: unset;
left: 50% !important;
transform: translateX(-50%);
font-size: 13px;
}
}
.ace-autocomplete-search {
padding: 8px;
padding-right: 48px; // Add right padding to avoid overlap with close button
border-bottom: 1px solid #eee;
background: #f8f9fa;
// Mobile: reduce padding
@media (max-width: 768px) {
padding: 6px;
padding-right: 56px;
}
}
.ace-autocomplete-search-input {
width: 100%;
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 13px;
outline: none;
box-sizing: border-box;
}
.ace-autocomplete-search-input:focus {
border-color: #007cba;
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.1);
}
.ace-autocomplete-items {
max-height: 240px;
overflow-y: auto;
/* Standard scrollbar styling (Firefox 64+) */
scrollbar-width: thin;
scrollbar-color: #c1c1c1 #f1f1f1;
}
/* Scrollbar styling for webkit browsers */
.ace-autocomplete-items::-webkit-scrollbar {
width: 6px;
}
.ace-autocomplete-items::-webkit-scrollbar-track {
background: #f1f1f1;
}
.ace-autocomplete-items::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.ace-autocomplete-items::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.ace-autocomplete-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
// Mobile: stack items vertically with reduced padding
@media (max-width: 768px) {
flex-direction: column;
align-items: flex-start;
padding: 6px 12px;
}
}
.ace-autocomplete-item:hover {
background-color: #f5f5f5;
}
.ace-autocomplete-item-selected {
background-color: #e6f3ff;
color: #0066cc;
}
.ace-autocomplete-item-selected:hover {
background-color: #cce7ff;
}
.command-name {
font-weight: 600;
color: #333;
flex: 1;
margin-right: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
// Mobile adaptations
@media (max-width: 768px) {
margin-right: 0;
margin-bottom: 2px; // Reduced from 4px
width: 100%;
font-size: 14px;
}
}
.command-description {
color: #666;
font-size: 12px;
font-style: italic;
flex-shrink: 0;
// Mobile adaptations
@media (max-width: 768px) {
font-size: 11px;
width: 100%;
word-break: break-word;
}
}
.ace-autocomplete-no-results {
padding: 16px 12px;
text-align: center;
color: #999;
font-style: italic;
}
// Close button styles
.ace-autocomplete-close-btn {
position: absolute;
top: 8px;
right: 8px;
background: none;
border: none;
font-size: 20px;
font-weight: bold;
color: #666;
cursor: pointer;
padding: 4px 8px;
line-height: 1;
border-radius: 3px;
z-index: 1;
min-width: 30px;
min-height: 30px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #f0f0f0;
color: #333;
}
&:active {
background-color: #e0e0e0;
}
// Mobile-friendly touch target
@media (max-width: 768px) {
min-width: 40px;
min-height: 40px;
font-size: 24px;
top: 4px;
right: 4px;
}
}

View File

@@ -1,75 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="cetmix_tower_server.AutocompletePopup" owl="1">
<div
class="ace-autocomplete-popup"
t-ref="popupRef"
tabindex="0"
role="combobox"
t-att-aria-label="props.type === 'secrets' ? 'Secret search' : 'Variable search'"
aria-expanded="true"
aria-haspopup="listbox"
t-att-aria-owns="props.type === 'secrets' ? 'secrets-list' : 'variables-list'"
>
<!-- Close button for mobile convenience -->
<button
class="ace-autocomplete-close-btn"
t-on-click="onCloseClick"
type="button"
title="Close"
t-att-aria-label="props.type === 'secrets' ? 'Close secret search' : 'Close variable search'"
>
×
</button>
<!-- Search input field -->
<div class="ace-autocomplete-search">
<input
type="text"
class="ace-autocomplete-search-input"
t-att-placeholder="props.type === 'secrets' ? 'Search secrets...' : 'Search variables...'"
t-model="state.searchTerm"
t-ref="searchInput"
t-on-input="onSearchInput"
t-on-keydown="onSearchKeyDown"
t-on-focus="onSearchFocus"
t-on-blur="onSearchBlur"
t-att-aria-label="props.type === 'secrets' ? 'Search secrets' : 'Search variables'"
t-att-aria-controls="props.type === 'secrets' ? 'secrets-list' : 'variables-list'"
aria-autocomplete="list"
t-att-aria-activedescendant="`${props.type === 'secrets' ? 'sec' : 'var'}-opt-${props.selectedIndex}`"
/>
</div>
<!-- Items list -->
<div
class="ace-autocomplete-items"
t-ref="itemsContainer"
t-att-id="props.type === 'secrets' ? 'secrets-list' : 'variables-list'"
role="listbox"
>
<div
t-foreach="filteredCommands"
t-as="command"
t-key="command.name"
t-att-id="`${props.type === 'secrets' ? 'sec' : 'var'}-opt-${command_index}`"
t-att-class="getItemClass(command_index)"
t-on-click="() => this.onItemClick(command)"
role="option"
t-att-aria-selected="command_index === props.selectedIndex ? 'true' : 'false'"
>
<span class="command-name" t-esc="command.name" />
<span
class="command-description"
t-esc="props.type === 'secrets' ? `${command.reference}` : `{{ ${command.reference} }}`"
/>
</div>
<div
t-if="!filteredCommands.length"
class="ace-autocomplete-no-results"
>
<t t-if="props.type === 'secrets'">No secrets found</t>
<t t-else="">No variables found</t>
</div>
</div>
</div>
</t>
</templates>

View File

@@ -1,33 +0,0 @@
/** @odoo-module */
import {registry} from "@web/core/registry";
import {StateSelectionField} from "@web/views/fields/state_selection/state_selection_field";
import {STATUS_COLORS, STATUS_COLOR_PREFIX} from "../../utils/server_utils.esm";
export class ServerStatusField extends StateSelectionField {
/**
* @override
*/
setup() {
super.setup();
this.colorPrefix = STATUS_COLOR_PREFIX;
this.colors = STATUS_COLORS;
}
/**
* @override
*/
get options() {
return [[false, "Undefined"], ...super.options];
}
/**
* @override
*/
get showLabel() {
return !this.props.hideLabel;
}
}
registry.category("fields").add("server_status", ServerStatusField);

View File

@@ -1,33 +0,0 @@
.o_server_status_bubble {
@extend .o_status;
&.o_color_server_status_bubble_info {
background-color: $o-info;
}
&.o_color_server_status_bubble_success {
background-color: $o-success;
}
&.o_color_server_status_bubble_danger {
background-color: $o-danger;
}
&.o_color_server_status_bubble_warning {
background-color: $o-warning;
}
}
.o_field_server_status {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
margin: 0px 16px;
border-radius: 5px;
border: 1px solid #e5e5e5;
width: fit-content !important;
.o_status_label {
color: #4c4c4c;
font-size: 14px;
margin-left: 0.5rem !important;
display: block;
}
}

View File

@@ -1,17 +0,0 @@
/** @odoo-module */
/**
* List of colors according to the selection value
*/
export const STATUS_COLORS = {
false: "info",
stopped: "danger",
starting: "warning",
running: "success",
stopping: "warning",
restarting: "warning",
delete_error: "danger",
};
export const STATUS_COLOR_PREFIX =
"o_server_status_bubble mx-0 o_color_server_status_bubble_";