Skip to content

Commit 176f297

Browse files
committed
[15.0] add tutorial view module
1 parent 4406818 commit 176f297

21 files changed

+485
-6
lines changed

.eslintrc.yml

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ env:
55
# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449
66
parserOptions:
77
ecmaVersion: 2019
8+
sourceType: module
89

910
overrides:
1011
- files:
1112
- "**/*.esm.js"
12-
parserOptions:
13-
sourceType: module
1413

1514
# Globals available in Odoo that shouldn't produce errorings
1615
globals:

.pylintrc-mandatory

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ manifest_required_authors=Coding Dodo
88
manifest_required_keys=license
99
manifest_deprecated_keys=description,active
1010
license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
11-
valid_odoo_versions=16.0
11+
valid_odoo_versions=15.0
1212

1313
[MESSAGES CONTROL]
1414
disable=all

owl_tutorial_views/LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Coding Dodo
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

owl_tutorial_views/README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Coding Dodo - Odoo 15 Tutorial Creating an OWL View from scratch
2+
3+
This addon was originally created in
4+
[Odoo JavaScript 101 - Part 2: Creating an OWL View from scratch](https://codingdodo.com/odoo-javascript-tutorial-101-part-2-creating-an-owl-view/).
5+
6+
This branch is the companion piece of the
7+
[Article about migrating that view to Odoo 15.](https://codingdodo.com/odoo-15-owl-view-migration-guide)
8+
9+
### Author
10+
11+
[![Coding Dodo](https://res.cloudinary.com/phildl-cloudinary/image/upload/w_300/v1617638212/codingdodo/Coding_Dodo_rplksw.png)](https://codingdodo.com)

owl_tutorial_views/README.rst

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
Coding Dodo - Odoo 15 Tutorial Creating an OWL View from scratch
3+
================================================================
4+
5+
This addon was originally created in `Odoo JavaScript 101 - Part 2: Creating an OWL View from scratch <https://codingdodo.com/odoo-javascript-tutorial-101-part-2-creating-an-owl-view/>`_.
6+
7+
This branch is the companion piece of the `Article about migrating that view to Odoo 15. <https://codingdodo.com/odoo-15-owl-view-migration-guide>`_
8+
9+
Author
10+
^^^^^^
11+
12+
13+
.. image:: https://res.cloudinary.com/phildl-cloudinary/image/upload/w_300/v1617638212/codingdodo/Coding_Dodo_rplksw.png
14+
:target: https://codingdodo.com
15+
:alt: Coding Dodo
16+

owl_tutorial_views/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

owl_tutorial_views/__manifest__.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "Coding Dodo - OWL Tutorial Views",
3+
"summary": "Tutorial about Creating an OWL View from scratch.",
4+
"author": "Coding Dodo",
5+
"website": "https://github.com/Coding-Dodo/web",
6+
"category": "Tools",
7+
"version": "15.0.1.0.0",
8+
"license": "AGPL-3",
9+
"depends": ["base", "web", "mail", "product"],
10+
"data": [
11+
"views/product_views.xml",
12+
],
13+
"assets": {
14+
"web.assets_qweb": [
15+
"/owl_tutorial_views/static/src/components/tree_item/TreeItem.xml",
16+
"/owl_tutorial_views/static/src/xml/owl_tree_view.xml",
17+
],
18+
"web.assets_backend": [
19+
"/owl_tutorial_views/static/src/components/tree_item/tree_item.scss",
20+
"/owl_tutorial_views/static/src/owl_tree_view/owl_tree_view.scss",
21+
"/owl_tutorial_views/static/src/components/tree_item/TreeItem.js",
22+
"/owl_tutorial_views/static/src/owl_tree_view/owl_tree_view.js",
23+
"/owl_tutorial_views/static/src/owl_tree_view/owl_tree_model.js",
24+
"/owl_tutorial_views/static/src/owl_tree_view/owl_tree_renderer.js",
25+
],
26+
},
27+
}

owl_tutorial_views/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import ir_ui_view
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from odoo import fields, models
2+
3+
4+
class View(models.Model):
5+
_inherit = "ir.ui.view"
6+
7+
type = fields.Selection(selection_add=[("owl_tree", "OWL Tree Vizualisation")])
104 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/** @odoo-module **/
2+
const {Component} = owl;
3+
const {useState} = owl.hooks;
4+
5+
export class TreeItem extends Component {
6+
/**
7+
* @override
8+
*/
9+
constructor(...args) {
10+
super(...args);
11+
this.state = useState({
12+
isDraggedOn: false,
13+
});
14+
}
15+
16+
toggleChildren() {
17+
if (this.props.item.child_id.length > 0) {
18+
this.trigger("tree_item_clicked", {data: this.props.item});
19+
}
20+
}
21+
22+
onDragstart(event) {
23+
event.dataTransfer.setData("TreeItem", JSON.stringify(this.props.item));
24+
}
25+
26+
onDragover() {
27+
return;
28+
}
29+
30+
onDragenter() {
31+
Object.assign(this.state, {isDraggedOn: true});
32+
}
33+
34+
onDragleave() {
35+
Object.assign(this.state, {isDraggedOn: false});
36+
}
37+
38+
onDrop(event) {
39+
Object.assign(this.state, {isDraggedOn: false});
40+
const droppedItem = JSON.parse(event.dataTransfer.getData("TreeItem"));
41+
if (droppedItem.id == this.props.item.id || droppedItem.parent_id[0] == this.props.item.id) {
42+
console.log("Drop inside itself or same parent has no effect");
43+
return;
44+
}
45+
if (this.props.item.parent_path.startsWith(droppedItem.parent_path)) {
46+
console.log("Oops, drop inside child item is forbidden.");
47+
return;
48+
}
49+
this.trigger("change_item_tree", {
50+
itemMoved: droppedItem,
51+
newParent: this.props.item,
52+
});
53+
}
54+
}
55+
56+
Object.assign(TreeItem, {
57+
components: {TreeItem},
58+
props: {
59+
item: {},
60+
countField: "",
61+
},
62+
template: "owl_tutorial_views.TreeItem",
63+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<templates xml:space="preserve">
3+
4+
<t t-name="owl_tutorial_views.TreeItem" owl="1">
5+
<div class="tree-item-wrapper">
6+
<div
7+
draggable="true"
8+
t-on-dragstart="onDragstart"
9+
t-on-drop.stop.prevent="onDrop"
10+
t-on-dragover.prevent="onDragover"
11+
t-on-dragenter.prevent="onDragenter"
12+
t-on-dragleave.prevent="onDragleave"
13+
t-attf-class="list-group-item list-group-item-action d-flex justify-content-between align-items-center owl-tree-item {{ state.isDraggedOn ? 'list-group-item-warning': '' }}"
14+
>
15+
<a href="#" t-on-click.stop.prevent="toggleChildren" t-if="props.item.child_id.length > 0">
16+
<t t-esc="props.item.display_name" />
17+
<i
18+
t-attf-class="pl-2 fa {{ props.item.childrenVisible ? 'fa-caret-down': 'fa-caret-right'}}"
19+
/>
20+
</a>
21+
<span t-else="">
22+
<t t-esc="props.item.display_name" />
23+
</span>
24+
<span
25+
t-if="props.countField !== '' and props.item.hasOwnProperty(props.countField)"
26+
class="badge badge-primary badge-pill"
27+
t-esc="props.item[props.countField]"
28+
>
29+
</span>
30+
</div>
31+
<t t-if="props.item.child_id.length > 0">
32+
<div
33+
class="d-flex pl-4 py-1 flex-row treeview"
34+
t-if="props.item.children and props.item.children.length > 0 and props.item.childrenVisible"
35+
>
36+
<div class="list-group">
37+
<t t-foreach="props.item.children" t-as="child_item" t-key="child_item.id">
38+
<TreeItem item="child_item" />
39+
</t>
40+
</div>
41+
</div>
42+
</t>
43+
</div>
44+
</t>
45+
</templates>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.tree-item-wrapper {
2+
min-width: 50em;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/** @odoo-module alias=owl_tutorial_views.OWLTreeModel default=0 **/
2+
import {Model} from "@web/views/helpers/model";
3+
import {KeepLast} from "@web/core/utils/concurrency";
4+
5+
export default class OWLTreeModel extends Model {
6+
setup(params, {orm}) {
7+
this.modelName = params.resModel;
8+
this.orm = orm;
9+
this.keepLast = new KeepLast();
10+
}
11+
12+
/**
13+
* Make an RPC 'write' method call to update the parent_id of
14+
* an existing record.
15+
*
16+
* @param {integer} id ID Of the item we want to update
17+
* @param {integer} parent_id The parent item ID
18+
*/
19+
async changeParent(id, parent_id) {
20+
await this.orm.write(this.modelName, [id], {parent_id: parent_id});
21+
this.notify();
22+
}
23+
24+
/**
25+
* Refresh a node get fresh data from the server for a given item.
26+
* A search_read is executed via RPC Call and then the item is modified
27+
* in place in the hierarchical tree structure.
28+
*
29+
* @param {integer} id ID Of the item we want to refresh
30+
*/
31+
async refreshNode(id) {
32+
var self = this;
33+
var result = null;
34+
const itemUpdated = await this.orm.read(this.modelName, [id], []);
35+
36+
const path = itemUpdated[0].parent_path;
37+
let target_node = self.__target_parent_node_with_path(
38+
path.split("/").filter((i) => i),
39+
self.data
40+
);
41+
target_node = itemUpdated[0];
42+
result = itemUpdated[0];
43+
this.notify();
44+
return result;
45+
}
46+
47+
/**
48+
* Make an RPC call to get the child of the target itm then navigates
49+
* the nodes to the target the item and assign its "children" property
50+
* to the result of the RPC call.
51+
*
52+
* @param {integer} parentId Category we will "open/expand"
53+
* @param {String} path The parent_path represents the parents ids like "1/3/32/123/"
54+
*/
55+
async expandChildrenOf(parentId, path) {
56+
var self = this;
57+
const children = await this.orm.searchRead(this.modelName, [["parent_id", "=", parentId]]);
58+
var target_node = self.__target_parent_node_with_path(
59+
path.split("/").filter((i) => i),
60+
self.data
61+
);
62+
target_node.children = children;
63+
target_node.child_id = children.map((i) => i.id);
64+
target_node.childrenVisible = true;
65+
this.notify();
66+
}
67+
68+
async toggleChildrenVisibleForItem(item) {
69+
var target_node = this.__target_parent_node_with_path(
70+
item.parent_path.split("/").filter((i) => i),
71+
this.data
72+
);
73+
target_node.childrenVisible = !target_node.childrenVisible;
74+
this.notify();
75+
}
76+
77+
async _recursivelyOpenParents(item) {
78+
if (item.parent_id) {
79+
const parent = await this.orm.read(this.modelName, [item.parent_id[0]], []);
80+
const directParent = parent[0];
81+
directParent.children = [item];
82+
directParent.childrenVisible = true;
83+
return await this._recursivelyOpenParents(directParent);
84+
}
85+
return [item];
86+
}
87+
/**
88+
* Search for the Node corresponding to the given path.
89+
* Paths are present in the property `parent_path` of any nested item they are
90+
* in the form "1/3/32/123/" we have to split the string to manipulate an Array.
91+
* Each item in the Array will correspond to an item ID in the tree, each one
92+
* level deeper than the last.
93+
*
94+
* @private
95+
* @param {Array} path for example ["1", "3", "32", "123"]
96+
* @param {Array} items the items to search in
97+
* @param {integer} n The current index of deep inside the tree
98+
* @returns {Object|undefined} the tree Node corresponding to the path
99+
**/
100+
__target_parent_node_with_path(path, items, n = 0) {
101+
for (const item of items) {
102+
if (item.id == parseInt(path[n])) {
103+
if (n < path.length - 1) {
104+
return this.__target_parent_node_with_path(path, item.children, n + 1);
105+
}
106+
return item;
107+
}
108+
}
109+
return undefined;
110+
}
111+
112+
async load(params) {
113+
let isSearch = false;
114+
let domain = [["parent_id", "=", false]];
115+
if (params.domain && params.domain.length > 0) {
116+
isSearch = true;
117+
domain = params.domain;
118+
}
119+
const result = await this.keepLast.add(this.orm.searchRead(this.modelName, domain, []));
120+
if (isSearch) {
121+
for (const item of result) {
122+
this.data = await this._recursivelyOpenParents(item);
123+
}
124+
} else {
125+
this.data = result;
126+
}
127+
this.notify();
128+
}
129+
}
130+
OWLTreeModel.services = ["orm"];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/** @odoo-module alias=owl_tutorial_views.OWLTreeRenderer **/
2+
const {useState} = owl.hooks;
3+
import AbstractRendererOwl from "web.AbstractRendererOwl";
4+
import {TreeItem} from "@owl_tutorial_views/components/tree_item/TreeItem";
5+
6+
export default class OWLTreeRenderer extends AbstractRendererOwl {
7+
constructor(parent, props) {
8+
super(...arguments);
9+
this.state = useState({
10+
countField: "",
11+
});
12+
if (this.props.archOptions.count_field) {
13+
Object.assign(this.state, {
14+
countField: this.props.archOptions.count_field,
15+
});
16+
}
17+
}
18+
}
19+
20+
const components = {TreeItem};
21+
Object.assign(OWLTreeRenderer, {
22+
components,
23+
defaultProps: {
24+
archOptions: {},
25+
},
26+
props: {
27+
archOptions: {
28+
type: Object,
29+
optional: true,
30+
},
31+
model: {
32+
type: Object,
33+
},
34+
},
35+
template: "owl_tutorial_views.OWLTreeRenderer",
36+
});

0 commit comments

Comments
 (0)