Wipe addons/: full reset for clean re-upload
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 117 KiB |
@@ -1,801 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Cetmix Tower Server</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="cetmix-tower-server">
|
||||
<h1 class="title">Cetmix Tower Server</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:4e04c56ceb53a86825bfbc09ed6acd8bbcaa032e85cef16c43997437620f429d
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_server"><img alt="cetmix/cetmix-tower" src="https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github" /></a></p>
|
||||
<p><a class="reference external" href="https://cetmix.com/tower">Cetmix Tower</a> offers a streamlined
|
||||
solution for managing remote servers and applications via SSH or API
|
||||
calls directly from <a class="reference external" href="https://odoo.com">Odoo</a>. It is designed for
|
||||
versatility across different operating systems and software
|
||||
environments, providing a practical option for those looking to manage
|
||||
servers without getting tied down by vendor or technology constraints.</p>
|
||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
||||
documentation</a> for detailed information.</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
|
||||
<li><a class="reference internal" href="#changelog" id="toc-entry-3">Changelog</a><ul>
|
||||
<li><a class="reference internal" href="#section-1" id="toc-entry-4">16.0.3.0.1 (2026-03-27)</a></li>
|
||||
<li><a class="reference internal" href="#section-2" id="toc-entry-5">16.0.3.0.0 (2026-03-23)</a></li>
|
||||
<li><a class="reference internal" href="#section-3" id="toc-entry-6">16.0.2.2.14 (2026-02-17)</a></li>
|
||||
<li><a class="reference internal" href="#section-4" id="toc-entry-7">16.0.2.2.13 (2026-01-12)</a></li>
|
||||
<li><a class="reference internal" href="#section-5" id="toc-entry-8">16.0.2.2.12 (2026-01-11)</a></li>
|
||||
<li><a class="reference internal" href="#section-6" id="toc-entry-9">16.0.2.2.11 (2026-01-08)</a></li>
|
||||
<li><a class="reference internal" href="#section-7" id="toc-entry-10">16.0.2.2.10 (2026-01-08)</a></li>
|
||||
<li><a class="reference internal" href="#section-8" id="toc-entry-11">16.0.2.2.8 (2025-12-22)</a></li>
|
||||
<li><a class="reference internal" href="#section-9" id="toc-entry-12">16.0.2.2.7 (2025-12-16)</a></li>
|
||||
<li><a class="reference internal" href="#section-10" id="toc-entry-13">16.0.2.2.6 (2025-12-11)</a></li>
|
||||
<li><a class="reference internal" href="#section-11" id="toc-entry-14">16.0.2.2.5 (2025-12-10)</a></li>
|
||||
<li><a class="reference internal" href="#section-12" id="toc-entry-15">16.0.2.2.4 (2025-12-10)</a></li>
|
||||
<li><a class="reference internal" href="#section-13" id="toc-entry-16">16.0.2.2.3 (2025-12-03)</a></li>
|
||||
<li><a class="reference internal" href="#section-14" id="toc-entry-17">16.0.2.2.2 (2025-12-03)</a></li>
|
||||
<li><a class="reference internal" href="#section-15" id="toc-entry-18">16.0.2.2.0 (2025-11-12)</a></li>
|
||||
<li><a class="reference internal" href="#section-16" id="toc-entry-19">16.0.2.0.6 (2025-10-27)</a></li>
|
||||
<li><a class="reference internal" href="#section-17" id="toc-entry-20">16.0.2.0.5 (2025-10-16)</a></li>
|
||||
<li><a class="reference internal" href="#section-18" id="toc-entry-21">16.0.2.0.4 (2025-10-13)</a></li>
|
||||
<li><a class="reference internal" href="#section-19" id="toc-entry-22">16.0.2.0.3 (2025-10-13)</a></li>
|
||||
<li><a class="reference internal" href="#section-20" id="toc-entry-23">16.0.2.0.2 (2025-10-08)</a></li>
|
||||
<li><a class="reference internal" href="#section-21" id="toc-entry-24">16.0.2.0.1 (2025-10-08)</a></li>
|
||||
<li><a class="reference internal" href="#section-22" id="toc-entry-25">16.0.2.0.0 (2025-10-07)</a></li>
|
||||
<li><a class="reference internal" href="#section-23" id="toc-entry-26">16.0.1.7.2 (2025-09-18)</a></li>
|
||||
<li><a class="reference internal" href="#section-24" id="toc-entry-27">16.0.1.7.1 (2025-09-10)</a></li>
|
||||
<li><a class="reference internal" href="#section-25" id="toc-entry-28">16.0.1.6.4 (2025-08-18)</a></li>
|
||||
<li><a class="reference internal" href="#section-26" id="toc-entry-29">16.0.1.6.3 (2025-08-13)</a></li>
|
||||
<li><a class="reference internal" href="#section-27" id="toc-entry-30">16.0.1.6.2 (2025-08-05)</a></li>
|
||||
<li><a class="reference internal" href="#section-28" id="toc-entry-31">16.0.1.6.0 (2025-07-30)</a></li>
|
||||
<li><a class="reference internal" href="#section-29" id="toc-entry-32">16.0.1.5.3 (2025-07-29)</a></li>
|
||||
<li><a class="reference internal" href="#section-30" id="toc-entry-33">16.0.1.5.1 (2025-07-25)</a></li>
|
||||
<li><a class="reference internal" href="#section-31" id="toc-entry-34">16.0.1.5.0 (2025-07-22)</a></li>
|
||||
<li><a class="reference internal" href="#section-32" id="toc-entry-35">16.0.1.3.0 (2025-07-17)</a></li>
|
||||
<li><a class="reference internal" href="#section-33" id="toc-entry-36">16.0.1.1.4 (2025-07-07)</a></li>
|
||||
<li><a class="reference internal" href="#section-34" id="toc-entry-37">16.0.1.1.2 (2025-06-25)</a></li>
|
||||
<li><a class="reference internal" href="#section-35" id="toc-entry-38">16.0.1.1.1 (2025-06-21)</a></li>
|
||||
<li><a class="reference internal" href="#section-36" id="toc-entry-39">16.0.1.1.0 (2025-06-20)</a></li>
|
||||
<li><a class="reference internal" href="#section-37" id="toc-entry-40">16.0.1.0.12 (2025-06-06)</a></li>
|
||||
<li><a class="reference internal" href="#section-38" id="toc-entry-41">16.0.1.0.11 (2025-06-06)</a></li>
|
||||
<li><a class="reference internal" href="#section-39" id="toc-entry-42">16.0.1.0.10 (2025-05-24)</a></li>
|
||||
<li><a class="reference internal" href="#section-40" id="toc-entry-43">16.0.1.0.9 (2025-05-23)</a></li>
|
||||
<li><a class="reference internal" href="#section-41" id="toc-entry-44">16.0.1.0.8 (2025-05-21)</a></li>
|
||||
<li><a class="reference internal" href="#section-42" id="toc-entry-45">16.0.1.0.7 (2025-05-16)</a></li>
|
||||
<li><a class="reference internal" href="#section-43" id="toc-entry-46">16.0.1.0.6 (2025-05-16)</a></li>
|
||||
<li><a class="reference internal" href="#section-44" id="toc-entry-47">16.0.1.0.5 (2025-05-09)</a></li>
|
||||
<li><a class="reference internal" href="#section-45" id="toc-entry-48">16.0.1.0.4 (2025-04-30)</a></li>
|
||||
<li><a class="reference internal" href="#section-46" id="toc-entry-49">16.0.1.0.3 (2025-04-22)</a></li>
|
||||
<li><a class="reference internal" href="#section-47" id="toc-entry-50">16.0.1.0.2 (2025-04-22)</a></li>
|
||||
<li><a class="reference internal" href="#section-48" id="toc-entry-51">16.0.1.0.1</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-52">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-53">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-54">Authors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-55">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
|
||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
||||
documentation</a> for detailed configuration
|
||||
instructions.</p>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
|
||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
||||
documentation</a> for detailed usage
|
||||
instructions.</p>
|
||||
</div>
|
||||
<div class="section" id="changelog">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Changelog</a></h1>
|
||||
<div class="section" id="section-1">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">16.0.3.0.1 (2026-03-27)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Waypoint behavior improvements. (5313)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-2">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">16.0.3.0.0 (2026-03-23)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Jets! (4700)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-3">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">16.0.2.2.14 (2026-02-17)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Blacklist filter for Python commands, value checker for
|
||||
Vault. (5253)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-4">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">16.0.2.2.13 (2026-01-12)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Last flight plan line post-run action was not triggered
|
||||
correctly. (5120)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-5">
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">16.0.2.2.12 (2026-01-11)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Improve the ‘File using template’ command flow, fix the
|
||||
flight plan line view layout. (5197)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-6">
|
||||
<h2><a class="toc-backref" href="#toc-entry-9">16.0.2.2.11 (2026-01-08)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Ensure custom values can be updated even if not provided
|
||||
initially. (5175)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-7">
|
||||
<h2><a class="toc-backref" href="#toc-entry-10">16.0.2.2.10 (2026-01-08)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Scheduled tasks: allow to select specific days of week.
|
||||
(5190)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-8">
|
||||
<h2><a class="toc-backref" href="#toc-entry-11">16.0.2.2.8 (2025-12-22)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Handle malformed expressions in flight plan line conditions.
|
||||
(5154)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-9">
|
||||
<h2><a class="toc-backref" href="#toc-entry-12">16.0.2.2.7 (2025-12-16)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Support for ANSI formatting in server logs. (5141)</li>
|
||||
<li>Bugfixes: UI/UX fixed and improvements. (5141)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-10">
|
||||
<h2><a class="toc-backref" href="#toc-entry-13">16.0.2.2.6 (2025-12-11)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Improve search views, implement the search panel for
|
||||
selected views. (5139)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-11">
|
||||
<h2><a class="toc-backref" href="#toc-entry-14">16.0.2.2.5 (2025-12-10)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Custom values in flight plan are lost in a skipped command
|
||||
and are not available after it. (5129)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-12">
|
||||
<h2><a class="toc-backref" href="#toc-entry-15">16.0.2.2.4 (2025-12-10)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Parse empty or missing key values as ‘None’ instead of
|
||||
leaving key reference as is. (5134)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-13">
|
||||
<h2><a class="toc-backref" href="#toc-entry-16">16.0.2.2.3 (2025-12-03)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Save correct error message in log when SSH connection fails.
|
||||
(5109)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-14">
|
||||
<h2><a class="toc-backref" href="#toc-entry-17">16.0.2.2.2 (2025-12-03)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Make variables selectable in scheduled tasks (5105)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-15">
|
||||
<h2><a class="toc-backref" href="#toc-entry-18">16.0.2.2.0 (2025-11-12)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Integrate user notifications into the main module, drop the
|
||||
‘cetmix_tower_notify_backend’ module. (5074)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-16">
|
||||
<h2><a class="toc-backref" href="#toc-entry-19">16.0.2.0.6 (2025-10-27)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Tag mixin and helper commands. (5039)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-17">
|
||||
<h2><a class="toc-backref" href="#toc-entry-20">16.0.2.0.5 (2025-10-16)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Flight plan command exception handling (4930)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-18">
|
||||
<h2><a class="toc-backref" href="#toc-entry-21">16.0.2.0.4 (2025-10-13)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Auto update references for related records (5005)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-19">
|
||||
<h2><a class="toc-backref" href="#toc-entry-22">16.0.2.0.3 (2025-10-13)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Terminate running flight plan manually (3410)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-20">
|
||||
<h2><a class="toc-backref" href="#toc-entry-23">16.0.2.0.2 (2025-10-08)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: UI/UX improvements (4996)</li>
|
||||
<li>Bugfixes: Handle secret values when a record is duplicated using
|
||||
copy() (4996)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-21">
|
||||
<h2><a class="toc-backref" href="#toc-entry-24">16.0.2.0.1 (2025-10-08)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Improve variable value references uniqueness (4961)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-22">
|
||||
<h2><a class="toc-backref" href="#toc-entry-25">16.0.2.0.0 (2025-10-07)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: ‘Cetmix Tower Vault’ - new way of centralized password/key
|
||||
management (4824)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-23">
|
||||
<h2><a class="toc-backref" href="#toc-entry-26">16.0.1.7.2 (2025-09-18)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Set ‘Auto Sync’ in files from file templates (4949)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-24">
|
||||
<h2><a class="toc-backref" href="#toc-entry-27">16.0.1.7.1 (2025-09-10)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Check custom values in flight plan line condition (4922)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-25">
|
||||
<h2><a class="toc-backref" href="#toc-entry-28">16.0.1.6.4 (2025-08-18)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Improve the extendability of the file upload command. (4759)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-26">
|
||||
<h2><a class="toc-backref" href="#toc-entry-29">16.0.1.6.3 (2025-08-13)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Improve access settings for logs (4866)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-27">
|
||||
<h2><a class="toc-backref" href="#toc-entry-30">16.0.1.6.2 (2025-08-05)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Pin paramiko version to “<4” to maintain compatibility with
|
||||
legacy installations (4891)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-28">
|
||||
<h2><a class="toc-backref" href="#toc-entry-31">16.0.1.6.0 (2025-07-30)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Optional behaviour when file uploaded by command already
|
||||
exists on the server. (4740)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-29">
|
||||
<h2><a class="toc-backref" href="#toc-entry-32">16.0.1.5.3 (2025-07-29)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Make file references server dependent to be more unique
|
||||
(4870)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-30">
|
||||
<h2><a class="toc-backref" href="#toc-entry-33">16.0.1.5.1 (2025-07-25)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Select secrets from dropdown list in the code fields (4853)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-31">
|
||||
<h2><a class="toc-backref" href="#toc-entry-34">16.0.1.5.0 (2025-07-22)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Select variables from dropdown list in the code fields
|
||||
(4827)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-32">
|
||||
<h2><a class="toc-backref" href="#toc-entry-35">16.0.1.3.0 (2025-07-17)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Add the tldextract and dnspython libraries. (4737)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-33">
|
||||
<h2><a class="toc-backref" href="#toc-entry-36">16.0.1.1.4 (2025-07-07)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Command log sorting (4816)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-34">
|
||||
<h2><a class="toc-backref" href="#toc-entry-37">16.0.1.1.2 (2025-06-25)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Required variables in servers (4779)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-35">
|
||||
<h2><a class="toc-backref" href="#toc-entry-38">16.0.1.1.1 (2025-06-21)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Command view improvements (4753)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-36">
|
||||
<h2><a class="toc-backref" href="#toc-entry-39">16.0.1.1.0 (2025-06-20)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Run commands and flight plans using scheduled tasks. (4650)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-37">
|
||||
<h2><a class="toc-backref" href="#toc-entry-40">16.0.1.0.12 (2025-06-06)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Improve command and flight plan log management. (4749)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-38">
|
||||
<h2><a class="toc-backref" href="#toc-entry-41">16.0.1.0.11 (2025-06-06)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Host key cannot be retrieved from the UI. (4747)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-39">
|
||||
<h2><a class="toc-backref" href="#toc-entry-42">16.0.1.0.10 (2025-05-24)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Improve command log and flight plan form views (4697)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-40">
|
||||
<h2><a class="toc-backref" href="#toc-entry-43">16.0.1.0.9 (2025-05-23)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Error when rendering a file not attached to a server. (4715)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-41">
|
||||
<h2><a class="toc-backref" href="#toc-entry-44">16.0.1.0.8 (2025-05-21)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: References for secret values. (4696)</li>
|
||||
<li>Features: Make the “Host key” field non-required in the form view to
|
||||
improve the UX. (4699)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-42">
|
||||
<h2><a class="toc-backref" href="#toc-entry-45">16.0.1.0.7 (2025-05-16)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Option to preserve command splitting when using sudo. (4641)</li>
|
||||
<li>Features: Record references for files. (4670)</li>
|
||||
<li>Features: Use <tt class="docutils literal">sudo</tt> parameter to pass sudo mode to command runner
|
||||
instead of using context. (4678)</li>
|
||||
<li>Bugfixes: Incorrect sudo usage in commands run in wizard. Pass ‘No
|
||||
split for sudo’ property to commands run in wizard. (4679)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-43">
|
||||
<h2><a class="toc-backref" href="#toc-entry-46">16.0.1.0.6 (2025-05-16)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Improve the key storage functionality. (4686)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-44">
|
||||
<h2><a class="toc-backref" href="#toc-entry-47">16.0.1.0.5 (2025-05-09)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Non-critical issues and performance improvements. (4663)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-45">
|
||||
<h2><a class="toc-backref" href="#toc-entry-48">16.0.1.0.4 (2025-04-30)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: UI/UX improvements. (4642)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-46">
|
||||
<h2><a class="toc-backref" href="#toc-entry-49">16.0.1.0.3 (2025-04-22)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Allow to pass custom variable values to commands (4524)</li>
|
||||
<li>Features: Cetmix Tower Odoo Automation model: pass custom variable
|
||||
values to the <tt class="docutils literal">server_run_command</tt> method. (4547)</li>
|
||||
<li>Bugfixes: Random id generation, sudo command parsing, record rule
|
||||
names, spelling errors in descriptions. (4612)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-47">
|
||||
<h2><a class="toc-backref" href="#toc-entry-50">16.0.1.0.2 (2025-04-22)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Refactor secret value handling, fix the new server template
|
||||
creation wizard. (4601)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-48">
|
||||
<h2><a class="toc-backref" href="#toc-entry-51">16.0.1.0.1</a></h2>
|
||||
<p>Release for Odoo 16.0</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-52">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues">GitHub Issues</a>.
|
||||
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
|
||||
<a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_server%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-53">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-54">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Cetmix</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-55">Maintainers</a></h2>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_server">cetmix/cetmix-tower</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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_";
|
||||