Compare commits
1 Commits
gmz-odoo-p
...
17.0-js-tu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1107a5115 |
@@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
.. _howtos/javascript_client_action:
|
||||||
|
|
||||||
======================
|
======================
|
||||||
Create a client action
|
Create a client action
|
||||||
======================
|
======================
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.. _frontend/components:
|
.. _frontend/components:
|
||||||
|
|
||||||
==============
|
==============
|
||||||
Owl Components
|
Owl components
|
||||||
==============
|
==============
|
||||||
|
|
||||||
The Odoo Javascript framework uses a custom component framework called Owl. It
|
The Odoo Javascript framework uses a custom component framework called Owl. It
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ Getters and setters are supported too:
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
.. _frontend/patching_class:
|
||||||
|
|
||||||
Patching a javascript class
|
Patching a javascript class
|
||||||
===========================
|
===========================
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
:show-content:
|
:show-content:
|
||||||
|
|
||||||
=========================
|
=========================
|
||||||
Discover the JS Framework
|
Discover the JS framework
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
@@ -10,24 +10,27 @@ Discover the JS Framework
|
|||||||
|
|
||||||
discover_js_framework/*
|
discover_js_framework/*
|
||||||
|
|
||||||
This tutorial is designed to introduce you to the basics of the Odoo Javascript framework. Whether
|
This two parts tutorial is designed to introduce you to the basics of the Odoo Javascript framework. Whether
|
||||||
you are new to the framework or have some prior experience, this tutorial will provide you with a
|
you are new to the framework or have some prior experience, this tutorial will provide you with a
|
||||||
solid foundation for using the Odoo JavaScript framework in your projects.
|
solid foundation for using the Odoo JavaScript framework in your projects.
|
||||||
|
|
||||||
This tutorial is divided into two parts. The first part covers the basics of Owl components, which
|
The first part covers the basics of Owl components, which
|
||||||
are a key part of the Odoo JS framework. Owl components are reusable UI components that can be used
|
are a key part of the Odoo JS framework. Owl components are reusable UI components that can be used
|
||||||
to build complex web interfaces quickly and efficiently. We will explore how to create and use Owl
|
to build complex web interfaces quickly and efficiently. We will explore how to create and use Owl
|
||||||
components in Odoo.
|
components in Odoo. Then, in the second part of this tutorial, we focus on creating a dashboard using various
|
||||||
|
features of Odoo. Dashboards are an essential part of any web application, and provide a nice starting
|
||||||
The second part of the tutorial focuses on creating a dashboard using various features of Odoo.
|
point to use and interact with the Odoo codebase.
|
||||||
Dashboards are an essential part of any web application, and provide a nice starting point to use
|
|
||||||
and interact with the Odoo codebase.
|
|
||||||
|
|
||||||
This tutorial assumes that you have some basic knowledge of development with Odoo in general
|
This tutorial assumes that you have some basic knowledge of development with Odoo in general
|
||||||
(models, controllers, QWeb, ...). If you are new to Odoo, we recommend that you start with the
|
(models, controllers, QWeb, ...). If you are new to Odoo, we recommend that you start with the
|
||||||
:doc:`Getting started </developer/tutorials/getting_started>` tutorial before proceeding with this
|
:doc:`Getting started </developer/tutorials/getting_started>` tutorial before proceeding with this
|
||||||
one.
|
one.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Each chapter of this tutorial is an independant project. If you feel comfortable with Owl, you can
|
||||||
|
start directly with chapter 2.
|
||||||
|
|
||||||
.. _tutorials/discover_js_framework/setup:
|
.. _tutorials/discover_js_framework/setup:
|
||||||
|
|
||||||
Setup
|
Setup
|
||||||
@@ -35,11 +38,12 @@ Setup
|
|||||||
|
|
||||||
#. Clone the `official Odoo tutorials repository <https://github.com/odoo/tutorials>`_ and switch to
|
#. Clone the `official Odoo tutorials repository <https://github.com/odoo/tutorials>`_ and switch to
|
||||||
the branch `{CURRENT_MAJOR_BRANCH}`.
|
the branch `{CURRENT_MAJOR_BRANCH}`.
|
||||||
#. Add the cloned repository to the :option:`--addons-path <odoo-bin --addons-path>`.
|
#. Add the cloned repository to your :option:`--addons-path <odoo-bin --addons-path>`.
|
||||||
#. Start a new Odoo database and install the modules `owl_playground` and `awesome_tshirt`.
|
#. Start a new Odoo database and install the modules `awesome_owl` (for chapter 1) and `awesome_dashboard`
|
||||||
|
(for chapter 2).
|
||||||
|
|
||||||
Content
|
Content
|
||||||
=======
|
=======
|
||||||
|
|
||||||
- :doc:`discover_js_framework/01_owl_components`
|
- :doc:`discover_js_framework/01_owl_components`
|
||||||
- :doc:`discover_js_framework/02_web_framework`
|
- :doc:`discover_js_framework/02_build_a_dashboard`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
=========================
|
=========================
|
||||||
Chapter 1: Owl Components
|
Chapter 1: Owl components
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
This chapter introduces the `Owl framework <https://github.com/odoo/owl>`_, a tailor-made component
|
This chapter introduces the `Owl framework <https://github.com/odoo/owl>`_, a tailor-made component
|
||||||
@@ -10,27 +10,28 @@ In Owl, every part of user interface is managed by a component: they hold the lo
|
|||||||
templates that are used to render the user interface. In practice, a component is represented by a
|
templates that are used to render the user interface. In practice, a component is represented by a
|
||||||
small JavaScript class subclassing the `Component` class.
|
small JavaScript class subclassing the `Component` class.
|
||||||
|
|
||||||
Before getting into the exercises, make sure you have followed all the steps described in this
|
To get started, you need a running Odoo server and a development environment setup. Before getting
|
||||||
|
into the exercises, make sure you have followed all the steps described in this
|
||||||
:ref:`tutorial introduction <tutorials/discover_js_framework/setup>`.
|
:ref:`tutorial introduction <tutorials/discover_js_framework/setup>`.
|
||||||
|
|
||||||
.. spoiler:: Solutions
|
|
||||||
|
|
||||||
The solutions for each exercise of the chapter are hosted on the `official Odoo tutorials
|
|
||||||
repository
|
|
||||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/owl_playground>`_. It
|
|
||||||
is recommended to try to solve them first without looking at the solution!
|
|
||||||
|
|
||||||
.. tip::
|
.. tip::
|
||||||
If you use Chrome as your web browser, you can install the `Owl Devtools` extension. This
|
If you use Chrome as your web browser, you can install the `Owl Devtools` extension. This
|
||||||
extension provides many features to help you understand and profile any Owl application.
|
extension provides many features to help you understand and profile any Owl application.
|
||||||
|
|
||||||
`Video: How to use the DevTools <https://www.youtube.com/watch?v=IUyQjwnrpzM>`_
|
`Video: How to use the DevTools <https://www.youtube.com/watch?v=IUyQjwnrpzM>`_
|
||||||
|
|
||||||
In this chapter, we use the `owl_playground` addon, which provides a simplified environment that
|
In this chapter, we use the `awesome_owl` addon, which provides a simplified environment that
|
||||||
only contains Owl and a few other files. The goal is to learn Owl itself, without relying on Odoo
|
only contains Owl and a few other files. The goal is to learn Owl itself, without relying on Odoo
|
||||||
web client code. To get started, open the `/owl_playground/playground` route with your browser: it
|
web client code. To get started, open the `/awesome_owl` route with your browser: it
|
||||||
should display an Owl component with the text *hello world*.
|
should display an Owl component with the text *hello world*.
|
||||||
|
|
||||||
|
.. spoiler:: Solutions
|
||||||
|
|
||||||
|
The solutions for each exercise of the chapter are hosted on the `official Odoo tutorials
|
||||||
|
repository
|
||||||
|
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-discover-js-framework-solutions/awesome_owl>`_. It
|
||||||
|
is recommended to try to solve them first without looking at the solution!
|
||||||
|
|
||||||
Example: a `Counter` component
|
Example: a `Counter` component
|
||||||
==============================
|
==============================
|
||||||
|
|
||||||
@@ -54,8 +55,8 @@ button.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
The `Counter` component specifies the name of the template to render. The template is written in XML
|
The `Counter` component specifies the name of a template that represents its html. It is written in XML
|
||||||
and defines a part of user interface:
|
using the QWeb language:
|
||||||
|
|
||||||
.. code-block:: xml
|
.. code-block:: xml
|
||||||
|
|
||||||
@@ -70,11 +71,13 @@ and defines a part of user interface:
|
|||||||
1. Displaying a counter
|
1. Displaying a counter
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
As a first exercise, let us implement a counter in the `Playground` component located in
|
.. image:: 01_owl_components/counter.png
|
||||||
:file:`owl_playground/static/src/`. To see the result, you can go to the `/owl_playground/playground`
|
:align: center
|
||||||
route with your browser.
|
|
||||||
|
As a first exercise, let us modify the `Playground` component located in
|
||||||
|
:file:`awesome_owl/static/src/` to turn it into a counter. To see the result, you can go to the
|
||||||
|
`/awesome_owl` route with your browser.
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will
|
#. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will
|
||||||
need to use the `useState hook
|
need to use the `useState hook
|
||||||
@@ -87,112 +90,226 @@ route with your browser.
|
|||||||
<{OWL_PATH}/doc/reference/event_handling.md#event-handling>`_ attribute in the button to
|
<{OWL_PATH}/doc/reference/event_handling.md#event-handling>`_ attribute in the button to
|
||||||
trigger the `increment` method whenever the button is clicked.
|
trigger the `increment` method whenever the button is clicked.
|
||||||
|
|
||||||
.. image:: 01_owl_components/counter.png
|
|
||||||
:scale: 70%
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. tip::
|
.. tip::
|
||||||
The Odoo JavaScript files downloaded by the browser are minified. For debugging purpose, it's
|
The Odoo JavaScript files downloaded by the browser are minified. For debugging purpose, it's
|
||||||
easier when the files are not minified. Switch to
|
easier when the files are not minified. Switch to
|
||||||
:ref:`debug mode with assets <developer-mode/url>` so that the files are not minified.
|
:ref:`debug mode with assets <developer-mode/url>` so that the files are not minified.
|
||||||
|
|
||||||
2. Extract counter in a component
|
This exercise showcases an important feature of Owl: the `reactivity system <{OWL_PATH}/doc/reference/reactivity.md>`_.
|
||||||
=================================
|
The `useState` function wraps a value in a proxy so Owl can keep track of which component
|
||||||
|
needs which part of the state, so it can be updated whenever a value has been changed. Try
|
||||||
|
removing the `useState` function and see what happens.
|
||||||
|
|
||||||
For now we have the logic of a counter in the `Playground` component, let us see how to create a
|
2. Extract `Counter` in a sub component
|
||||||
`sub-component <{OWL_PATH}/doc/reference/component.md#sub-components>`_ from it.
|
=======================================
|
||||||
|
|
||||||
.. exercise::
|
For now we have the logic of a counter in the `Playground` component, but it is not reusable. Let us
|
||||||
|
see how to create a `sub-component <{OWL_PATH}/doc/reference/component.md#sub-components>`_ from it:
|
||||||
|
|
||||||
#. Extract the counter code from the `Playground` component into a new `Counter` component.
|
#. Extract the counter code from the `Playground` component into a new `Counter` component.
|
||||||
#. You can do it in the same file first, but once it's done, update your code to move the
|
#. You can do it in the same file first, but once it's done, update your code to move the
|
||||||
`Counter` in its own folder and file. Import it relatively from `./counter/counter`. Make sure
|
`Counter` in its own folder and file. Import it relatively from `./counter/counter`. Make sure
|
||||||
the template is in its own file, with the same name.
|
the template is in its own file, with the same name.
|
||||||
|
#. Add two counters in your playground.
|
||||||
|
|
||||||
|
.. image:: 01_owl_components/double_counter.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
By convention, most components code, template and css should have the same snake-cased name
|
||||||
|
as the component. For example, if we have a `TodoList` component, its code should be in
|
||||||
|
`todo_list.js`, `todo_list.xml` and if necessary, `todo_list.scss`
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
Don't forget :code:`/** @odoo-module **/` in your JavaScript files. More information on this can
|
Don't forget :code:`/** @odoo-module **/` in your JavaScript files. More information on this can
|
||||||
be found :ref:`here <frontend/modules/native_js>`.
|
be found :ref:`here <frontend/modules/native_js>`.
|
||||||
|
|
||||||
3. A todo component
|
.. _tutorials/discover_js_framework/simple_card:
|
||||||
===================
|
|
||||||
|
|
||||||
We will create new components in :file:`owl_playground/static/src/` to keep track of a list of
|
3. A simple `Card` component
|
||||||
todos. This will be done incrementally in multiple exercises that will introduce various concepts.
|
============================
|
||||||
|
|
||||||
.. exercise::
|
Components are really the most natural way to divide a complicated user interface into multiple
|
||||||
|
reusable pieces. But to make them truly useful, it is necessary to be able to communicate
|
||||||
|
some information between them. Let us see how a parent component can provide information to a
|
||||||
|
sub component by using attributes (most commonly known as `props <{OWL_PATH}/doc/reference/props.md>`_).
|
||||||
|
|
||||||
#. Create a `Todo` component that receive a `todo` object in `props
|
The goal of this exercise is to create a `Card` component, that takes two props: `title` and `content`.
|
||||||
<{OWL_PATH}/doc/reference/props.md>`_, and display it. It should show something like
|
For example, here is how it could be used:
|
||||||
**3. buy milk**.
|
|
||||||
#. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on the task if it is
|
|
||||||
done. To do that, you can use `dynamic attributes
|
|
||||||
<{OWL_PATH}/doc/reference/templates.md#dynamic-attributes>`_.
|
|
||||||
#. Modify :file:`owl_playground/static/src/playground.js` and
|
|
||||||
:file:`owl_playground/static/src/playground.xml` to display your new `Todo` component with
|
|
||||||
some hard-coded props to test it first.
|
|
||||||
|
|
||||||
.. example::
|
.. code-block:: xml
|
||||||
|
|
||||||
.. code-block:: javascript
|
<Card title="'my title'" content="'some content'"/>
|
||||||
|
|
||||||
setup() {
|
The above example should produce some html using bootstrap that look like this:
|
||||||
...
|
|
||||||
this.todo = { id: 3, description: "buy milk", done: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
.. image:: 01_owl_components/todo.png
|
.. code-block:: html
|
||||||
:scale: 70%
|
|
||||||
|
<div class="card d-inline-block m-2" style="width: 18rem;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">my title</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
some content
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
#. Create a `Card` component
|
||||||
|
#. Import it in `Playground` and display a few cards in its template
|
||||||
|
|
||||||
|
.. image:: 01_owl_components/simple_card.png
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. seealso::
|
4. Using `markup` to display html
|
||||||
`Owl: Dynamic class attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-class-attribute>`_
|
=================================
|
||||||
|
|
||||||
4. Props validation
|
If you used `t-esc` in the previous exercise, then you may have noticed that Owl will automatically escape
|
||||||
|
its content. For example, if you try to display some html like this: `<Card title="'my title'" content="this.html"/>`
|
||||||
|
with `this.html = "<div>some content</div>""`,
|
||||||
|
the resulting output will simply display the html as a string.
|
||||||
|
|
||||||
|
In this case, since the `Card` component may be used to display any kind of content, it makes sense
|
||||||
|
to allow the user to display some html. This is done with the
|
||||||
|
`t-out directive <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_.
|
||||||
|
|
||||||
|
However, displaying arbitrary content as html is dangerous, it could be used to inject malicious code, so
|
||||||
|
by default, Owl will always escape a string unless it has been explicitely marked as safe with the `markup`
|
||||||
|
function.
|
||||||
|
|
||||||
|
#. Update `Card` to use `t-out`
|
||||||
|
#. Update `Playground` to import `markup`, and use it on some html values
|
||||||
|
#. Make sure that you see that normal strings are always escaped, unlike markuped strings.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The `t-esc` directive can still be used in Owl templates. It is slightly faster than `t-out`.
|
||||||
|
|
||||||
|
.. image:: 01_owl_components/markup.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
5. Props validation
|
||||||
===================
|
===================
|
||||||
|
|
||||||
The `Todo` component has an implicit API. It expects to receive in its props the description of a
|
The `Card` component has an implicit API. It expects to receive two strings in its props: the `title`
|
||||||
todo object in a specified format: `id`, `description` and `done`. Let us make that API more
|
and the `content`. Let us make that API more
|
||||||
explicit. We can add a props definition that will let Owl perform a validation step in `dev mode
|
explicit. We can add a props definition that will let Owl perform a validation step in `dev mode
|
||||||
<{OWL_PATH}/doc/reference/app.md#dev-mode>`_. You can activate the dev mode in the `App
|
<{OWL_PATH}/doc/reference/app.md#dev-mode>`_. You can activate the dev mode in the `App
|
||||||
configuration <{OWL_PATH}/doc/reference/app.md#configuration>`_.
|
configuration <{OWL_PATH}/doc/reference/app.md#configuration>`_ (but it is activated by default
|
||||||
|
on the `awesome_owl` playground).
|
||||||
|
|
||||||
It is a good practice to do props validation for every component.
|
It is a good practice to do props validation for every component.
|
||||||
|
|
||||||
.. exercise::
|
#. Add `props validation <{OWL_PATH}/doc/reference/props.md#props-validation>`_ to the `Card`
|
||||||
|
|
||||||
#. Add `props validation <{OWL_PATH}/doc/reference/props.md#props-validation>`_ to the `Todo`
|
|
||||||
component.
|
component.
|
||||||
#. Open the :guilabel:`Console` tab of your browser's dev tools and make sure the props
|
#. Rename the `title` props into something else in the playground template, then check in the
|
||||||
validation passes in dev mode, which is activated by default in `owl_playground`. The dev mode
|
:guilabel:`Console` tab of your browser's dev tools that you can see an error.
|
||||||
can be activated and deactivated by modifying the `dev` attribute in the in the `config`
|
|
||||||
parameter of the `mount <{OWL_PATH}/doc/reference/app.md#mount-helper>`_ function in
|
|
||||||
:file:`owl_playground/static/src/main.js`.
|
|
||||||
#. Remove `done` from the props and reload the page. The validation should fail.
|
|
||||||
|
|
||||||
5. A list of todos
|
6. The sum of two `Counter`
|
||||||
==================
|
===========================
|
||||||
|
|
||||||
Now, let us display a list of todos instead of just one todo. For now, we can still hard-code the
|
We saw in a previous exercise that `props` can be used to provide information from a parent
|
||||||
list.
|
to a child component. Now, let us see how we can communicate information in the opposite
|
||||||
|
direction: in this exercise, we want to display two `Counter` components, and below them, the sum of
|
||||||
|
their values. So, the parent component (`Playground`) need to be informed whenever one of
|
||||||
|
the `Counter` value is changed.
|
||||||
|
|
||||||
.. exercise::
|
This can be done by using a `callback prop <{OWL_PATH}/doc/reference/props.md#binding-function-props>`_:
|
||||||
|
a prop that is a function meant to be called back. The child component can choose to call
|
||||||
|
that function with any argument. In our case, we will simply add an optional `onChange` prop that will
|
||||||
|
be called whenever the `Counter` component is incremented.
|
||||||
|
|
||||||
#. Change the code to display a list of todos instead of just one. Create a new `TodoList`
|
#. Add prop validation to the `Counter` component: it should accept an optional `onChange`
|
||||||
component to hold the `Todo` components and use `t-foreach
|
function prop.
|
||||||
<{OWL_PATH}/doc/reference/templates.md#loops>`_ in its template.
|
#. Update the `Counter` component to call the `onChange` prop (if it exists) whenever it
|
||||||
#. Think about how it should be keyed with the `t-key` directive.
|
is incremented.
|
||||||
|
#. Modify the `Playground` component to maintain a local state value (`sum`), initially
|
||||||
|
set to 2, and display it in its template
|
||||||
|
#. Implement an `incrementSum` method in `Playground`
|
||||||
|
#. Give that method as a prop to two (or more!) sub `Counter` components.
|
||||||
|
|
||||||
.. image:: 01_owl_components/todo_list.png
|
.. image:: 01_owl_components/sum_counter.png
|
||||||
:scale: 70%
|
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
6. Adding a todo
|
.. important::
|
||||||
|
|
||||||
|
There is a subtlety with callback props: they usually should be defined with the `.bind`
|
||||||
|
suffix. See the `documentation <{OWL_PATH}/doc/reference/props.md#binding-function-props>`_
|
||||||
|
|
||||||
|
7. A todo list
|
||||||
|
==============
|
||||||
|
|
||||||
|
Let us now discover various features of Owl by creating a todo list. We need two components: a
|
||||||
|
`TodoList` component that will display a list of `TodoItem` components. The list of todos is a
|
||||||
|
state that should be maintained by the `TodoList`.
|
||||||
|
|
||||||
|
For this tutorial, a `todo` is an object that contains three values: an `id` (number), a `description`
|
||||||
|
(string) and a flag `isCompleted` (boolean):
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
{ id: 3, description: "buy milk", isCompleted: false }
|
||||||
|
|
||||||
|
#. Create a `TodoList` and a `TodoItem` components
|
||||||
|
#. The `TodoItem` component should receive a `todo` as a prop, and display its `id` and `description` in a `div`.
|
||||||
|
#. For now, hardcode the list of todos:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
// in TodoList
|
||||||
|
this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
|
||||||
|
|
||||||
|
#. Use `t-foreach <{OWL_PATH}/doc/reference/templates.md#loops>`_ to display each todo in a `TodoItem`
|
||||||
|
#. Display a `TodoList` in the playground
|
||||||
|
#. Add props validation to `TodoItem`
|
||||||
|
|
||||||
|
.. image:: 01_owl_components/todo_list.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
Note that the `t-foreach` directive is not exactly the same in Owl as the QWeb python implementation: it
|
||||||
|
requires a `t-key` unique value, so Owl can properly reconciliate each element.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
Since the `TodoList` and `TodoItem` components are so tightly coupled, it makes
|
||||||
|
sense to put them in the same folder
|
||||||
|
|
||||||
|
8. Use dynamic attributes
|
||||||
|
=========================
|
||||||
|
|
||||||
|
For now, the `TodoItem` component does not visually show if the `todo` is completed. Let us do that by
|
||||||
|
using a `dynamic attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-attributes>`_.
|
||||||
|
|
||||||
|
#. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on the `TodoItem` root element
|
||||||
|
if it is completed.
|
||||||
|
#. Change the hardcoded `todo` value to check that it is properly displayed.
|
||||||
|
|
||||||
|
Even though the directive is named `t-att` (for attribute), it can be used to set a `class` value (and
|
||||||
|
html properties such as the `value` of an input).
|
||||||
|
|
||||||
|
.. image:: 01_owl_components/muted_todo.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
Owl let you combine static class values with dynamic values. The following example will work as expected:
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<div class="a" t-att-class="someExpression"/>
|
||||||
|
|
||||||
|
See also: `Owl: Dynamic class attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-class-attribute>`_
|
||||||
|
|
||||||
|
9. Adding a todo
|
||||||
================
|
================
|
||||||
|
|
||||||
So far, the todos in our list are hard-coded. Let us make it more useful by allowing the user to add
|
So far, the todos in our list are hard-coded. Let us make it more useful by allowing the user to add
|
||||||
a todo to the list.
|
a todo to the list.
|
||||||
|
|
||||||
.. exercise::
|
#. Remove the hardcoded values in the `TodoList` component
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
this.todos = useState([]);
|
||||||
|
|
||||||
#. Add an input above the task list with placeholder *Enter a new task*.
|
#. Add an input above the task list with placeholder *Enter a new task*.
|
||||||
#. Add an `event handler <{OWL_PATH}/doc/reference/event_handling.md>`_ on the `keyup` event
|
#. Add an `event handler <{OWL_PATH}/doc/reference/event_handling.md>`_ on the `keyup` event
|
||||||
@@ -201,75 +318,167 @@ a todo to the list.
|
|||||||
case, create a new todo with the current content of the input as the description and clear the
|
case, create a new todo with the current content of the input as the description and clear the
|
||||||
input of all content.
|
input of all content.
|
||||||
#. Make sure the todo has a unique id. It can be just a counter that increments at each todo.
|
#. Make sure the todo has a unique id. It can be just a counter that increments at each todo.
|
||||||
#. Wrap the todo list in a `useState` hook to let Owl know that it should update the UI when the
|
|
||||||
list is modified.
|
|
||||||
#. Bonus point: don't do anything if the input is empty.
|
#. Bonus point: don't do anything if the input is empty.
|
||||||
|
|
||||||
.. code-block:: javascript
|
|
||||||
|
|
||||||
this.todos = useState([]);
|
|
||||||
|
|
||||||
.. image:: 01_owl_components/create_todo.png
|
.. image:: 01_owl_components/create_todo.png
|
||||||
:scale: 70%
|
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
`Owl: Reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_
|
`Owl: Reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_
|
||||||
|
|
||||||
7. Focusing the input
|
Theory: Component lifecycle and hooks
|
||||||
=====================
|
=====================================
|
||||||
|
|
||||||
|
So far, we have seen one example of a hook function: `useState`. A `hook <{OWL_PATH}/doc/reference/hooks.md>`_
|
||||||
|
is a special function that *hook into* the internals of the component. In the case of
|
||||||
|
`useState`, it generates a proxy object linked to the current component. This is why
|
||||||
|
hook functions have to be called in the `setup` method, and no later!
|
||||||
|
|
||||||
|
|
||||||
|
.. flowchart LR
|
||||||
|
|
||||||
|
.. classDef hook fill:#ccf
|
||||||
|
|
||||||
|
.. subgraph "creation"
|
||||||
|
.. direction TB
|
||||||
|
.. A:::hook
|
||||||
|
.. B:::hook
|
||||||
|
.. M:::hook
|
||||||
|
.. A[setup]-->B
|
||||||
|
.. B[onWillStart] --> C(render)
|
||||||
|
.. C --> D("mount (in DOM)")
|
||||||
|
.. D --> M[onMounted]
|
||||||
|
.. end
|
||||||
|
|
||||||
|
.. subgraph updates
|
||||||
|
.. direction TB
|
||||||
|
.. E:::hook
|
||||||
|
.. F:::hook
|
||||||
|
.. H:::hook
|
||||||
|
.. E["(onWillUpdateProps)"] --> L(render)
|
||||||
|
.. L --> F[onWillPatch]
|
||||||
|
.. F --> G(patch DOM)
|
||||||
|
.. G --> H[onPatched]
|
||||||
|
.. end
|
||||||
|
|
||||||
|
.. subgraph destruction
|
||||||
|
.. direction TB
|
||||||
|
.. I:::hook
|
||||||
|
.. J:::hook
|
||||||
|
.. I[onWillUnmount] --> J[onWillDestroy]
|
||||||
|
.. J --> N(removed from DOM)
|
||||||
|
|
||||||
|
.. end
|
||||||
|
|
||||||
|
.. creation --> updates
|
||||||
|
.. updates --> destruction
|
||||||
|
|
||||||
|
|
||||||
|
.. figure:: 01_owl_components/component_lifecycle.svg
|
||||||
|
:align: center
|
||||||
|
:width: 50%
|
||||||
|
|
||||||
|
|
||||||
|
An Owl component goes through a lot of phases: it can be instantiated, rendered,
|
||||||
|
mounted, updated, detached, destroyed, ... This is the `component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_.
|
||||||
|
The figure above show the most important events in the life of a component (hooks are shown in purple).
|
||||||
|
Roughly speaking, a component is created, then updated (potentially many times), then is destroyed.
|
||||||
|
|
||||||
|
Owl provides a variety of built-in `hooks functions <{OWL_PATH}/doc/reference/hooks.md>`_. All of them have to be called in
|
||||||
|
the `setup` function. For example, if you want to execute some code when your component is mounted, you can use the `onMounted`
|
||||||
|
hook:
|
||||||
|
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
onMounted(() => {
|
||||||
|
// do something here
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
All hook functions start with `use` or `on`. For example: `useState` or `onMounted`.
|
||||||
|
|
||||||
|
|
||||||
|
10. Focusing the input
|
||||||
|
======================
|
||||||
|
|
||||||
Let's see how we can access the DOM with `t-ref <{OWL_PATH}/doc/reference/refs.md>`_ and `useRef
|
Let's see how we can access the DOM with `t-ref <{OWL_PATH}/doc/reference/refs.md>`_ and `useRef
|
||||||
<{OWL_PATH}/doc/reference/hooks.md#useref>`_.
|
<{OWL_PATH}/doc/reference/hooks.md#useref>`_. The main idea is that you need to mark
|
||||||
|
the target element in the component template with a `t-ref`:
|
||||||
|
|
||||||
.. exercise::
|
.. code-block:: xml
|
||||||
|
|
||||||
#. Focus the `input` from the previous exercise when the dashboard is `mounted
|
<div t-ref="some_name">hello</div>
|
||||||
<{OWL_PATH}/doc/reference/component.md#mounted>`_. This this should be done from the
|
|
||||||
`TodoList` component.
|
Then you can access it in the JS with the `useRef hook <{OWL_PATH}/doc/reference/hooks.md#useref>`_.
|
||||||
|
However, there is a problem if you think about it: the actual html element for a
|
||||||
|
component does not exist when the component is created. It only exists when the
|
||||||
|
component is mounted. But hooks have to be called in the `setup` method. So, `useRef`
|
||||||
|
return an object that contains a `el` (for element) key that is only defined when the
|
||||||
|
component is mounted.
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.myRef = useRef('some_name');
|
||||||
|
onMounted(() => {
|
||||||
|
console.log(this.myRef.el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#. Focus the `input` from the previous exercise. This this should be done from the
|
||||||
|
`TodoList` component (note that there is a `focus` method on the input html element).
|
||||||
#. Bonus point: extract the code into a specialized `hook <{OWL_PATH}/doc/reference/hooks.md>`_
|
#. Bonus point: extract the code into a specialized `hook <{OWL_PATH}/doc/reference/hooks.md>`_
|
||||||
`useAutofocus` in a new :file:`owl_playground/utils.js` file.
|
`useAutofocus` in a new :file:`awesome_owl/utils.js` file.
|
||||||
|
|
||||||
.. seealso::
|
.. image:: 01_owl_components/autofocus.png
|
||||||
`Owl: Component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_
|
:align: center
|
||||||
|
|
||||||
8. Toggling todos
|
.. tip::
|
||||||
=================
|
|
||||||
|
Refs are usually suffixed by `Ref` to make it obvious that they are special objects:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
this.inputRef = useRef('refname');
|
||||||
|
|
||||||
|
11. Toggling todos
|
||||||
|
==================
|
||||||
|
|
||||||
Now, let's add a new feature: mark a todo as completed. This is actually trickier than one might
|
Now, let's add a new feature: mark a todo as completed. This is actually trickier than one might
|
||||||
think. The owner of the state is not the same as the component that displays it. So, the `Todo`
|
think. The owner of the state is not the same as the component that displays it. So, the `TodoItem`
|
||||||
component needs to communicate to its parent that the todo state needs to be toggled. One classic
|
component needs to communicate to its parent that the todo state needs to be toggled. One classic
|
||||||
way to do this is by using a `callback prop
|
way to do this is by using a `callback prop
|
||||||
<{OWL_PATH}/doc/reference/props.md#binding-function-props>`_ `toggleState`.
|
<{OWL_PATH}/doc/reference/props.md#binding-function-props>`_ `toggleState`.
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Add an input with the attribute :code:`type="checkbox"` before the id of the task, which must
|
#. Add an input with the attribute :code:`type="checkbox"` before the id of the task, which must
|
||||||
be checked if the state `done` is true.
|
be checked if the state `isCompleted` is true.
|
||||||
|
|
||||||
.. tip::
|
.. tip::
|
||||||
QWeb does not create attributes computed with the `t-att` directive if it evaluates to a
|
Owl does not create attributes computed with the `t-att` directive if it evaluates to a
|
||||||
falsy value.
|
falsy value.
|
||||||
|
|
||||||
#. Add a callback props `toggleState`.
|
#. Add a callback props `toggleState` to `TodoItem`.
|
||||||
#. Add a `click` event handler on the input in the `Todo` component and make sure it calls the
|
#. Add a `click` event handler on the input in the `TodoItem` component and make sure it calls the
|
||||||
`toggleState` function with the todo id.
|
`toggleState` function with the todo id.
|
||||||
#. Make it work!
|
#. Make it work!
|
||||||
|
|
||||||
.. image:: 01_owl_components/toggle_todo.png
|
.. image:: 01_owl_components/toggle_todo.png
|
||||||
:scale: 70%
|
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
9. Deleting todos
|
12. Deleting todos
|
||||||
=================
|
==================
|
||||||
|
|
||||||
The final touch is to let the user delete a todo.
|
The final touch is to let the user delete a todo.
|
||||||
|
|
||||||
.. exercise::
|
#. Add a new callback prop `removeTodo` in `TodoItem`.
|
||||||
|
#. Insert :code:`<span class="fa fa-remove"/>` in the template of the `TodoItem` component.
|
||||||
#. Add a new callback prop `removeTodo`.
|
|
||||||
#. Insert :code:`<span class="fa fa-remove"/>` in the template of the `Todo` component.
|
|
||||||
#. Whenever the user clicks on it, it should call the `removeTodo` method.
|
#. Whenever the user clicks on it, it should call the `removeTodo` method.
|
||||||
|
#. Make it work!
|
||||||
|
|
||||||
.. tip::
|
.. tip::
|
||||||
If you're using an array to store your todo list, you can use the JavaScript `splice`
|
If you're using an array to store your todo list, you can use the JavaScript `splice`
|
||||||
@@ -285,61 +494,45 @@ The final touch is to let the user delete a todo.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.. image:: 01_owl_components/delete_todo.png
|
.. image:: 01_owl_components/delete_todo.png
|
||||||
:scale: 70%
|
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. _tutorials/discover_js_framework/generic_card:
|
.. _tutorials/discover_js_framework/generic_card:
|
||||||
|
|
||||||
10. Generic card with slots
|
13. Generic `Card` with slots
|
||||||
===========================
|
=============================
|
||||||
|
|
||||||
Owl has a powerful `slot <{OWL_PATH}/doc/reference/slots.md>`_ system to allow you to write generic
|
In a :ref:`previous exercise <tutorials/discover_js_framework/simple_card>`, we built
|
||||||
components. This is useful to factorize the common layout between different parts of the interface.
|
a simple `Card` component. But it is honestly quite limited. What if we want
|
||||||
|
to display some arbitrary content inside a card, such as a sub component? Well,
|
||||||
|
it does not work, since the content of the card is described by a string. It would
|
||||||
|
however be very convenient if we could describe the content as a piece of template.
|
||||||
|
|
||||||
.. exercise::
|
This is exactly what Owl `slot <{OWL_PATH}/doc/reference/slots.md>`_ system is designed
|
||||||
|
for: allowing to write generic components.
|
||||||
|
|
||||||
#. Insert a new `Card` component between the `Counter` and `Todolist` components. Use the
|
Let us modify the `Card` component to use slots:
|
||||||
following Bootstrap HTML structure for the card:
|
|
||||||
|
|
||||||
.. code-block:: html
|
#. Remove the `content` prop
|
||||||
|
#. Use the default slot to define the body
|
||||||
|
#. Insert a few cards with arbitrary content, such as a `Counter` component
|
||||||
|
#. (bonus) Add prop validation
|
||||||
|
|
||||||
<div class="card" style="width: 18rem;">
|
.. image:: 01_owl_components/generic_card.png
|
||||||
<img src="..." class="card-img-top" alt="..." />
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Card title</h5>
|
|
||||||
<p class="card-text">
|
|
||||||
Some quick example text to build on the card title and make up the bulk
|
|
||||||
of the card's content.
|
|
||||||
</p>
|
|
||||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
#. This component should have two slots: one slot for the title, and one for the content (the
|
|
||||||
default slot). It should be possible to use the `Card` component as follows:
|
|
||||||
|
|
||||||
.. code-block:: html
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<t t-set-slot="title">Card title</t>
|
|
||||||
<p class="card-text">Some quick example text...</p>
|
|
||||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
#. Bonus point: if the `title` slot is not given, the `h5` should not be rendered at all.
|
|
||||||
|
|
||||||
.. image:: 01_owl_components/card.png
|
|
||||||
:scale: 70%
|
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
`Bootstrap: documentation on cards <https://getbootstrap.com/docs/5.2/components/card/>`_
|
`Bootstrap: documentation on cards <https://getbootstrap.com/docs/5.2/components/card/>`_
|
||||||
|
|
||||||
11. Extensive props validation
|
14. Minimizing card content
|
||||||
==============================
|
===========================
|
||||||
|
|
||||||
.. exercise::
|
Finally, let's add a feature to the `Card` component, to make it more interesting: we
|
||||||
|
want a button to toggle its content (show it or hide it)
|
||||||
|
|
||||||
#. Add prop validation on the `Card` component.
|
#. Add a state to the `Card` component to track if it is open (the default) or not
|
||||||
#. Try to express in the props validation system that it requires a `default` slot, and an
|
#. Add a `t-if` in the template to conditionally render the content
|
||||||
optional `title` slot.
|
#. Add a button in the header, and modify the code to flip the state when the button is clicked
|
||||||
|
|
||||||
|
.. image:: 01_owl_components/toggle_card.png
|
||||||
|
:scale: 90%
|
||||||
|
:align: center
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,457 @@
|
|||||||
|
============================
|
||||||
|
Chapter 2: Build a dashboard
|
||||||
|
============================
|
||||||
|
|
||||||
|
The first part of this tutorial introduced you to most of Owl ideas. It is now time to learn
|
||||||
|
about the Odoo JavaScript framework in its entirety, as used by the web client.
|
||||||
|
|
||||||
|
.. graph TD
|
||||||
|
.. subgraph "Owl"
|
||||||
|
.. C[Component]
|
||||||
|
.. T[Template]
|
||||||
|
.. H[Hook]
|
||||||
|
.. S[Slot]
|
||||||
|
.. E[Event]
|
||||||
|
.. end
|
||||||
|
|
||||||
|
.. odoo[Odoo JavaScript framework] --> Owl
|
||||||
|
|
||||||
|
.. figure:: 02_web_framework/previously_learned.svg
|
||||||
|
:align: center
|
||||||
|
:width: 50%
|
||||||
|
|
||||||
|
To get started, you need a running Odoo server and a development environment setup. Before getting
|
||||||
|
into the exercises, make sure you have followed all the steps described in this
|
||||||
|
:ref:`tutorial introduction <tutorials/discover_js_framework/setup>`. For this chapter, we will start
|
||||||
|
from the empty dashboard provided by the `awesome_dashboard` addon. We will progressively add
|
||||||
|
features to it, using the Odoo JavaScript framework.
|
||||||
|
|
||||||
|
.. admonition:: Goal
|
||||||
|
|
||||||
|
.. image:: 02_web_framework/overview_02.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. spoiler:: Solutions
|
||||||
|
|
||||||
|
The solutions for each exercise of the chapter are hosted on the
|
||||||
|
`official Odoo tutorials repository
|
||||||
|
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-discover-js-framework-solutions/awesome_dashboard>`_.
|
||||||
|
|
||||||
|
1. A new Layout
|
||||||
|
===============
|
||||||
|
|
||||||
|
Most screens in the Odoo web client uses a common layout: a control panel on top, with some buttons,
|
||||||
|
and a main content zone just below. This is done using the `Layout component
|
||||||
|
<{GITHUB_PATH}/addons/web/static/src/search/layout.js>`_, available in `@web/search/layout`.
|
||||||
|
|
||||||
|
#. Update the `AwesomeDashboard` component located in :file:`awesome_dashboard/static/src/` to use the
|
||||||
|
`Layout` component. You can use
|
||||||
|
:code:`{controlPanel: {} }` for the `display` props of
|
||||||
|
the `Layout` component.
|
||||||
|
#. Add a `className` prop to `Layout`: `className="'o_dashboard h-100'"`
|
||||||
|
#. Add a `dashboard.scss` file in which you set the background-color of `.o_dashboard` to gray (or your
|
||||||
|
favorite color)
|
||||||
|
|
||||||
|
Open http://localhost:8069/web, then open the :guilabel:`Awesome Dashboard` app, and see the
|
||||||
|
result.
|
||||||
|
|
||||||
|
.. image:: 02_web_framework/new_layout.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Example: use of Layout in client action
|
||||||
|
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.js>`_ and
|
||||||
|
`template <{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.xml>`_
|
||||||
|
- `Example: use of Layout in kanban view
|
||||||
|
<{GITHUB_PATH}/addons/web/static/src/views/kanban/kanban_controller.xml>`_
|
||||||
|
|
||||||
|
.. _tutorials/discover_js_framework/services:
|
||||||
|
|
||||||
|
Theory: Services
|
||||||
|
================
|
||||||
|
|
||||||
|
In practice, every component (except the root component) may be destroyed at any time and replaced
|
||||||
|
(or not) with another component. This means that each component internal state is not persistent.
|
||||||
|
This is fine in many cases, but there certainly are situations where we want to keep some data around.
|
||||||
|
For example, all discuss messages should not be reloaded every time we display a channel.
|
||||||
|
|
||||||
|
Also, it may happen that we need to write some code that is not a component. Maybe something that
|
||||||
|
process all barcodes, or that manages the user configuration (context, ...).
|
||||||
|
|
||||||
|
The Odoo framework defines the idea of a :ref:`service <frontend/services>`, which is a persistent
|
||||||
|
piece of code that exports state and/or functions. Each service can depend on other services, and
|
||||||
|
components can import a service.
|
||||||
|
|
||||||
|
The following example registers a simple service that displays a notification every 5 seconds:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
|
||||||
|
const myService = {
|
||||||
|
dependencies: ["notification"],
|
||||||
|
start(env, { notification }) {
|
||||||
|
let counter = 1;
|
||||||
|
setInterval(() => {
|
||||||
|
notification.add(`Tick Tock ${counter++}`);
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("services").add("myService", myService);
|
||||||
|
|
||||||
|
Services can be accessed by any component. Imagine that we have a service to maintain some shared
|
||||||
|
state:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
|
||||||
|
const sharedStateService = {
|
||||||
|
start(env) {
|
||||||
|
let state = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getValue(key) {
|
||||||
|
return state[key];
|
||||||
|
},
|
||||||
|
setValue(key, value) {
|
||||||
|
state[key] = value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("services").add("shared_state", sharedStateService);
|
||||||
|
|
||||||
|
Then, any component can do this:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.sharedState = useService("shared_state");
|
||||||
|
const value = this.sharedState.getValue("somekey");
|
||||||
|
// do something with value
|
||||||
|
}
|
||||||
|
|
||||||
|
2. Add some buttons for quick navigation
|
||||||
|
========================================
|
||||||
|
|
||||||
|
One important service provided by Odoo is the `action` service: it can execute
|
||||||
|
all kind of standard actions defined by Odoo. For example, here is how one
|
||||||
|
component could execute an action by its xml id:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
...
|
||||||
|
setup() {
|
||||||
|
this.action = useService("action");
|
||||||
|
}
|
||||||
|
openSettings() {
|
||||||
|
this.action.doAction("base_setup.action_general_configuration");
|
||||||
|
}
|
||||||
|
...
|
||||||
|
|
||||||
|
Let us now add two buttons to our control panel:
|
||||||
|
|
||||||
|
#. A button `Customers`, which opens a kanban view with all customers (this action already
|
||||||
|
exists, so you should use `its xml id
|
||||||
|
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/odoo/addons/base/views/res_partner_views.xml#L510>`_).
|
||||||
|
|
||||||
|
#. A button `Leads`, which opens a dynamic action on the `crm.lead` model with a list and a form view.
|
||||||
|
|
||||||
|
.. image:: 02_web_framework/navigation_buttons.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
- `Example: doAction use
|
||||||
|
<{GITHUB_PATH}/addons/account/static/src/components/journal_dashboard_activity
|
||||||
|
/journal_dashboard_activity.js#L35>`_
|
||||||
|
- `Code: action service
|
||||||
|
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/action_service.js>`_
|
||||||
|
|
||||||
|
3. Add a DashboardItem
|
||||||
|
======================
|
||||||
|
|
||||||
|
Let us now improve our content.
|
||||||
|
|
||||||
|
#. Create a generic `DashboardItem` component that display its default slot in a nice card layout
|
||||||
|
It should take an optional `size` number props, that default to `1`
|
||||||
|
The width should be hardcoded to `(18*size)rem`.
|
||||||
|
#. Add a few cards in the dashboard, with no size and a size of 2.
|
||||||
|
|
||||||
|
.. image:: 02_web_framework/dashboard_item.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
- `Owl slot system <{OWL_PATH}/doc/reference/slots.md>`_
|
||||||
|
|
||||||
|
4. Call the server, add some statistics
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
Let's improve the dashboard by adding a few dashboard items to display *real* business data.
|
||||||
|
The *awesome_dashboard* addon provides a `/awesome_dashboard/statistics` route that is meant
|
||||||
|
to return some interesting information.
|
||||||
|
|
||||||
|
To call a specific controller, we need to use the :ref:`rpc service <frontend/services/rpc>`.
|
||||||
|
It only exports a single function that perform the request: :code:`rpc(route, params, settings)`.
|
||||||
|
A basic request could look like this:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.rpc = useService("rpc");
|
||||||
|
onWillStart(async () => {
|
||||||
|
const result = await this.rpc("/my/controller", {a: 1, b: 2});
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#. Update `Dashboard` so that it uses the `rpc` service.
|
||||||
|
#. Call the statistics route `/awesome_dashboard/statistics` in the `onWillStart` hook.
|
||||||
|
#. Display a few cards in the dashboard containing:
|
||||||
|
|
||||||
|
- Number of new orders this month
|
||||||
|
- Total amount of new orders this month
|
||||||
|
- Average amount of t-shirt by order this month
|
||||||
|
- Number of cancelled orders this month
|
||||||
|
- Average time for an order to go from 'new' to 'sent' or 'cancelled'
|
||||||
|
|
||||||
|
.. image:: 02_web_framework/statistics.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Code: rpc service <{GITHUB_PATH}/addons/web/static/src/core/network/rpc_service.js>`_
|
||||||
|
- `Example: calling a route in onWillStart
|
||||||
|
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||||
|
addons/lunch/static/src/views/search_model.js#L21>`_
|
||||||
|
|
||||||
|
5. Cache network calls, create a service
|
||||||
|
========================================
|
||||||
|
|
||||||
|
If you open the :guilabel:`Network` tab of your browser's dev tools, you will see that the call to
|
||||||
|
`/awesome_dashboard/statistics` is done every time the client action is displayed. This is because the
|
||||||
|
`onWillStart` hook is called each time the `Dashboard` component is mounted. But in this case, we
|
||||||
|
would prefer to do it only the first time, so we actually need to maintain some state outside of the
|
||||||
|
`Dashboard` component. This is a nice use case for a service!
|
||||||
|
|
||||||
|
#. Register and import a new `awesome_dashboard.statistics` service.
|
||||||
|
#. It should provide a function `loadStatistics` that, once called, performs the actual rpc, and
|
||||||
|
always return the same information.
|
||||||
|
#. Use the `memoize <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||||
|
addons/web/static/src/core/utils/functions.js#L11>`_ utility function from
|
||||||
|
`@web/core/utils/functions` that will allow caching the statistics.
|
||||||
|
#. Use this service in the `Dashboard` component.
|
||||||
|
#. Check that it works as expected
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
- `Example: simple service <{GITHUB_PATH}/addons/web/static/src/core/network/http_service.js>`_
|
||||||
|
- `Example: service with a dependency
|
||||||
|
<{GITHUB_PATH}/addons/web/static/src/core/user_service.js>`_
|
||||||
|
|
||||||
|
6. Display a pie chart
|
||||||
|
======================
|
||||||
|
|
||||||
|
Everyone likes charts (!), so let us add a pie chart in our dashboard. It will display the
|
||||||
|
proportions of t-shirts sold for each size: S/M/L/XL/XXL.
|
||||||
|
|
||||||
|
For this exercise, we will use `Chart.js <https://www.chartjs.org/>`_. It is the chart library used
|
||||||
|
by the graph view. However, it is not loaded by default, so we will need to either add it to our
|
||||||
|
assets bundle, or lazy load it. Lazy loading is usually better since our users will not have to load
|
||||||
|
the chartjs code every time if they don't need it.
|
||||||
|
|
||||||
|
#. Create a `PieChart` component
|
||||||
|
#. In its `onWillStart` method, load chartjs, you can use the `loadJs
|
||||||
|
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||||
|
addons/web/static/src/core/assets.js#L23>`_ function to load
|
||||||
|
:file:`/web/static/lib/Chart/Chart.js`.
|
||||||
|
#. Use the `PieChart` component in a `DashboardItem` to display a `pie chart
|
||||||
|
<https://www.chartjs.org/docs/2.8.0/charts/doughnut.html>`_ that shows the
|
||||||
|
correct quantity for each sold t-shirts in each size (that information is available in the
|
||||||
|
statistics route). Note that you can use the `size` property to make it look larger
|
||||||
|
#. The `PieChart` component will need to render a canvas, and draw on it using `chart.js`.
|
||||||
|
#. Make it work!
|
||||||
|
|
||||||
|
.. image:: 02_web_framework/pie_chart.png
|
||||||
|
:align: center
|
||||||
|
:scale: 80%
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
- `Example: lazy loading a js file
|
||||||
|
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||||
|
addons/web/static/src/views/graph/graph_renderer.js#L57>`_
|
||||||
|
- `Example: rendering a chart in a component
|
||||||
|
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||||
|
addons/web/static/src/views/graph/graph_renderer.js#L618>`_
|
||||||
|
|
||||||
|
7. Real life update
|
||||||
|
===================
|
||||||
|
|
||||||
|
Since we moved the data loading in a cache, it does not ever updates. But let us say that we
|
||||||
|
are looking at fast moving data, so we want to periodically (for example, every 10min) reload
|
||||||
|
fresh data.
|
||||||
|
|
||||||
|
This is quite simple to implement, with a `setTimeout` or `setInterval` in the dashboard service.
|
||||||
|
However, here is the tricky part: if the dashboard is currently being displayed, it should be
|
||||||
|
updated immediately.
|
||||||
|
|
||||||
|
To do that, one can use a `reactive` object: it is just like the proxy returned by `useState`,
|
||||||
|
but not linked to any component. A component can then do a `useState` on it to subscribe to its
|
||||||
|
changes.
|
||||||
|
|
||||||
|
|
||||||
|
#. Update the dashboard service to reload data every 10 minutes (to test it, use 10s instead!)
|
||||||
|
#. Modify it to return a `reactive <{OWL_PATH}/doc/reference/reactivity.md#reactive>`_ object.
|
||||||
|
Reloading data should update the reactive object in place.
|
||||||
|
#. The `Dashboard` component can now use it with a `useState`
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
- `Documentation on reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_
|
||||||
|
- `Example: Use of reactive in a service
|
||||||
|
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||||
|
addons/web/static/src/core/debug/profiling/profiling_service.js#L30>`_
|
||||||
|
|
||||||
|
8. Lazy loading the dashboard
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Let us imagine that our dashboard is getting quite big, and is only of interest to some
|
||||||
|
of our users. In that case, it could make sense to lazy load our dashboard, and all
|
||||||
|
related assets, so we only pay the cost of loading the code when we actually want to
|
||||||
|
look at it.
|
||||||
|
|
||||||
|
To do that, we will need to create a new bundle containing all our dashboard assets,
|
||||||
|
then use the `LazyComponent` (located in `@web/core/assets`).
|
||||||
|
|
||||||
|
#. Move all dashboard assets into a sub folder `/dashboard` to make it easier to
|
||||||
|
add to a bundle.
|
||||||
|
#. Create a `awesome_dashboard.dashboard` assets bundle containing all content of
|
||||||
|
the `/dashboard` folder
|
||||||
|
#. Modify `dashboard.js` to register itself in the `lazy_components` registry, and not
|
||||||
|
in the `action` registry.
|
||||||
|
#. Add in `src/` a file `dashboard_action` that import `LazyComponent` and register
|
||||||
|
it to the `action` registry
|
||||||
|
|
||||||
|
9. Making our dashboard generic
|
||||||
|
===============================
|
||||||
|
|
||||||
|
So far, we have a nice working dashboard. But it is currently hardcoded in the dashboard
|
||||||
|
template. What if we want to customize our dashboard? Maybe some users have different
|
||||||
|
needs, and want to see some other data.
|
||||||
|
|
||||||
|
So, the next step is then to make our dashboard generic: instead of hardcoding its content
|
||||||
|
in the template, it can just iterate over a list of dashboard items. But then, many
|
||||||
|
questions comes up: how to represent a dashboard item, how to register it, what data
|
||||||
|
should it receive, and so on. There are many different ways to design such a system,
|
||||||
|
with different trade offs.
|
||||||
|
|
||||||
|
For this tutorial, we will say that a dashboard item is an object with the folowing structure:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id: "average_quantity",
|
||||||
|
description: "Average amount of t-shirt",
|
||||||
|
Component: StandardItem,
|
||||||
|
// size and props are optionals
|
||||||
|
size: 3,
|
||||||
|
props: (data) => ({
|
||||||
|
title: "Average amount of t-shirt by order this month",
|
||||||
|
value: data.average_quantity
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
The `description` value will be useful in a later exercise to show the name of items that the
|
||||||
|
user can choose to add to his dashboard. The `size` number is optional, and simply describes
|
||||||
|
the size of the dashboard item that will be displayed. Finally, the `props` function is optional.
|
||||||
|
If not given, we will simply give the `statistics` object as data. But if it is defined, it will
|
||||||
|
be used to compute specific props for the component.
|
||||||
|
|
||||||
|
The goal is to replace the content of the dashboard with the following snippet:
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<t t-foreach="items" t-as="item" t-key="item.id">
|
||||||
|
<DashboardItem size="item.size || 1">
|
||||||
|
<t t-set="itemProp" t-value="item.props ? item.props(statistics) : {'data': statistics}"/>
|
||||||
|
<t t-component="item.Component" t-props="itemProp" />
|
||||||
|
</DashboardItem>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
Note that the above example features two advanced features of Owl: dynamic components, and dynamic props.
|
||||||
|
|
||||||
|
We currently have two kinds of item components: number cards, with a title and a number, and pie cards, with
|
||||||
|
some label and a pie chart.
|
||||||
|
|
||||||
|
#. create and implement two components: `NumberCard` and `PieChartCard`, with the corresponding props
|
||||||
|
#. create a file `dashboard_items.js` in which you define and export a list of items, using `NumberCard`
|
||||||
|
and `PieChartCard` to recreate our current dashboard
|
||||||
|
#. import that list of items in our `Dashboard` component, add it to the component, and update the template
|
||||||
|
to use a `t-foreach` like shown above
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
And now, our dashboard template is generic!
|
||||||
|
|
||||||
|
10. Making our dashboard extensible
|
||||||
|
===================================
|
||||||
|
|
||||||
|
However, the content of our item list is still hardcoded. Let us fix that by using a registry:
|
||||||
|
|
||||||
|
#. Instead of exporting a list, register all dashboard items in a `awesome_dashboard` registry
|
||||||
|
#. Import all the items of the `awesome_dashboard` registry in the `Dashboard` component
|
||||||
|
|
||||||
|
The dashboard is now easily extensible. Any other odoo addon that want to register a new item to the
|
||||||
|
dashboard can just add it to the registry.
|
||||||
|
|
||||||
|
11. Add and remove dashboard items
|
||||||
|
==================================
|
||||||
|
|
||||||
|
Let us see how we can make our dashboard customizable. To make it simple, we will save the user
|
||||||
|
dashboard configuration in the local storage, so it is persistent, but we don't have to deal
|
||||||
|
with the server for now.
|
||||||
|
|
||||||
|
The dashboard configuration will be saved as a list of removed item ids.
|
||||||
|
|
||||||
|
#. Add a button in the control panel with a gear icon, to indicate that it is a settings button
|
||||||
|
#. Clicking on that button should open a dialog
|
||||||
|
#. In that dialog, we want to see a list of all existing dashboard items, each with a checkbox
|
||||||
|
#. There should be a `Apply` button in the footer. Clicking on it will build a list of all item ids
|
||||||
|
that are unchecked
|
||||||
|
#. We want to store that value in the local storage
|
||||||
|
#. And modify the `Dashboard` component to filter the current items by removing the ids of items
|
||||||
|
from the configuration
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: 02_web_framework/items_configuration.png
|
||||||
|
:width: 80%
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
12. Going further
|
||||||
|
=================
|
||||||
|
|
||||||
|
Here is a list of some small improvements you could try to do if you have the time:
|
||||||
|
|
||||||
|
#. Make sure your application can be :ref:`translated <reference/translations>` (with
|
||||||
|
`env._t`).
|
||||||
|
#. Clicking on a section of the pie chart should open a list view of all orders which have the
|
||||||
|
corresponding size.
|
||||||
|
#. Save the content of the dashboard in a user settings on the server!
|
||||||
|
#. Make it responsive: in mobile mode, each card should take 100% of the width
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
- `Example: use of env._t function
|
||||||
|
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||||
|
addons/account/static/src/components/bills_upload/bills_upload.js#L64>`_
|
||||||
|
- `Code: translation code in web/
|
||||||
|
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||||
|
addons/web/static/src/core/l10n/translation.js#L16>`_
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
=============================
|
|
||||||
Chapter 2: Odoo Web Framework
|
|
||||||
=============================
|
|
||||||
|
|
||||||
The first part of this tutorial introduced you to most of Owl ideas. It is now time to learn
|
|
||||||
about the Odoo JavaScript framework in its entirety, as used by the web client.
|
|
||||||
|
|
||||||
.. graph TD
|
|
||||||
.. subgraph "Owl"
|
|
||||||
.. C[Component]
|
|
||||||
.. T[Template]
|
|
||||||
.. H[Hook]
|
|
||||||
.. S[Slot]
|
|
||||||
.. E[Event]
|
|
||||||
.. end
|
|
||||||
|
|
||||||
.. odoo[Odoo JavaScript framework] --> Owl
|
|
||||||
|
|
||||||
.. figure:: 02_web_framework/previously_learned.svg
|
|
||||||
:align: center
|
|
||||||
:width: 50%
|
|
||||||
|
|
||||||
For this chapter, we will start from the empty dashboard provided by the `awesome_tshirt`
|
|
||||||
addon. We will progressively add features to it, using the Odoo JavaScript framework.
|
|
||||||
|
|
||||||
.. admonition:: Goal
|
|
||||||
|
|
||||||
.. image:: 02_web_framework/overview_02.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. spoiler:: Solutions
|
|
||||||
|
|
||||||
The solutions for each exercise of the chapter are hosted on the
|
|
||||||
`official Odoo tutorials repository
|
|
||||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_tshirt>`_.
|
|
||||||
|
|
||||||
1. A new Layout
|
|
||||||
===============
|
|
||||||
|
|
||||||
Most screens in the Odoo web client uses a common layout: a control panel on top, with some buttons,
|
|
||||||
and a main content zone just below. This is done using the `Layout component
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/search/layout.js>`_, available in `@web/search/layout`.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
Update the `AwesomeDashboard` component located in :file:`awesome_tshirt/static/src/` to use the
|
|
||||||
`Layout` component. You can use
|
|
||||||
:code:`{controlPanel: { "top-right": false, "bottom-right": false } }` for the `display` props of
|
|
||||||
the `Layout` component.
|
|
||||||
|
|
||||||
Open http://localhost:8069/web, then open the :guilabel:`Awesome T-Shirts` app, and see the
|
|
||||||
result.
|
|
||||||
|
|
||||||
.. image:: 02_web_framework/new_layout.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
|
|
||||||
- `Example: use of Layout in client action
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.js>`_ and
|
|
||||||
`template <{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.xml>`_
|
|
||||||
- `Example: use of Layout in kanban view
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/views/kanban/kanban_controller.xml>`_
|
|
||||||
|
|
||||||
2. Add some buttons for quick navigation
|
|
||||||
========================================
|
|
||||||
|
|
||||||
Let us now use the action service for an easy access to the common views in Odoo.
|
|
||||||
|
|
||||||
:ref:`Services <frontend/services>` is a notion defined by the Odoo JavaScript framework; it is a
|
|
||||||
persistent piece of code that exports a state and/or functions. Each service can depend on other
|
|
||||||
services, and components can import a service with the `useService()` hook.
|
|
||||||
|
|
||||||
.. example::
|
|
||||||
|
|
||||||
This shows how to open the settings view from a component using the action service.
|
|
||||||
|
|
||||||
.. code-block:: js
|
|
||||||
|
|
||||||
import { useService } from "@web/core/utils/hooks";
|
|
||||||
...
|
|
||||||
setup() {
|
|
||||||
this.action = useService("action");
|
|
||||||
}
|
|
||||||
openSettings() {
|
|
||||||
this.action.doAction("base_setup.action_general_configuration");
|
|
||||||
}
|
|
||||||
...
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
Let us add three buttons in the control panel bottom left zone.
|
|
||||||
|
|
||||||
#. A button `Customers`, which opens a kanban view with all customers (this action already
|
|
||||||
exists, so you should use `its xml id
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
odoo/addons/base/views/res_partner_views.xml#L525>`_).
|
|
||||||
#. A button `New Orders`, which opens a list view with all orders created in the last 7 days. Use
|
|
||||||
the `Domain <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
/addons/web/static/src/core/domain.js#L19>`_ helper class to represent the domain.
|
|
||||||
|
|
||||||
.. tip::
|
|
||||||
One way to represent the desired domain could be
|
|
||||||
:code:`[('create_date','>=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]`
|
|
||||||
|
|
||||||
#. A button `Cancelled Order`, which opens a list of all orders created in the last 7 days, but
|
|
||||||
already cancelled. Rather than defining the action twice, factorize it in a new `openOrders`
|
|
||||||
method.
|
|
||||||
|
|
||||||
.. image:: 02_web_framework/navigation_buttons.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
- `Example: doAction use
|
|
||||||
<{GITHUB_PATH}/addons/account/static/src/components/journal_dashboard_activity
|
|
||||||
/journal_dashboard_activity.js#L35>`_
|
|
||||||
- `Code: action service
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/action_service.js>`_
|
|
||||||
|
|
||||||
3. Call the server, add some statistics
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Let's improve the dashboard by adding a few cards (see the `Card` component :ref:`made in the
|
|
||||||
previous chapter <tutorials/discover_js_framework/generic_card>`) containing a few statistics. There
|
|
||||||
is a route `/awesome_tshirt/statistics` that performs some computations and returns an object
|
|
||||||
containing some useful information.
|
|
||||||
|
|
||||||
Whenever we need to call a specific controller, we need to use the :ref:`rpc service
|
|
||||||
<frontend/services/rpc>`. It only exports a single function that perform the request:
|
|
||||||
:code:`rpc(route, params, settings)`
|
|
||||||
|
|
||||||
Here is a short explanation on the various arguments:
|
|
||||||
|
|
||||||
- `route` is the target route, as a string. For example `/myroute/`.
|
|
||||||
- `params` is an object that contains all data that will be given to the controller. (optional)
|
|
||||||
- `settings` are for advanced controls on the request. Make it silent, or using a specific xhr
|
|
||||||
instance. (optional)
|
|
||||||
|
|
||||||
.. example::
|
|
||||||
|
|
||||||
A basic request could look like this:
|
|
||||||
|
|
||||||
.. code-block:: js
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
this.rpc = useService("rpc");
|
|
||||||
onWillStart(async () => {
|
|
||||||
const result = await this.rpc("/my/controller", {a: 1, b: 2});
|
|
||||||
// ...
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
#. Change `Dashboard` so that it uses the `rpc` service.
|
|
||||||
#. Call the statistics route `/awesome_tshirt/statistics` in the `onWillStart` hook.
|
|
||||||
#. Display a few cards in the dashboard containing:
|
|
||||||
|
|
||||||
- Number of new orders this month
|
|
||||||
- Total amount of new orders this month
|
|
||||||
- Average amount of t-shirt by order this month
|
|
||||||
- Number of cancelled orders this month
|
|
||||||
- Average time for an order to go from 'new' to 'sent' or 'cancelled'
|
|
||||||
|
|
||||||
.. image:: 02_web_framework/statistics.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
|
|
||||||
- `Code: rpc service <{GITHUB_PATH}/addons/web/static/src/core/network/rpc_service.js>`_
|
|
||||||
- `Example: calling a route in onWillStart
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/lunch/static/src/views/search_model.js#L21>`_
|
|
||||||
|
|
||||||
4. Cache network calls, create a service
|
|
||||||
========================================
|
|
||||||
|
|
||||||
If you open the :guilabel:`Network` tab of your browser's dev tools, you will see that the call to
|
|
||||||
`/awesome_tshirt/statistics` is done every time the client action is displayed. This is because the
|
|
||||||
`onWillStart` hook is called each time the `Dashboard` component is mounted. But in this case, we
|
|
||||||
would prefer to do it only the first time, so we actually need to maintain some state outside of the
|
|
||||||
`Dashboard` component. This is a nice use case for a service!
|
|
||||||
|
|
||||||
.. example::
|
|
||||||
|
|
||||||
The following example registers a simple service that displays a notification every 5 seconds.
|
|
||||||
|
|
||||||
.. code-block:: js
|
|
||||||
|
|
||||||
import { registry } from "@web/core/registry";
|
|
||||||
const myService = {
|
|
||||||
dependencies: ["notification"],
|
|
||||||
start(env, { notification }) {
|
|
||||||
let counter = 1;
|
|
||||||
setInterval(() => {
|
|
||||||
notification.add(`Tick Tock ${counter++}`);
|
|
||||||
}, 5000);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
registry.category("services").add("myService", myService);
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Register and import a new `awesome_tshirt.statistics` service.
|
|
||||||
#. It should provide a function `loadStatistics` that, once called, performs the actual rpc, and
|
|
||||||
always return the same information.
|
|
||||||
#. Use the `memoize <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/core/utils/functions.js#L11>`_ utility function from
|
|
||||||
`@web/core/utils/functions` that will allow caching the statistics.
|
|
||||||
#. Use this service in the `Dashboard` component.
|
|
||||||
#. Check that it works as expected
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
- `Example: simple service <{GITHUB_PATH}/addons/web/static/src/core/network/http_service.js>`_
|
|
||||||
- `Example: service with a dependency
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/core/user_service.js>`_
|
|
||||||
|
|
||||||
5. Display a pie chart
|
|
||||||
======================
|
|
||||||
|
|
||||||
Everyone likes charts (!), so let us add a pie chart in our dashboard. It will display the
|
|
||||||
proportions of t-shirts sold for each size: S/M/L/XL/XXL.
|
|
||||||
|
|
||||||
For this exercise, we will use `Chart.js <https://www.chartjs.org/>`_. It is the chart library used
|
|
||||||
by the graph view. However, it is not loaded by default, so we will need to either add it to our
|
|
||||||
assets bundle, or lazy load it. Lazy loading is usually better since our users will not have to load
|
|
||||||
the chartjs code every time if they don't need it.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
#. Load chartjs, you can use the `loadJs
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/core/assets.js#L23>`_ function to load
|
|
||||||
:file:`/web/static/lib/Chart/Chart.js`.
|
|
||||||
#. In a `Card` (from previous exercises), display a `pie chart
|
|
||||||
<https://www.chartjs.org/docs/2.8.0/charts/doughnut.html>`_ in the dashboard that displays the
|
|
||||||
correct quantity for each sold t-shirts in each size (that information is available in the
|
|
||||||
statistics route).
|
|
||||||
|
|
||||||
.. image:: 02_web_framework/pie_chart.png
|
|
||||||
:align: center
|
|
||||||
:scale: 50%
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
- `Example: lazy loading a js file
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/views/graph/graph_renderer.js#L57>`_
|
|
||||||
- `Example: rendering a chart in a component
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/views/graph/graph_renderer.js#L618>`_
|
|
||||||
|
|
||||||
6. Going further
|
|
||||||
================
|
|
||||||
|
|
||||||
Here is a list of some small improvements you could try to do if you have the time:
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Make sure your application can be :ref:`translated <reference/translations>` (with
|
|
||||||
`env._t`).
|
|
||||||
#. Clicking on a section of the pie chart should open a list view of all orders which have the
|
|
||||||
corresponding size.
|
|
||||||
#. Add a SCSS file and see if you can change the background color of the dashboard action.
|
|
||||||
|
|
||||||
.. image:: 02_web_framework/misc.png
|
|
||||||
:align: center
|
|
||||||
:scale: 50%
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
- `Example: use of env._t function
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/account/static/src/components/bills_upload/bills_upload.js#L64>`_
|
|
||||||
- `Code: translation code in web/
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/core/l10n/translation.js#L16>`_
|
|
||||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,7 +1,7 @@
|
|||||||
:show-content:
|
:show-content:
|
||||||
|
|
||||||
=============================
|
=============================
|
||||||
Master the Odoo Web Framework
|
Master the Odoo web framework
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
@@ -11,32 +11,49 @@ Master the Odoo Web Framework
|
|||||||
master_odoo_web_framework/*
|
master_odoo_web_framework/*
|
||||||
|
|
||||||
This tutorial is designed for those who have completed the :doc:`discover_js_framework` tutorial and
|
This tutorial is designed for those who have completed the :doc:`discover_js_framework` tutorial and
|
||||||
are looking to deepen their knowledge of the Odoo web framework.
|
are looking to deepen their knowledge of the Odoo web framework. It is organized in four independant
|
||||||
|
projects, each focusing on different features of Odoo.
|
||||||
|
|
||||||
For this training, we will step into the shoes of the IT staff at the fictional company Awesome
|
.. note::
|
||||||
T-Shirt, which is dedicated to printing custom t-shirts for online customers. The Awesome T-Shirt
|
|
||||||
company uses Odoo to manage orders and has created a dedicated Odoo module to manage their workflow.
|
|
||||||
|
|
||||||
In this tutorial, we will explore various aspects of the Odoo web framework in detail, including
|
Each of these chapters can be done independantly, in any order. Also, be aware that some of them
|
||||||
fields and views, notifications, command palette, and much more. This tutorial will provide you with
|
cover a lot of material, so they may be quite long.
|
||||||
the knowledge and skills you need to take full advantage of the Odoo web framework. So, let's get
|
|
||||||
started!
|
|
||||||
|
|
||||||
.. _howtos/master_odoo_web_framework/setup:
|
The first project is about building a `clicker game <https://en.wikipedia.org/wiki/Incremental_game>`_.
|
||||||
|
While working on it, you will learn various aspects of the web framework: systray, command palette,
|
||||||
|
dialogs, notifications, customizing existing components and much more.
|
||||||
|
|
||||||
|
The second project is focused on an important category of components: fields. Field components
|
||||||
|
represent the value of a field for a record, they appear in many places in the web client: in form
|
||||||
|
views, obviously, but also in kanban and list views, and may even be used alone, without a view.
|
||||||
|
Due to their importance, it makes sense to learn how to create and manipulate such components.
|
||||||
|
|
||||||
|
In the context of the web framework, views usually refers to the javascript implementation of a
|
||||||
|
component that represents one or many records, depending on a description (`ir.ui.view`). Such
|
||||||
|
components are actually quite complicated and usually requires various sub systems (a renderer,
|
||||||
|
a model, a controller, a arch parser, ...). In chapter 3, we create a new view from scratch to
|
||||||
|
represent a list of images.
|
||||||
|
|
||||||
|
Finally, the last project in chapter 4 is about customizing an existing view (a kanban view) by
|
||||||
|
adding a search panel on its left. It is interesting to see how one can take existing code, and
|
||||||
|
modify it to suit our needs. Also, it is a realistic project, that will feature many common issues
|
||||||
|
that arises while working on Odoo.
|
||||||
|
|
||||||
|
|
||||||
|
.. _tutorials/master_odoo_web_framework/setup:
|
||||||
|
|
||||||
Setup
|
Setup
|
||||||
=====
|
=====
|
||||||
|
|
||||||
#. Clone the `official Odoo tutorials repository <https://github.com/odoo/tutorials>`_ and switch to
|
#. Clone the `official Odoo tutorials repository <https://github.com/odoo/tutorials>`_ and switch to
|
||||||
the branch `{CURRENT_MAJOR_BRANCH}`.
|
the branch `{CURRENT_MAJOR_BRANCH}`.
|
||||||
#. Add the cloned repository to the :option:`--addons-path <odoo-bin --addons-path>`.
|
#. Add the cloned repository to your :option:`--addons-path <odoo-bin --addons-path>`.
|
||||||
#. Start a new Odoo database and install the modules `awesome_tshirt` and `awesome_gallery`.
|
#. Start a new Odoo database and install the modules for each chapter that you want to work on:
|
||||||
|
`awesome_clicker` (for chapter 1), `awesome_fields` (for chapter 2), `awesome_gallery` (for chapter 3) or `awesome_kanban` (for chapter 4).
|
||||||
|
|
||||||
Content
|
Content
|
||||||
=======
|
=======
|
||||||
|
|
||||||
- :doc:`master_odoo_web_framework/01_fields_and_views`
|
- :doc:`master_odoo_web_framework/01_build_clicker_game`
|
||||||
- :doc:`master_odoo_web_framework/02_miscellaneous`
|
- :doc:`master_odoo_web_framework/02_create_gallery_view`
|
||||||
- :doc:`master_odoo_web_framework/03_custom_kanban_view`
|
- :doc:`master_odoo_web_framework/03_customize_kanban_view`
|
||||||
- :doc:`master_odoo_web_framework/04_creating_view_from_scratch`
|
|
||||||
- :doc:`master_odoo_web_framework/05_testing`
|
|
||||||
|
|||||||
@@ -0,0 +1,447 @@
|
|||||||
|
===============================
|
||||||
|
Chapter 1: Build a Clicker game
|
||||||
|
===============================
|
||||||
|
|
||||||
|
For this project, we will build together a `clicker game <https://en.wikipedia.org/wiki/Incremental_game>`_,
|
||||||
|
completely integrated with Odoo. In this game, the goal is to accumulate a large number of clicks, and
|
||||||
|
to automate the system. The interesting part is that we will use the Odoo user interface as our playground.
|
||||||
|
For example, we will hide bonuses in some random parts of the web client.
|
||||||
|
|
||||||
|
To get started, you need a running Odoo server and a development environment. Before getting
|
||||||
|
into the exercises, make sure you have followed all the steps described in this
|
||||||
|
:ref:`tutorial introduction <tutorials/master_odoo_web_framework/setup>`.
|
||||||
|
|
||||||
|
.. admonition:: Goal
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/final.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. spoiler:: Solutions
|
||||||
|
|
||||||
|
The solutions for each exercise of the chapter are hosted on the
|
||||||
|
`official Odoo tutorials repository
|
||||||
|
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-master-odoo-web-framework-solutions/awesome_clicker>`_.
|
||||||
|
|
||||||
|
|
||||||
|
1. Create a systray item
|
||||||
|
========================
|
||||||
|
|
||||||
|
To get started, we want to display a counter in the systray.
|
||||||
|
|
||||||
|
#. Create a `clicker_systray_item.js` (and `xml`) file with a hello world Owl component.
|
||||||
|
#. Register it to the systray registry, and make sure it is visible.
|
||||||
|
#. Update the content of the item so that it displays the following string: `Clicks: 0`, and
|
||||||
|
add a button on the right to increment the value.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/systray.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
And voila, we have a completely working clicker game!
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- :ref:`Documentation on the systray registry <frontend/registries/systray>`
|
||||||
|
- `Example: adding a systray item to the registry
|
||||||
|
<https://github.com/odoo/odoo/blob/c4fb9c92d7826ddbc183d38b867ca4446b2fb709/addons/web/static/src/webclient/user_menu/user_menu.js#L41-L42>`_
|
||||||
|
|
||||||
|
2. Count external clicks
|
||||||
|
========================
|
||||||
|
|
||||||
|
Well, to be honest, it is not much fun yet. So let us add a new feature: we want all clicks in the
|
||||||
|
user interface to count, so the user is incentivized to use Odoo as much as possible! But obviously,
|
||||||
|
the intentional clicks on the main counter should still count more.
|
||||||
|
|
||||||
|
#. Use `useExternalListener` to listen on all clicks on `document.body`.
|
||||||
|
#. Each of these clicks should increase the counter value by 1.
|
||||||
|
#. Modify the code so that each click on the counter increased the value by 10
|
||||||
|
#. Make sure that a click on the counter does not increase the value by 11!
|
||||||
|
#. Also additional challenge: make sure the external listener capture the events, so we don't
|
||||||
|
miss any clicks.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Owl documentation on useExternalListener <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#useexternallistener>`_
|
||||||
|
- `MDN page on event capture <https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_capture>`_
|
||||||
|
|
||||||
|
|
||||||
|
3. Create a client action
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Currently, the current user interface is quite small: it is just a systray item. We certainly need
|
||||||
|
more room to display more of our game. To do that, let us create a client action. A client action
|
||||||
|
is a main action, managed by the web client, that displays a component.
|
||||||
|
|
||||||
|
#. Create a `client_action.js` (and `xml`) file, with a hello world component.
|
||||||
|
#. Register that client action in the action registry under the name `awesome_clicker.client_action`
|
||||||
|
#. Add a button on the systray item with the text `Open`. Clicking on it should open the
|
||||||
|
client action `awesome_clicker.client_action` (use the action service to do that).
|
||||||
|
#. To avoid disrupting employees' workflow, we prefer the client action to open within a popover
|
||||||
|
rather than in fullscreen mode. Modify the `doAction` call to open it in a popover.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
You can use `target: "new"` in the `doAction` to open the action in a popover:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
{
|
||||||
|
type: "ir.actions.client",
|
||||||
|
tag: "awesome_clicker.client_action",
|
||||||
|
target: "new",
|
||||||
|
name: "Clicker"
|
||||||
|
}
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/client_action.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- :ref:`How to create a client action <howtos/javascript_client_action>`
|
||||||
|
|
||||||
|
4. Move the state to a service
|
||||||
|
==============================
|
||||||
|
|
||||||
|
For now, our client action is just a hello world component. We want it to display our game state, but
|
||||||
|
that state is currently only available in the systray item. So it means that we need to change the
|
||||||
|
location of our state to make it available for all our components. This is a perfect use case for services.
|
||||||
|
|
||||||
|
#. Create a `clicker_service.js` file with the corresponding service.
|
||||||
|
#. This service should export a reactive value (the number of clicks) and a few functions to update it:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
const state = reactive({ clicks: 0 });
|
||||||
|
...
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
increment(inc) {
|
||||||
|
state.clicks += inc
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#. Access the state in both the systray item and the client action (don't forget to `useState` it). Modify
|
||||||
|
the systray item to remove its own local state and use it. Also, you can remove the `+10 clicks` button.
|
||||||
|
#. Display the state in the client action, and add a `+10` clicks button in it.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/increment_button.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- :ref:`Short explanation on services <tutorials/discover_js_framework/services>`
|
||||||
|
|
||||||
|
5. Use a custom hook
|
||||||
|
====================
|
||||||
|
|
||||||
|
Right now, every part of the code that will need to use our clicker service will have to import `useService` and
|
||||||
|
`useState`. Since it is quite common, let us use a custom hook. It is also useful to put more emphasis on the
|
||||||
|
`clicker` part, and less emphasis on the `service` part.
|
||||||
|
|
||||||
|
#. Export a `useClicker` hook.
|
||||||
|
#. Update all current uses of the clicker service to the new hook:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
this.clicker = useClicker();
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Documentation on hooks: <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md>`_
|
||||||
|
|
||||||
|
6. Humanize the displayed value
|
||||||
|
===============================
|
||||||
|
|
||||||
|
We will in the future display large numbers, so let us get ready for that. There is a `humanNumber` function that
|
||||||
|
format numbers in a easier to comprehend way: for example, `1234` could be formatted as `1.2k`
|
||||||
|
|
||||||
|
#. Use it to display our counters (both in the systray item and the client action).
|
||||||
|
#. Create a `ClickValue` component that display the value.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Owl allows component that contains just text nodes!
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/humanized_number.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `definition of humanNumber function <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/utils/numbers.js#L119>`_
|
||||||
|
|
||||||
|
7. Add a tooltip in `ClickValue` component
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
With the `humanNumber` function, we actually lost some precision on our interface. Let us display the real number
|
||||||
|
as a tooltip.
|
||||||
|
|
||||||
|
#. Tooltip needs an html element. Change the `ClickValue` to wrap the value in a `<span/>` element
|
||||||
|
#. Add a dynamic `data-tooltip` attribute to display the exact value.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/humanized_tooltip.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Documentation in the tooltip service <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/tooltip/tooltip_service.js#L17>`_
|
||||||
|
|
||||||
|
8. Buy ClickBots
|
||||||
|
================
|
||||||
|
|
||||||
|
Let us make our game even more interesting: once a player get to 1000 clicks for the first time, the game
|
||||||
|
should unlock a new feature: the player can buy robots for 1000 clicks. These robots will generate 10 clicks
|
||||||
|
every 10 seconds.
|
||||||
|
|
||||||
|
#. Add a `level` number to our state. This is a number that will be incremented at some milestones, and
|
||||||
|
open new features
|
||||||
|
#. Add a `clickBots` number to our state. It represents the number of robots that have been purchased.
|
||||||
|
#. Modify the client action to display the number of click bots (only if `level >= 1`), with a `Buy`
|
||||||
|
button that is enabled if `clicks >= 1000`. The `Buy` button should increment the number of clickbots by 1.
|
||||||
|
#. Set a 10s interval in the service that will increment the number of clicks by `10*clickBots`.
|
||||||
|
#. Make sure the Buy button is disabled if the player does not have enough clicks.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/clickbot.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
9. Refactor to a class model
|
||||||
|
============================
|
||||||
|
|
||||||
|
The current code is written in a somewhat functional style. But to do so, we have to export the state and all its
|
||||||
|
update functions in our clicker object. As this project grows, this may become more and more complex. To make it
|
||||||
|
simpler, let us split our business logic out of our service and into a class.
|
||||||
|
|
||||||
|
#. Create a `clicker_model` file that exports a reactive class. Move all the state and update functions from
|
||||||
|
the service into the model.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
You can extends the ClickerModel with the `Reactive` class from
|
||||||
|
:file:`@web/core/utils/reactive`. The `Reactive` class wrap the model into a reactive proxy.
|
||||||
|
|
||||||
|
#. Rewrite the clicker service to instantiate and export the clicker model class.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Example of subclassing Reactive <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/model/relational_model/datapoint.js#L32>`_
|
||||||
|
|
||||||
|
10. Notify when a milestone is reached
|
||||||
|
======================================
|
||||||
|
|
||||||
|
There is not much feedback that something changed when we reached 1k clicks. Let us use the `effect` service
|
||||||
|
to communicate that information clearly. The problem is that our click model does not have access to services.
|
||||||
|
Also, we want to keep as much as possible the UI concern out of the model. So, we can explore a new strategy
|
||||||
|
for communication: event buses.
|
||||||
|
|
||||||
|
#. Update the clicker model to instantiate a bus, and to trigger a `MILESTONE_1k` event when we reach 1000 clicks
|
||||||
|
for the first time.
|
||||||
|
#. Change the clicker service to listen to the same event on the model bus.
|
||||||
|
#. When that happens, use the `effect` service to display a rainbow man.
|
||||||
|
#. Add some text to explain that the user can now buy clickbots.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/milestone.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Owl documentation on event bus <https://github.com/odoo/owl/blob/master/doc/reference/utils.md#eventbus>`_
|
||||||
|
- :ref:`Documentation on effect service <frontend/services/effect>`
|
||||||
|
|
||||||
|
11. Add BigBots
|
||||||
|
===============
|
||||||
|
|
||||||
|
Clearly, we need a way to provide the player with more choices. Let us add a new type of clickbot: `BigBots`,
|
||||||
|
which are just more powerful: they provide with 100 clicks each 10s, but they cost 5000 clicks
|
||||||
|
|
||||||
|
#. increment `level` when it gets to 5k (so it should be 2)
|
||||||
|
#. Update the state to keep track of bigbots
|
||||||
|
#. bigbots should be available at `level >=2`
|
||||||
|
#. Display the corresponding information in the client action
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
If you need to use `<` or `>` in a template as a javascript expression, be careful since it might class with
|
||||||
|
the xml parser. To solve that, you can use one of the special aliases: `gt, gte, lt` or `lte`. See the
|
||||||
|
`Owl documentation page on template expressions <https://github.com/odoo/owl/blob/master/doc/reference/templates.md#expression-evaluation>`_.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/bigbot.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
12. Add a new type of resource: power
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Now, to add another scaling point, let us add a new type of resource: a power multiplier. This is a number
|
||||||
|
that can be increased at `level >= 3`, and multiplies the action of the bots (so, instead of providing
|
||||||
|
one click, clickbots now provide us with `multiplier` clicks).
|
||||||
|
|
||||||
|
#. increment `level` when it gets to 100k (so it should be 3).
|
||||||
|
#. update the state to keep track of the power (initial value is 1).
|
||||||
|
#. change bots to use that number as a multiplier.
|
||||||
|
#. Update the user interface to display and let the user purchase a new power level (costs: 50k).
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/bigbot.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
13. Define some random rewards
|
||||||
|
==============================
|
||||||
|
|
||||||
|
We want the user to obtain sometimes bonuses, to reward using Odoo.
|
||||||
|
|
||||||
|
#. Define a list of rewards in `click_rewards.js`. A reward is an object with:
|
||||||
|
- a `description` string.
|
||||||
|
- a `apply` function that take the game state in argument and can modify it.
|
||||||
|
- a `minLevel` number (optional) that describes at which unlock level the bonus is available.
|
||||||
|
- a `maxLevel` number (optional) that describes at which unlock level a bonus is no longer available.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
export const rewards = [
|
||||||
|
{
|
||||||
|
description: "Get 1 click bot",
|
||||||
|
apply(clicker) {
|
||||||
|
clicker.increment(1);
|
||||||
|
},
|
||||||
|
maxLevel: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Get 10 click bot",
|
||||||
|
apply(clicker) {
|
||||||
|
clicker.increment(10);
|
||||||
|
},
|
||||||
|
minLevel: 3,
|
||||||
|
maxLevel: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Increase bot power!",
|
||||||
|
apply(clicker) {
|
||||||
|
clicker.multipler += 1;
|
||||||
|
},
|
||||||
|
minLevel: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
You can add whatever you want to that list!
|
||||||
|
|
||||||
|
#. Define a function `getReward` that will select a random reward from the list of rewards that matches
|
||||||
|
the current unlock level.
|
||||||
|
#. Extract the code that choose randomly in an array in a function `choose` that you can move to another `utils.js` file.
|
||||||
|
|
||||||
|
14. Provide a reward when opening a form view
|
||||||
|
=============================================
|
||||||
|
|
||||||
|
#. Patch the form controller. Each time a form controller is created, it should randomly decides (1% chance)
|
||||||
|
if a reward should be given.
|
||||||
|
#. If the answer is yes, call a method `getReward` on the model.
|
||||||
|
#. That method should choose a reward, send a sticky notification, with a button `Collect` that will
|
||||||
|
then apply the reward, and finally, it should open the `clicker` client action.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/reward.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- :ref:`Documentation on patching a class <frontend/patching_class>`
|
||||||
|
- `Definition of patch function <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/utils/patch.js#L71>`_
|
||||||
|
- `Example of patching a class <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/pos_mercury/static/src/app/screens/receipt_screen/receipt_screen.js#L6>`_
|
||||||
|
|
||||||
|
15. Add commands in command palette
|
||||||
|
===================================
|
||||||
|
|
||||||
|
#. Add a command `Open Clicker Game` to the command palette.
|
||||||
|
#. Add another command: `Buy 1 click bot`.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/command_palette.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Example of use of command provider registry <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/debug/debug_providers.js#L10>`_
|
||||||
|
|
||||||
|
16. Add yet another resource: trees
|
||||||
|
===================================
|
||||||
|
|
||||||
|
It is now time to introduce a completely new type of resources. Here is one that should not be too controversial: trees.
|
||||||
|
We will now allow the user to plant (collect?) fruit trees. A tree costs 1 million clicks, but it will provide us with
|
||||||
|
fruits (either pears or cherries).
|
||||||
|
|
||||||
|
#. Update the state to keep track of various types of trees: pear/cherries, and their fruits.
|
||||||
|
#. Add a function that computes the total number of trees and fruits.
|
||||||
|
#. Define a new unlock level at `clicks >= 1 000 000`.
|
||||||
|
#. Update the client user interface to display the number of trees and fruits, and also, to buy trees.
|
||||||
|
#. Increment the fruit number by 1 for each tree every 30s.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/trees.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
17. Use a dropdown menu for the systray item
|
||||||
|
============================================
|
||||||
|
|
||||||
|
Our game starts to become interesting. But for now, the systray only displays the total number of clicks. We
|
||||||
|
want to see more information: the total number of trees and fruits. Also, it would be useful to have a quick
|
||||||
|
access to some commands and some more information. Let us use a dropdown menu!
|
||||||
|
|
||||||
|
#. Replace the systray item by a dropdown menu.
|
||||||
|
#. It should display the numbers of clicks, trees, and fruits, each with a nice icon.
|
||||||
|
#. Clicking on it should open a dropdown menu that displays more detailed information: each types of trees
|
||||||
|
and fruits.
|
||||||
|
#. Also, a few dropdown items with some commands: open the clicker game, buy a clickbot, ...
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/dropdown.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
18. Use a Notebook component
|
||||||
|
============================
|
||||||
|
|
||||||
|
We now keep track of a lot more information. Let us improve our client interface by organizing the information
|
||||||
|
and features in various tabs, with the `Notebook` component:
|
||||||
|
|
||||||
|
#. Use the `Notebook` component.
|
||||||
|
#. All `click` content should be displayed in one tab.
|
||||||
|
#. All `tree/fruits` content should be displayed in another tab.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/notebook.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- :ref:`Odoo: Documentation on Notebook component <frontend/owl/notebook>`
|
||||||
|
- `Owl: Documentation on slots <https://github.com/odoo/owl/blob/master/doc/reference/slots.md>`_
|
||||||
|
- `Tests of Notebook component <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/tests/core/notebook_tests.js#L27>`_
|
||||||
|
|
||||||
|
19. Persist the game state
|
||||||
|
===========================
|
||||||
|
|
||||||
|
You certainly noticed a big flaw in our game: it is transient. The game state is lost each time the user closes the
|
||||||
|
browser tab. Let us fix that. We will use the local storage to persist the state.
|
||||||
|
|
||||||
|
#. Import `browser` from :file:`@web/core/browser/browser` to access the localstorage.
|
||||||
|
#. Serialize the state every 10s (in the same interval code) and store it on the local storage.
|
||||||
|
#. When the `clicker` service is started, it should load the state from the local storage (if any), or initialize itself
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
20. Introduce state migration system
|
||||||
|
====================================
|
||||||
|
|
||||||
|
Once you persist state somewhere, a new problem arises: what happens when you update your code, so the shape of the state
|
||||||
|
changes, and the user opens its browser with a state that was created with an old version? Welcome to the world of
|
||||||
|
migration issues!
|
||||||
|
|
||||||
|
It is probably wise to tackle the problem early. What we will do here is add a version number to the state, and introduce
|
||||||
|
a system to automatically update the states if it is not up to date.
|
||||||
|
|
||||||
|
#. Add a version number to the state.
|
||||||
|
#. Define an (empty) list of migrations. A migration is an object with a `fromVersion` number, a `toVersion` number, and a `apply` function.
|
||||||
|
#. Whenever the code loads the state from the local storage, it should check the version number. If the state is not
|
||||||
|
uptodate, it should apply all necessary migrations.
|
||||||
|
|
||||||
|
21. Add another type of trees
|
||||||
|
=============================
|
||||||
|
|
||||||
|
To test our migration system, let us add a new type of trees: peaches.
|
||||||
|
|
||||||
|
#. Add `peach` trees.
|
||||||
|
#. Increment the state version number.
|
||||||
|
#. Define a migration.
|
||||||
|
|
||||||
|
.. image:: 01_build_clicker_game/peach_tree.png
|
||||||
|
:align: center
|
||||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -1,403 +0,0 @@
|
|||||||
===========================
|
|
||||||
Chapter 1: Fields and Views
|
|
||||||
===========================
|
|
||||||
|
|
||||||
In the previous chapter, we learned a range of skills, including how to create and use services,
|
|
||||||
work with the Layout component, make the dashboard translatable, and lazy load a JavaScript library
|
|
||||||
like Chart.js. Now, let's learn how to create new fields and views.
|
|
||||||
|
|
||||||
.. spoiler:: Solutions
|
|
||||||
|
|
||||||
The solutions for each exercise of the chapter are hosted on the
|
|
||||||
`official Odoo tutorials repository
|
|
||||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_tshirt>`_. It
|
|
||||||
is recommended not to look at them before trying the exercises.
|
|
||||||
|
|
||||||
Fields and views are among the most important concepts in the Odoo user interface. They are key to
|
|
||||||
many important user interactions, and should therefore work perfectly. In the context of the
|
|
||||||
JavaScript framework, fields are components specialized for visualizing/editing a specific field for
|
|
||||||
a given record. For example, a (Python) model may define a char field, which will be represented by
|
|
||||||
a field component `CharField`.
|
|
||||||
|
|
||||||
A field component is basically just a component registered in the `fields` :ref:`registry
|
|
||||||
<frontend/registries>`. The field component may define some additional static keys (metadata), such
|
|
||||||
as `displayName` or `supportedTypes`, and the most important one: `extractProps`, which prepare the
|
|
||||||
base props received by the `CharField`.
|
|
||||||
|
|
||||||
Example: a simple field
|
|
||||||
=======================
|
|
||||||
|
|
||||||
Let us discuss a simplified implementation of a `CharField`. First, here is the template:
|
|
||||||
|
|
||||||
.. code-block:: xml
|
|
||||||
|
|
||||||
<t t-name="web.CharField">
|
|
||||||
<t t-if="props.readonly">
|
|
||||||
<span t-esc="formattedValue" />
|
|
||||||
</t>
|
|
||||||
<t t-else="">
|
|
||||||
<input
|
|
||||||
class="o_input"
|
|
||||||
t-att-type="props.isPassword ? 'password' : 'text'"
|
|
||||||
t-att-placeholder="props.placeholder"
|
|
||||||
t-on-change="updateValue"
|
|
||||||
/>
|
|
||||||
</t>
|
|
||||||
</t>
|
|
||||||
|
|
||||||
It features a readonly mode and an edit mode, which is an input with a few attributes. Now, here
|
|
||||||
is the JavaScript code:
|
|
||||||
|
|
||||||
.. code-block:: js
|
|
||||||
|
|
||||||
export class CharField extends Component {
|
|
||||||
get formattedValue() {
|
|
||||||
return formatChar(this.props.value, { isPassword: this.props.isPassword });
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(ev) {
|
|
||||||
let value = ev.target.value;
|
|
||||||
if (this.props.shouldTrim) {
|
|
||||||
value = value.trim();
|
|
||||||
}
|
|
||||||
this.props.update(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CharField.template = "web.CharField";
|
|
||||||
CharField.displayName = _lt("Text");
|
|
||||||
CharField.supportedTypes = ["char"];
|
|
||||||
|
|
||||||
CharField.extractProps = ({ attrs, field }) => {
|
|
||||||
return {
|
|
||||||
shouldTrim: field.trim && !archParseBoolean(attrs.password),
|
|
||||||
maxLength: field.size,
|
|
||||||
isPassword: archParseBoolean(attrs.password),
|
|
||||||
placeholder: attrs.placeholder,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.category("fields").add("char", CharField);
|
|
||||||
|
|
||||||
There are a few important things to notice:
|
|
||||||
|
|
||||||
- The `CharField` receives its (raw) value in props. It needs to format it before displaying it.
|
|
||||||
- It receives an `update` function in its props, which is used by the field to notify the owner of
|
|
||||||
the state that the value of this field has been changed. Note that the field does not (and should
|
|
||||||
not) maintain a local state with its value. Whenever the change has been applied, it will come
|
|
||||||
back (possibly after an onchange) by the way of the props.
|
|
||||||
- It defines an `extractProps` function. This is a step that translates generic standard props,
|
|
||||||
specific to a view, to specialized props, useful to the component. This allows the component to
|
|
||||||
have a better API, and may make it so that it is reusable.
|
|
||||||
|
|
||||||
Fields have to be registered in the `fields` registry. Once it's done, they can be used in some
|
|
||||||
views (namely: `form`, `list`, `kanban`) by using the `widget` attribute.
|
|
||||||
|
|
||||||
.. example::
|
|
||||||
|
|
||||||
.. code-block:: xml
|
|
||||||
|
|
||||||
<field name="preview_moves" widget="account_resequence_widget"/>
|
|
||||||
|
|
||||||
.. _tutorials/master_odoo_web_framework/image_preview_field:
|
|
||||||
|
|
||||||
1. An `image_preview` field
|
|
||||||
===========================
|
|
||||||
|
|
||||||
Each new order on the website will be created as an `awesome_tshirt.order`. This model has a
|
|
||||||
`image_url` field (of type `char`), which is currently only visible as a string. We want to be able
|
|
||||||
to see the image itself in the form view.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Create a new `ImagePreview` component and register it in the proper :ref:`registry
|
|
||||||
<frontend/registries>`. Use the `CharField` component in your template. You can use `t-props
|
|
||||||
<{OWL_PATH}/doc/reference/props.md#dynamic-props>`_ to pass props received by `ImagePreview`
|
|
||||||
to `CharField`. Update the arch of the form view to use your new field by setting the `widget`
|
|
||||||
attribute.
|
|
||||||
#. Change the code of the `ImagePreview` component so that the image is displayed below the URL.
|
|
||||||
#. When the field is readonly, only the image should be displayed and the URL should be hidden.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
It is possible to solve this exercise by inheriting `CharField`, but the goal of this exercise is
|
|
||||||
to create a field from scratch.
|
|
||||||
|
|
||||||
.. image:: 01_fields_and_views/image_field.png
|
|
||||||
:align: center
|
|
||||||
:scale: 50%
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
|
|
||||||
`Code: CharField <{GITHUB_PATH}/addons/web/static/src/views/fields/char/char_field.js>`_
|
|
||||||
|
|
||||||
2. Improving the `image_preview` field
|
|
||||||
======================================
|
|
||||||
|
|
||||||
We want to improve the field of the previous task to help the staff recognize orders for which some
|
|
||||||
action should be done.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
Display a warning "MISSING TSHIRT DESIGN" in red if there is no image URL specified on the order.
|
|
||||||
|
|
||||||
.. image:: 01_fields_and_views/missing_image.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
3. Customizing a field component
|
|
||||||
================================
|
|
||||||
|
|
||||||
Let's see how to use inheritance to extend an existing component.
|
|
||||||
|
|
||||||
There is a `is_late`, readonly, boolean field on the order model. That would be useful information
|
|
||||||
to see on the list/kanban/view. Then, let us say that we want to add a red word "Late!" next to it
|
|
||||||
whenever it is set to `true`.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Create a new `LateOrderBoolean` field inheriting from `BooleanField`. The template of
|
|
||||||
`LateOrderBoolean` can also :ref:`inherit <reference/qweb/template_inheritance>` from the
|
|
||||||
`BooleanField` template.
|
|
||||||
#. Use it in the list/kanban/form view.
|
|
||||||
#. Modify it to add a red `Late` next to it, as requested.
|
|
||||||
|
|
||||||
.. image:: 01_fields_and_views/late_field.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
- `Example: A field inheriting another
|
|
||||||
<{GITHUB_PATH}/addons/account/static/src/components/account_type_selection>`_
|
|
||||||
- :ref:`Documentation on xpath <reference/view_records/inheritance>`
|
|
||||||
|
|
||||||
4. Message for some customers
|
|
||||||
=============================
|
|
||||||
|
|
||||||
Odoo form views support a `widget` API, which is like a field, but more generic. It is useful to
|
|
||||||
insert arbitrary components in the form view. Let us see how we can use it.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
For a super efficient workflow, we would like to display an alert block with specific messages
|
|
||||||
depending on some conditions:
|
|
||||||
|
|
||||||
- If the `image_url` field is not set, it should display "No image".
|
|
||||||
- If the `amount` of the order is higher than 100 euros, it should display "Add promotional
|
|
||||||
material".
|
|
||||||
- Make sure that your widget is updated in real time.
|
|
||||||
|
|
||||||
.. tip::
|
|
||||||
Try to evaluate `props.record` in the :guilabel:`Console` tab of your browser's dev tools.
|
|
||||||
|
|
||||||
.. image:: 01_fields_and_views/warning_widget.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
|
|
||||||
- `Example: Using the tag <widget> in a form view
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/calendar/views/calendar_views.xml#L197>`_
|
|
||||||
- `Example: Implementation of a widget
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days>`_
|
|
||||||
|
|
||||||
5. Use `markup`
|
|
||||||
===============
|
|
||||||
|
|
||||||
Let’s see how we can display raw HTML in a template. The `t-out` directive can be used for that
|
|
||||||
propose. Indeed, `it generally acts like t-esc, unless the data has been marked explicitly with a
|
|
||||||
markup function <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_. In that case, its value
|
|
||||||
is injected as HTML.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Modify the previous exercise to put the `image` and `material` words in bold.
|
|
||||||
#. The warnings should be markuped, and the template should be modified to use `t-out`.
|
|
||||||
#. Import the `markup` function from Owl and, for each message, replace it with a call of the
|
|
||||||
function with the message passed as argument.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
This is an example of a safe use of `t-out`, since the string is static.
|
|
||||||
|
|
||||||
.. image:: 01_fields_and_views/warning_widget2.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
6. Add buttons in the control panel
|
|
||||||
===================================
|
|
||||||
|
|
||||||
Views are among the most important components in Odoo: they allow users to interact with their
|
|
||||||
data. Let us discuss how Odoo views are designed.
|
|
||||||
|
|
||||||
The power of Odoo views is that they declare how a particular screen should work with an XML
|
|
||||||
document (usually named `arch`, short for architecture). This description can be extended/modified
|
|
||||||
by xpaths serverside. Then, the browser loads that document, parses it (fancy word to say that it
|
|
||||||
extracts the useful information), and then represents the data accordingly.
|
|
||||||
|
|
||||||
.. example::
|
|
||||||
|
|
||||||
The `arch` document is view specific. Here is how a `graph` view or a `calendar` view could be
|
|
||||||
defined:
|
|
||||||
|
|
||||||
.. code-block:: xml
|
|
||||||
|
|
||||||
<graph string="Invoices Analysis" type="line" sample="1">
|
|
||||||
<field name="product_categ_id"/>
|
|
||||||
<field name="price_subtotal" type="measure"/>
|
|
||||||
</graph>
|
|
||||||
|
|
||||||
<calendar string="Leads Generation" create="0" mode="month" date_start="activity_date_deadline" color="user_id" hide_time="true" event_limit="5">
|
|
||||||
<field name="expected_revenue"/>
|
|
||||||
<field name="partner_id" avatar_field="avatar_128"/>
|
|
||||||
<field name="user_id" filters="1" invisible="1"/>
|
|
||||||
</calendar>
|
|
||||||
|
|
||||||
A view is defined in the view registry by an object with a few specific keys.
|
|
||||||
|
|
||||||
- `type`: The (base) type of a view (for example, `form`, `list`...).
|
|
||||||
- `display_name`: What should be displayed in the tooltip in the view switcher.
|
|
||||||
- `icon`: Which icon to use in the view switcher.
|
|
||||||
- `multiRecord`: Whether the view is supposed to manage a single record or a set of records.
|
|
||||||
- `Controller`: The component that will be used to render the view (the most important information).
|
|
||||||
|
|
||||||
.. example::
|
|
||||||
|
|
||||||
Here is a minimal `Hello` view, which does not display anything:
|
|
||||||
|
|
||||||
.. code-block:: js
|
|
||||||
|
|
||||||
/** @odoo-module */
|
|
||||||
|
|
||||||
import { registry } from "@web/core/registry";
|
|
||||||
|
|
||||||
export const helloView = {
|
|
||||||
type: "hello",
|
|
||||||
display_name: "Hello",
|
|
||||||
icon: "fa fa-picture-o",
|
|
||||||
multiRecord: true,
|
|
||||||
Controller: Component,
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.category("views").add("hello", helloView);
|
|
||||||
|
|
||||||
Most (or all?) Odoo views share a common architecture:
|
|
||||||
|
|
||||||
.. ```mermaid
|
|
||||||
.. graph TD
|
|
||||||
.. subgraph View description
|
|
||||||
.. V(props function)
|
|
||||||
.. G(generic props)
|
|
||||||
.. X(arch parser)
|
|
||||||
.. S(others ...)
|
|
||||||
.. V --> X
|
|
||||||
.. V --> S
|
|
||||||
.. V --> G
|
|
||||||
.. end
|
|
||||||
.. A[Controller]
|
|
||||||
.. L[Layout]
|
|
||||||
.. B[Renderer]
|
|
||||||
.. C[Model]
|
|
||||||
|
|
||||||
.. V == compute props ==> A
|
|
||||||
.. A --- L
|
|
||||||
.. L --- B
|
|
||||||
.. A --- C
|
|
||||||
.. ```
|
|
||||||
|
|
||||||
.. image:: 01_fields_and_views/view_architecture.svg
|
|
||||||
:align: center
|
|
||||||
:width: 75%
|
|
||||||
:class: o-no-modal
|
|
||||||
|
|
||||||
The view description can define a `props` function, which receives the standard props, and computes
|
|
||||||
the base props of the concrete view. The `props` function is executed only once, and can be thought
|
|
||||||
of as being some kind of factory. It is useful to parse the `arch` XML document, and to allow the
|
|
||||||
view to be parameterized (for example, it can return a Renderer component that will be used as
|
|
||||||
Renderer). Then, it is easy to customize the specific renderer used by a sub view.
|
|
||||||
|
|
||||||
The props will be extended before being given to the Controller. In particular, the search props
|
|
||||||
(domain/context/groupby) will be added.
|
|
||||||
|
|
||||||
Finally, the root component, commonly called the `Controller`, coordinates everything. It uses the
|
|
||||||
generic `Layout` component (to add a control panel), instantiates a `Model`, and uses a `Renderer`
|
|
||||||
component in the `Layout` default slot. The `Model` is tasked with loading and updating data, and
|
|
||||||
the `Renderer` is supposed to handle all rendering work, along with all user interactions.
|
|
||||||
|
|
||||||
In practice, once the t-shirt order is printed, we need to print a label to put on the package. To
|
|
||||||
do that, let us add a button in the order's form view's control panel, which will call a model
|
|
||||||
method.
|
|
||||||
|
|
||||||
There is a service dedicated to calling models methods: `orm_service`, located in
|
|
||||||
`core/orm_service.js`. It provides a way to call common model methods, as well as a generic
|
|
||||||
`call(model, method, args, kwargs)` method.
|
|
||||||
|
|
||||||
.. example::
|
|
||||||
|
|
||||||
.. code-block:: js
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
this.orm = useService("orm");
|
|
||||||
onWillStart(async () => {
|
|
||||||
// will read the fields 'id' and 'descr' from the record with id=3 of my.model
|
|
||||||
const data = await this.orm.read("my.model", [3], ["id", "descr"]);
|
|
||||||
// ...
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Create a customized form view extending the `web` form view and register it as
|
|
||||||
`awesome_tshirt.order_form_view`.
|
|
||||||
#. Add a `js_class="awesome_tshirt.order_form_view"` attribute to the arch of the form view so
|
|
||||||
that Odoo will load it.
|
|
||||||
#. Create a new template inheriting from the form controller template and add a "Print Label"
|
|
||||||
button after the "New" button.
|
|
||||||
#. Clicking on this button should call the method `print_label` from the model
|
|
||||||
`awesome_tshirt.order` with the proper id.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
`print_label` is a mock method; it only displays a message in the logs.
|
|
||||||
|
|
||||||
#. The button should not be disabled if the current order is in `create` mode (i.e., it does not
|
|
||||||
exist yet).
|
|
||||||
|
|
||||||
.. tip::
|
|
||||||
Log `this.props.resId` and `this.model.root.resId` and compare the two values before and
|
|
||||||
after entering `create` mode.
|
|
||||||
|
|
||||||
#. The button should be displayed as a primary button if the customer is properly set and if the
|
|
||||||
task stage is `printed`. Otherwise, it should be displayed as a secondary button.
|
|
||||||
#. Bonus point: clicking twice on the button should not trigger 2 RPCs.
|
|
||||||
|
|
||||||
.. image:: 01_fields_and_views/form_button.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
- `Example: Extending a view (JS)
|
|
||||||
<{GITHUB_PATH}/addons/mass_mailing/static/src/views/mailing_contact_view_kanban.js>`_
|
|
||||||
- `Example: Extending a view (XML)
|
|
||||||
<{GITHUB_PATH}/addons/mass_mailing/static/src/views/mass_mailing_views.xml>`_
|
|
||||||
- `Example: Using a js_class attribute
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/mass_mailing/views/mailing_contact_views.xml#L44>`_
|
|
||||||
- `Code: orm service <{GITHUB_PATH}/addons/web/static/src/core/orm_service.js>`_
|
|
||||||
- `Example: Using the orm service
|
|
||||||
<{GITHUB_PATH}/addons/account/static/src/components/open_move_widget/open_move_widget.js>`_
|
|
||||||
- `Code: useDebounced hook
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/core/utils/timing.js#L117>`_
|
|
||||||
|
|
||||||
7. Auto-reload the kanban view
|
|
||||||
==============================
|
|
||||||
|
|
||||||
Bafien is upset: he wants to see the kanban view of the tshirt orders on his external monitor, but
|
|
||||||
the view needs to be up-to-date. He is tired of clicking on the :guilabel:`refresh` icon every 30s,
|
|
||||||
so he tasked you to find a way to do it automatically.
|
|
||||||
|
|
||||||
Just like the previous exercise, that kind of customization requires creating a new JavaScript view.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Extend the kanban view/controller to reload its data every minute.
|
|
||||||
#. Register it in the view registry, under `awesome_tshirt.autoreloadkanban`.
|
|
||||||
#. Use it in the arch of the kanban view (with the `js_class` attribute).
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
If you use `setInterval` or something similar, make sure that it is properly canceled when your
|
|
||||||
component is unmounted. Otherwise, you will introduce a memory leak.
|
|
||||||
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 453 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,462 @@
|
|||||||
|
================================
|
||||||
|
Chapter 2: Create a Gallery View
|
||||||
|
================================
|
||||||
|
|
||||||
|
Let us see how one can create a new view, completely from scratch. In a way, it is not very
|
||||||
|
difficult to do, but there are no really good resources on how to do it. Note that most situations
|
||||||
|
should be solved by either customizing an existing view, or with a client action.
|
||||||
|
|
||||||
|
For this exercise, let's assume that we want to create a `gallery` view, which is a view that lets
|
||||||
|
us represent a set of records with an image field.
|
||||||
|
|
||||||
|
The problem could certainly be solved with a kanban view, but this means that it is not possible to
|
||||||
|
have our normal kanban view and the gallery view in the same action.
|
||||||
|
|
||||||
|
Let us make a gallery view. Each gallery view will be defined by an `image_field` attribute in its
|
||||||
|
arch:
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<gallery image_field="some_field"/>
|
||||||
|
|
||||||
|
To complete the tasks in this chapter, you will need to install the awesome_gallery addon. This
|
||||||
|
addon includes the necessary server files to add a new view.
|
||||||
|
|
||||||
|
.. admonition:: Goal
|
||||||
|
|
||||||
|
.. image:: 02_create_gallery_view/overview.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. spoiler:: Solutions
|
||||||
|
|
||||||
|
The solutions for each exercise of the chapter are hosted on the
|
||||||
|
`official Odoo tutorials repository
|
||||||
|
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-master-odoo-web-framework-solutions/awesome_gallery>`_.
|
||||||
|
|
||||||
|
1. Make a hello world view
|
||||||
|
==========================
|
||||||
|
|
||||||
|
First step is to create a JavaScript implementation with a simple component.
|
||||||
|
|
||||||
|
#. Create the `gallery_view.js` , `gallery_controller.js` and `gallery_controller.xml` files in
|
||||||
|
`static/src`.
|
||||||
|
#. Implement a simple hello world component in `gallery_controller.js`.
|
||||||
|
#. In `gallery_view.js`, import the controller, create a view object, and register it in the
|
||||||
|
view registry under the name `gallery`.
|
||||||
|
|
||||||
|
.. example::
|
||||||
|
Here is an example on how to define a view object:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { MyController } from "./my_controller";
|
||||||
|
|
||||||
|
export const myView = {
|
||||||
|
type: "my_view",
|
||||||
|
display_name: "MyView",
|
||||||
|
icon: "oi oi-view-list",
|
||||||
|
multiRecord: true,
|
||||||
|
Controller: MyController,
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("views").add("my_controller", myView);
|
||||||
|
|
||||||
|
#. Add `gallery` as one of the view type in the `contacts.action_contacts` action.
|
||||||
|
#. Make sure that you can see your hello world component when switching to the gallery view.
|
||||||
|
|
||||||
|
.. image:: 02_create_gallery_view/view_button.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. image:: 02_create_gallery_view/new_view.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
2. Use the Layout component
|
||||||
|
===========================
|
||||||
|
|
||||||
|
So far, our gallery view does not look like a standard view. Let's use the `Layout` component to
|
||||||
|
have the standard features like other views.
|
||||||
|
|
||||||
|
#. Import the `Layout` component and add it to the `components` of `GalleryController`.
|
||||||
|
#. Update the template to use `Layout`. It needs a `display` prop, which can be found in
|
||||||
|
`props.display`.
|
||||||
|
|
||||||
|
.. image:: 02_create_gallery_view/layout.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
3. Parse the arch
|
||||||
|
=================
|
||||||
|
|
||||||
|
For now, our gallery view does not do much. Let's start by reading the information contained in the
|
||||||
|
arch of the view.
|
||||||
|
|
||||||
|
The process of parsing an arch is usually done with a `ArchParser`, specific to each view. It
|
||||||
|
inherits from a generic `XMLParser` class.
|
||||||
|
|
||||||
|
.. example::
|
||||||
|
|
||||||
|
Here is an example of what an ArchParser might look like:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
export class MyCustomArchParser {
|
||||||
|
parse(xmlDoc) {
|
||||||
|
const myAttribute = xmlDoc.getAttribute("my_attribute")
|
||||||
|
return {
|
||||||
|
myAttribute,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#. Create the `ArchParser` class in its own file.
|
||||||
|
#. Use it to read the `image_field` information.
|
||||||
|
#. Update the `gallery` view code to add it to the props received by the controller.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
It is probably a little overkill to do it like that, since we basically only need to read one
|
||||||
|
attribute from the arch, but it is a design that is used by every other odoo views, since it
|
||||||
|
lets us extract some upfront processing out of the controller.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
`Example: The graph arch parser
|
||||||
|
<{GITHUB_PATH}/addons/web/static/src/views/graph/graph_arch_parser.js>`_
|
||||||
|
|
||||||
|
4. Load some data
|
||||||
|
=================
|
||||||
|
|
||||||
|
Let us now get some real data from the server. For that we must use `webSearchRead` from the orm
|
||||||
|
service.
|
||||||
|
|
||||||
|
.. example::
|
||||||
|
|
||||||
|
Here is an example of a `webSearchRead` to get the records from a model:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
const { length, records } = this.orm.webSearchRead(this.resModel, domain, {
|
||||||
|
specification: {
|
||||||
|
[this.fieldToFetch]: {},
|
||||||
|
[this.secondFieldToFetch]: {},
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
bin_size: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
#. Add a :code:`loadImages(domain) {...}` method to the `GalleryController`. It should perform a
|
||||||
|
`webSearchRead` call from the orm service to fetch records corresponding to the domain, and
|
||||||
|
use `imageField` received in props.
|
||||||
|
#. If you didn't include `bin_size` in the context of the call, you will receive the image field
|
||||||
|
encoded in base64. Make sure to put `bin_size` in the context to receive the size of the image
|
||||||
|
field. We will display the image later.
|
||||||
|
#. Modify the `setup` code to call that method in the `onWillStart` and `onWillUpdateProps`
|
||||||
|
hooks.
|
||||||
|
#. Modify the template to display the id and the size of each image inside the default slot of
|
||||||
|
the `Layout` component.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
The loading data code will be moved into a proper model in a next exercise.
|
||||||
|
|
||||||
|
.. image:: 02_create_gallery_view/gallery_data.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
5. Solve the concurrency problem
|
||||||
|
================================
|
||||||
|
|
||||||
|
For now, our code is not concurrency proof. If one changes the domain twice, it will trigger the
|
||||||
|
`loadImages(domain)` twice. We have thus two requests that can arrive at different time depending
|
||||||
|
on different factors. Receiving the response for the first request after receiving the response
|
||||||
|
for the second request will lead to an inconsistent state.
|
||||||
|
|
||||||
|
The `KeepLast` primitive from Odoo solves this problem, it manages a list of tasks, and only
|
||||||
|
keeps the last task active.
|
||||||
|
|
||||||
|
#. Import `KeepLast` from :file:`@web/core/utils/concurrency`.
|
||||||
|
#. Instanciate a `KeepLast` object in the model.
|
||||||
|
#. Add the `webSearchRead` call in the `KeepLast` so that only the last call is resolved.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
`Example: usage of KeepLast <https://github.com/odoo/odoo/blob/ebf646b44f747567ff8788c884f7f18dffd453e0/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js#L164>`_
|
||||||
|
|
||||||
|
6. Reorganize code
|
||||||
|
==================
|
||||||
|
|
||||||
|
Real views are a little bit more organized. This may be overkill in this example, but it is intended
|
||||||
|
to learn how to structure code in Odoo. Also, this will scale better with changing requirements.
|
||||||
|
|
||||||
|
#. Move all the model code in its own `GalleryModel` class.
|
||||||
|
#. Move all the rendering code in a `GalleryRenderer` component.
|
||||||
|
#. Import `GalleryModel` and `GalleryRenderer` in `GalleryController` to make it work.
|
||||||
|
|
||||||
|
7. Make the view extensible
|
||||||
|
===========================
|
||||||
|
|
||||||
|
To extends the view, one could import the gallery view object to modify it to their taste. The
|
||||||
|
problem is that for the moment, it is not possible to define a custom model or renderer because it
|
||||||
|
is hardcoded in the controller.
|
||||||
|
|
||||||
|
#. Import `GalleryModel` and `GalleryRenderer` in the gallery view file.
|
||||||
|
#. Add a `Model` and `Renderer` key to the gallery view object and assign them to `GalleryModel`
|
||||||
|
and `GalleryRenderer`. Pass `Model` and `Renderer` as props to the controller.
|
||||||
|
#. Remove the hardcoded import in the controller and get them from the props.
|
||||||
|
#. Use `t-component
|
||||||
|
<https://github.com/odoo/owl/blob/master/doc/reference/component.md#dynamic-sub-components>`_ to
|
||||||
|
have dynamic sub component.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This is how someone could now extend the gallery view by modifying the renderer:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
/** @odoo-module */
|
||||||
|
|
||||||
|
import { registry } from '@web/core/registry';
|
||||||
|
import { galleryView } from '@awesome_gallery/gallery_view';
|
||||||
|
import { GalleryRenderer } from '@awesome_gallery/gallery_renderer';
|
||||||
|
|
||||||
|
export class MyExtendedGalleryRenderer extends GalleryRenderer {
|
||||||
|
static template = "my_module.MyExtendedGalleryRenderer";
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
console.log("my gallery renderer extension");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.category("views").add("my_gallery", {
|
||||||
|
...galleryView,
|
||||||
|
Renderer: MyExtendedGalleryRenderer,
|
||||||
|
});
|
||||||
|
|
||||||
|
8. Display images
|
||||||
|
=================
|
||||||
|
|
||||||
|
Update the renderer to display images in a nice way, if the field is set. If `image_field` is
|
||||||
|
empty, display an empty box instead.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
There is a controller that allows to retrieve an image from a record. You can use this
|
||||||
|
snippet to construct the link:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
import { url } from "@web/core/utils/urls";
|
||||||
|
const url = url("/web/image", {
|
||||||
|
model: resModel,
|
||||||
|
id: image_id,
|
||||||
|
field: imageField,
|
||||||
|
});
|
||||||
|
|
||||||
|
.. image:: 02_create_gallery_view/tshirt_images.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
9. Switch to form view on click
|
||||||
|
===============================
|
||||||
|
|
||||||
|
Update the renderer to react to a click on an image and switch to a form view. You can use the
|
||||||
|
`switchView` function from the action service.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
`Code: The switchView function <https://github.com/odoo/odoo/blob/db2092d8d389fdd285f54e9b34a5a99cc9523d27/addons/web/static/src/webclient/actions/action_service.js#L1064>`_
|
||||||
|
|
||||||
|
10. Add an optional tooltip
|
||||||
|
===========================
|
||||||
|
|
||||||
|
It is useful to have some additional information on mouse hover.
|
||||||
|
|
||||||
|
#. Update the code to allow an optional additional attribute on the arch:
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<gallery image_field="some_field" tooltip_field="some_other_field"/>
|
||||||
|
|
||||||
|
#. On mouse hover, display the content of the tooltip field. It should work if the field is a
|
||||||
|
char field, a number field or a many2one field. To put a tooltip to an html element, you can
|
||||||
|
put the string in the `data-tooltip` attribute of the element.
|
||||||
|
#. Update the customer gallery view arch to add the customer as tooltip field.
|
||||||
|
|
||||||
|
.. image:: 02_create_gallery_view/image_tooltip.png
|
||||||
|
:align: center
|
||||||
|
:scale: 50%
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
`Example: usage of t-att-data-tooltip <https://github.com/odoo/odoo/blob/145fe958c212ddef9fab56a232c8b2d3db635c8e/addons/survey/static/src/views/widgets/survey_question_trigger/survey_question_trigger.xml#L8>`_
|
||||||
|
|
||||||
|
11. Add pagination
|
||||||
|
==================
|
||||||
|
|
||||||
|
Let's add a pager on the control panel and manage all the pagination like in a normal Odoo view.
|
||||||
|
|
||||||
|
.. image:: 02_create_gallery_view/pagination.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
- `Code: The usePager hook <{GITHUB_PATH}/addons/web/static/src/search/pager_hook.js>`_
|
||||||
|
- `Example: usePager in list controller <https://github.com/odoo/odoo/blob/48ef812a635f70571b395f82ffdb2969ce99da9e/addons/web/static/src/views/list/list_controller.js#L109-L128>`_
|
||||||
|
|
||||||
|
12. Validating views
|
||||||
|
=====================
|
||||||
|
|
||||||
|
We have a nice and useful view so far. But in real life, we may have issue with users incorrectly
|
||||||
|
encoding the `arch` of their Gallery view: it is currently only an unstructured piece of XML.
|
||||||
|
|
||||||
|
Let us add some validation! In Odoo, XML documents can be described with an RN file
|
||||||
|
:dfn:`(Relax NG file)`, and then validated.
|
||||||
|
|
||||||
|
#. Add an RNG file that describes the current grammar:
|
||||||
|
|
||||||
|
- A mandatory attribute `image_field`.
|
||||||
|
- An optional attribute: `tooltip_field`.
|
||||||
|
|
||||||
|
#. Add some code to make sure all views are validated against this RNG file.
|
||||||
|
#. While we are at it, let us make sure that `image_field` and `tooltip_field` are fields from
|
||||||
|
the current model.
|
||||||
|
|
||||||
|
Since validating an RNG file is not trivial, here is a snippet to help:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from odoo.loglevels import ustr
|
||||||
|
from odoo.tools import misc, view_validation
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_viewname_validator = None
|
||||||
|
|
||||||
|
@view_validation.validate('viewname')
|
||||||
|
def schema_viewname(arch, **kwargs):
|
||||||
|
""" Check the gallery view against its schema
|
||||||
|
|
||||||
|
:type arch: etree._Element
|
||||||
|
"""
|
||||||
|
global _viewname_validator
|
||||||
|
|
||||||
|
if _viewname_validator is None:
|
||||||
|
with misc.file_open(os.path.join('modulename', 'rng', 'viewname.rng')) as f:
|
||||||
|
_viewname_validator = etree.RelaxNG(etree.parse(f))
|
||||||
|
|
||||||
|
if _viewname_validator.validate(arch):
|
||||||
|
return True
|
||||||
|
|
||||||
|
for error in _viewname_validator.error_log:
|
||||||
|
_logger.error(ustr(error))
|
||||||
|
return False
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
`Example: The RNG file of the graph view <https://github.com/odoo/odoo/blob/70942e4cfb7a8993904b4d142e3b1749a40db806/odoo/addons/base/rng/graph_view.rng>`_
|
||||||
|
|
||||||
|
13. Uploading an image
|
||||||
|
======================
|
||||||
|
|
||||||
|
Our gallery view does not allow users to upload images. Let us implement that.
|
||||||
|
|
||||||
|
#. Add a button on each image by using the `FileUploader` component.
|
||||||
|
#. The `FileUploader` component accepts the `onUploaded` props, which is called when the user
|
||||||
|
uploads an image. Make sure to call `webSave` from the orm service to upload the new image.
|
||||||
|
#. You maybe noticed that the image is uploaded but it is not re-rendered by the browser.
|
||||||
|
This is because the image link did not change so the browser do not re-fetch them. Include
|
||||||
|
the `write_date` from the record to the image url.
|
||||||
|
#. Make sure that clicking on the upload button does not trigger the switchView.
|
||||||
|
|
||||||
|
.. image:: 02_create_gallery_view/upload_image.png
|
||||||
|
:align: center
|
||||||
|
:scale: 50%
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Example: usage of FileUploader <https://github.com/odoo/odoo/blob/7710c3331ebd22f8396870bd0731f8c1152d9c41/addons/mail/static/src/web/activity/activity.xml#L48-L52>`_
|
||||||
|
- `Odoo: webSave definition <https://github.com/odoo/odoo/blob/ebd538a1942c532bcf1c9deeab3c25efe23b6893/addons/web/static/src/core/orm_service.js#L312>`_
|
||||||
|
|
||||||
|
14. Advanced tooltip template
|
||||||
|
=============================
|
||||||
|
|
||||||
|
For now we can only specify a tooltip field. But what if we want to allow to write a specific
|
||||||
|
template for it ?
|
||||||
|
|
||||||
|
.. example::
|
||||||
|
|
||||||
|
This is an example of a gallery arch view that should work after this exercise.
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<record id="contacts_gallery_view" model="ir.ui.view">
|
||||||
|
<field name="name">awesome_gallery.orders.gallery</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<gallery image_field="image_1920" tooltip_field="name">
|
||||||
|
<field name="email"/> <!-- Specify to the model that email should be fetched -->
|
||||||
|
<field name="name"/> <!-- Specify to the model that name should be fetched -->
|
||||||
|
<tooltip-template> <!-- Specify the owl template for the tooltip -->
|
||||||
|
<p class="m-0">name: <field name="name"/></p> <!-- field is compiled into a t-esc-->
|
||||||
|
<p class="m-0">e-mail: <field name="email"/></p>
|
||||||
|
</tooltip-template>
|
||||||
|
</gallery>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
#. Replace the `res.partner` gallery arch view in :file:`awesome_gallery/views/views.xml` with
|
||||||
|
the arch in example above. Don't worry if it does not pass the rng validation.
|
||||||
|
#. Modify the gallery rng validator to accept the new arch structure.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
You can use this rng snippet to validate the tooltip-template tag
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<rng:define name="tooltip-template">
|
||||||
|
<rng:element name="tooltip-template">
|
||||||
|
<rng:zeroOrMore>
|
||||||
|
<rng:text/>
|
||||||
|
<rng:ref name="any"/>
|
||||||
|
</rng:zeroOrMore>
|
||||||
|
</rng:element>
|
||||||
|
</rng:define>
|
||||||
|
|
||||||
|
<rng:define name="any">
|
||||||
|
<rng:element>
|
||||||
|
<rng:anyName/>
|
||||||
|
<rng:zeroOrMore>
|
||||||
|
<rng:choice>
|
||||||
|
<rng:attribute>
|
||||||
|
<rng:anyName/>
|
||||||
|
</rng:attribute>
|
||||||
|
<rng:text/>
|
||||||
|
<rng:ref name="any"/>
|
||||||
|
</rng:choice>
|
||||||
|
</rng:zeroOrMore>
|
||||||
|
</rng:element>
|
||||||
|
</rng:define>
|
||||||
|
#. The arch parser should parse the fields and the tooltip template. Import `visitXML` from
|
||||||
|
:file:`@web/core/utils/xml` and use it to parse field names and the tooltip template.
|
||||||
|
#. Make sure that the model call the `webSearchRead` by including the parsed field names in the
|
||||||
|
specification.
|
||||||
|
#. The renderer (or any sub-component you created for it) should receive the parsed tooltip
|
||||||
|
template. Manipulate this template to replace the `<field>` element into a `<t t-esc="x">`
|
||||||
|
element.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
The template is an `Element` object so it can be manipulated like a HTML element.
|
||||||
|
|
||||||
|
#. Register the template to Owl thanks to the `xml` function from :file:`@odoo/owl`.
|
||||||
|
#. Use the `useTooltip` hook from :file:`@web/core/tooltip/tooltip_hook` to display the
|
||||||
|
tooltips. This hooks take as argument the Owl template and the variable needed by the
|
||||||
|
template.
|
||||||
|
|
||||||
|
.. image:: 02_create_gallery_view/advanced_tooltip.png
|
||||||
|
:align: center
|
||||||
|
:scale: 50%
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Example: useTooltip used in Kaban <https://github.com/odoo/odoo/blob/0e6481f359e2e4dd4f5b5147a1754bb3cca57311/addons/web/static/src/views/kanban/kanban_record.js#L189-L192>`_
|
||||||
|
- `Example: visitXML usage <https://github.com/odoo/odoo/blob/48ef812a635f70571b395f82ffdb2969ce99da9e/addons/web/static/src/views/list/list_arch_parser.js#L19>`_
|
||||||
|
- `Owl: Inline templates with xml helper function <https://github.com/odoo/owl/blob/master/doc/reference/templates.md#inline-templates>`_
|
||||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 608 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 905 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -1,299 +0,0 @@
|
|||||||
========================
|
|
||||||
Chapter 2: Miscellaneous
|
|
||||||
========================
|
|
||||||
|
|
||||||
In the previous task, we learned how to create fields and views. There is still much more to
|
|
||||||
discover in the feature-rich Odoo web framework, so let's dive in and explore more in this chapter!
|
|
||||||
|
|
||||||
.. graph TD
|
|
||||||
.. subgraph "Owl"
|
|
||||||
.. C[Component]
|
|
||||||
.. T[Template]
|
|
||||||
.. H[Hook]
|
|
||||||
.. S[Slot]
|
|
||||||
.. E[Event]
|
|
||||||
.. end
|
|
||||||
|
|
||||||
.. subgraph "odoo"[Odoo Javascript framework]
|
|
||||||
.. Services
|
|
||||||
.. Translation
|
|
||||||
.. lazy[Lazy loading libraries]
|
|
||||||
.. SCSS
|
|
||||||
.. action --> Services
|
|
||||||
.. rpc --> Services
|
|
||||||
.. orm --> Services
|
|
||||||
.. Fields
|
|
||||||
.. Views
|
|
||||||
.. Registries
|
|
||||||
.. end
|
|
||||||
|
|
||||||
.. odoo[Odoo JavaScript framework] --> Owl
|
|
||||||
|
|
||||||
.. figure:: 02_miscellaneous/previously_learned.svg
|
|
||||||
:align: center
|
|
||||||
:width: 70%
|
|
||||||
|
|
||||||
This is the progress that we have made in discovering the JavaScript web framework at the end of
|
|
||||||
:doc:`01_fields_and_views`.
|
|
||||||
|
|
||||||
.. admonition:: Goal
|
|
||||||
|
|
||||||
.. image:: 02_miscellaneous/kitten_mode.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. spoiler:: Solutions
|
|
||||||
|
|
||||||
The solutions for each exercise of the chapter are hosted on the
|
|
||||||
`official Odoo tutorials repository
|
|
||||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_tshirt>`_.
|
|
||||||
|
|
||||||
1. Interacting with the notification system
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
This task depends on :doc:`the previous exercises <01_fields_and_views>`.
|
|
||||||
|
|
||||||
After using the :guilabel:`Print Label` button for some t-shirt tasks, it is apparent that there
|
|
||||||
should be some feedback that the `print_label` action is completed (or failed, for example, the
|
|
||||||
printer is not connected or ran out of paper).
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
#. Display a :ref:`notification <frontend/services/notification>` message when the action is
|
|
||||||
completed successfully, and a warning if it failed.
|
|
||||||
#. If it failed, the notification should be permanent.
|
|
||||||
|
|
||||||
.. image:: 02_miscellaneous/notification.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
`Example: Using the notification service
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/views/fields/image_url/image_url_field.js>`_
|
|
||||||
|
|
||||||
2. Add a systray item
|
|
||||||
=====================
|
|
||||||
|
|
||||||
Our beloved leader wants to keep a close eye on new orders. He wants to see the number of new,
|
|
||||||
unprocessed orders at all time. Let's do that with a systray item.
|
|
||||||
|
|
||||||
A :ref:`systray <frontend/registries/systray>` item is an element that appears in the system tray,
|
|
||||||
which is a small area located on the right-hand side of the navbar. The systray is used to display
|
|
||||||
notifications and provide access to certain features.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Create a systray component that connects to the statistics service we made previously.
|
|
||||||
#. Use it to display the number of new orders.
|
|
||||||
#. Clicking on it should open a list view with all of those orders.
|
|
||||||
#. Bonus point: avoid making the initial RPC by adding the information to the session info. The
|
|
||||||
session info is given to the web client by the server in the initial response.
|
|
||||||
|
|
||||||
.. image:: 02_miscellaneous/systray.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
- `Example: Systray item <{GITHUB_PATH}/addons/web/static/src/webclient/user_menu/user_menu.js>`_
|
|
||||||
- `Example: Adding some information to the "session info"
|
|
||||||
<{GITHUB_PATH}/addons/barcodes/models/ir_http.py>`_
|
|
||||||
- `Example: Reading the session information
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/barcodes/static/src/barcode_service.js#L5>`_
|
|
||||||
|
|
||||||
3. Real life update
|
|
||||||
===================
|
|
||||||
|
|
||||||
So far, the systray item from above does not update unless the user refreshes the browser. Let us
|
|
||||||
do that by calling periodically (for example, every minute) the server to reload the information.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. The `tshirt` service should periodically reload its data.
|
|
||||||
|
|
||||||
Now, the question arises: how is the systray item notified that it should re-render itself? It can
|
|
||||||
be done in various ways but, for this training, we choose to use the most *declarative* approach:
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
2. Modify the `tshirt` service to return a `reactive
|
|
||||||
<{OWL_PATH}/doc/reference/reactivity.md#reactive>`_ object. Reloading data should update the
|
|
||||||
reactive object in place.
|
|
||||||
3. The systray item can then perform a `useState
|
|
||||||
<{OWL_PATH}/doc/reference/reactivity.md#usestate>`_ on the service return value.
|
|
||||||
4. This is not really necessary, but you can also *package* the calls to `useService` and
|
|
||||||
`useState` in a custom hook `useStatistics`.
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
- `Documentation on reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_
|
|
||||||
- `Example: Use of reactive in a service
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/core/debug/profiling/profiling_service.js#L30>`_
|
|
||||||
|
|
||||||
4. Add a command to the command palette
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
Now, let us see how we can interact with the command palette. The command palette is a feature that
|
|
||||||
allows users to quickly access various commands and functions within the application. It is accessed
|
|
||||||
by pressing `CTRL+K` in the Odoo interface.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
Modify the :ref:`image preview field <tutorials/master_odoo_web_framework/image_preview_field>`
|
|
||||||
to add a command to the command palette to open the image in a new browser tab (or window).
|
|
||||||
|
|
||||||
Ensure the command is only active whenever a field preview is visible on the screen.
|
|
||||||
|
|
||||||
.. image:: 02_miscellaneous/new_command.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
`Example: Using the useCommand hook
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/core/debug/debug_menu.js#L15>`_
|
|
||||||
|
|
||||||
5. Monkey patching a component
|
|
||||||
==============================
|
|
||||||
|
|
||||||
Often, we can achieve what we want by using existing extension points that allow for customization,
|
|
||||||
such as registering something in a registry. Sometimes, however, it happens that we want to modify
|
|
||||||
something that has no such mechanism. In that case, we must fall back on a less safe form of
|
|
||||||
customization: monkey patching. Almost everything in Odoo can be monkey patched.
|
|
||||||
|
|
||||||
Bafien, our beloved leader, heard about employees performing better if they are constantly being
|
|
||||||
watched. Since he cannot be there in person for each of his employees, he tasked you with updating
|
|
||||||
the user interface to add a blinking red eye in the control panel. Clicking on that eye should open
|
|
||||||
a dialog with the following message: "Bafien is watching you. This interaction is recorded and may
|
|
||||||
be used in legal proceedings if necessary. Do you agree to these terms?"
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. :ref:`Inherit <reference/qweb/template_inheritance>` the `web.Breadcrumbs` template of the
|
|
||||||
`ControlPanel component <{GITHUB_PATH}/addons/web/static/src/search/control_panel>`_ to add an
|
|
||||||
icon next to the breadcrumbs. You might want to use the `fa-eye` or `fa-eyes` icons.
|
|
||||||
#. :doc:`Patch </developer/reference/frontend/patching_code>` the component to display the
|
|
||||||
message on click by using `the dialog service
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/core/dialog/dialog_service.js>`_. You can use
|
|
||||||
`ConfirmationDialog
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js>`_.
|
|
||||||
#. Add the CSS class `blink` to the element representing the eye and paste the following code in
|
|
||||||
a new CSS file located in your patch's directory.
|
|
||||||
|
|
||||||
.. code-block:: css
|
|
||||||
|
|
||||||
.blink {
|
|
||||||
animation: blink-animation 1s steps(5, start) infinite;
|
|
||||||
-webkit-animation: blink-animation 1s steps(5, start) infinite;
|
|
||||||
}
|
|
||||||
@keyframes blink-animation {
|
|
||||||
to {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@-webkit-keyframes blink-animation {
|
|
||||||
to {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.. image:: 02_miscellaneous/bafien_eye.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
|
||||||
.. image:: 02_miscellaneous/confirmation_dialog.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
- `Code: The patch function
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/core/utils/patch.js#L16>`_
|
|
||||||
- `The Font Awesome website <https://fontawesome.com/>`_
|
|
||||||
- `Example: Using the dialog service
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/board/static/src/board_controller.js#L88>`_
|
|
||||||
|
|
||||||
6. Fetching orders from a customer
|
|
||||||
==================================
|
|
||||||
|
|
||||||
Let's see how to use some standard components to build a powerful feature combining autocomplete,
|
|
||||||
fetching data, and fuzzy lookup. We will add an input in our dashboard to easily search all orders
|
|
||||||
from a given customer.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Update :file:`tshirt_service.js` to add a `loadCustomers` method, which returns a promise that
|
|
||||||
returns the list of all customers (and only performs the call once).
|
|
||||||
#. Add the `AutoComplete component <{GITHUB_PATH}/addons/web/static/src/core/autocomplete>`_ to
|
|
||||||
the dashboard, next to the buttons in the control panel.
|
|
||||||
#. Fetch the list of customers with the tshirt service, and display it in the AutoComplete
|
|
||||||
component, filtered by the `fuzzyLookup
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/core/utils/search.js>`_ method.
|
|
||||||
|
|
||||||
.. image:: 02_miscellaneous/autocomplete.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
|
||||||
7. Reintroduce Kitten Mode
|
|
||||||
==========================
|
|
||||||
|
|
||||||
Let us add a special mode to Odoo: whenever the URL contains `kitten=1`, we will display a kitten in
|
|
||||||
the background of Odoo, because we like kittens.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Create a `kitten` service, which should check the content of the active URL hash with the
|
|
||||||
help of the :ref:`router service <frontend/services/router>`. If `kitten` is set in the URL,
|
|
||||||
add the class `o-kitten-mode` to the document body.
|
|
||||||
#. Add the following SCSS in :file:`kitten_mode.scss`:
|
|
||||||
|
|
||||||
.. code-block:: css
|
|
||||||
|
|
||||||
.o-kitten-mode {
|
|
||||||
background-image: url(https://upload.wikimedia.org/wikipedia/commons/5/58/Mellow_kitten_%28Unsplash%29.jpg);
|
|
||||||
background-size: cover;
|
|
||||||
background-attachment: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.o-kitten-mode > * {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
#. Add a command to the command palette to toggle the kitten mode. Toggling the kitten mode
|
|
||||||
should toggle the class `o-kitten-mode` and update the current URL accordingly.
|
|
||||||
|
|
||||||
.. image:: 02_miscellaneous/kitten_mode.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
8. Lazy loading our dashboard
|
|
||||||
=============================
|
|
||||||
|
|
||||||
This is not really necessary, but the exercise is interesting. Imagine that our awesome dashboard is
|
|
||||||
a large application with potentially multiple external libraries and lots of code/styles/templates.
|
|
||||||
Also, suppose that the dashboard is used only by some users in some business flows. It would be
|
|
||||||
interesting to lazy load it in order to speed up the loading of the web client in most cases.
|
|
||||||
|
|
||||||
So, let us do that!
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Modify the manifest to create a new :ref:`bundle <reference/assets_bundle>`
|
|
||||||
`awesome_tshirt.dashboard`.
|
|
||||||
#. Add the awesome dashboard code to this bundle. Create folders and move files if needed.
|
|
||||||
#. Remove the code from the `web.assets_backend` bundle so that it is not loaded twice.
|
|
||||||
|
|
||||||
So far, we only removed the dashboard from the main bundle; we now want to lazy load it. Currently,
|
|
||||||
no client action is registered in the action registry.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
4. Create a new file :file:`dashboard_loader.js`.
|
|
||||||
5. Copy the code registering `AwesomeDashboard` to the dashboard loader.
|
|
||||||
6. Register `AwesomeDashboard` as a `LazyComponent
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/core/assets.js#L265-L282>`_.
|
|
||||||
7. Modify the code in the dashboard loader to use the lazy component `AwesomeDashboard`.
|
|
||||||
|
|
||||||
If you open the :guilabel:`Network` tab of your browser's dev tools, you should see that
|
|
||||||
:file:`awesome_tshirt.dashboard.min.js` is now loaded only when the Dashboard is first accessed.
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
:ref:`Documentation on assets <reference/assets>`
|
|
||||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
@@ -1,171 +0,0 @@
|
|||||||
=============================
|
|
||||||
Chapter 3: Custom kanban view
|
|
||||||
=============================
|
|
||||||
|
|
||||||
.. todo:: It'd be cool to follow the naming convention of the previous chapters: "Chapter N: The concept studied in the chapter"
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
It is highly recommended that you complete :doc:`01_fields_and_views` before starting this
|
|
||||||
chapter. The concepts introduced in Chapter 3, including views and examples, will be essential
|
|
||||||
for understanding the material covered in this chapter.
|
|
||||||
|
|
||||||
We have gained an understanding of the numerous capabilities offered by the Odoo web framework. As a
|
|
||||||
next step, we will customize a kanban view. This is a more complicated project that will showcase
|
|
||||||
some non trivial aspects of the framework. The goal is to practice composing views, coordinating
|
|
||||||
various aspects of the UI, and doing it in a maintainable way.
|
|
||||||
|
|
||||||
Bafien had the greatest idea ever: a mix of a kanban view and a list view would be perfect for your
|
|
||||||
needs! In a nutshell, he wants a list of customers on the left of the task's kanban view. When you
|
|
||||||
click on a customer on the left sidebar, the kanban view on the right is filtered to only display
|
|
||||||
orders linked to that customer.
|
|
||||||
|
|
||||||
.. admonition:: Goal
|
|
||||||
|
|
||||||
.. image:: 03_custom_kanban_view/overview.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. spoiler:: Solutions
|
|
||||||
|
|
||||||
The solutions for each exercise of the chapter are hosted on the
|
|
||||||
`official Odoo tutorials repository
|
|
||||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_tshirt>`_.
|
|
||||||
|
|
||||||
1. Create a new kanban view
|
|
||||||
===========================
|
|
||||||
|
|
||||||
Since we are customizing the kanban view, let us start by extending it and using our extension in
|
|
||||||
the kanban view for the tshirt orders.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Extend the kanban view by extending the kanban controller and by creating a new view object.
|
|
||||||
#. Register it in the views registry under `awesome_tshirt.customer_kanban`.
|
|
||||||
#. Update the kanban arch to use the extended view. This can be done with the `js_class`
|
|
||||||
attribute.
|
|
||||||
|
|
||||||
2. Create a CustomerList component
|
|
||||||
==================================
|
|
||||||
|
|
||||||
We will need to display a list of customers, so we might as well create the component.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Create a `CustomerList` component which only displays a `div` with some text for now.
|
|
||||||
#. It should have a `selectCustomer` prop.
|
|
||||||
#. Create a new template extending (XPath) the kanban controller template to add the
|
|
||||||
`CustomerList` next to the kanban renderer. Give it an empty function as `selectCustomer` for
|
|
||||||
now.
|
|
||||||
#. Subclass the kanban controller to add `CustomerList` in its sub-components.
|
|
||||||
#. Make sure you see your component in the kanban view.
|
|
||||||
|
|
||||||
.. image:: 03_custom_kanban_view/customer_list.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
|
||||||
3. Load and display data
|
|
||||||
========================
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Modify the `CustomerList` component to fetch a list of all customers in `onWillStart`.
|
|
||||||
#. Display the list in the template with a `t-foreach`.
|
|
||||||
#. Whenever a customer is selected, call the `selectCustomer` function prop.
|
|
||||||
|
|
||||||
.. image:: 03_custom_kanban_view/customer_data.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
|
||||||
4. Update the main kanban view
|
|
||||||
==============================
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Implement `selectCustomer` in the kanban controller to add the proper domain.
|
|
||||||
#. Modify the template to give the real function to the `CustomerList` `selectCustomer` prop.
|
|
||||||
|
|
||||||
Since it is not trivial to interact with the search view, here is a quick snippet to help:
|
|
||||||
|
|
||||||
.. code-block:: js
|
|
||||||
|
|
||||||
selectCustomer(customer_id, customer_name) {
|
|
||||||
this.env.searchModel.setDomainParts({
|
|
||||||
customer: {
|
|
||||||
domain: [["customer_id", "=", customer_id]],
|
|
||||||
facetLabel: customer_name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
.. image:: 03_custom_kanban_view/customer_filter.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
|
||||||
5. Only display customers which have an active order
|
|
||||||
====================================================
|
|
||||||
|
|
||||||
There is a `has_active_order` field on `res.partner`. Let us allow the user to filter results on
|
|
||||||
customers with an active order.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Add an input of type checkbox in the `CustomerList` component, with a label "Active customers"
|
|
||||||
next to it.
|
|
||||||
#. Changing the value of the checkbox should filter the list on customers with an active order.
|
|
||||||
|
|
||||||
.. image:: 03_custom_kanban_view/active_customer.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
|
||||||
6. Add a search bar to the customer list
|
|
||||||
========================================
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
Add an input above the customer list that allows the user to enter a string and to filter the
|
|
||||||
displayed customers, according to their name.
|
|
||||||
|
|
||||||
.. tip::
|
|
||||||
You can use the `fuzzyLookup` function to perform the filter.
|
|
||||||
|
|
||||||
.. image:: 03_custom_kanban_view/customer_search.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
|
|
||||||
- `Code: The fuzzylookup function <{GITHUB_PATH}/addons/web/static/src/core/utils/search.js>`_
|
|
||||||
- `Example: Using fuzzyLookup
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/tests/core/utils/search_test.js#L17>`_
|
|
||||||
|
|
||||||
7. Refactor the code to use `t-model`
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
To solve the previous two exercises, it is likely that you used an event listener on the inputs. Let
|
|
||||||
us see how we could do it in a more declarative way, with the `t-model
|
|
||||||
<{OWL_PATH}/doc/reference/input_bindings.md>`_ directive.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Make sure you have a reactive object that represents the fact that the filter is active
|
|
||||||
(something like
|
|
||||||
:code:`this.state = useState({ displayActiveCustomers: false, searchString: ''})`).
|
|
||||||
#. Modify the code to add a getter `displayedCustomers` which returns the currently active list
|
|
||||||
of customers.
|
|
||||||
#. Modify the template to use `t-model`.
|
|
||||||
|
|
||||||
8. Paginate customers!
|
|
||||||
======================
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Add a :ref:`pager <frontend/pager>` in the `CustomerList`, and only load/render the first 20
|
|
||||||
customers.
|
|
||||||
#. Whenever the pager is changed, the customer list should update accordingly.
|
|
||||||
|
|
||||||
This is actually pretty hard, in particular in combination with the filtering done in the
|
|
||||||
previous exercise. There are many edge cases to take into account.
|
|
||||||
|
|
||||||
.. image:: 03_custom_kanban_view/customer_pager.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,200 @@
|
|||||||
|
==================================
|
||||||
|
Chapter 3: Customize a kanban view
|
||||||
|
==================================
|
||||||
|
|
||||||
|
We have gained an understanding of the numerous capabilities offered by the Odoo web framework. As a
|
||||||
|
next step, we will customize a kanban view. This is a more complicated project that will showcase
|
||||||
|
some non trivial aspects of the framework. The goal is to practice composing views, coordinating
|
||||||
|
various aspects of the UI, and doing it in a maintainable way.
|
||||||
|
|
||||||
|
Bafien had the greatest idea ever: a mix of a kanban view and a list view would be perfect for your
|
||||||
|
needs! In a nutshell, he wants a list of customers on the left of the CRM kanban view. When you
|
||||||
|
click on a customer on the left sidebar, the kanban view on the right is filtered to only display
|
||||||
|
leads linked to that customer.
|
||||||
|
|
||||||
|
.. admonition:: Goal
|
||||||
|
|
||||||
|
.. image:: 03_customize_kanban_view/overview.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. spoiler:: Solutions
|
||||||
|
|
||||||
|
The solutions for each exercise of the chapter are hosted on the
|
||||||
|
`official Odoo tutorials repository
|
||||||
|
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-master-odoo-web-framework-solutions/awesome_kanban>`_.
|
||||||
|
|
||||||
|
1. Create a new kanban view
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Since we are customizing the kanban view, let us start by extending it and using our extension in
|
||||||
|
the kanban view of CRM.
|
||||||
|
|
||||||
|
#. Create a new empty component that extends the `KanbanController` component from
|
||||||
|
:file:`@web/views/kanban/kanban_controller`.
|
||||||
|
#. Create a new view object and assign all keys and values from `kanbanView` from
|
||||||
|
:file:`@web/views/kanban/kanban_view`. Override the Controller key by putting your newly
|
||||||
|
created controller.
|
||||||
|
#. Register it in the views registry under `awesome_kanban`.
|
||||||
|
#. Update the crm kanban arch in :file:`awesome_kanban/views/views.xml` to use the extended view.
|
||||||
|
This can be done by specifying the `js_class` attribute in the kanban node.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
`Example: Create a new view by extending a pre-existing one <https://github.com/odoo/odoo/blob/0a59f37e7dd73daff2e9926542312195b3de4154/addons/todo/static/src/views/todo_conversion_form/todo_conversion_form_view.js>`_
|
||||||
|
|
||||||
|
2. Create a CustomerList component
|
||||||
|
==================================
|
||||||
|
|
||||||
|
We will need to display a list of customers, so we might as well create the component.
|
||||||
|
|
||||||
|
#. Create a `CustomerList` component which only displays a `div` with some text for now.
|
||||||
|
#. It should have a `selectCustomer` prop.
|
||||||
|
#. Create a new template extending (XPath) the kanban controller template `web.KanbanView` to add
|
||||||
|
the `CustomerList` next to the kanban renderer. Give it an empty function as `selectCustomer`
|
||||||
|
for now.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
You can use this xpath inside the template to add a div before the renderer component.
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<xpath expr="//t[@t-component='props.Renderer']" position="before">
|
||||||
|
...
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
#. Subclass the kanban controller to add `CustomerList` in its sub-components.
|
||||||
|
#. Make sure you see your component in the kanban view.
|
||||||
|
|
||||||
|
.. image:: 03_customize_kanban_view/customer_list_component.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:ref:`Template inheritance <reference/qweb/template_inheritance>`
|
||||||
|
|
||||||
|
3. Load and display data
|
||||||
|
========================
|
||||||
|
|
||||||
|
#. Modify the `CustomerList` component to fetch a list of all customers in `onWillStart`.
|
||||||
|
#. Display the list in the template with a `t-foreach`.
|
||||||
|
#. Whenever a customer is selected, call the `selectCustomer` function prop.
|
||||||
|
|
||||||
|
.. image:: 03_customize_kanban_view/customer_data.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Example: fetching records from a model <https://github.com/odoo/odoo/blob/986c00c1bd1b3ca16a04ab25f5a2504108136112/addons/project/static/src/views/burndown_chart/burndown_chart_model.js#L26-L31>`_
|
||||||
|
|
||||||
|
4. Update the main kanban view
|
||||||
|
==============================
|
||||||
|
|
||||||
|
#. Implement `selectCustomer` in the kanban controller to add the proper domain.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
Since it is not trivial to interact with the search view, here is a snippet to create a
|
||||||
|
filter:
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
this.env.searchModel.createNewFilters([{
|
||||||
|
description: partner_name,
|
||||||
|
domain: [["partner_id", "=", partner_id]],
|
||||||
|
isFromAwesomeKanban: true, // this is a custom key to retrieve our filters later
|
||||||
|
}])
|
||||||
|
|
||||||
|
#. By clicking on multiple customers, you can see that the old customer filter is not replaced.
|
||||||
|
Make sure that by clicking on a customer, the old filter is replaced by the new one.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
|
||||||
|
You can use this snippet to get the customers filters and toggle them.
|
||||||
|
|
||||||
|
.. code-block:: js
|
||||||
|
|
||||||
|
const customerFilters = this.env.searchModel.getSearchItems((searchItem) =>
|
||||||
|
searchItem.isFromAwesomeKanban
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const customerFilter of customerFilters) {
|
||||||
|
if (customerFilter.isActive) {
|
||||||
|
this.env.searchModel.toggleSearchItem(customerFilter.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#. Modify the template to give the real function to the `CustomerList` `selectCustomer` prop.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You can use `Symbol
|
||||||
|
<https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol>`_
|
||||||
|
to make sure that the custom `isFromAwesomeKanban` key will not collide with keys any other
|
||||||
|
code might add to the object.
|
||||||
|
|
||||||
|
.. image:: 03_customize_kanban_view/customer_filter.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
5. Only display customers which have an active order
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
There is a `opportunity_ids` field on `res.partner`. Let us allow the user to filter results on
|
||||||
|
customers with at least one opportunity.
|
||||||
|
|
||||||
|
#. Add an input of type checkbox in the `CustomerList` component, with a label "Active customers"
|
||||||
|
next to it.
|
||||||
|
#. Changing the value of the checkbox should filter the list of customers.
|
||||||
|
|
||||||
|
.. image:: 03_customize_kanban_view/active_customer.png
|
||||||
|
:align: center
|
||||||
|
:scale: 60%
|
||||||
|
|
||||||
|
6. Add a search bar to the customer list
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Add an input above the customer list that allows the user to enter a string and to filter the
|
||||||
|
displayed customers, according to their name.
|
||||||
|
|
||||||
|
.. tip::
|
||||||
|
You can use the `fuzzyLookup` from :file:`@web/core/utils/search` function to perform the
|
||||||
|
filter.
|
||||||
|
|
||||||
|
.. image:: 03_customize_kanban_view/customer_search.png
|
||||||
|
:align: center
|
||||||
|
:scale: 60%
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
- `Code: The fuzzylookup function <https://github.com/odoo/odoo/blob/235fc69280a18a5805d8eb84d76ada91ba49fe67/addons/web/static/src/core/utils/search.js#L41-L54>`_
|
||||||
|
- `Example: Using fuzzyLookup
|
||||||
|
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||||
|
addons/web/static/tests/core/utils/search_test.js#L17>`_
|
||||||
|
|
||||||
|
7. Refactor the code to use `t-model`
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
To solve the previous two exercises, it is likely that you used an event listener on the inputs. Let
|
||||||
|
us see how we could do it in a more declarative way, with the `t-model
|
||||||
|
<{OWL_PATH}/doc/reference/input_bindings.md>`_ directive.
|
||||||
|
|
||||||
|
#. Make sure you have a reactive object that represents the fact that the filter is active
|
||||||
|
(something like
|
||||||
|
:code:`this.state = useState({ displayActiveCustomers: false, searchString: ''})`).
|
||||||
|
#. Modify the code to add a getter `displayedCustomers` which returns the currently active list
|
||||||
|
of customers.
|
||||||
|
#. Modify the template to use `t-model`.
|
||||||
|
|
||||||
|
8. Paginate customers!
|
||||||
|
======================
|
||||||
|
|
||||||
|
#. Add a :ref:`pager <frontend/pager>` in the `CustomerList`, and only load/render the first 20
|
||||||
|
customers.
|
||||||
|
#. Whenever the pager is changed, the customer list should update accordingly.
|
||||||
|
|
||||||
|
This is actually pretty hard, in particular in combination with the filtering done in the
|
||||||
|
previous exercise. There are many edge cases to take into account.
|
||||||
|
|
||||||
|
.. image:: 03_customize_kanban_view/customer_pager.png
|
||||||
|
:align: center
|
||||||
|
:scale: 60%
|
||||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 95 KiB |
@@ -1,272 +0,0 @@
|
|||||||
=======================================
|
|
||||||
Chapter 4: Creating a view from scratch
|
|
||||||
=======================================
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
It is highly recommended that you complete :doc:`01_fields_and_views` before starting this
|
|
||||||
chapter. The concepts introduced in Chapter 3, including views and examples, will be essential
|
|
||||||
for understanding the material covered in this chapter.
|
|
||||||
|
|
||||||
Let us see how one can create a new view, completely from scratch. In a way, it is not very
|
|
||||||
difficult to do, but there are no really good resources on how to do it. Note that most situations
|
|
||||||
should be solved by either customizing an existing view, or with a client action.
|
|
||||||
|
|
||||||
For this exercise, let's assume that we want to create a `gallery` view, which is a view that lets
|
|
||||||
us represent a set of records with an image field. In our Awesome Tshirt scenario, we would like to
|
|
||||||
be able to see a set of t-shirts images.
|
|
||||||
|
|
||||||
The problem could certainly be solved with a kanban view, but this means that it is not possible to
|
|
||||||
have our normal kanban view and the gallery view in the same action.
|
|
||||||
|
|
||||||
Let us make a gallery view. Each gallery view will be defined by an `image_field` attribute in its
|
|
||||||
arch:
|
|
||||||
|
|
||||||
.. code-block:: xml
|
|
||||||
|
|
||||||
<gallery image_field="some_field"/>
|
|
||||||
|
|
||||||
To complete the tasks in this chapter, you will need to install the awesome_gallery addon. This
|
|
||||||
addon includes the necessary server files to add a new view.
|
|
||||||
|
|
||||||
.. admonition:: Goal
|
|
||||||
|
|
||||||
.. image:: 04_creating_view_from_scratch/overview.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. spoiler:: Solutions
|
|
||||||
|
|
||||||
The solutions for each exercise of the chapter are hosted on the
|
|
||||||
`official Odoo tutorials repository
|
|
||||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_gallery>`_.
|
|
||||||
|
|
||||||
1. Make a hello world view
|
|
||||||
==========================
|
|
||||||
|
|
||||||
First step is to create a JavaScript implementation with a simple component.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Create the `gallery_view.js` , `gallery_controller.js` and `gallery_controller.xml` files in
|
|
||||||
`static/src`.
|
|
||||||
#. Implement a simple hello world component in `gallery_controller.js`.
|
|
||||||
#. In `gallery_view.js`, import the controller, create a view object, and register it in the
|
|
||||||
view registry under the name `gallery`.
|
|
||||||
#. Add `gallery` as one of the view type in the orders action.
|
|
||||||
#. Make sure that you can see your hello world component when switching to the gallery view.
|
|
||||||
|
|
||||||
.. image:: 04_creating_view_from_scratch/view_button.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. image:: 04_creating_view_from_scratch/new_view.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
2. Use the Layout component
|
|
||||||
===========================
|
|
||||||
|
|
||||||
So far, our gallery view does not look like a standard view. Let's use the `Layout` component to
|
|
||||||
have the standard features like other views.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Import the `Layout` component and add it to the `components` of `GalleryController`.
|
|
||||||
#. Update the template to use `Layout`. It needs a `display` prop, which can be found in
|
|
||||||
`props.display`.
|
|
||||||
|
|
||||||
.. image:: 04_creating_view_from_scratch/layout.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
3. Parse the arch
|
|
||||||
=================
|
|
||||||
|
|
||||||
For now, our gallery view does not do much. Let's start by reading the information contained in the
|
|
||||||
arch of the view.
|
|
||||||
|
|
||||||
The process of parsing an arch is usually done with a `ArchParser`, specific to each view. It
|
|
||||||
inherits from a generic `XMLParser` class.
|
|
||||||
|
|
||||||
.. example::
|
|
||||||
|
|
||||||
Here is an example of what an ArchParser might look like:
|
|
||||||
|
|
||||||
.. code-block:: js
|
|
||||||
|
|
||||||
import { XMLParser } from "@web/core/utils/xml";
|
|
||||||
|
|
||||||
export class GraphArchParser extends XMLParser {
|
|
||||||
parse(arch, fields) {
|
|
||||||
const result = {};
|
|
||||||
this.visitXML(arch, (node) => {
|
|
||||||
...
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Create the `ArchParser` class in its own file. It can inherit from `XMLParser` in
|
|
||||||
`@web/core/utils/xml`.
|
|
||||||
#. Use it to read the `image_field` information.
|
|
||||||
#. Update the `gallery` view code to add it to the props received by the controller.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
It is probably a little overkill to do it like that, since we basically only need to read one
|
|
||||||
attribute from the arch, but it is a design that is used by every other odoo views, since it
|
|
||||||
lets us extract some upfront processing out of the controller.
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
`Example: The graph arch parser
|
|
||||||
<{GITHUB_PATH}/addons/web/static/src/views/graph/graph_arch_parser.js>`_
|
|
||||||
|
|
||||||
4. Load some data
|
|
||||||
=================
|
|
||||||
|
|
||||||
Let us now get some real data.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Add a :code:`loadImages(domain) {...}` method to the `GalleryController`. It should perform a
|
|
||||||
`webSearchRead` call from the orm service to fetch records corresponding to the domain, and
|
|
||||||
use `imageField` received in props.
|
|
||||||
#. Modify the `setup` code to call that method in the `onWillStart` and `onWillUpdateProps`
|
|
||||||
hooks.
|
|
||||||
#. Modify the template to display the data inside the default slot of the `Layout` component.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
The loading data code will be moved into a proper model in the next exercise.
|
|
||||||
|
|
||||||
.. image:: 04_creating_view_from_scratch/gallery_data.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
5. Reorganize code
|
|
||||||
==================
|
|
||||||
|
|
||||||
Real views are a little bit more organized. This may be overkill in this example, but it is intended
|
|
||||||
to learn how to structure code in Odoo. Also, this will scale better with changing requirements.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Move all the model code in its own `GalleryModel` class.
|
|
||||||
#. Move all the rendering code in a `GalleryRenderer` component.
|
|
||||||
#. Adapt the `GalleryController` and `gallery_view` to make it work.
|
|
||||||
|
|
||||||
6. Display images
|
|
||||||
=================
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
Update the renderer to display images in a nice way, if the field is set. If `image_field` is
|
|
||||||
empty, display an empty box instead.
|
|
||||||
|
|
||||||
.. image:: 04_creating_view_from_scratch/tshirt_images.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
7. Switch to form view on click
|
|
||||||
===============================
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
Update the renderer to react to a click on an image and switch to a form view. You can use the
|
|
||||||
`switchView` function from the action service.
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
`Code: The switchView function
|
|
||||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
|
||||||
addons/web/static/src/webclient/actions/action_service.js#L1329>`_
|
|
||||||
|
|
||||||
8. Add an optional tooltip
|
|
||||||
==========================
|
|
||||||
|
|
||||||
It is useful to have some additional information on mouse hover.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Update the code to allow an optional additional attribute on the arch:
|
|
||||||
|
|
||||||
.. code-block:: xml
|
|
||||||
|
|
||||||
<gallery image_field="some_field" tooltip_field="some_other_field"/>
|
|
||||||
|
|
||||||
#. On mouse hover, display the content of the tooltip field. It should work if the field is a
|
|
||||||
char field, a number field or a many2one field.
|
|
||||||
#. Update the orders gallery view to add the customer as tooltip field.
|
|
||||||
|
|
||||||
.. image:: 04_creating_view_from_scratch/image_tooltip.png
|
|
||||||
:align: center
|
|
||||||
:scale: 60%
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
`Code: The tooltip hook <{GITHUB_PATH}/addons/web/static/src/core/tooltip/tooltip_hook.js>`_
|
|
||||||
|
|
||||||
9. Add pagination
|
|
||||||
=================
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
Let's add a pager on the control panel and manage all the pagination like in a normal Odoo view.
|
|
||||||
Note that it is surprisingly difficult.
|
|
||||||
|
|
||||||
.. image:: 04_creating_view_from_scratch/pagination.png
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
`Code: The usePager hook <{GITHUB_PATH}/addons/web/static/src/search/pager_hook.js>`_
|
|
||||||
|
|
||||||
10. Validating views
|
|
||||||
=====================
|
|
||||||
|
|
||||||
We have a nice and useful view so far. But in real life, we may have issue with users incorrectly
|
|
||||||
encoding the `arch` of their Gallery view: it is currently only an unstructured piece of XML.
|
|
||||||
|
|
||||||
Let us add some validation! In Odoo, XML documents can be described with an RN file
|
|
||||||
:dfn:`(Relax NG file)`, and then validated.
|
|
||||||
|
|
||||||
.. exercise::
|
|
||||||
|
|
||||||
#. Add an RNG file that describes the current grammar:
|
|
||||||
|
|
||||||
- A mandatory attribute `image_field`.
|
|
||||||
- An optional attribute: `tooltip_field`.
|
|
||||||
|
|
||||||
#. Add some code to make sure all views are validated against this RNG file.
|
|
||||||
#. While we are at it, let us make sure that `image_field` and `tooltip_field` are fields from
|
|
||||||
the current model.
|
|
||||||
|
|
||||||
Since validating an RNG file is not trivial, here is a snippet to help:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
from odoo.loglevels import ustr
|
|
||||||
from odoo.tools import misc, view_validation
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_viewname_validator = None
|
|
||||||
|
|
||||||
@view_validation.validate('viewname')
|
|
||||||
def schema_viewname(arch, **kwargs):
|
|
||||||
""" Check the gallery view against its schema
|
|
||||||
|
|
||||||
:type arch: etree._Element
|
|
||||||
"""
|
|
||||||
global _viewname_validator
|
|
||||||
|
|
||||||
if _viewname_validator is None:
|
|
||||||
with misc.file_open(os.path.join('modulename', 'rng', 'viewname.rng')) as f:
|
|
||||||
_viewname_validator = etree.RelaxNG(etree.parse(f))
|
|
||||||
|
|
||||||
if _viewname_validator.validate(arch):
|
|
||||||
return True
|
|
||||||
|
|
||||||
for error in _viewname_validator.error_log:
|
|
||||||
_logger.error(ustr(error))
|
|
||||||
return False
|
|
||||||
|
|
||||||
.. seealso::
|
|
||||||
`Example: The RNG file of the graph view <{GITHUB_PATH}/addons/base/rng/graph_view.rng>`_
|
|
||||||
|
Before Width: | Height: | Size: 69 KiB |