Compare commits
1 Commits
evtf-odoo-
...
master-js-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4f3d6475d |
@@ -1,4 +1,6 @@
|
||||
|
||||
.. _howtos/javascript_client_action:
|
||||
|
||||
======================
|
||||
Create a client action
|
||||
======================
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.. _frontend/components:
|
||||
|
||||
==============
|
||||
Owl Components
|
||||
Owl components
|
||||
==============
|
||||
|
||||
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
|
||||
===========================
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
:show-content:
|
||||
|
||||
=========================
|
||||
Discover the JS Framework
|
||||
Discover the JS framework
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
@@ -10,24 +10,27 @@ Discover the 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
|
||||
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
|
||||
to build complex web interfaces quickly and efficiently. We will explore how to create and use Owl
|
||||
components in Odoo.
|
||||
|
||||
The second part of the tutorial focuses on creating a dashboard using various features of Odoo.
|
||||
Dashboards are an essential part of any web application, and provide a nice starting point to use
|
||||
and interact with the Odoo codebase.
|
||||
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
|
||||
point to use and interact with the Odoo codebase.
|
||||
|
||||
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
|
||||
:doc:`Getting started </developer/tutorials/getting_started>` tutorial before proceeding with this
|
||||
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:
|
||||
|
||||
Setup
|
||||
@@ -35,11 +38,12 @@ Setup
|
||||
|
||||
#. Clone the `official Odoo tutorials repository <https://github.com/odoo/tutorials>`_ and switch to
|
||||
the branch `{CURRENT_MAJOR_BRANCH}`.
|
||||
#. Add the cloned repository to the :option:`--addons-path <odoo-bin --addons-path>`.
|
||||
#. Start a new Odoo database and install the modules `owl_playground` and `awesome_tshirt`.
|
||||
#. Add the cloned repository to your :option:`--addons-path <odoo-bin --addons-path>`.
|
||||
#. Start a new Odoo database and install the modules `awesome_owl` (for chapter 1) and `awesome_dashboard`
|
||||
(for chapter 2).
|
||||
|
||||
Content
|
||||
=======
|
||||
|
||||
- :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
|
||||
@@ -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
|
||||
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>`.
|
||||
|
||||
.. 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::
|
||||
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.
|
||||
|
||||
`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
|
||||
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*.
|
||||
|
||||
.. 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
|
||||
==============================
|
||||
|
||||
@@ -54,8 +55,8 @@ button.
|
||||
}
|
||||
}
|
||||
|
||||
The `Counter` component specifies the name of the template to render. The template is written in XML
|
||||
and defines a part of user interface:
|
||||
The `Counter` component specifies the name of a template that represents its html. It is written in XML
|
||||
using the QWeb language:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
@@ -70,276 +71,468 @@ and defines a part of user interface:
|
||||
1. Displaying a counter
|
||||
=======================
|
||||
|
||||
As a first exercise, let us implement a counter in the `Playground` component located in
|
||||
:file:`owl_playground/static/src/`. To see the result, you can go to the `/owl_playground/playground`
|
||||
route with your browser.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will
|
||||
need to use the `useState hook
|
||||
<{OWL_PATH}/doc/reference/hooks.md#usestate>`_ so that the component is re-rendered
|
||||
whenever any part of the state object that has been read by this component is modified.
|
||||
#. In the same component, create an `increment` method.
|
||||
#. Modify the template in :file:`playground.xml` so that it displays your counter variable. Use
|
||||
`t-esc <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_ to output the data.
|
||||
#. Add a button in the template and specify a `t-on-click
|
||||
<{OWL_PATH}/doc/reference/event_handling.md#event-handling>`_ attribute in the button to
|
||||
trigger the `increment` method whenever the button is clicked.
|
||||
|
||||
.. image:: 01_owl_components/counter.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
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.
|
||||
|
||||
|
||||
#. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will
|
||||
need to use the `useState hook
|
||||
<{OWL_PATH}/doc/reference/hooks.md#usestate>`_ so that the component is re-rendered
|
||||
whenever any part of the state object that has been read by this component is modified.
|
||||
#. In the same component, create an `increment` method.
|
||||
#. Modify the template in :file:`playground.xml` so that it displays your counter variable. Use
|
||||
`t-esc <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_ to output the data.
|
||||
#. Add a button in the template and specify a `t-on-click
|
||||
<{OWL_PATH}/doc/reference/event_handling.md#event-handling>`_ attribute in the button to
|
||||
trigger the `increment` method whenever the button is clicked.
|
||||
|
||||
.. tip::
|
||||
The Odoo JavaScript files downloaded by the browser are minified. For debugging purpose, it's
|
||||
easier when the files are not minified. Switch to
|
||||
: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
|
||||
`sub-component <{OWL_PATH}/doc/reference/component.md#sub-components>`_ from it.
|
||||
2. Extract `Counter` in a sub component
|
||||
=======================================
|
||||
|
||||
.. 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.
|
||||
#. 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
|
||||
the template is in its own file, with the same name.
|
||||
#. 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
|
||||
`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.
|
||||
#. 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::
|
||||
Don't forget :code:`/** @odoo-module **/` in your JavaScript files. More information on this can
|
||||
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
|
||||
todos. This will be done incrementally in multiple exercises that will introduce various concepts.
|
||||
3. A simple `Card` component
|
||||
============================
|
||||
|
||||
.. 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
|
||||
<{OWL_PATH}/doc/reference/props.md>`_, and display it. It should show something like
|
||||
**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.
|
||||
The goal of this exercise is to create a `Card` component, that takes two props: `title` and `content`.
|
||||
For example, here is how it could be used:
|
||||
|
||||
.. example::
|
||||
.. code-block:: xml
|
||||
|
||||
.. code-block:: javascript
|
||||
<Card title="'my title'" content="'some content'"/>
|
||||
|
||||
setup() {
|
||||
...
|
||||
this.todo = { id: 3, description: "buy milk", done: false };
|
||||
}
|
||||
The above example should produce some html using bootstrap that look like this:
|
||||
|
||||
.. image:: 01_owl_components/todo.png
|
||||
:scale: 70%
|
||||
.. code-block:: html
|
||||
|
||||
<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
|
||||
|
||||
.. seealso::
|
||||
`Owl: Dynamic class attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-class-attribute>`_
|
||||
4. Using `markup` to display html
|
||||
=================================
|
||||
|
||||
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
|
||||
todo object in a specified format: `id`, `description` and `done`. Let us make that API more
|
||||
The `Card` component has an implicit API. It expects to receive two strings in its props: the `title`
|
||||
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
|
||||
<{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`
|
||||
component.
|
||||
#. Rename the `title` props into something else in the playground template, then check in the
|
||||
:guilabel:`Console` tab of your browser's dev tools that you can see an error.
|
||||
|
||||
#. Add `props validation <{OWL_PATH}/doc/reference/props.md#props-validation>`_ to the `Todo`
|
||||
component.
|
||||
#. Open the :guilabel:`Console` tab of your browser's dev tools and make sure the props
|
||||
validation passes in dev mode, which is activated by default in `owl_playground`. The dev mode
|
||||
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.
|
||||
6. The sum of two `Counter`
|
||||
===========================
|
||||
|
||||
5. A list of todos
|
||||
==================
|
||||
We saw in a previous exercise that `props` can be used to provide information from a parent
|
||||
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.
|
||||
|
||||
Now, let us display a list of todos instead of just one todo. For now, we can still hard-code the
|
||||
list.
|
||||
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.
|
||||
|
||||
.. exercise::
|
||||
#. Add prop validation to the `Counter` component: it should accept an optional `onChange`
|
||||
function prop.
|
||||
#. Update the `Counter` component to call the `onChange` prop (if it exists) whenever it
|
||||
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.
|
||||
|
||||
#. Change the code to display a list of todos instead of just one. Create a new `TodoList`
|
||||
component to hold the `Todo` components and use `t-foreach
|
||||
<{OWL_PATH}/doc/reference/templates.md#loops>`_ in its template.
|
||||
#. Think about how it should be keyed with the `t-key` directive.
|
||||
|
||||
.. image:: 01_owl_components/todo_list.png
|
||||
:scale: 70%
|
||||
.. image:: 01_owl_components/sum_counter.png
|
||||
: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
|
||||
a todo to the list.
|
||||
|
||||
.. exercise::
|
||||
#. Remove the hardcoded values in the `TodoList` component
|
||||
|
||||
#. 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
|
||||
named `addTodo`.
|
||||
#. Implement `addTodo` to check if enter was pressed (:code:`ev.keyCode === 13`), and in that
|
||||
case, create a new todo with the current content of the input as the description and clear the
|
||||
input of all content.
|
||||
#. 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.
|
||||
.. code-block:: javascript
|
||||
|
||||
.. code-block:: javascript
|
||||
this.todos = useState([]);
|
||||
|
||||
#. 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
|
||||
named `addTodo`.
|
||||
#. Implement `addTodo` to check if enter was pressed (:code:`ev.keyCode === 13`), and in that
|
||||
case, create a new todo with the current content of the input as the description and clear the
|
||||
input of all content.
|
||||
#. Make sure the todo has a unique id. It can be just a counter that increments at each todo.
|
||||
#. Bonus point: don't do anything if the input is empty.
|
||||
|
||||
this.todos = useState([]);
|
||||
|
||||
.. image:: 01_owl_components/create_todo.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`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
|
||||
<{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
|
||||
<{OWL_PATH}/doc/reference/component.md#mounted>`_. This this should be done from the
|
||||
`TodoList` component.
|
||||
#. 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.
|
||||
<div t-ref="some_name">hello</div>
|
||||
|
||||
.. seealso::
|
||||
`Owl: Component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_
|
||||
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.
|
||||
|
||||
8. Toggling todos
|
||||
=================
|
||||
.. 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>`_
|
||||
`useAutofocus` in a new :file:`awesome_owl/utils.js` file.
|
||||
|
||||
.. image:: 01_owl_components/autofocus.png
|
||||
:align: center
|
||||
|
||||
.. 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
|
||||
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
|
||||
way to do this is by using a `callback prop
|
||||
<{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
|
||||
be checked if the state `isCompleted` is true.
|
||||
|
||||
#. 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.
|
||||
.. tip::
|
||||
Owl does not create attributes computed with the `t-att` directive if it evaluates to a
|
||||
falsy value.
|
||||
|
||||
.. tip::
|
||||
QWeb does not create attributes computed with the `t-att` directive if it evaluates to a
|
||||
falsy value.
|
||||
|
||||
#. Add a callback props `toggleState`.
|
||||
#. Add a `click` event handler on the input in the `Todo` component and make sure it calls the
|
||||
`toggleState` function with the todo id.
|
||||
#. Make it work!
|
||||
#. Add a callback props `toggleState` to `TodoItem`.
|
||||
#. Add a `click` event handler on the input in the `TodoItem` component and make sure it calls the
|
||||
`toggleState` function with the todo id.
|
||||
#. Make it work!
|
||||
|
||||
.. image:: 01_owl_components/toggle_todo.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
9. Deleting todos
|
||||
=================
|
||||
12. Deleting todos
|
||||
==================
|
||||
|
||||
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.
|
||||
#. Whenever the user clicks on it, it should call the `removeTodo` method.
|
||||
#. Make it work!
|
||||
|
||||
#. 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.
|
||||
.. tip::
|
||||
If you're using an array to store your todo list, you can use the JavaScript `splice`
|
||||
function to remove a todo from it.
|
||||
|
||||
.. tip::
|
||||
If you're using an array to store your todo list, you can use the JavaScript `splice`
|
||||
function to remove a todo from it.
|
||||
.. code-block::
|
||||
|
||||
.. code-block::
|
||||
|
||||
// find the index of the element to delete
|
||||
const index = list.findIndex((elem) => elem.id === elemId);
|
||||
if (index >= 0) {
|
||||
// remove the element at index from list
|
||||
list.splice(index, 1);
|
||||
}
|
||||
// find the index of the element to delete
|
||||
const index = list.findIndex((elem) => elem.id === elemId);
|
||||
if (index >= 0) {
|
||||
// remove the element at index from list
|
||||
list.splice(index, 1);
|
||||
}
|
||||
|
||||
.. image:: 01_owl_components/delete_todo.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
.. _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
|
||||
components. This is useful to factorize the common layout between different parts of the interface.
|
||||
In a :ref:`previous exercise <tutorials/discover_js_framework/simple_card>`, we built
|
||||
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
|
||||
following Bootstrap HTML structure for the card:
|
||||
Let us modify the `Card` component to use slots:
|
||||
|
||||
.. 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;">
|
||||
<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%
|
||||
.. image:: 01_owl_components/generic_card.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`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.
|
||||
#. Try to express in the props validation system that it requires a `default` slot, and an
|
||||
optional `title` slot.
|
||||
#. Add a state to the `Card` component to track if it is open (the default) or not
|
||||
#. Add a `t-if` in the template to conditionally render the content
|
||||
#. 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:
|
||||
|
||||
=============================
|
||||
Master the Odoo Web Framework
|
||||
Master the Odoo web framework
|
||||
=============================
|
||||
|
||||
.. toctree::
|
||||
@@ -11,32 +11,49 @@ Master the Odoo Web Framework
|
||||
master_odoo_web_framework/*
|
||||
|
||||
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
|
||||
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.
|
||||
.. note::
|
||||
|
||||
In this tutorial, we will explore various aspects of the Odoo web framework in detail, including
|
||||
fields and views, notifications, command palette, and much more. This tutorial will provide you with
|
||||
the knowledge and skills you need to take full advantage of the Odoo web framework. So, let's get
|
||||
started!
|
||||
Each of these chapters can be done independantly, in any order. Also, be aware that some of them
|
||||
cover a lot of material, so they may be quite long.
|
||||
|
||||
.. _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
|
||||
=====
|
||||
|
||||
#. Clone the `official Odoo tutorials repository <https://github.com/odoo/tutorials>`_ and switch to
|
||||
the branch `{CURRENT_MAJOR_BRANCH}`.
|
||||
#. Add the cloned repository to the :option:`--addons-path <odoo-bin --addons-path>`.
|
||||
#. Start a new Odoo database and install the modules `awesome_tshirt` and `awesome_gallery`.
|
||||
#. Add the cloned repository to your :option:`--addons-path <odoo-bin --addons-path>`.
|
||||
#. 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
|
||||
=======
|
||||
|
||||
- :doc:`master_odoo_web_framework/01_fields_and_views`
|
||||
- :doc:`master_odoo_web_framework/02_miscellaneous`
|
||||
- :doc:`master_odoo_web_framework/03_custom_kanban_view`
|
||||
- :doc:`master_odoo_web_framework/04_creating_view_from_scratch`
|
||||
- :doc:`master_odoo_web_framework/05_testing`
|
||||
- :doc:`master_odoo_web_framework/01_build_clicker_game`
|
||||
- :doc:`master_odoo_web_framework/02_create_gallery_view`
|
||||
- :doc:`master_odoo_web_framework/03_customize_kanban_view`
|
||||
|
||||
@@ -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 |