convert rst to markdown

This commit is contained in:
2025-02-27 18:56:07 +07:00
parent ef68fdc2d9
commit 637a4dd9fd
5527 changed files with 189985 additions and 45 deletions
@@ -0,0 +1,449 @@
# 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
```
:::
```{eval-rst}
.. 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.
1. Create a `clicker_systray_item.js` (and `xml`) file with a hello world Owl component.
2. Register it to the systray registry, and make sure it is visible.
3. 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.
1. Use `useExternalListener` to listen on all clicks on `document.body`.
2. Each of these clicks should increase the counter value by 1.
3. Modify the code so that each click on the counter increased the value by 10
4. Make sure that a click on the counter does not increase the value by 11!
5. 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.
1. Create a `client_action.js` (and `xml`) file, with a hello world component.
2. Register that client action in the action registry under the name `awesome_clicker.client_action`
3. 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).
4. 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:
```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.
1. Create a `clicker_service.js` file with the corresponding service.
2. This service should export a reactive value (the number of clicks) and a few functions to update it:
```js
const state = reactive({ clicks: 0 });
...
return {
state,
increment(inc) {
state.clicks += inc
}
};
```
3. 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.
4. 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.
1. Export a `useClicker` hook.
2. Update all current uses of the clicker service to the new hook:
```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`
1. Use it to display our counters (both in the systray item and the client action).
2. 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.
1. Tooltip needs an html element. Change the `ClickValue` to wrap the value in a `<span/>` element
2. 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.
1. Add a `level` number to our state. This is a number that will be incremented at some milestones, and
open new features
2. Add a `clickBots` number to our state. It represents the number of robots that have been purchased.
3. 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.
4. Set a 10s interval in the service that will increment the number of clicks by `10*clickBots`.
5. 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.
1. 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.
:::
2. 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.
1. Update the clicker model to instantiate a bus, and to trigger a `MILESTONE_1k` event when we reach 1000 clicks
for the first time.
2. Change the clicker service to listen to the same event on the model bus.
3. When that happens, use the `effect` service to display a rainbow man.
4. 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
1. increment `level` when it gets to 5k (so it should be 2)
2. Update the state to keep track of bigbots
3. bigbots should be available at `level >=2`
4. 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).
1. increment `level` when it gets to 100k (so it should be 3).
2. update the state to keep track of the power (initial value is 1).
3. change bots to use that number as a multiplier.
4. 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.
1. 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:
```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!
2. Define a function `getReward` that will select a random reward from the list of rewards that matches
the current unlock level.
3. 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
1. Patch the form controller. Each time a form controller is created, it should randomly decides (1% chance)
if a reward should be given.
2. If the answer is yes, call a method `getReward` on the model.
3. 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
1. Add a command `Open Clicker Game` to the command palette.
2. 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).
1. Update the state to keep track of various types of trees: pear/cherries, and their fruits.
2. Add a function that computes the total number of trees and fruits.
3. Define a new unlock level at `clicks >= 1 000 000`.
4. Update the client user interface to display the number of trees and fruits, and also, to buy trees.
5. 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!
1. Replace the systray item by a dropdown menu.
2. It should display the numbers of clicks, trees, and fruits, each with a nice icon.
3. Clicking on it should open a dropdown menu that displays more detailed information: each types of trees
and fruits.
4. 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:
1. Use the `Notebook` component.
2. All `click` content should be displayed in one tab.
3. 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.
1. Import `browser` from {file}`@web/core/browser/browser` to access the localstorage.
2. Serialize the state every 10s (in the same interval code) and store it on the local storage.
3. 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.
1. Add a version number to the state.
2. Define an (empty) list of migrations. A migration is an object with a `fromVersion` number, a `toVersion` number, and a `apply` function.
3. 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.
1. Add `peach` trees.
2. Increment the state version number.
3. Define a migration.
```{image} 01_build_clicker_game/peach_tree.png
:align: center
```
@@ -1,447 +0,0 @@
===============================
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
@@ -0,0 +1,481 @@
# 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:
```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
```
:::
```{eval-rst}
.. 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.
1. Create the `gallery_view.js` , `gallery_controller.js` and `gallery_controller.xml` files in
`static/src`.
2. Implement a simple hello world component in `gallery_controller.js`.
3. In `gallery_view.js`, import the controller, create a view object, and register it in the
view registry under the name `gallery`.
```{eval-rst}
.. 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);
```
4. Add `gallery` as one of the view type in the `contacts.action_contacts` action.
5. 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.
1. Import the `Layout` component and add it to the `components` of `GalleryController`.
2. 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.
```{eval-rst}
.. 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,
}
}
}
```
1. Create the `ArchParser` class in its own file.
2. Use it to read the `image_field` information.
3. 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.
```{eval-rst}
.. 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,
}
})
```
1. 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.
2. 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.
3. Modify the `setup` code to call that method in the `onWillStart` and `onWillUpdateProps`
hooks.
4. 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.
1. Import `KeepLast` from {file}`@web/core/utils/concurrency`.
2. Instanciate a `KeepLast` object in the model.
3. 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.
1. Move all the model code in its own `GalleryModel` class.
2. Move all the rendering code in a `GalleryRenderer` component.
3. 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.
1. Import `GalleryModel` and `GalleryRenderer` in the gallery view file.
2. 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.
3. Remove the hardcoded import in the controller and get them from the props.
4. 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:
```js
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:
```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.
1. Update the code to allow an optional additional attribute on the arch:
```xml
<gallery image_field="some_field" tooltip_field="some_other_field"/>
```
2. 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.
3. 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.
1. Add an RNG file that describes the current grammar:
- A mandatory attribute `image_field`.
- An optional attribute: `tooltip_field`.
2. Add some code to make sure all views are validated against this RNG file.
3. 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:
```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.
1. Add a button on each image by using the `FileUploader` component.
2. 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.
3. 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.
4. 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 ?
```{eval-rst}
.. 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>
```
1. 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.
2. Modify the gallery rng validator to accept the new arch structure.
:::{tip}
You can use this rng snippet to validate the tooltip-template tag
```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>
```
:::
3. 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.
4. Make sure that the model call the `webSearchRead` by including the parsed field names in the
specification.
5. 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.
:::
6. Register the template to Owl thanks to the `xml` function from {file}`@odoo/owl`.
7. 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)
:::
@@ -1,460 +0,0 @@
================================
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
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>`_
@@ -0,0 +1,200 @@
# Chapter 3: Customize a kanban view
We have gained an understanding of the numerous capabilities offered by the 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
```
:::
```{eval-rst}
.. 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.
1. Create a new empty component that extends the `KanbanController` component from
{file}`@web/views/kanban/kanban_controller`.
2. 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.
3. Register it in the views registry under `awesome_kanban`.
4. 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.
1. Create a `CustomerList` component which only displays a `div` with some text for now.
2. It should have a `selectCustomer` prop.
3. 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.
```xml
<xpath expr="//t[@t-component='props.Renderer']" position="before">
...
</xpath>
```
:::
4. Subclass the kanban controller to add `CustomerList` in its sub-components.
5. 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
1. Modify the `CustomerList` component to fetch a list of all customers in `onWillStart`.
2. Display the list in the template with a `t-foreach`.
3. 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
1. 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:
```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
}])
```
:::
2. 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.
```js
const customerFilters = this.env.searchModel.getSearchItems((searchItem) =>
searchItem.isFromAwesomeKanban
);
for (const customerFilter of customerFilters) {
if (customerFilter.isActive) {
this.env.searchModel.toggleSearchItem(customerFilter.id);
}
}
```
:::
3. 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.
1. Add an input of type checkbox in the `CustomerList` component, with a label "Active customers"
next to it.
2. 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.
1. 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: ''})`).
2. Modify the code to add a getter `displayedCustomers` which returns the currently active list
of customers.
3. Modify the template to use `t-model`.
## 8. Paginate customers!
1. Add a {ref}`pager <frontend/pager>` in the `CustomerList`, and only load/render the first 20
customers.
2. 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%
```
@@ -1,200 +0,0 @@
==================================
Chapter 3: Customize a kanban view
==================================
We have gained an understanding of the numerous capabilities offered by the 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%