This commit is contained in:
Géry Debongnie
2023-05-17 13:08:57 +02:00
parent c3b1865b75
commit 889ba7dfb5
2 changed files with 237 additions and 249 deletions

View File

@@ -2,298 +2,286 @@
Chapter 1: Build a Clicker game
===============================
In the previous task, we learned how to create fields and views. There is still much more to
discover in the feature-rich Odoo web framework, so let's dive in and explore more in this chapter!
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.
.. graph TD
.. subgraph "Owl"
.. C[Component]
.. T[Template]
.. H[Hook]
.. S[Slot]
.. E[Event]
.. end
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>`.
.. subgraph "odoo"[Odoo Javascript framework]
.. Services
.. Translation
.. lazy[Lazy loading libraries]
.. SCSS
.. action --> Services
.. rpc --> Services
.. orm --> Services
.. Fields
.. Views
.. Registries
.. end
.. odoo[Odoo JavaScript framework] --> Owl
.. figure:: 01_build_clicker_game/previously_learned.svg
:align: center
:width: 70%
This is the progress that we have made in discovering the JavaScript web framework at the end of
:doc:`02_create_customize_fields`.
.. admonition:: Goal
.. image:: 01_build_clicker_game/kitten_mode.png
:align: center
.. spoiler:: Solutions
The solutions for each exercise of the chapter are hosted on the
`official Odoo tutorials repository
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_tshirt>`_.
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_clicker>`_.
1. Interacting with the notification system
===========================================
.. note::
This task depends on :doc:`the previous exercises <02_create_customize_fields>`.
1. Create a systray item
========================
After using the :guilabel:`Print Label` button for some t-shirt tasks, it is apparent that there
should be some feedback that the `print_label` action is completed (or failed, for example, the
printer is not connected or ran out of paper).
To get started, we want to display a counter in the systray.
.. exercise::
#. Display a :ref:`notification <frontend/services/notification>` message when the action is
completed successfully, and a warning if it failed.
#. If it failed, the notification should be permanent.
#. Create a `clicker_systray_item.js` (and `xml`) file with a hello world 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/notification.png
:align: center
:scale: 60%
And voila, we have a completely working clicker game!
.. seealso::
`Example: Using the notification service
<{GITHUB_PATH}/addons/web/static/src/views/fields/image_url/image_url_field.js>`_
2. Count external clicks
========================
2. Add a systray item
=====================
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.
Our beloved leader wants to keep a close eye on new orders. He wants to see the number of new,
unprocessed orders at all time. Let's do that with a systray item.
#. 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
A :ref:`systray <frontend/registries/systray>` item is an element that appears in the system tray,
which is a small area located on the right-hand side of the navbar. The systray is used to display
notifications and provide access to certain features.
Make sure that a click on the counter does not increase the value by 11!
.. exercise::
3. Create a client action
=========================
#. Create a systray component that connects to the statistics service we made previously.
#. Use it to display the number of new orders.
#. Clicking on it should open a list view with all of those orders.
#. Bonus point: avoid making the initial RPC by adding the information to the session info. The
session info is given to the web client by the server in the initial response.
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.
.. image:: 01_build_clicker_game/systray.png
:align: center
#. Create a `clicker_client_action.js` (and `xml`) file, with a hello world component
#. Register that client action in the action registry under the name `clicker_action`
#. Add a button on the systray item with the text `Open`. Clicking on it should open the
client action `clicker_action` (use the action service to do that)
.. seealso::
- `Example: Systray item <{GITHUB_PATH}/addons/web/static/src/webclient/user_menu/user_menu.js>`_
- `Example: Adding some information to the "session info"
<{GITHUB_PATH}/addons/barcodes/models/ir_http.py>`_
- `Example: Reading the session information
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/barcodes/static/src/barcode_service.js#L5>`_
3. Real life update
===================
So far, the systray item from above does not update unless the user refreshes the browser. Let us
do that by calling periodically (for example, every minute) the server to reload the information.
.. exercise::
#. The `tshirt` service should periodically reload its data.
Now, the question arises: how is the systray item notified that it should re-render itself? It can
be done in various ways but, for this training, we choose to use the most *declarative* approach:
.. exercise::
2. Modify the `tshirt` service to return a `reactive
<{OWL_PATH}/doc/reference/reactivity.md#reactive>`_ object. Reloading data should update the
reactive object in place.
3. The systray item can then perform a `useState
<{OWL_PATH}/doc/reference/reactivity.md#usestate>`_ on the service return value.
4. This is not really necessary, but you can also *package* the calls to `useService` and
`useState` in a custom hook `useStatistics`.
.. seealso::
- `Documentation on reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_
- `Example: Use of reactive in a service
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/debug/profiling/profiling_service.js#L30>`_
4. Add a command to the command palette
=======================================
Now, let us see how we can interact with the command palette. The command palette is a feature that
allows users to quickly access various commands and functions within the application. It is accessed
by pressing `CTRL+K` in the Odoo interface.
.. exercise::
Modify the :ref:`image preview field <tutorials/master_odoo_web_framework/image_preview_field>`
to add a command to the command palette to open the image in a new browser tab (or window).
Ensure the command is only active whenever a field preview is visible on the screen.
.. image:: 01_build_clicker_game/new_command.png
:align: center
.. seealso::
`Example: Using the useCommand hook
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/debug/debug_menu.js#L15>`_
5. Monkey patching a component
4. Move the state to a service
==============================
Often, we can achieve what we want by using existing extension points that allow for customization,
such as registering something in a registry. Sometimes, however, it happens that we want to modify
something that has no such mechanism. In that case, we must fall back on a less safe form of
customization: monkey patching. Almost everything in Odoo can be monkey patched.
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.
Bafien, our beloved leader, heard about employees performing better if they are constantly being
watched. Since he cannot be there in person for each of his employees, he tasked you with updating
the user interface to add a blinking red eye in the control panel. Clicking on that eye should open
a dialog with the following message: "Bafien is watching you. This interaction is recorded and may
be used in legal proceedings if necessary. Do you agree to these terms?"
#. 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:
.. exercise::
.. code-block:: js
#. :ref:`Inherit <reference/qweb/template_inheritance>` the `web.Breadcrumbs` template of the
`ControlPanel component <{GITHUB_PATH}/addons/web/static/src/search/control_panel>`_ to add an
icon next to the breadcrumbs. You might want to use the `fa-eye` or `fa-eyes` icons.
#. :doc:`Patch </developer/reference/frontend/patching_code>` the component to display the
message on click by using `the dialog service
<{GITHUB_PATH}/addons/web/static/src/core/dialog/dialog_service.js>`_. You can use
`ConfirmationDialog
<{GITHUB_PATH}/addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js>`_.
#. Add the CSS class `blink` to the element representing the eye and paste the following code in
a new CSS file located in your patch's directory.
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.
.. code-block:: css
5. Humanize the displayed value
===============================
.blink {
animation: blink-animation 1s steps(5, start) infinite;
-webkit-animation: blink-animation 1s steps(5, start) infinite;
}
@keyframes blink-animation {
to {
visibility: hidden;
}
}
@-webkit-keyframes blink-animation {
to {
visibility: hidden;
}
}
We will in the future display large numbers, so let us get ready for that. There is a `humanize` function that
format numbers in a easier to comprehend way: for example, `1234` could be formatted as `1.2k`
.. image:: 01_build_clicker_game/bafien_eye.png
:align: center
:scale: 60%
#. Use it to display our counters (both in the systray item and the client action)
#. Wrap the value in a span element with a tooltip that display the exact value
#. Factorize both of these use in a `ClickValue` component
.. image:: 01_build_clicker_game/confirmation_dialog.png
:align: center
:scale: 60%
6. Buy ClickBots
==================
.. seealso::
- `Code: The patch function
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/utils/patch.js#L16>`_
- `The Font Awesome website <https://fontawesome.com/>`_
- `Example: Using the dialog service
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/board/static/src/board_controller.js#L88>`_
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.
6. Fetching orders from a customer
==================================
#. Add a `unlockLevel` 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 `unlockLevel >= 1`), with a `Buy`
button that is enabled if `clicks >= 1000`. The `Buy` button should increment the number of clickbots by 1.
Let's see how to use some standard components to build a powerful feature combining autocomplete,
fetching data, and fuzzy lookup. We will add an input in our dashboard to easily search all orders
from a given customer.
#. Set a 10s interval in the service that will increment the number of clicks by `10*clickBots`.
.. exercise::
7. Notify when a milestone is reached
=====================================
#. Update :file:`tshirt_service.js` to add a `loadCustomers` method, which returns a promise that
returns the list of all customers (and only performs the call once).
#. Add the `AutoComplete component <{GITHUB_PATH}/addons/web/static/src/core/autocomplete>`_ to
the dashboard, next to the buttons in the control panel.
#. Fetch the list of customers with the tshirt service, and display it in the AutoComplete
component, filtered by the `fuzzyLookup
<{GITHUB_PATH}/addons/web/static/src/core/utils/search.js>`_ method.
There is not much feedback that something changed when we reached 1k clicks. Let us use the `effect` service
to communicate that information clearly.
.. image:: 01_build_clicker_game/autocomplete.png
:align: center
:scale: 60%
#. When we reach 1000 clicks, use the `effect` service to display a rainbow man.
#. Add some text to explain that the user can now buy clickbots.
7. Reintroduce Kitten Mode
8. 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 `unlockLevel` when it gets to 5k (so it should be 2)
#. Update the state to keep track of bigbots
#. bigbots should be available at `unlockLevel >=2`
#. Add the corresponding information to the client action
9. 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 `unlockLevel >= 3`, and multiplies the action of the bots (so, instead of providing
one click, clickbots now provide us with `multiplier` clicks).
#. increment `unlockLevel` 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)
10. 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(state) {
state.clickbots += 1;
},
maxLevel: 3,
},
{
description: "Get 10 click bot",
apply(state) {
state.clickbots += 10;
},
minLevel: 3,
maxLevel: 4,
},
{
description: "Increase bot power!",
apply(state) {
state.power += 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.
11. 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 `giveReward` on the service
#. 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
12. Only Open the client action if necessary
============================================
Now, the previous exercise has a small flaw: imagine that the player opens a form view, get a reward notification,
then open the client action from the systray item, and finally collect the reward: the game will then open
the client action twice (look at the breadcrumbs).
This is actually quite a tricky situation: we want to open the `clicker` client action only if it is not
currently being open. This is easy to solve: the action service provides us with a way to check what the current
action controller is: `getCurrentController`.
#. Use `getCurrentController` from the action service to check if the current action is the game, and only open
it if it is not true.
11. Add commands in command palette
===================================
#. Add a command `Open Clicker Game` to the command palette
#. Add another command: `Buy 1 click bot`
12. 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
13. 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, ...
14. 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
15. Persist the game state
==========================
Let us add a special mode to Odoo: whenever the URL contains `kitten=1`, we will display a kitten in
the background of Odoo, because we like kittens.
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.
.. exercise::
#. Use the `localstorage` service
#. 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
#. Create a `kitten` service, which should check the content of the active URL hash with the
help of the :ref:`router service <frontend/services/router>`. If `kitten` is set in the URL,
add the class `o-kitten-mode` to the document body.
#. Add the following SCSS in :file:`kitten_mode.scss`:
16. Introduce state migration system
====================================
.. code-block:: css
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!
.o-kitten-mode {
background-image: url(https://upload.wikimedia.org/wikipedia/commons/5/58/Mellow_kitten_%28Unsplash%29.jpg);
background-size: cover;
background-attachment: fixed;
}
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.
.o-kitten-mode > * {
opacity: 0.9;
}
#. Add a version number to the state
#. Define an (empty) list of migrations. A migration is an object with a `fromVersion` 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
#. Add a command to the command palette to toggle the kitten mode. Toggling the kitten mode
should toggle the class `o-kitten-mode` and update the current URL accordingly.
.. image:: 01_build_clicker_game/kitten_mode.png
:align: center
8. Lazy loading our dashboard
17. Add another type of trees
=============================
This is not really necessary, but the exercise is interesting. Imagine that our awesome dashboard is
a large application with potentially multiple external libraries and lots of code/styles/templates.
Also, suppose that the dashboard is used only by some users in some business flows. It would be
interesting to lazy load it in order to speed up the loading of the web client in most cases.
To test our migration system, let us add a new type of trees: peaches.
So, let us do that!
.. exercise::
#. Modify the manifest to create a new :ref:`bundle <reference/assets_bundle>`
`awesome_tshirt.dashboard`.
#. Add the awesome dashboard code to this bundle. Create folders and move files if needed.
#. Remove the code from the `web.assets_backend` bundle so that it is not loaded twice.
So far, we only removed the dashboard from the main bundle; we now want to lazy load it. Currently,
no client action is registered in the action registry.
.. exercise::
4. Create a new file :file:`dashboard_loader.js`.
5. Copy the code registering `AwesomeDashboard` to the dashboard loader.
6. Register `AwesomeDashboard` as a `LazyComponent
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
addons/web/static/src/core/assets.js#L265-L282>`_.
7. Modify the code in the dashboard loader to use the lazy component `AwesomeDashboard`.
If you open the :guilabel:`Network` tab of your browser's dev tools, you should see that
:file:`awesome_tshirt.dashboard.min.js` is now loaded only when the Dashboard is first accessed.
.. seealso::
:ref:`Documentation on assets <reference/assets>`
#. Add `peach` trees
#. Increment the state version number
#. Define a migration