From 889ba7dfb50e31b95b652fded85c9cebba8ea136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 17 May 2023 13:08:57 +0200 Subject: [PATCH] wip --- .../tutorials/master_odoo_web_framework.rst | 2 +- .../01_build_clicker_game.rst | 484 +++++++++--------- 2 files changed, 237 insertions(+), 249 deletions(-) diff --git a/content/developer/tutorials/master_odoo_web_framework.rst b/content/developer/tutorials/master_odoo_web_framework.rst index bf6f676f7..cdc67e10f 100644 --- a/content/developer/tutorials/master_odoo_web_framework.rst +++ b/content/developer/tutorials/master_odoo_web_framework.rst @@ -40,7 +40,7 @@ modify it to suit our needs. Also, it is a realistic project, that will feature that arises while working on Odoo. -.. _howtos/master_odoo_web_framework/setup: +.. _tutorials/master_odoo_web_framework/setup: Setup ===== diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game.rst b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game.rst index 4f55baf1b..7f53a4cd7 100644 --- a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game.rst +++ b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game.rst @@ -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 `_, +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 `. -.. 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 - `_. + `_. -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 ` 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 ` 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 - `_ - -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 - `_ - -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 ` - 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 - `_ - -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 ` 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 ` 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 - `_ - - `The Font Awesome website `_ - - `Example: Using the dialog service - `_ +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 `. 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 ` - `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 - `_. - 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 ` +#. Add `peach` trees +#. Increment the state version number +#. Define a migration \ No newline at end of file