convert rst to markdown
This commit is contained in:
@@ -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%
|
||||
Reference in New Issue
Block a user