This commit is contained in:
2025-08-19 15:05:55 +07:00
parent 70ae66ee4b
commit f78dc34aee
161 changed files with 13703 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+136
View File
@@ -0,0 +1,136 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import {
Component,
onWillStart,
onWillRender,
onRendered,
onMounted,
onWillUpdateProps,
onWillPatch,
onPatched,
onWillUnmount,
onWillDestroy,
onError,
useState,
useRef
} from "@odoo/owl";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
import { Layout } from "@web/search/layout";
class CheatOwl extends Component {
static template = "cheat_owl";
static components = { Layout };
static props = {
...standardActionServiceProps,
};
getRandomInteger(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
setup() {
this.useRefCardTextRef = useRef("useRefCardText");
//#region non reactive
this.nonReactiveRandomValue = this.getRandomInteger(0, 100);
this.nonReactiveRandomValue1Ref = useRef("nonReactiveRandomValue1");
this.nonReactiveRandomValue2Ref = useRef("nonReactiveRandomValue2");
this.nonReactiveRandomValue3Ref = useRef("nonReactiveRandomValue3");
this.nonReactiveRandomValue4Ref = useRef("nonReactiveRandomValue4");
//#endregion
//#region reactive
this.state = useState({
randomValue: this.getRandomInteger(0, 100),
});
//#endregion
//#region input binding
this.bindingValue = "";
this.inputBindingRef = useRef("inputBinding");
//#endregion
//#region two way input binding
this.bindingState = useState({
valueStandardInputBinding: "",
valueTextAreaInputBinding: "",
valueCheckBoxInputBinding: false,
valueRadioButtonInputBinding: "",
valueSelectionInputBinding: "",
valueRangeInputBinding: 0,
});
//#endregion
//#region owl lifecycle
onWillStart(() => {
console.log("onWIllStart.");
});
onWillRender(() => {
console.log("onWillRender.");
});
onRendered(() => {
console.log("onRendered.");
});
onMounted(() => {
console.log("onMounted.");
});
onWillUpdateProps((nextProps) => {
console.log("onWillUpdateProps:", nextProps);
});
onWillPatch(() => {
console.log("onWillPatch.");
});
onPatched(() => {
console.log("onPatched.");
});
onWillUnmount(() => {
console.log("onWillUnmount.");
});
onWillDestroy(() => {
console.log("onWillDestroy.");
});
onError(() => {
console.log("onError.");
});
//#endregion
}
//#region functions
generateNonReactiveRandomValue() {
let randomVal = this.getRandomInteger(0, 100);
this.nonReactiveRandomValue1Ref.el.innerText = randomVal;
this.nonReactiveRandomValue2Ref.el.innerText = randomVal;
this.nonReactiveRandomValue3Ref.el.innerText = randomVal;
this.nonReactiveRandomValue4Ref.el.innerText = randomVal;
}
generateRandomValue() {
this.state.randomValue = this.getRandomInteger(0, 100);
}
changeUseRefCardTextColor() {
this.useRefCardTextRef.el.classList.toggle("text-danger");
}
getInputBindingValue(){
this.inputBindingRef.el.innerText = this.bindingValue;
}
changeValueStandardInputBinding(){
this.bindingState.valueStandardInputBinding = "Random value: " + this.getRandomInteger(0, 100);
}
//#endregion
}
registry.category("actions").add("cheat_owl", CheatOwl);
+199
View File
@@ -0,0 +1,199 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="cheat_owl" owl="1">
<Layout display="{ controlPanel: {} }" className="'overflow-auto h-100'">
<div class="cheat-web-container">
<div class="row g-2">
<div class="col-xl-2 col-sm-6">
<div class="card">
<h5 class="card-header">
<a class="collapsed d-block" data-bs-toggle="collapse" href="#collapse-owl-use-ref" aria-expanded="true" aria-controls="collapse-collapsed">
<em>useRef</em> Example
<i class="fa fa-chevron-down float-end"></i>
</a>
</h5>
<div id="collapse-owl-use-ref" class="collapse">
<div class="card-body">
<p class="card-text" t-ref="useRefCardText">
This text color can be changed by clicking the button below.
</p>
</div>
<div class="card-footer">
<div class="d-flex justify-content-end">
<button t-on-click="changeUseRefCardTextColor" class="btn btn-sm btn-outline-primary w-50">Change Color</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card">
<h5 class="card-header">
<a class="collapsed d-block" data-bs-toggle="collapse" href="#collapse-owl-use-state" aria-expanded="true" aria-controls="collapse-collapsed">
<em>useState</em> Example
<i class="fa fa-chevron-down float-end"></i>
</a>
</h5>
<div id="collapse-owl-use-state" class="collapse">
<div class="card-body">
<p class="card-text">
This is a random value: <br/>
<span class="text-primary"><t t-out="this.state.randomValue" /></span>
<span class="text-danger ms-1"><t t-out="this.state.randomValue" /></span>
<span class="text-warning ms-1"><t t-out="this.state.randomValue" /></span>
<span class="text-info ms-1"><t t-out="this.state.randomValue" /></span>
</p>
</div>
<div class="card-footer">
<div class="d-flex justify-content-end">
<button t-on-click="generateRandomValue" class="btn btn-sm btn-outline-primary w-50">Randomize</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card">
<h5 class="card-header">
<a class="collapsed d-block" data-bs-toggle="collapse" href="#collapse-owl-non-reactive" aria-expanded="true" aria-controls="collapse-collapsed">
Non Reactive Example
<i class="fa fa-chevron-down float-end"></i>
</a>
</h5>
<div id="collapse-owl-non-reactive" class="collapse">
<div class="card-body">
<p class="card-text">
This is a random value: <br/>
<span class="text-primary" t-ref="nonReactiveRandomValue1"><t t-out="this.nonReactiveRandomValue" /></span>
<span class="text-danger ms-1" t-ref="nonReactiveRandomValue2"><t t-out="this.nonReactiveRandomValue" /></span>
<span class="text-warning ms-1" t-ref="nonReactiveRandomValue3"><t t-out="this.nonReactiveRandomValue" /></span>
<span class="text-info ms-1" t-ref="nonReactiveRandomValue4"><t t-out="this.nonReactiveRandomValue" /></span>
</p>
</div>
<div class="card-footer">
<div class="d-flex justify-content-end">
<button t-on-click="generateNonReactiveRandomValue" class="btn btn-sm btn-outline-primary w-50">Randomize</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-1 g-2">
<div class="col-12">
<div class="card">
<h5 class="card-header">
<a class="collapsed d-block" data-bs-toggle="collapse" href="#collapse-owl-input-binding" aria-expanded="true" aria-controls="collapse-collapsed">
Input Binding
<i class="fa fa-chevron-down float-end"></i>
</a>
</h5>
<div id="collapse-owl-input-binding" class="collapse">
<div class="row m-2 mt-0 g-2">
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-keyboard-o" /> Input</h5>
<div class="card-body">
<p class="card-text">
<input t-model="this.bindingValue"/>
<hr/>
<div>Input value: <span t-ref="inputBinding"> <t t-out="this.bindingValue" /></span></div>
<button class="btn btn-sm btn-outline-primary" t-on-click="getInputBindingValue">Get Input Value</button>
</p>
</div>
</div>
</div>
</div>
<div class="row m-2 mt-0 g-2">
<div class="hr-sect">Two-way Binding</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-keyboard-o" /> Input</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<input t-model="this.bindingState.valueStandardInputBinding"/>
<div><hr/>Input value: <t t-out="this.bindingState.valueStandardInputBinding" /></div>
<button class="btn btn-sm btn-outline-primary" t-on-click="changeValueStandardInputBinding">Randomize</button>
</p>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-font" /> Textarea</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<textarea t-model="this.bindingState.valueTextAreaInputBinding"/>
<div><hr/>Input value: <t t-out="this.bindingState.valueTextAreaInputBinding" /></div>
</p>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-check-square" /> Checkbox</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="cbInputBindingCheck" t-model="this.bindingState.valueCheckBoxInputBinding"/>
<label class="form-check-label" for="cbInputBindingCheck">Checkbox</label>
</div>
<div><hr/>Input value: <t t-out="this.bindingState.valueCheckBoxInputBinding" /></div>
</p>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-stop-circle-o" /> Radio Buttons</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<div>
<div class="form-check">
<input class="form-check-input" type="radio" id="cbInputBindingOne" value="one" t-model="this.bindingState.valueRadioButtonInputBinding"/>
<label class="form-check-label" for="cbInputBindingOne">One</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" id="cbInputBindingTwo" value="two" t-model="this.bindingState.valueRadioButtonInputBinding" />
<label class="form-check-label" for="cbInputBindingTwo">Two</label>
</div>
</div>
<div><hr/>Input value: <t t-out="this.bindingState.valueRadioButtonInputBinding" /></div>
</p>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-caret-square-o-down" /> Select</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<select class="form-select" t-model="this.bindingState.valueSelectionInputBinding">
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
<div><hr/>Input value: <t t-out="this.bindingState.valueSelectionInputBinding" /></div>
</p>
</div>
</div>
</div>
<div class="col-xl-2 col-sm-6">
<div class="card h-100">
<h5 class="card-header"><i class="fa fa-arrows-h" /> Range</h5>
<div class="card-body">
<p class="card-text justify-content-between d-flex flex-column h-100">
<input class="form-range" min="0" max="5" step="0.5" type="range" t-model="this.bindingState.valueRangeInputBinding"/>
<div><hr/>Input value: <t t-out="this.bindingState.valueRangeInputBinding" /></div>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
</t>
</templates>
+79
View File
@@ -0,0 +1,79 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { Component, useState, markup } from "@odoo/owl";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
import { Layout } from "@web/search/layout";
import { HAccordion } from "./core/haccordion/haccordion";
import { Collapsible } from "./core/collapsible/collapsible";
import { CheatOwlQwebInlineTemplate } from "./cheat_owl_qweb_inline_template"
class CheatOwlQweb extends Component {
static template = "cheat_owl_qweb";
static components = { Layout, CheatOwlQwebInlineTemplate, HAccordion, Collapsible };
static props = {
...standardActionServiceProps,
};
//#region Misc Functions
getRandomInteger(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
get randomString() {
var letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var llen = letters.length;
var res = "";
for (var i = 0; i < 6; i++) {
res += letters[Math.floor(Math.random() * llen)];
}
return res;
}
get randomBoolean() {
return Math.round(Math.random()) == 1;
}
get dynamicTemplate() {
if (this.state.dynamicTemplate === "one") {
return "cheat_owl_qweb_sub_template_one"
} else if (this.state.dynamicTemplate === "two"){
return "cheat_owl_qweb_sub_template_two"
} else if (this.state.dynamicTemplate === "three"){
return "cheat_owl_qweb_sub_template_three"
} else {
return "cheat_owl_qweb_sub_template_four"
}
}
//#endregion
setup() {
this.state = useState({
randomValue: this.getRandomInteger(0, 100),
checkBoxValue: "one",
dynamicTag: "div",
dynamicTemplate: "one"
});
this.markup = markup;
this.divText = "<div>some text 1</div>";
this.markupDivText = markup("<div>some text 2</div>");
this.arrayOfObjects = [
{
id: 1,
name: "Item No. 1",
},
{
id: 2,
name: "Item No. 2",
},
{
id: 3,
name: "Item No. 3",
},
];
this.aSetOfObjects = new Set(this.arrayOfObjects);
}
}
registry.category("actions").add("cheat_owl_qweb", CheatOwlQweb);
+404
View File
@@ -0,0 +1,404 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="cheat_owl_qweb">
<Layout display="{ controlPanel: {} }" className="'overflow-auto h-100'">
<HAccordion name="'hacc'" startItem="0">
<t t-set-slot="basics-1" title.translate="The Basics - I">
<div class="d-flex w-100 flex-column gap-2 pt-2">
<div class="row g-2 w-100">
<div class="col-xl-4 col-sm-12">
<Collapsible name="'inline-template'" title.translate="Inline Template">
<CheatOwlQwebInlineTemplate />
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'white-spaces'" title.translate="White Spaces">
<p>
<span>This is white spaces in a <span class="badge">&lt;span&gt;</span> : ' '
</span>
</p>
<p>
<span>This is a <span class="badge">&lt;span&gt;</span> with a line break:
'
'
</span>
</p>
<p>
<pre>This is white spaces in a &lt;pre&gt;: ' '</pre>
</p>
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'outputs'" title.translate="Displaying Data">
<p>
<span>This is an html fragment <span class="badge">t-out</span> :
<pre><t t-out="divText" /></pre>
</span>
<span>This is an html fragment <span class="badge">t-out</span> with markup:
<pre><t t-out="markupDivText" /></pre>
</span>
</p>
<p>
<span>This is an html fragment <span class="badge">t-esc</span> :
<pre><t t-esc="divText" /></pre>
</span>
<span>This is an html fragment <span class="badge">t-esc</span> with markup:
<pre><t t-esc="markupDivText" /></pre>
</span>
</p>
</Collapsible>
</div>
</div>
<div class="row g-2 w-100">
<div class="col-xl-4 col-sm-12">
<Collapsible name="'variables'" title.translate="Variables">
<p>
<t t-set="foo" t-value="'bar'" />
<span>The variable <span class="badge">foo</span> is set to <span class="badge"><t t-out="foo"/></span>.</span>
</p>
<p>
<t t-set="wal">
<strong>do</strong>
</t>
<span>The variable <span class="badge">wal</span> is set to <span class="badge"><t t-out="wal"/></span> but inside tags.</span>
</p>
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'expressions'" title.translate="Basic Expressions">
<p>
<span>This is evaluation result of <span class="badge">1 + 3</span> :
<pre><t t-out="1 + 3" /></pre>
</span>
<span>With <span class="badge">state.randomValue = <t t-out="state.randomValue"/></span>, this expression <span class="badge">1 + state.randomValue</span> will result to:
<pre><t t-out="1 + state.randomValue" /></pre>
</span>
</p>
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'logical-expressions'" title.translate="Logical Expressions">
<p>
<table class="table table-striped table-hover">
<thead>
<tr class="table-secondary">
<th>Operation</th>
<th>Operator</th>
<th>Example</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>Equality</td>
<td>==</td>
<td>1 == 3</td>
<td><t t-out="1 == 3" /></td>
</tr>
<tr>
<td>And</td>
<td>and</td>
<td>true and false</td>
<td><t t-out="true and false" /></td>
</tr>
<tr>
<td>Or</td>
<td>or</td>
<td>true or false</td>
<td><t t-out="true or false" /></td>
</tr>
<tr>
<td>Greater Than</td>
<td>gt</td>
<td>1 gt 1</td>
<td><t t-out="1 gt 1" /></td>
</tr>
<tr>
<td>Greater Than or Equal</td>
<td>gte</td>
<td>1 gte 1</td>
<td><t t-out="1 gte 1" /></td>
</tr>
<tr>
<td>Less Than</td>
<td>lt</td>
<td>1 lt 1</td>
<td><t t-out="1 lt 1" /></td>
</tr>
<tr>
<td>Less Than or Equal</td>
<td>lte</td>
<td>1 lte 1</td>
<td><t t-out="1 lte 1" /></td>
</tr>
</tbody>
</table>
</p>
</Collapsible>
</div>
</div>
</div>
</t>
<t t-set-slot="basics-2" title.translate="The Basics - II">
<div class="d-flex w-100 flex-column gap-2 pt-2">
<div class="row g-2 w-100">
<div class="col-xl-4 col-sm-12">
<Collapsible name="'conditionals'" title.translate="Conditionals">
<p>
<div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbConditionalOne"
name="cbConditionals"
value="one"
t-model="state.checkBoxValue" />
<label class="form-check-label" for="cbConditionalOne">One</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbConditionalTwo"
name="cbConditionals"
value="two"
t-model="state.checkBoxValue" />
<label class="form-check-label" for="cbConditionalTwo">Two</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbConditionalThree"
name="cbConditionals"
value="three"
t-model="state.checkBoxValue" />
<label class="form-check-label" for="cbConditionalThree">Three</label>
</div>
</div>
<span t-if="state.checkBoxValue === 'one'">This will be displayed if the first radio button above is selected.</span>
<span t-elif="state.checkBoxValue === 'two'">This will be displayed if the second radio button is checked.</span>
<span t-else="">This will be displayed if the third radio button is checked.</span>
</p>
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'loops'" title.translate="Loops">
<p>
<span>This list is made using <span class="badge">t-foreach</span> loop:</span>
<ul>
<t t-foreach="[1, 2, 3]" t-as="i" t-key="i">
<li><t t-out="'Item: ' + i.toString()"/></li>
</t>
</ul>
</p>
<p>
<span>This list is made using <span class="badge">t-foreach</span> loop too but applied to the element instead of the <span class="badge">t</span> element:</span>
<ul>
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i"><t t-out="'Item: ' + i.toString()"/></li>
</ul>
</p>
<p>
<span>These demonstrate the <span class="badge">t-foreach</span>'s special variables:</span>
<table class="table table-striped table-hover mt-1">
<thead>
<tr class="table-secondary">
<td>x_index</td>
<td>x_value</td>
<td>x_first</td>
<td>x_last</td>
</tr>
</thead>
<tbody>
<tr t-foreach="arrayOfObjects" t-as="item" t-key="item.id">
<td><t t-out="item_index"/></td>
<td><t t-out="item_value.toString()"/></td>
<td><t t-out="item_first"/></td>
<td><t t-out="item_last"/></td>
</tr>
</tbody>
</table>
</p>
<p>
<span>This list is made using <span class="badge">Set</span> and spread operator <span class="badge">...</span> :</span>
<ul>
<t t-foreach="[...aSetOfObjects]" t-as="i" t-key="i.id">
<li><t t-out="i.name"/></li>
</t>
</ul>
</p>
</Collapsible>
</div>
<div class="col-xl-4 col-sm-12">
<Collapsible name="'dynamics'" title.translate="Dynamics">
<p>
<span>The placeholder attribute is set using random value:</span>
<input type="text" class="form-control mt-1" t-att-placeholder="randomString" />
</p>
<p>
<span>The placeholder attribute is set using string interpolation with random value:</span>
<input type="text" class="form-control mt-1" t-attf-placeholder="This is a random string: {{randomString}}" />
</p>
<p>
<span>The CSS class is set using object with CSS class as key and boolean value to indicate the inclusion
<span class="badge">{'text-info': true, 'fw-bold': true}</span>:</span>
<p t-att-class="{'text-info': true, 'fw-bold': true}">This is an example text.</p>
</p>
<p>
<span>This is an exmple of dynamic tag:</span>
<div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTagDiv"
name="cbDynamicTag"
value="div"
t-model="state.dynamicTag" />
<label class="form-check-label" for="cbDynamicTagDiv">Div</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTagSpan"
name="cbDynamicTag"
value="pre"
t-model="state.dynamicTag" />
<label class="form-check-label" for="cbDynamicTagSpan">Pre</label>
</div>
</div>
<t t-tag="state.dynamicTag">
<span>This text is inside <span class="badge"><t t-out="state.dynamicTag"/> element</span></span>
</t>
</p>
</Collapsible>
</div>
</div>
</div>
</t>
<t t-set-slot="more-on-templates" title.translate="More On Templates">
<div class="d-flex w-100 flex-column gap-2 pt-2">
<div class="row g-2 w-100">
<div class="col-xl-6 col-sm-12">
<Collapsible name="'sub-templates'" title.translate="Sub Templates">
<p>
<div>Sub templates are called with <span class="badge">t-call</span>:</div>
<t t-call="cheat_owl_qweb_sub_template_two"/>
</p>
<p>
<div>This templates is using special placeholder <span class="badge">t-out="0"</span>:</div>
<t t-call="cheat_owl_qweb_sub_template_three">
<span>This text is rendered inside the sub template.</span>
</t>
</p>
<p>
<div>Variables are passed down to the sub template:</div>
<t t-set="parentTemplateVariable" t-value="'This is a string declared on parent template.'"/>
<t t-call="cheat_owl_qweb_sub_template_four">
<t t-set="scopedTemplateVariable" t-value="'This is declared inside the t element.'"/>
</t>
<!-- scopedTemplateVariable does not exist here -->
</p>
<p>
<span>This is an example of dynamic sub templates:</span>
<div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTemplateOne"
name="cbDynamicTemplate"
value="one"
t-model="state.dynamicTemplate" />
<label class="form-check-label" for="cbDynamicTemplateOne">One</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTemplateTwo"
name="cbDynamicTemplate"
value="two"
t-model="state.dynamicTemplate" />
<label class="form-check-label" for="cbDynamicTemplateTwo">Two</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTemplateThree"
name="cbDynamicTemplate"
value="three"
t-model="state.dynamicTemplate" />
<label class="form-check-label" for="cbDynamicTemplateThree">Three</label>
</div>
<div class="form-check">
<input class="form-check-input"
type="radio"
id="cbDynamicTemplateFour"
name="cbDynamicTemplate"
value="four"
t-model="state.dynamicTemplate" />
<label class="form-check-label" for="cbDynamicTemplateFour">Four</label>
</div>
</div>
<t t-call="{{ dynamicTemplate }}"/>
</p>
</Collapsible>
</div>
<div class="col-xl-6 col-sm-12">
<Collapsible name="'template-inheritance'" title.translate="Template Inheritance">
<p>
<div>We call the parent template, but since there's an extension child which modified the content we get the modified version:</div>
<t t-call="cheat_owl.qweb_parent_template"/>
</p>
<p>
<div>This is the primary child template, which we can call directly by it's name:</div>
<t t-call="cheat_owl.qweb_primary_child_template"/>
</p>
</Collapsible>
</div>
</div>
</div>
</t>
<t t-set-slot="subscribe" title.translate="Subscribe for more tutorials">
<div class="d-flex d-flex h-100 justify-content-center align-items-start">
<div class="card" style="width: 150px;">
<img src="/cheat_web/static/img/exploring-odoo.png" class="card-img-top" />
<div class="card-body text-center">
<p class="card-text">
<a href="https://youtube.com/@exploring-odoo">Exploring Odoo</a>
</p>
</div>
</div>
</div>
</t>
</HAccordion>
</Layout>
</t>
<t t-name="cheat_owl_qweb_sub_template_one">
<div>This text is inside a template <span class="badge">cheat_owl_qweb_sub_template_one</span>.</div>
</t>
<t t-name="cheat_owl_qweb_sub_template_two">
<div>This text is inside a template <span class="badge">cheat_owl_qweb_sub_template_two</span>.</div>
<t t-call="cheat_owl_qweb_sub_template_one"/>
</t>
<t t-name="cheat_owl_qweb_sub_template_three">
<div>This text is inside a template <span class="badge">cheat_owl_qweb_sub_template_three</span>.</div>
<t t-out="0" />
</t>
<t t-name="cheat_owl_qweb_sub_template_four">
<div>This text is inside a template <span class="badge">cheat_owl_qweb_sub_template_four</span>.</div>
<div><t t-out="parentTemplateVariable" /></div>
<div><t t-out="scopedTemplateVariable" /></div>
</t>
<t t-name="cheat_owl.qweb_parent_template">
<p>This is parent template.</p>
</t>
<t t-name="cheat_owl.qweb_primary_child_template" t-inherit="cheat_owl.qweb_parent_template" t-inherit-mode="primary">
<xpath expr="//p[1]" position="after">
<p>This is primary child template.</p>
</xpath>
</t>
<t t-inherit="cheat_owl.qweb_parent_template" t-inherit-mode="extension">
<xpath expr="//p[1]" position="after">
<p>This is extension child template.</p>
</xpath>
</t>
</templates>
@@ -0,0 +1,11 @@
/** @odoo-module */
import { Component, xml } from "@odoo/owl";
export class CheatOwlQwebInlineTemplate extends Component {
static template = xml`
<p>
This text is in an inline template
</p>`;
static props = { };
}
+63
View File
@@ -0,0 +1,63 @@
.cheat-web-container {
margin: 2rem !important;
}
.cheat-web-user-profile {
margin: 1rem;
}
.cheat-web-user-profile .card {
width: 400px;
border: none;
border-radius: 10px;
background-color: #fff;
}
.cheat-web-user-profile .stats {
background: #f2f5f8 !important;
color: #000 !important;
}
.cheat-web-user-profile .articles {
font-size: 10px;
color: #a1aab9;
}
.cheat-web-user-profile .followers {
font-size: 10px;
color: #a1aab9;
}
.cheat-web-user-profile .rating {
font-size: 10px;
color: #a1aab9;
}
.cheat-web-user-profile .stat-number,
.cheat-web-user-profile .stat-string {
font-weight: 500;
}
.cheat-web-container .card-header .fa {
transition: 0.3s transform ease-in-out;
}
.cheat-web-container .card-header .collapsed .fa {
transform: rotate(90deg);
}
.hr-sect {
display: flex;
flex-basis: 100%;
align-items: center;
color: rgba(0, 0, 0, 0.35);
margin: 8px 0px;
}
.hr-sect::before,
.hr-sect::after {
content: "";
flex-grow: 1;
background: rgba(0, 0, 0, 0.35);
height: 1px;
font-size: 0px;
line-height: 0px;
margin: 0px 8px;
}
+298
View File
@@ -0,0 +1,298 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { Component, useState, xml } from "@odoo/owl";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
import { Layout } from "@web/search/layout";
import { AlertDialog, ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { Dialog } from "@web/core/dialog/dialog";
import { rpc } from "@web/core/network/rpc";
import { user } from "@web/core/user";
import { url } from "@web/core/utils/urls";
const { DateTime } = luxon;
//#region custom dialog
class MyDialog extends Component {
static components = { Dialog };
static template = xml`
<Dialog size="'md'" title="'This is my dialog title'">
<p>
This is my dialog content.
</p>
</Dialog>
`;
}
class MyDialogWithButtons extends Component {
static components = { Dialog };
static template = xml`
<Dialog size="'md'" title="'This is my dialog with buttons'">
<p>
This is my dialog with buttons content.
</p>
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="onConfirm">Ok</button>
<button class="btn btn-secondary" t-on-click="onCancel">Cancel</button>
</t>
</Dialog>
`;
setup() {
this.notification = useService("notification");
}
onConfirm() {
this.notification.add('You confirmed the dialog.');
this.props.close();
}
onCancel() {
this.notification.add('You canceled the dialog.');
this.props.close();
}
}
//#endregion
class CheatWeb extends Component {
//#region statics
static template = "cheat_web";
static components = { Layout };
static props = {
...standardActionServiceProps,
};
//#endregion
setup() {
//#region services
this.orm = useService("orm");
this.notification = useService("notification");
this.dialog = useService("dialog");
//#endregion
this.user = user;
this.state = useState({
userCharValue: user.settings.cheat_web_user_setting_char_field,
userIntegerValue: user.settings.cheat_web_user_setting_integer_field
});
}
//#region user
get userImage(){
return url("/web/image", {
model: 'res.users',
id: user.userId,
field: "avatar_128",
});
}
async dumpUserSettings(){
console.log("User:", user);
console.log("User settings:", user.settings);
}
async dumpState(){
console.log("state:", this.state);
}
async saveUserSettings(){
await user.setUserSettings(
"cheat_web_user_setting_char_field",
this.state.userCharValue
);
await user.setUserSettings(
"cheat_web_user_setting_integer_field",
this.state.userIntegerValue
);
}
//#endregion
//#region misc function
getRandomInteger(min, max) {
return Math.floor(Math.random() * (max - min) ) + min;
}
//#endregion
//#region rpc
async rpcDoSomething() {
const res = await rpc("/cheat/webrpc");
console.log(res);
}
async rpcDoSomethingElse() {
const res = await rpc("/cheat/webrpcwithparam", { param1: 1, param2: "2"});
console.log(res);
}
async rpcDoSomethingWithRouteParam() {
const param1 = 3;
const param2 = 4;
const res = await rpc(`/cheat/webrpc/${param1}/${param2}`);
console.log(res);
}
//#endregion
//#region orm
async ormCreate() {
const res = await this.orm.create("cheat.web", [
{
char_field: "Char Field #" + DateTime.now().toUnixInteger(),
int_field: this.getRandomInteger(1, 1000)
},
{
char_field: "Char Field #" + DateTime.now().toUnixInteger(),
int_field: this.getRandomInteger(1, 1000)
},
]);
console.log(res);
}
async ormSearch() {
const res = await this.orm.search("cheat.web", [['id', '>=', 0]], { order: "id desc", limit: 100, offset: 0 });
console.log(res);
}
async ormSearchCount() {
const res = await this.orm.searchCount("cheat.web", [['id', '>=', 0]]);
console.log(res);
}
async ormSearchRead() {
const res = await this.orm.searchRead("cheat.web", [['id', '>=', 0]], ["char_field", "int_field"]);
console.log(res);
}
async ormRead() {
const ids = await this.orm.search("cheat.web", [['id', '>=', 0]]);
if (ids.length > 0){
const res = await this.orm.read("cheat.web", [ids[0]], ["char_field"]);
console.log(res);
}
}
async ormWrite() {
const ids = await this.orm.search("cheat.web", [['id', '>=', 0]]);
if (ids.length > 0) {
let res = await this.orm.read("cheat.web", [ids[0]], ["char_field"]);
console.log('Old Values:', res);
await this.orm.write('cheat.web', [ids[0]],
{
'char_field': `Updated #${DateTime.now().toUnixInteger()}`
}
);
res = await this.orm.read("cheat.web", [ids[0]], ["char_field"]);
console.log('New Values:', res);
}
}
async ormUnlink() {
let ids = await this.orm.search("cheat.web", [['id', '>=', 0]], { order: "id desc" });
if (ids.length > 0) {
console.log('Search Result:', ids);
await this.orm.unlink('cheat.web', [ids[0]]);
ids = await this.orm.search("cheat.web", [['id', '>=', 0]], { order: "id desc" });
console.log('Search Result:', ids);
}
}
async ormDoSomething() {
const res = await this.orm.call("cheat.web", "do_something", [1]);
console.log(res);
}
async ormDoSomethingElse() {
const res = await this.orm.call("cheat.web", "do_something_else", [1], { param1: '1', param2: "2"});
console.log(res);
}
async ormDoModelMethod() {
const res = await this.orm.call("cheat.web", "do_model_method", [], { param1: "3", param2: "4" });
console.log(res);
}
//#endregion
//#region notification
async notifSimple(){
this.notification.add('This is a simple Notification');
}
async notifSticky(){
this.notification.add('This is a sticky Notification', {
title: 'Sticky Notification',
type: 'success',
sticky: true,
});
}
async notifCallback(){
this.notification.add('This is a Notification with onClose callback', {
title: 'Sticky Notification',
type: 'info',
sticky: true,
onClose: () => {
this.notification.add('This is from onclose callback.');
}
});
}
async notifWithButtons(){
this.notification.add('This is a Notification with Buttons', {
title: 'Sticky Notification',
type: 'warning',
sticky: true,
buttons: [
{
name: 'Button 1',
onClick: () => {
this.notification.add('This is from button 1 onclick.');
}
},
{
name: 'Button 2',
onClick: () => {
this.notification.add('This is from button 2 onclick.');
}
}
]
});
}
//#endregion
//#region dialog
async dialogAlert(){
this.dialog.add(AlertDialog, {
title: 'This is an Alert Dialog',
body: 'This is the alert message.',
contentClass: 'text-danger'
});
}
async dialogConfirm(){
this.dialog.add(ConfirmationDialog, {
title: 'This is an Confirmation Dialog',
body: 'This is the dialog message.',
confirmLabel: 'Click here to confirm.',
cancelLabel: "Click here to cancel.",
cancel: async () => {
this.notification.add('You canceled the dialog.');
},
confirm: async () => {
this.notification.add('You confirmed the dialog.');
},
});
}
async dialogCustom(){
this.dialog.add(MyDialog);
}
async dialogCustomWithButtons(){
this.dialog.add(MyDialogWithButtons);
}
//#endregion
}
registry.category("actions").add("cheat_web", CheatWeb);
+152
View File
@@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="cheat_web" owl="1">
<Layout display="{ controlPanel: {} }" className="'overflow-auto h-100'">
<div class="cheat-web-container">
<div class="accordion shadow-sm" id="mainAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseRpc" aria-expanded="true" aria-controls="collapseRpc">
RPC
</button>
</h2>
<div id="collapseRpc" class="accordion-collapse collapse show" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<div class="container mx-2 my-4 gx-2">
<div class="btn-group me-2" role="group">
<button class="btn btn-primary" t-on-click="rpcDoSomething">RPC Call</button>
<button class="btn btn-primary" t-on-click="rpcDoSomethingElse">RPC Call With Param</button>
<button class="btn btn-primary" t-on-click="rpcDoSomethingWithRouteParam">RPC Call With Router Param</button>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOrm" aria-expanded="true" aria-controls="collapseOrm">
ORM
</button>
</h2>
<div id="collapseOrm" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<div class="container mx-2 my-4 gx-2">
<div class="btn-group me-2" role="group">
<button class="btn btn-primary" t-on-click="ormCreate">ORM Create</button>
<button class="btn btn-primary" t-on-click="ormSearch">ORM Search</button>
<button class="btn btn-primary" t-on-click="ormSearchCount">ORM Search Count</button>
<button class="btn btn-primary" t-on-click="ormSearchRead">ORM Search Read</button>
<button class="btn btn-primary" t-on-click="ormRead">ORM Read</button>
<button class="btn btn-primary" t-on-click="ormWrite">ORM Write</button>
<button class="btn btn-primary" t-on-click="ormUnlink">ORM Unlink</button>
</div>
<div class="btn-group me-2" role="group">
<button class="btn btn-primary" t-on-click="ormDoSomething">ORM Call</button>
<button class="btn btn-primary" t-on-click="ormDoSomethingElse">ORM Call With Param</button>
<button class="btn btn-primary" t-on-click="ormDoModelMethod">ORM Call Model Method</button>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseNotification" aria-expanded="true" aria-controls="collapseNotification">
Notification
</button>
</h2>
<div id="collapseNotification" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<div class="container mx-2 my-4 gx-2">
<div class="btn-group me-2" role="group">
<button class="btn btn-primary" t-on-click="notifSimple">Simple Notification</button>
<button class="btn btn-primary" t-on-click="notifSticky">Sticky Notification</button>
<button class="btn btn-primary" t-on-click="notifCallback">Notification with On Close Callback</button>
<button class="btn btn-primary" t-on-click="notifWithButtons">Notification with Buttons</button>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDialogBox" aria-expanded="true" aria-controls="collapseDialogBox">
Dialog Box
</button>
</h2>
<div id="collapseDialogBox" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<div class="container mx-2 my-4 gx-2">
<div class="btn-group me-2" role="group">
<button class="btn btn-primary" t-on-click="dialogAlert">Alert Dialog</button>
<button class="btn btn-primary" t-on-click="dialogConfirm">Confirmation Dialog</button>
<button class="btn btn-primary" t-on-click="dialogCustom">Custom Dialog</button>
<button class="btn btn-primary" t-on-click="dialogCustomWithButtons">Custom Dialog with Buttons</button>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseUserService" aria-expanded="true" aria-controls="collapseUserService">
User Information
</button>
</h2>
<div id="collapseUserService" class="accordion-collapse collapse" data-bs-parent="#mainAccordion">
<div class="accordion-body">
<div class="container d-flex justify-content-center cheat-web-user-profile">
<div class="card p-3">
<div class="d-flex align-items-center">
<div class="image">
<img t-att-src="userImage" class="rounded" width="155" />
</div>
<div class="ms-3 w-100">
<h4 class="mb-0 mt-0"><t t-out="this.user.name" /></h4>
<span><t t-out="this.user.login" /></span>
<div class="p-2 mt-2 bg-primary d-flex justify-content-between rounded text-white stats">
<div class="d-flex flex-column">
<span class="articles">Language</span>
<span class="stat-string"><t t-out="this.user.lang" /></span>
</div>
<div class="d-flex flex-column">
<span class="followers">Timezone</span>
<span class="stat-string"><t t-out="this.user.tz" /></span>
</div>
</div>
<div class="button mt-2 d-flex flex-row align-items-center">
<button t-on-click="dumpUserSettings" class="btn btn-sm btn-outline-primary w-100">Dump User Data</button>
<button data-bs-toggle="collapse" data-bs-target="#collapseUserSettings" class="btn btn-sm btn-outline-primary w-100 ms-3">Show Settings</button>
</div>
</div>
</div>
<div class="collapse" id="collapseUserSettings">
<div class="card-body">
<div class="row mb-3">
<label for="userSettingCharField" class="col-sm-4 col-form-label">Char Field</label>
<div class="col-sm-8">
<input class="form-control" id="userSettingCharField" t-model.lazy="this.state.userCharValue" />
</div>
</div>
<div class="row mb-3">
<label for="userSettingIntegerField" class="col-sm-4 col-form-label">Integer Field</label>
<div class="col-sm-8">
<input class="form-control" id="userSettingIntegerField" t-model.lazy.number="this.state.userIntegerValue" />
</div>
</div>
<div class="button mt-2 d-flex flex-row align-items-center">
<button t-on-click="dumpState" class="btn btn-sm btn-outline-primary w-100">Dump State</button>
<button t-on-click="saveUserSettings" class="btn btn-sm btn-outline-primary w-100 ms-3">Save</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
</t>
</templates>
@@ -0,0 +1,17 @@
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
export class Collapsible extends Component {
static template = "web.collapsible";
static components = { };
static props = {
name: { type: String },
title: { type: String },
slots: { type: Object, optional: true },
collapsed: { type: Boolean, optional: true}
};
static defaultProps = {
collapsed: true
}
}
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="web.collapsible" owl="1">
<div class="card">
<h5 class="card-header">
<a class="d-block" data-bs-toggle="collapse" t-attf-href="{{ '#' + props.name }}" aria-expanded="true" aria-controls="collapse-collapsed">
<t t-out="props.title" />
<i class="fa fa-chevron-down float-end"></i>
</a>
</h5>
<div t-att-id="props.name" t-attf-class="{{ props.collapsed ? 'collapse' : 'collapse show' }}">
<div class="card-body">
<p class="card-text text-wrap overflow-auto" t-ref="useRefCardText">
<t t-slot="default"/>
</p>
</div>
<div class="card-footer" t-if="props.slots['footer']">
<t t-slot="footer"/>
</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,25 @@
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
export class HAccordion extends Component {
static template = "web.haccordion";
static components = { };
static props = {
name: { type: String },
startItem: { type: Number, optional: true },
slots: { type: Object, optional: true },
};
static defaultProps = {
startItem: 0
}
setup() {
this.state = useState({ activeItem: this.props.startItem });
this.itemNames = Object.keys(this.props.slots);
}
onClick(item) {
this.state.activeItem = item;
}
}
@@ -0,0 +1,71 @@
// Variables
$tab-width: 60px;
$screen-width: 100vw;
.haccordion-root {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
align-content: center;
position: relative;
min-height: 100%;
height: 100%;
width: 100%;
.haccordion-item {
flex-grow: 1;
height: 100%;
overflow: auto;
transition: width 500ms ease;
}
.haccordion-fold {
width: $tab-width !important;
display: flex;
cursor: pointer;
& .haccordion-title-container {
width: 100%;
height: 100%;
display: flex;
background-color: $o-gray-800 !important;
& .haccordion-title {
white-space: nowrap;
transform: rotate(-90deg);
align-self: end;
position: relative;
bottom: 75px;
left: 5px;
width: 100%;
& * {
color: $o-gray-600 !important;
&:hover {
color: $o-gray-300 !important;
}
}
}
}
& .haccordion-content {
display: none;
}
}
.haccordion-expand {
width: 100%;
& .haccordion-title {
display: none;
}
& .haccordion-content {
padding-left: 10px;
display: revert;
height: 100%;
}
}
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="web.haccordion" owl="1">
<div class="haccordion-root">
<div t-attf-class="{{ 'haccordion-item ' + (this.state.activeItem === item_index ? 'haccordion-expand' : 'haccordion-fold') }}"
t-foreach="itemNames"
t-as="item"
t-key="item_index"
t-on-click="() => this.state.activeItem = item_index" >
<div class="haccordion-title-container">
<div class="haccordion-title" t-attf-id="{{ props.name + '-' + item + '-' + item_index.toString()}}">
<h1><t t-out="props.slots[item].title" /></h1>
</div>
</div>
<div class="haccordion-content p-4">
<h1>
<t t-out="props.slots[item].title" />
</h1>
<t t-slot="{{ item }}"/>
</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,41 @@
import { visitXML } from "@web/core/utils/xml";
import { Field } from "@web/views/fields/field";
export class CheatArchParser {
parseFieldNode(node, models, modelName) {
return Field.parseFieldNode(node, models, modelName, "cheat");
}
parse(xmlDoc, models, modelName) {
console.log("Cheat Arch Parser - xmlDoc: ", xmlDoc);
console.log("Cheat Arch Parser - models: ", models);
console.log("Cheat Arch Parser - modelName: ", modelName);
const fieldNodes = {};
const limit = xmlDoc.getAttribute("limit") || 80;
let display_name_field = [...xmlDoc.children].find(o => o.attributes['name'].value === "display_name");
if (!display_name_field){
display_name_field = document.createElement("field");
display_name_field.setAttribute("name", "display_name");
xmlDoc.appendChild(display_name_field);
}
visitXML(xmlDoc, (node) => {
console.log("Cheat Arch Parser - node: ", node);
if (node.tagName === "field") {
const fieldNode = this.parseFieldNode(node, models, modelName);
const conserveLineBreaks = node.getAttribute("conserve-line-breaks");
fieldNode.conserveLineBreaks = conserveLineBreaks === 'true';
fieldNodes[fieldNode.name] = fieldNode;
}
});
return {
fieldNodes,
limit
};
}
}
@@ -0,0 +1,98 @@
import { _t } from "@web/core/l10n/translation";
import { Component, useState, useRef, onWillPatch } from "@odoo/owl";
import { Layout } from "@web/search/layout";
import { usePager } from "@web/search/pager_hook";
import { useSearchBarToggler } from "@web/search/search_bar/search_bar_toggler";
import { SearchBar } from "@web/search/search_bar/search_bar";
import { useModel } from "@web/model/model";
import { extractFieldsFromArchInfo } from "@web/model/relational_model/utils";
import { standardViewProps } from "@web/views/standard_view_props";
import { executeButtonCallback } from "@web/views/view_button/view_button_hook";
export class CheatController extends Component {
static template = `cheat_web.CheatView`;
static props = {
...standardViewProps,
offset: { type: Number, optional: true },
Model: Function,
Renderer: Function,
archInfo: Object,
}
static components = {
Layout,
SearchBar
};
setup() {
console.log("Statistic Controller - this: ", this);
console.log("Statistic Controller - props: ", this.props);
this.rootRef = useRef("root");
this.props.offset = 0;
this.model = useState(useModel(this.props.Model, this.modelParams));
usePager(() => {
return {
offset: this.model.offset,
limit: this.model.limit,
total: this.model.recordsLength,
onUpdate: async ({ offset, limit }) => {
this.props.offset = offset;
this.props.limit = limit;
await this.model.load(this.props);
},
};
});
this.searchBarToggler = useSearchBarToggler();
this.firstLoad = true;
onWillPatch(() => {
this.firstLoad = false;
});
}
get modelParams() {
const { activeFields, fields } = extractFieldsFromArchInfo(
this.props.archInfo,
this.props.fields
);
for (let [key, value] of Object.entries(activeFields)) {
let fieldNode = this.props.archInfo.fieldNodes[key];
value.conserveLineBreaks = fieldNode.conserveLineBreaks;
}
return {
resModel: this.props.resModel,
fields,
activeFields,
};
}
get rendererProps() {
return {
model: this.model,
records: this.model.records,
activeFields: this.model.config.activeFields,
editRecord: this.onClickEdit.bind(this),
};
}
get className() {
return this.props.className;
}
async onClickCreate() {
return executeButtonCallback(this.rootRef.el, () => this.props.createRecord());
}
async onClickEdit(ev, record) {
return executeButtonCallback(this.rootRef.el, () => this.props.selectRecord(record.id, true));
}
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="cheat_web.CheatView">
<div t-att-class="className" t-ref="root">
<Layout display="props.display" className="'h-100 overflow-auto'">
<t t-set-slot="control-panel-create-button">
<button type="button" class="btn btn-primary o_list_button_add" t-on-click="onClickCreate">
New
</button>
</t>
<t t-set-slot="layout-actions">
<SearchBar t-if="searchBarToggler.state.showSearchBar" autofocus="firstLoad"/>
</t>
<t t-component="props.Renderer" t-props="rendererProps"/>
</Layout>
</div>
</t>
</templates>
@@ -0,0 +1,46 @@
/** @odoo-module */
import { markRaw } from "@odoo/owl";
import { KeepLast } from "@web/core/utils/concurrency";
import { Model } from "@web/model/model";
import { getFieldsSpec } from "@web/model/relational_model/utils";
import { orderByToString } from "@web/search/utils/order_by";
export class CheatModel extends Model {
setup(params) {
console.log("Cheat Model - this: ", this);
console.log("Cheat Model - setup params: ", params);
this.keepLast = markRaw(new KeepLast());
this.config = params;
}
_getNextConfig(currentConfig, params) {
const config = Object.assign({}, currentConfig, params);
return config;
}
async load(params = {}) {
console.log("Cheat Model - load params: ", params);
const config = this._getNextConfig(this.config, params);
const kwargs = {
specification: getFieldsSpec(config.activeFields, config.fields, config.context),
offset: config.offset,
order: orderByToString(config.orderBy),
limit: config.limit,
context: { ...config.context }
};
const { length, records } = await this.keepLast.add(
this.orm.webSearchRead(config.resModel, config.domain, kwargs)
);
this.offset = config.offset;
this.limit = config.limit;
this.records = records;
this.recordsLength = length;
this.config = config;
}
}
@@ -0,0 +1,43 @@
import { Component, useRef } from "@odoo/owl";
import { executeButtonCallback } from "@web/views/view_button/view_button_hook";
export class CheatRenderer extends Component {
static template = `cheat_web.CheatRenderer`;
static props = {
model: Object,
records: Object,
activeFields: Object,
editRecord: Function
}
setup() {
console.log("Cheat Renderer - this: ", this);
this.rootRef = useRef("renderer_root");
}
getInputId(record, field){
return record.id + '_' + field
}
getFirstFiveFields(){
const fields = Object.keys(this.props.activeFields).filter(o => o !== "display_name" & o !== "id") .sort().slice(0, 4);
return fields;
}
getLabel(field){
const config = this.props.model.config;
return config.fields[field].string;
}
getFieldValue(record, field){
return record[field];
}
getConserveLineBreakSetting(field){
return this.props.activeFields[field].conserveLineBreaks;
}
onEditButtonClick(ev, record) {
this.props.editRecord(ev, record);
}
}
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="cheat_web.CheatRenderer">
<div class="row m-2" t-ref="renderer_root">
<t t-foreach="props.records" t-as="record" t-key="record.id">
<div class="col-lg-6 col-sm-12 p-2">
<div class="card">
<div class="row g-0">
<div class="col-md-3 d-flex justify-content-center align-items-center bg-secondary rounded-start">
<h1><t t-out="record.id"/></h1>
</div>
<div class="col-md-9">
<div class="card-body">
<h2 class="card-title"><t t-out="record.display_name"/></h2>
<div class="card-text card-text border rounded">
<t t-foreach="getFirstFiveFields()" t-as="field" t-key="field_index">
<div t-attf-class="form-group d-flex {{ field_last ? '' : 'border-bottom' }}">
<t t-set="fieldId" t-value="getInputId(record, field)"/>
<label t-att-for="fieldId" class="col-sm-4 col-form-label me-1 px-1 bg-secondary">
<t t-out="getLabel(field)"/>
</label>
<div class="col-sm-8 px-1">
<t t-if="getConserveLineBreakSetting(field)" >
<textarea readonly="1" rows="5" class="form-control-plaintext" t-att-id="fieldId">
<t t-out="getFieldValue(record, field)" />
</textarea>
</t>
<t t-else="" >
<input type="text" readonly="1" class="form-control-plaintext text-truncate"
t-att-id="fieldId" t-att-value="getFieldValue(record, field)"/>
</t>
</div>
</div>
</t>
</div>
<div class="d-flex justify-content-end">
<button t-on-click="(ev) => this.onEditButtonClick(ev, record)" class="btn btn-primary mt-2">Edit</button>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</div>
</t>
</templates>
@@ -0,0 +1,33 @@
import { registry } from "@web/core/registry";
import { CheatModel } from "./cheat_model";
import { CheatController } from "./cheat_controller";
import { CheatArchParser } from "./cheat_arch_parser";
import { CheatRenderer } from "./cheat_renderer";
export const cheatView = {
type: "cheat",
searchMenuTypes: ["filter", "favorite"],
Controller: CheatController,
Renderer: CheatRenderer,
Model: CheatModel,
ArchParser: CheatArchParser,
props(genericProps, view) {
console.log("Cheat View - this: ", this);
console.log("Cheat View - genericProps: ", genericProps);
console.log("Cheat View - view: ", view);
const { ArchParser } = view;
const { arch, relatedModels, resModel } = genericProps;
const archInfo = new ArchParser().parse(arch, relatedModels, resModel);
return {
...genericProps,
Model: view.Model,
Renderer: view.Renderer,
archInfo,
};
},
};
registry.category("views").add("cheat", cheatView);
@@ -0,0 +1,18 @@
import { Component } from "@odoo/owl";
import { standardViewProps } from "@web/views/standard_view_props";
import { Layout } from "@web/search/layout";
export class HelloController extends Component {
static template = `cheat_web.HelloView`;
static props = {
...standardViewProps,
aValue: { type: String }
};
static components = { Layout };
setup() {
console.log("Hello Controller - this: ", this);
console.log("Hello Controller - props: ", this.props);
}
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="cheat_web.HelloView">
<div class="h-100" t-ref="root">
<Layout display="props.display" className="'h-100'">
<div class="d-flex flex-column h-75 justify-content-center align-items-center">
<h1><span><i class="fa fa-smile-o" style="font-size: xxx-large !important;"></i></span></h1>
<h1>Hello there!</h1>
</div>
</Layout>
</div>
</t>
</templates>
@@ -0,0 +1,20 @@
import { registry } from "@web/core/registry";
import { HelloController } from "./hello_controller";
export const helloView = {
type: "hello",
Controller: HelloController,
props(genericProps, view) {
console.log("Hello View - this: ", this);
console.log("Hello View - genericProps: ", genericProps);
console.log("Hello View - view: ", view);
return {
...genericProps,
aValue: 'A value from view type.'
};
},
};
registry.category("views").add("hello", helloView);
@@ -0,0 +1,32 @@
import { Component, useState, useRef, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { standardViewProps } from "@web/views/standard_view_props";
import { Layout } from "@web/search/layout";
export class StatisticController extends Component {
static template = `cheat_web.StatisticView`;
static props = {
...standardViewProps,
Model: Function,
Renderer: Function,
};
static components = { Layout };
setup() {
console.log("Statistic Controller - this: ", this);
console.log("Statistic Controller - props: ", this.props);
this.orm = useService("orm");
this.model = new this.props.Model(
this.orm,
this.props.resModel,
this.props.fields
);
onWillStart(async () => {
await this.model.load(this.props);
});
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="cheat_web.StatisticView">
<div class="h-100">
<Layout display="props.display" className="'h-100'">
<t t-component="props.Renderer" model="model"/>
</Layout>
</div>
</t>
</templates>
@@ -0,0 +1,21 @@
import { KeepLast } from "@web/core/utils/concurrency";
export class StatisticModel {
constructor(orm, resModel, fields) {
this.orm = orm;
this.resModel = resModel;
this.fields = fields;
this.keepLast = new KeepLast();
console.log("Statistic Model - this: ", this);
}
async load(params) {
console.log("Statistic Model - load params: ", params);
const recordCount = await this.keepLast.add(
this.orm.searchCount(this.resModel, [])
);
this.recordCount = recordCount;
}
}
@@ -0,0 +1,26 @@
import {
Component
} from "@odoo/owl";
export class StatisticRenderer extends Component {
static template = `cheat_web.StatisticRenderer`;
static props = {
model: Object
}
fieldInfo(name){
return this.props.model.fields[name];
}
fieldCount() {
return Object.keys(this.props.model.fields).length;
}
fieldNames() {
return Object.keys(this.props.model.fields).sort();
}
setup() {
console.log("Statistic Renderer - this: ", this);
}
}
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="cheat_web.StatisticRenderer">
<div class="row m-3 d-flex justify-content-center align-items-center">
<div class="card p-0">
<div class="row g-0 ">
<div class="col-md-1 col-2 d-flex flex-column pt-4 justify-content-start align-items-center bg-secondary rounded-start">
<h1><t t-out="props.model.recordCount"/></h1>
<div>records</div>
</div>
<div class="col-md-11 col-10">
<div class="card-body overflow-y-auto">
<h2 class="card-title">Model: <t t-out="props.model.resModel"/></h2>
<div class="card-text">
<div>Fields count: <t t-out="fieldCount()"/></div>
<div style="max-height: 75dvh;">
<table class="table table-striped table-hover mt-3 no-cellpadding">
<thead>
<tr class="table-secondary">
<th>Name</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr t-foreach="fieldNames()" t-as="field" t-key="field_index">
<t t-set="fInfo" t-value="fieldInfo(field)" />
<td><t t-out="fInfo['name']"/></td>
<td><t t-out="fInfo['type']"/></td>
<td><t t-out="fInfo['help']"/></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,25 @@
import { registry } from "@web/core/registry";
import { StatisticModel } from "./statistic_model";
import { StatisticController } from "./statistic_controller";
import { StatisticRenderer } from "./statistic_renderer";
export const statisticView = {
type: "statistic",
Controller: StatisticController,
Renderer: StatisticRenderer,
Model: StatisticModel,
props(genericProps, view) {
console.log("Statistic View - this: ", this);
console.log("Statistic View - genericProps: ", genericProps);
console.log("Statistic View - view: ", view);
return {
...genericProps,
Model: view.Model,
Renderer: view.Renderer
};
},
};
registry.category("views").add("statistic", statisticView);