diff --git a/.gitignore b/.gitignore
index b6e47617de1..966147abab3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,6 @@ dmypy.json
# Pyre type checker
.pyre/
+
+# mac os garbage
+.DS_Store
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000000..277f7c9898f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python.languageServer": "None"
+}
diff --git a/1_learn_owl.md b/1_learn_owl.md
new file mode 100644
index 00000000000..9354066240a
--- /dev/null
+++ b/1_learn_owl.md
@@ -0,0 +1,560 @@
+# Module 1: Learn Owl 🦉
+
+This chapter introduces the Owl framework, a tailor-made component system for
+Odoo. The main building blocks of Owl are components and templates. In Owl,
+every part of the user interface is managed by a component: they hold the logic
+and define the templates that are used to render the user interface. In
+practice, a component is represented by a small JavaScript class subclassing the
+Component class.
+
+To get started, you need a running Odoo server and a development environment
+setup. Before getting into the exercises, make sure you have a working setup.
+
+Start your development environment with a new database, on the `master` branch,
+and make sure to add this repository in the addons path. Then, install the
+`awesome_owl` addon. Once it is done, you can open the `/awesome_owl` route
+(typically on `localhost:8069/awesome_owl`). If you see the `hello world`
+message, you are ready to start!
+
+The `awesome_owl addon` provides a simplified environment that only contains Owl
+and a few other files. The goal is to learn Owl itself, without relying on Odoo
+web client code.
+
+## Content
+
+- [Resources](#resources)
+- [Example: a Counter component](#example-a-counter-component)
+- [1. Displaying a Counter](#1-displaying-a-counter)
+- [2. Extract Counter in a sub component](#2-extract-counter-in-a-sub-component)
+- [3. A simple Card component](#3-a-simple-card-component)
+- [4. Using markup to display html](#4-using-markup-to-display-html)
+- [5. Props validation](#5-props-validation)
+- [6. The sum of two Counter](#6-the-sum-of-two-counter)
+- [6B. Bonus Project](#6b-bonus-project)
+- [7. A todo list](#7-a-todo-list)
+- [8. Use dynamic attributes](#8-use-dynamic-attributes)
+- [9. Adding a todo](#9-adding-a-todo)
+- [10. Focusing the input](#10-focusing-the-input)
+- [11. Toggling todos](#11-toggling-todos)
+- [12. Deleting todos](#12-deleting-todos)
+- [13. Improved state management](#13-improved-state-management)
+- [13B. Bonus Project: Todo class](#13b-bonus-project-todo-class)
+- [14. Generic Card with slots](#14-generic-card-with-slots)
+- [15. Minimizing card content](#15-minimizing-card-content)
+
+## Resources
+
+- [Owl repository](https://github.com/odoo/owl)
+- [Owl documentation](https://github.com/odoo/owl#documentation)
+
+## Example: a Counter component
+
+First, let us have a look at a simple example. The Counter component shown below
+is a component that maintains an internal number value, displays it, and updates
+it whenever the user clicks on the button.
+
+```js
+import { Component, useState } from "@odoo/owl";
+
+export class Counter extends Component {
+ static template = "my_module.Counter";
+
+ setup() {
+ this.state = useState({ value: 0 });
+ }
+
+ increment() {
+ this.state.value++;
+ }
+}
+```
+
+The Counter component specifies the name of a template that represents its html.
+It is written in XML using the QWeb language:
+
+```xml
+
+
+
Counter:
+
+
+
+```
+
+## 1. Displaying a counter
+
+
+
+As a first exercise, let us modify the `Playground` component located in
+`awesome_owl/static/src/` to turn it into a counter. To see the result, you can
+go to the `/awesome_owl` route with your browser.
+
+1. Modify `playground.js` so that it acts as a counter like in the example
+ above. Keep `Playground` for the class name. You will need to use the
+ `useState` hook so that the component is updated whenever the button is
+ clicked.
+2. In the same component, create an increment method.
+3. Modify the template in `playground.xml` so that it displays your counter
+ variable. Use
+ [`t-esc`](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#outputting-data)
+ to output the data.
+4. Add a button in the template and specify a
+ [`t-on-click`](https://github.com/odoo/owl/blob/master/doc/reference/event_handling.md#event-handling)
+ attribute in the button to trigger the increment method whenever the button
+ is clicked.
+
+This exercise showcases an important feature of Owl: the reactivity system. The
+`useState` function wraps a value in a proxy so Owl can keep track of which
+component needs which part of the state, so it can be updated whenever a value
+has been changed. Try removing the `useState` function and see what happens.
+
+**Tip:** the Odoo JavaScript files downloaded by the browser are minified. For
+debugging purpose, it’s easier when the files are not minified. Switch to debug
+mode with assets so that the files are not minified.
+
+## 2. Extract Counter in a sub component
+
+For now we have the logic of a counter in the `Playground` component, but it is
+not reusable. Let us see how to create a
+[sub-component](https://github.com/odoo/owl/blob/master/doc/reference/component.md#sub-components)
+from it:
+
+1. Extract the counter code from the `Playground` component into a new `Counter`
+ component.
+2. You can do it in the same file first, but once it’s done, update your code to
+ move the `Counter` in its own folder and file. Import it relatively from
+ ./counter/counter.
+3. Make sure the template is in its own file, with the same name.
+4. Use in the template of the Playground component to add two
+ counters in your playground.
+
+
+
+**Tip:** by convention, most components code, template and css should have the
+same snake-cased name as the component. For example, if we have a `TodoList`
+component, its code should be in `todo_list.js`, `todo_list.xml` and if
+necessary, `todo_list.scss`
+
+## 3. A simple Card component
+
+Components are really the most natural way to divide a complicated user
+interface into multiple reusable pieces. But to make them truly useful, it is
+necessary to be able to communicate some information between them. Let us see
+how a parent component can provide information to a sub component by using
+attributes (most commonly known as
+[props](https://github.com/odoo/owl/blob/master/doc/reference/props.md)).
+
+The goal of this exercise is to create a `Card` component, that takes two props:
+`title` and `content`. For example, here is how it could be used:
+
+```xml
+
+```
+
+The above example should produce some html using bootstrap that look like this:
+
+```html
+
+
+
my title
+
some content
+
+
+```
+
+1. Create a `Card` component
+2. Import it in `Playground` and display a few cards in its template
+
+
+
+## 4. Using markup to display html
+
+If you used `t-esc` in the previous exercise, then you may have noticed that Owl
+automatically escapes its content. For example, if you try to display some html
+like this: `` with
+`this.html = "
some content
""`, the resulting output will simply
+display the html as a string.
+
+In this case, since the `Card` component may be used to display any kind of
+content, it makes sense to allow the user to display some html. This is done
+with the
+[`t-out`](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#outputting-data)
+directive.
+
+However, displaying arbitrary content as html is dangerous, it could be used to
+inject malicious code, so by default, Owl will always escape a string unless it
+has been explicitely marked as safe with the `markup` function.
+
+1. Update `Card` to use `t-out`,
+2. Update `Playground` to import `markup`, and use it on some html values
+3. Make sure that you see that normal strings are always escaped, unlike
+ markuped strings.
+
+
+
+**Note:** the `t-esc` directive can still be used in Owl templates. It is
+slightly faster than `t-out`.
+
+## 5. Props validation
+
+The `Card` component has an implicit API. It expects to receive two strings in
+its props object: the `title` and the `content`. Let us make that API more
+explicit. We can add a props definition that will let Owl perform a validation
+step in
+[dev mode](https://github.com/odoo/owl/blob/master/doc/reference/app.md#dev-mode).
+You can activate the dev mode in the
+[App configuration](https://github.com/odoo/owl/blob/master/doc/reference/app.md#configuration)
+(but it is activated by default on the `awesome_owl` playground).
+
+It is a good practice to do props validation for every component.
+
+1. Add
+ [props validation](https://github.com/odoo/owl/blob/master/doc/reference/props.md#props-validation)
+ to the Card component.
+2. Rename the `title` props into something else in the playground template, then
+ check in the Console tab of your browser’s dev tools that you can see an
+ error.
+
+## 6. The sum of two Counter
+
+We saw in a previous exercise that `props` can be used to provide information
+from a parent to a child component. Now, let us see how we can communicate
+information in the opposite direction: in this exercise, we want to display two
+`Counter` components, and below them, the sum of their values. So, the parent
+component (`Playground`) need to be informed whenever one of the `Counter` value
+is changed.
+
+This can be done by using a
+[callback prop](https://github.com/odoo/owl/blob/master/doc/reference/props.md#binding-function-props):
+a prop that is a function meant to be called back. The child component can
+choose to call that function with any argument. In our case, we will simply add
+an optional `onChange` prop that will be called whenever the `Counter` component
+is incremented.
+
+1. Add prop validation to the `Counter` component: it should accept an optional
+ `onChange` function prop.
+2. Update the `Counter` component to call the `onChange` prop (if it exists)
+ whenever it is incremented.
+3. Modify the `Playground` component to maintain a local state value (`sum`),
+ initially set to 2, and display it in its template
+4. Implement an `incrementSum` method in `Playground`
+5. Give that method as a prop to two (or more!) sub `Counter` components.
+
+
+
+**Important:** there is a subtlety with callback props: they usually should be
+defined with the .bind suffix. See the
+[documentation](https://github.com/odoo/owl/blob/master/doc/reference/props.md#binding-function-props).
+
+## 6B. Bonus Project
+
+The code for the previous exercise is designed from a pedagogical perspective,
+but the design is actually somewhat strange/fragile. This is because we are in a
+situation where a parent component need to compute some value that are actually
+owned by its children. So, we end up with a fragile design, where we use events
+to coordinate components.
+
+A better solution would be to reorganize the code so that the playground hold a
+list of values, and give them to each `Counter`.
+
+1. Move the state from each `Counter` component to the `Playground` component
+2. Use a getter to define the sum of each value as a derived state
+
+## 7. A todo list
+
+Let us now discover various features of Owl by creating a todo list. We need two
+components: a `TodoList` component that will display a list of `TodoItem`
+components. The list of todos is a state that should be maintained by the
+`TodoList`.
+
+For this tutorial, a `todo` is an object that contains three values:
+
+- an `id` (number),
+- a `description` (string),
+- and a flag `isCompleted` (boolean):
+
+```js
+{ id: 3, description: "buy milk", isCompleted: false }
+```
+
+1. Create a `TodoList` and a `TodoItem` components.
+2. The `TodoItem` component should receive a `todo` as a prop, and display its
+ `id` and `description` in a `div`.
+3. For now, hardcode the list of todos:
+
+ ```js
+ // in TodoList
+ this.todos = useState([
+ { id: 3, description: "buy milk", isCompleted: false },
+ ]);
+ ```
+
+4. Use
+ [t-foreach](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#loops)
+ to display each todo in a `TodoItem`.
+5. Display a `TodoList` in the playground.
+6. Add props validation to `TodoItem`.
+
+
+
+**Tip:** since the `TodoList` and `TodoItem` components are so tightly coupled,
+it makes sense to put them in the same folder.
+
+**Note:** the `t-foreach` directive is not exactly the same in Owl as the QWeb
+python implementation: it requires a `t-key` unique value, so that Owl can
+properly reconcile each element.
+
+## 8. Use dynamic attributes
+
+For now, the `TodoItem` component does not visually show if the todo is
+completed. Let us do that by using a
+[dynamic attributes](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#dynamic-attributes).
+
+1. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on
+ the `TodoItem` root element if it is completed.
+2. Change the hardcoded `this.todos` value to check that it is properly
+ displayed.
+
+Even though the directive is named `t-att` (for attribute), it can be used to
+set a class value (and other html properties such as the value of an input).
+
+
+
+**Tip:** Owl let you combine static class values with dynamic values. The
+following example will work as expected:
+
+```xml
+
+```
+
+See also:
+[Owl: Dynamic class attributes](https://github.com/odoo/owl/blob/master/doc/reference/templates.md#dynamic-class-attribute)
+
+## 9. Adding a todo
+
+So far, the todos in our list are hard-coded. Let us make it more useful by
+allowing the user to add a todo to the list.
+
+
+
+1. Remove the hardcoded values in the `TodoList` component:
+
+```js
+this.todos = useState([]);
+```
+
+2. Add an input above the task list with placeholder _Enter a new task_.
+3. Add an
+ [event handler](https://github.com/odoo/owl/blob/master/doc/reference/event_handling.md)
+ on the keyup event named addTodo.
+4. Implement `addTodo` to check if enter was pressed (`ev.keyCode === 13`), and
+ in that case, create a new todo with the current content of the input as the
+ description and clear the input of all content.
+5. Make sure the todo has a unique id. It can be just a counter that increments
+ at each todo.
+6. Bonus point: don’t do anything if the input is empty.
+
+See also:
+[Owl reactivity](https://github.com/odoo/owl/blob/master/doc/reference/reactivity.md)
+
+## 10. Focusing the input
+
+Let’s see how we can access the DOM with
+[t-ref](https://github.com/odoo/owl/blob/master/doc/reference/refs.md) and
+[useRef](https://github.com/odoo/owl/blob/master/doc/reference/refs.md). The
+main idea is that you need to mark the target element in the component template
+with a `t-ref`:
+
+```xml
+
hello
+```
+
+Then you can access it in the JS with the
+[useRef](https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#useref)
+hook. However, there is a problem if you think about it: the actual html element
+for a component does not exist when the component is created. It only exists
+when the component is mounted. But hooks have to be called in the setup method.
+So, `useRef` returns an object that contains a `el` (for element) key that is
+only defined when the component is mounted.
+
+```js
+setup() {
+ this.myRef = useRef('some_name');
+ onMounted(() => {
+ console.log(this.myRef.el);
+ });
+}
+```
+
+1. Focus the `input` from the previous exercise. This should be done from the
+ `TodoList` component (note that there is a `focus` method on the input html
+ element).
+2. Bonus point: extract the code into a specialized hook `useAutofocus` in a new
+ `utils.js` file.
+
+
+
+**Tip:** Refs are usually suffixed by `Ref` to make it obvious that they are
+special objects:
+
+```js
+this.inputRef = useRef("input");
+```
+
+## 11. Toggling todos
+
+Now, let’s add a new feature: mark a todo as completed. This is actually
+trickier than one might think. The owner of the state is not the same as the
+component that displays it. So, the `TodoItem` component needs to communicate to
+its parent that the todo state needs to be toggled. One classic way to do this
+is by adding a
+[callback prop](https://github.com/odoo/owl/blob/master/doc/reference/props.md#binding-function-props)
+`toggleState`.
+
+1. Add an input with the attribute `type="checkbox"` before the id of the task,
+ which must be checked if the state `isCompleted` is true.
+2. Add a callback props `toggleState` to `TodoItem`.
+3. Add a `change` event handler on the input in the `TodoItem` component and
+ make sure it calls the `toggleState` function with the todo `id`.
+4. Make it work!
+
+
+
+**Tip:** Owl does not create attributes computed with the `t-att` directive if
+its expression evaluates to a falsy value.
+
+## 12. Deleting todos
+
+The final touch is to let the user delete a todo.
+
+1. Add a new callback prop `removeTodo` in `TodoItem`.
+2. Insert `` in the template of the
+ `TodoItem` component.
+3. Whenever the user clicks on it, it should call the `removeTodo` method.
+4. Make it work!
+
+
+
+**Tip:** If you’re using an array to store your todo list, you can use the
+JavaScript `splice` function to remove a todo from it.
+
+```js
+// find the index of the element to delete
+const index = list.findIndex((elem) => elem.id === elemId);
+if (index >= 0) {
+ // remove the element at index from list
+ list.splice(index, 1);
+}
+```
+
+## 13. Improved state management
+
+Note: this exercise (and the next one) is more advanced. Feel free to skip it.
+
+So far, the `TodoList` has a simple architecture: a parent component `TodoList`
+that holds the state and the update methods (add/remove/toggle), and a child
+component `TodoItem`. This is fine for most situations, but at some point, if we
+expect more and more complex features to be implemented, it is useful to
+separate the _state management_ (or _model_) code from the UI code.
+
+1. Define a `TodoModel` class. It should have a list of todos, and four methods:
+ `getTodo`, `add`, `remove`, `toggle`.
+2. Move all state related code from `TodoList` to `TodoModel`
+3. Modify `TodoList` to instantiate a `TodoModel` in a `useState`:
+
+ ```js
+ // useState is here to make the model reactive
+ this.model = useState(new TodoModel());
+ ```
+
+4. Update `TodoItem` to take 2 props: `model` and `id`
+5. Make it work!
+
+Congratulation, your state management code is now separate from your UI code!
+
+**Tip:** it is often useful to use a `t-set` directive in a template to compute
+once an important value. For example, in the template for `TodoItem`, we can do
+this:
+
+```xml
+
+```
+
+## 13B. Bonus Project: Todo class
+
+The previous exercise successfully refactored the code to make it easier to
+maintain. However, there is still something quite awkward: the `TodoItem`
+receive two props, the model and the id for the todo. It is necessary to allow
+the `TodoItem` to toggle and to remove the todo.
+
+It would be nicer if we could package the state and its update function in one
+convenient object. That's what OOP is for! It turns out that using simple
+classes work well with Owl and the reactivity system (as long as you stay away
+from private fields).
+
+1. In `todo_model.js`, define the following `Todo` class:
+
+ ```js
+ export class Todo {
+ static nextId = 1;
+
+ constructor(model, description) {
+ this._model = model;
+ this.id = Todo.nextId++;
+ this.description = description;
+ this.isCompleted = false;
+ }
+
+ toggle() {
+ this.isCompleted = !this.isCompleted;
+ }
+
+ remove() {
+ this._model.remove(this.id);
+ }
+ }
+ ```
+
+2. Adapt `TodoModel` to use it
+3. Remove `toggle` method from `TodoModel` (no longer necessary)
+4. adapt `TodoItem` component to only receive a `Todo` instance as props
+5. make it work!
+
+This is a very useful pattern when working with complicated objects.
+
+## 14. Generic Card with slots
+
+In a previous exercise, we built a simple `Card` component. But it is honestly
+quite limited. What if we want to display some arbitrary content inside a card,
+such as a sub-component? Well, it does not work, since the content of the card
+is described by a string. It would however be very convenient if we could
+describe the content as a piece of template.
+
+This is exactly what Owl’s
+[slot](https://github.com/odoo/owl/blob/master/doc/reference/slots.md) system is
+designed for: allowing to write generic components.
+
+Let us modify the `Card` component to use slots:
+
+1. Remove the `content` prop.
+2. Use the default slot to define the body.
+3. Insert a few cards with arbitrary content, such as a `Counter` component.
+4. (bonus) Add prop validation.
+
+
+
+**See also:**
+[Bootstrap: documentation on cards](https://getbootstrap.com/docs/5.2/components/card/)
+
+## 15. Minimizing card content
+
+Finally, let’s add a feature to the `Card` component, to make it more
+interesting: we want a button to toggle its content (show it or hide it).
+
+1. Add a state to the `Card` component to track if it is open (the default) or
+ not
+2. Add a `t-if` in the template to conditionally render the content
+3. Add a button in the header, and modify the code to flip the state when the
+ button is clicked
+
+
diff --git a/2_make_a_dashboard.md b/2_make_a_dashboard.md
new file mode 100644
index 00000000000..b6650ba8655
--- /dev/null
+++ b/2_make_a_dashboard.md
@@ -0,0 +1,400 @@
+# Module 2: Make a Dashboard
+
+It is now time to learn about the Odoo JavaScript framework in its entirety, as
+used by the web client. This document is a complete standalone project, in which
+we will implement a dashboard client action. It is mostly an excuse to discover
+and practice many features of the odoo web framework!
+
+
+
+To get started, you need a running Odoo server and a development environment
+setup. Before getting into the exercises, make sure you have a working setup.
+Start your odoo server with this repository in the addons path, then install the
+`awesome_dashboard` addon.
+
+For this project, we will start from the empty dashboard provided by the
+`awesome_dashboard` addon. Then, we will progressively add features to it using
+the Odoo JavaScript framework.
+
+Note that a lot of theory is covered in the slides for this event. Also, don't
+hesitate to ask questions!
+
+## Content
+
+- [1. A Common Layout](#1-a-common-layout)
+- [2. Add some buttons for quick navigation](#2-add-some-buttons-for-quick-navigation)
+- [3. Add a dashboard item](#3-add-a-dashboard-item)
+- [4. Call the server, add some statistics](#4-call-the-server-add-some-statistics)
+- [5. Cache network calls, create a service](#5-cache-network-calls-create-a-service)
+- [6. Display a pie chart](#6-display-a-pie-chart)
+- [7. Periodic Updates](#7-periodic-updates)
+- [8. Lazy loading the dashboard](#8-lazy-loading-the-dashboard)
+- [9. Making our dashboard generic](#9-making-our-dashboard-generic)
+- [10. Making our dashboard extensible](#10-making-our-dashboard-extensible)
+- [11. Add and remove dashboard items](#11-add-and-remove-dashboard-items)
+- [12. Going further](#12-going-further)
+
+## 1. A Common Layout
+
+Most screens in the Odoo web client uses a common layout: a control panel on
+top, with some buttons, and a main content zone just below. This is done using
+the `Layout` component, available in `@web/search/layout`.
+
+1. Update the `AwesomeDashboard` component located in
+ `awesome_dashboard/static/src/` to use the `Layout` component. You can use
+ `{controlPanel: {} }` for the display props of the `Layout` component.
+2. Add a className prop to `Layout`: `className="'o_dashboard h-100'"`
+3. Add a `dashboard.scss` file in which you set the `background-color` of
+ .`o_dashboard` to gray (or your favorite color)
+4. Open http://localhost:8069/odoo, then open the Awesome Dashboard app, and see
+ the result.
+
+
+
+#### See also
+
+- Example:
+ [use of Layout in client action](https://github.com/odoo/odoo/blob/master/addons/web/static/src/webclient/actions/reports/report_action.js)
+ and
+ [template](https://github.com/odoo/odoo/blob/master/addons/web/static/src/webclient/actions/reports/report_action.xml)
+- Example:
+ [use of Layout in kanban views](https://github.com/odoo/odoo/blob/master/addons/web/static/src/views/kanban/kanban_controller.xml)
+
+## 2. Add some buttons for quick navigation
+
+One important service provided by Odoo is the `action` service: it can execute
+all kind of standard actions defined by Odoo. For example, here is how one
+component could execute an action by its xml id:
+
+```js
+import { useService } from "@web/core/utils/hooks";
+...
+setup() {
+ this.action = useService("action");
+}
+openSettings() {
+ this.action.doAction("base_setup.action_general_configuration");
+}
+...
+```
+
+Let us now add two buttons to our control panel:
+
+1. A button `Customers`, which opens a kanban view with all customers (this
+ action already exists, so you should use its
+ [xml id](https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/odoo/addons/base/views/res_partner_views.xml#L510)).
+2. A button `Leads`, which opens a dynamic action on the `crm.lead` model with a
+ list and a form view. Follow the example of
+ [this use of the action service](https://github.com/odoo/odoo/blob/ef424a9dc22a5abbe7b0a6eff61cf113826f04c0/addons/account/static/src/components/journal_dashboard_activity/journal_dashboard_activity.js#L28-L35).
+
+
+
+**See also:**
+[code: action service](https://github.com/odoo/odoo/blob/master/addons/web/static/src/webclient/actions/action_service.js)
+
+## 3. Add a dashboard item
+
+Let us now improve the content of this dashboard.
+
+1. Create a generic `DashboardItem` component that display its default slot in a
+ nice card layout. It should take an optional `size` number props, that
+ default to 1. The width should be hardcoded to `(18*size)rem`.
+2. Add two cards to the dashboard. One with no size, and the other with a size
+ of 2.
+
+
+
+**See also:**
+[Owl slot system](https://github.com/odoo/owl/blob/master/doc/reference/slots.md)
+
+## 4. Call the server, add some statistics
+
+Let’s improve the dashboard by adding a few dashboard items to display real
+business data. The `awesome_dashboard` addon provides a
+`/awesome_dashboard/statistics` route that is meant to return some interesting
+information.
+
+To call a specific controller, we need to use the `rpc` function. This function
+`rpc(route, params, settings)` is the low level communication code that will
+create a network request to the server in jsonrpc, then will return a promise
+with the result. A basic request could look like this:
+
+```js
+import { rpc } from "@web/core/network/rpc";
+...
+setup() {
+ onWillStart(async () => {
+ const result = await rpc("/my/controller", {a: 1, b: 2});
+ // ...
+ });
+}
+...
+```
+
+1. Update Dashboard so that it uses the `rpc` function.
+2. Call the statistics route `/awesome_dashboard/statistics` in the
+ `onWillStart` hook.
+3. Display a few cards in the dashboard containing:
+ - Number of new orders this month
+ - Total amount of new orders this month
+ - Average amount of t-shirt by order this month
+ - Number of cancelled orders this month
+ - Average time for an order to go from ‘new’ to ‘sent’ or ‘cancelled’
+
+
+
+## 5. Cache network calls, create a service
+
+If you open the Network tab of your browser’s dev tools, you will see that the
+call to `/awesome_dashboard/statistics` is done every time the client action is
+displayed. This is because the `onWillStart` hook is called each time the
+`Dashboard` component is mounted. But in this case, we would prefer to do it
+only the first time, so we actually need to maintain some state outside of the
+`Dashboard` component. This is a nice use case for a service!
+
+1. Register and import a `new awesome_dashboard.statistics` service.
+2. It should provide a function `loadStatistics` that, once called, performs the
+ actual rpc, and always return the same information.
+3. Use the `memoize` utility function from `@web/core/utils/functions` that
+ allows caching the statistics.
+4. Use this service in the `Dashboard` component.
+5. Check that it works as expected.
+
+#### See also
+
+- [Example: simple service](https://github.com/odoo/odoo/blob/master/addons/web/static/src/core/network/http_service.js)
+- [Example: service with a dependency](https://github.com/odoo/odoo/blob/master/addons/web/static/src/core/network/http_service.js)
+
+## 6. Display a pie chart
+
+Everyone likes charts (!), so let us add a pie chart in our dashboard. It will
+display the proportions of t-shirts sold for each size: S/M/L/XL/XXL.
+
+For this exercise, we will use [Chart.js](https://www.chartjs.org/). It is the
+chart library used by the graph view. However, it is not loaded by default, so
+we will need to either add it to our assets bundle, or lazy load it. Lazy
+loading is usually better since our users will not have to load the chartjs code
+every time if they don’t need it.
+
+1. Create a `PieChart` component.
+2. In its `onWillStart` method, load chartjs, you can use the
+ [loadJs function](https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/addons/web/static/src/core/assets.js#L23)
+ to load `/web/static/lib/Chart/Chart.js`.
+3. Use the `PieChart` component in a `DashboardItem` to display a
+ [pie chart](https://www.chartjs.org/docs/2.8.0/charts/doughnut.html) that
+ shows the quantity for each sold t-shirts in each size (that information is
+ available in the `/statistics` route). Note that you can use the size
+ property to make it look larger.
+4. The `PieChart` component will need to render a canvas, and draw on it using
+ chart.js. You can use this code to create the pie chart:
+
+ ```js
+ import { getColor } from "@web/core/colors/colors";
+ ...
+ renderChart() {
+ const labels = Object.keys(this.props.data);
+ const data = Object.values(this.props.data);
+ const color = labels.map((_, index) => getColor(index));
+ this.chart = new Chart(this.canvasRef.el, {
+ type: "pie",
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: this.props.label,
+ data: data,
+ backgroundColor: color,
+ },
+ ],
+ },
+ });
+ }
+ ```
+
+5. Make it work!
+
+
+
+#### See also
+
+- Example:
+ [lazy loading a js file](https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/addons/web/static/src/views/graph/graph_renderer.js#L57)
+- Example:
+ [rendering a chart in a component](https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/addons/web/static/src/views/graph/graph_renderer.js#L618)
+
+## 7. Periodic Updates
+
+Since we moved the data loading in a cache, it never updates. But let us say
+that we are looking at fast moving data, so we want to periodically (for
+example, every 10min) reload fresh data.
+
+This is quite simple to implement, with a `setTimeout` or `setInterval` in the
+statistics service. However, here is the tricky part: if the dashboard is
+currently being displayed, it should be updated immediately.
+
+To do that, one can use a `reactive` object: it is just like the proxy returned
+by `useState`, but not linked to any component. A component can then do a
+`useState` on it to subscribe to its changes.
+
+1. Update the statistics service to reload data every 10 minutes (to test it,
+ use 10s instead!)
+2. Modify it to return a
+ [reactive](https://github.com/odoo/owl/blob/master/doc/reference/reactivity.md#reactive)
+ object. Reloading data should update the reactive object in place.
+3. Update the `Dashboard` component to wrap the reactive object in a `useState`
+
+#### See also
+
+- [Documentation on reactivity](https://github.com/odoo/owl/blob/master/doc/reference/reactivity.md)
+- [Example: Use of reactive in a service](https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/addons/web/static/src/core/debug/profiling/profiling_service.js#L30)
+
+## 8. Lazy loading the dashboard
+
+Let us imagine that our dashboard is getting quite big, and is only of interest
+to some of our users. In that case, it could make sense to lazy load our
+dashboard, and all related assets, so we only pay the cost of loading the code
+when we actually want to look at it.
+
+One way to do this is to use `LazyComponent` (from @web/core/assets) as an
+intermediate that will load an asset bundle before displaying our component.
+
+For example, it can be used like this:
+
+```js
+export class ExampleComponentLoader extends Component {
+ static components = { LazyComponent };
+ static template = xml`
+
+ `;
+}
+
+registry
+ .category("actions")
+ .add("example_module.example_action", ExampleComponentLoader);
+```
+
+1. Move all dashboard assets into a sub folder `/dashboard` to make it easier to
+ add to a bundle.
+2. Create a `awesome_dashboard.dashboard` assets bundle containing all content
+ of the `/dashboard` folder.
+3. Modify `dashboard.js` to register itself to the `lazy_components` registry
+ instead of `actions`.
+4. In `src/dashboard_action.js`, create an intermediate component that uses
+ `LazyComponent` and register it to the `actions` registry.
+
+## 9. Making our dashboard generic
+
+So far, we have a nice working dashboard. But it is currently hardcoded in the
+dashboard template. What if we want to customize our dashboard? Maybe some users
+have different needs and want to see other data.
+
+So, the next step is to make our dashboard generic: instead of hard-coding its
+content in the template, it can just iterate over a list of dashboard items. But
+then, many questions come up: how to represent a dashboard item, how to register
+it, what data should it receive, and so on. There are many different ways to
+design such a system, with different trade-offs.
+
+For this tutorial, we will say that a dashboard item is an object with the
+following structure:
+
+```js
+const item = {
+ id: "average_quantity",
+ description: "Average amount of t-shirt",
+ Component: StandardItem,
+ // size and props are optionals
+ size: 3,
+ props: (data) => ({
+ title: "Average amount of t-shirt by order this month",
+ value: data.average_quantity,
+ }),
+};
+```
+
+The `description` value will be useful in a later exercise to show the name of
+items that the user can add to their dashboard. The `size` number is optional,
+and simply describes the size of the dashboard item that will be displayed.
+Finally, the `props` function is optional. If not given, we will simply give the
+`statistics` object as data. But if it is defined, it will be used to compute
+specific props for the component.
+
+The goal is to replace the content of the dashboard with something that look
+like the following snippet:
+
+```xml
+
+
+
+
+
+
+```
+
+Note that the above example features two advanced features of Owl: dynamic
+components and dynamic props.
+
+We currently have two kinds of item components: number cards with a title and a
+number, and pie cards with some label and a pie chart.
+
+1. Create and implement two components: `NumberCard` and `PieChartCard`, with
+ the corresponding props.
+2. Create a file `dashboard_items.js` in which you define and export a list of
+ items, using `NumberCard` and `PieChartCard` to recreate our current
+ dashboard.
+3. Import that list of items in our `Dashboard` component, add it to the
+ component, and update the template to use a `t-foreach` like shown above.
+
+ ```js
+ setup() {
+ this.items = items;
+ }
+ ```
+
+And now, our dashboard template is generic!
+
+## 10. Making our dashboard extensible
+
+However, the content of our item list is still hardcoded. Let us fix that by
+using a registry:
+
+1. Instead of exporting a list, register all dashboard items in a
+ `awesome_dashboard` registry
+2. Import all the items of the `awesome_dashboard` registry in the `Dashboard`
+ component
+
+The dashboard is now easily extensible. Any other Odoo addon that wants to
+register a new item to the dashboard can just add it to the registry.
+
+## 11. Add and remove dashboard items
+
+Let us see how we can make our dashboard customizable. To make it simple, we
+will save the user dashboard configuration in the local storage, so that we
+don’t have to deal with the server for now.
+
+For this exercise, the dashboard configuration will be saved as a list of
+removed item ids.
+
+1. Add a button in the control panel with a gear icon to indicate that it is a
+ settings button.
+2. Clicking on that button should open a dialog.
+3. In that dialog, we want to see a list of all existing dashboard items, each
+ with a checkbox.
+4. There should be a Apply button in the footer. Clicking on it will build a
+ list of all item ids that are unchecked.
+5. We want to store that value in the local storage.
+6. And modify the Dashboard component to filter the current items by removing
+ the ids of items from the configuration.
+
+
+
+## 12. Going further
+
+Here is a list of some small (and not so small) improvements you could try to do
+if you have the time:
+
+- Make sure your application can be translated (with `_t`).
+- Clicking on a section of the pie chart should open a list view of all orders
+ that have the corresponding size.
+- Save the content of the dashboard in a user setting on the server!
+- Make it responsive: in mobile mode, each card should take 100% of the width.
+- Update the dashboard in real time by using the bus (hard)
diff --git a/3_intro_to_website.md b/3_intro_to_website.md
new file mode 100644
index 00000000000..87dd35f69f2
--- /dev/null
+++ b/3_intro_to_website.md
@@ -0,0 +1,648 @@
+# Module 3: Learn the Website Framework
+
+In this chapter, we will learn all about the website-related framework(s).
+
+To understand the website codebase and how to hook yourself into it, you need to
+take a few key factors into account:
+- We make a website *builder*. That means that you should give the keys to the
+user to design their site the way they intend: what can they drop on the page
+and where? What can they change and how? What can they delete and how do they
+re-add it? Not everything is customizable, but a lot is. That also means that
+you cannot take a specific element, attribute or class as granted if there is
+any way to move or get rid of them.
+- The website module can be split into 2 scopes: there is the end-user-facing
+frontend (let's call it the frontend), and there is the editor interface for the
+website administrator or designer (let's call it the edit mode).
+
+On the frontend, we use `Interactions` to inject dynamic behaviors with
+JavaScript. Interactions are mostly independent from the OWL framework, but they
+use some similar syntax.
+
+In edit mode, we both supercharge the frontend interactions to add, remove or
+alter some features, and we create *options* to give the keys to the designer.
+Those options take full advantage of the HTML Builder framework, which relies on
+OWL (for `OptionComponent`s) and on the HTML Editor's `Plugin`s.
+
+But enough introduction, let's stop reading and start doing!
+To get started, you need a running Odoo server and a development environment
+setup. Before getting into the exercises, make sure you have a working setup.
+Start your Odoo server with this repository in the addons path, then install the
+`awesome_website` addon.
+
+For this project, we will start with a very simple interaction, then create a
+snippet (a block that is droppable on the page) and see how to work with it in
+edit mode. Finally, we will create a more complex snippet with asynchronous
+logic and services.
+
+💡 **Tip**: we will be talking about several features. What is their respective
+scopes?
+- Interaction / Colibri => `/web` (used on the frontend: portal / website)
+- Builder options => `/html_builder` (used by website and mass mailing)
+- Edit interactions => `/website`
+- Preview interactions => `/website`
+
+
+## Content
+
+- [Example of an interaction](#example-of-an-interaction)
+- [1. Create an interaction](#1-create-an-interaction)
+- [2. Create a cursor highlighter](#2-create-a-cursor-highlighter)
+- [3. Create a before/after image snippet](#3-create-a-beforeafter-image-snippet)
+- [4. Set up the before/after image interaction](#4-set-up-the-beforeafter-image-interaction)
+- [5. Make an edit interaction](#5-make-an-edit-interaction)
+- [6. Add builder options](#6-add-builder-options)
+- [7. Use an OWL component](#7-use-an-owl-component)
+- [8. Design a weather forecast snippet](#8-design-a-weather-forecast-snippet)
+- [9. Consolidate your interaction with a service](#9-consolidate-your-interaction-with-a-service)
+- [10. Add a test for your interaction](#10-add-a-test-for-your-interaction)
+- [11. Add a test for your option](#11-add-a-test-for-your-option)
+
+## Example of an interaction
+
+What does an interaction look like?
+It is a class that takes a `selector`, uses a life cycle (available methods are
+`setup`, `willStart`, `start` and `destroy`), and attaches dynamic content
+through the `dynamicContent` property, with available directives being `t-out`
+(to output text or markup), `t-on-*` for events, `t-att-*` for attributes
+(`t-att-class`, `t-att-style`, etc.) and `t-component` to attach a component.
+
+```js
+import { registry } from "@web/core/registry";
+import { Interaction } from "@web/public/interaction";
+
+export class MyCustomInteraction extends Interaction {
+ static selector = ".my-selector";
+ dynamicContent = {
+ _root: {
+ "t-out": () => this.weather.temperature,
+ "t-att-style": () => ({
+ "background-color": this.freezing ? "blue" : "yellow",
+ }),
+ },
+ ".button": {
+ "t-on-click": this.doSomething,
+ }
+ };
+
+ setup() {
+ this.freezing = false;
+ }
+
+ async willStart() {
+ this.weather = await rpc("/awesome_website/weather");
+ this.freezing = this.weather.temperature < 0;
+ }
+
+ doSomething() {
+ console.log("Hello world");
+ }
+}
+
+registry.category("public.interactions").add(
+ "awesome_website.my_custom_interaction",
+ MyCustomInteraction
+);
+```
+
+Notice how the interaction is added to the `public.interactions` registry.
+
+The `dynamicContent` directives are attached after `willStart` has resolved, and
+are then recomputed every time an event is triggered (or through other
+Interaction helpers).
+
+💡 **Tips**:
+- interactions can only be attached inside the `#wrapwrap` element (or within
+the `body` if there is no `#wrapwrap`).
+- Do you need a complex selector with `:has(*)` or `:not(:has(*))`? Use the
+static properties `selectorHas` and `selectorNotHas` instead: as interactions
+should work for end-users (as opposed to Odoo's customers), we cannot rely on
+evergreen browsers and tend to code defensively.
+
+💡 **Tip**: Have a look at the [Interaction](https://github.com/odoo/odoo/blob/19.0/addons/web/static/src/public/interaction.js)
+base class file to dive deeper into the implementation details. As it is
+thoroughly commented, this is a good starting point to have a better grasp on
+how it works and what the available methods are.
+
+## 1. Create an interaction
+
+Let's start gently with an interaction that updates the content of the ``
+element with the current time.
+
+
+
+1. Create the `Main` interaction inside
+`awesome_website/static/src/interactions` and target the `` tag.
+2. Add a `dynamicContent` property. In our case, we can use the magic selector
+`_root` to target the selector defined in the previous step, and output the
+current date and time.
+3. Add the file to the `web.assets_frontend` bundle in the manifest.
+4. Open http://localhost:8069/ to see the result.
+
+## 2. Create a cursor highlighter
+
+Now that we have a basic understanding of the interactions, we can go a step
+further. Let's enhance our awesome website with a cursor highlighter, a colored
+dot that follows the user's cursor.
+
+
+
+1. We are going to insert a new element on the page through the interaction. For
+the selector, you can choose to focus the `#wrapwrap` directly.
+2. In `setup()`, set up the variables that you will need: the x and y
+coordinates and the new highlighter element (use the `.x_mouse_follower` class,
+the styles are already ready).
+3. In `start()`, append the highlighter. Look for the `insert()` method from the
+`Interaction` class: it will automatically remove the element once the
+interaction is destroyed. (This is also the reason why you need to append
+elements in `start`: `destroy` is only called after an interaction has started.
+If you added it in the `setup` and for some reason the interaction never
+started, the element would not be removed.)
+4. Now, for the magic to happen, we need to listen to the `pointermove` event
+and track our cursor position.
+5. You also need to adapt the highlighter's position. You could do that in the
+handler of the `pointermove` event, but the position would not be cleaned on
+destroy*. It is better to use the `dynamicContent` with the `t-att-style`
+directive to achieve our goal! Everything that is done through the
+`dynamicContent` will be reset upon destroy.
+
+🤓☝️ *Well actually, given that the element itself will be cleaned up, it
+doesn't matter. But keep in mind that it is good practice to apply your
+modifications through the `dynamicContent` property!
+
+💡 **Tip**: You want to debounce the `pointermove` event? Have a look at the
+`debounced` method from the `Interaction` base class.
+
+## 3. Create a before/after image snippet
+
+Let's back up a little: we know how to attach interactions to existing elements,
+and this is more than enough for plenty of things that you would want to do both
+in the Website module and in modules with a dependency on the Portal module.
+But you may have noticed that website pages work with building blocks (that we
+call snippets): blocks that you can drag and drop in edit mode onto your page,
+and that you can edit with options afterwards.
+
+How can you create a new snippet from scratch and make it available for website
+designers?
+
+Snippet templates are defined in `/views/snippets` as XML files: those are QWeb
+templates, processed by the server.
+
+We are not going to cover all the possibilities offered by QWeb templates, let's
+use a standard structure. Open the file `s_image_comparison.xml` and insert your
+HTML structure for the before/after image snippet.
+
+
+ Check your code against the example
+
+ ```xml
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ 💡 **Tips**: Notice that we use a `` tag: this is necessary if you want to add a
+ block snippet, and it will give you a few default options in edit mode. On the
+ contrary, if you only want to add an _inner snippet_ (a snippet that you can add
+ inside any other block), you don't necessarily need the ``.
+
+
+
+
+A stylesheet is defined in `/static/src/snippets/s_image_comparison/000.scss`,
+but it does not work for now. You need to register the asset through the XML
+view like so:
+
+```xml
+
+ web.assets_frontend
+ awesome_website/static/src/snippets/s_image_comparison/000.scss
+
+```
+
+💡 **Tips**: Why "000"? That is a versioning system so that we do not break
+existing customers websites if we want to introduce breaking changes. A complete
+revamp of the styles would then be called `001.scss`.
+We also have some `000.xml` for templates called throughout the life of the
+interaction.
+
+Finally, you must add your snippet XML in the manifest, among the `data` files.
+
+The new snippet is ready, but you still cannot use it: you have to add it to a
+snippet category so that it appears when a user opens the snippet dialog.
+
+> *The snippets categories as they appear on the interface*
+>
+> 
+
+All snippet categories are defined in `website/views/snippets/snippets.xml`. If
+you are adding a snippet directly in the website module, you can edit that file.
+If, like in our `awesome_website` case here, you need to add it from another
+module, you can simple xpath your way into it.
+Have a look at the original website file to understand the syntax.
+
+For our `s_image_comparison` snippet, I think the "Images" category would be a
+perfect fit. Create a `snippets.xml` file and write your xpath.
+For demo purposes, let's add the snippet preview in the 1st place, just before
+`website.s_picture`. Feel free to add as many **keywords** as you wish: those
+keywords are used to search for a snippet in the snippets dialog.
+
+> *Searching for the snippet*
+>
+> 
+
+
+ Unfold to see the full xpath
+
+ ```xml
+
+
+
+ image comparison, before, after, compare, side-by-side, photo, picture, contrast, split, match, slider
+
+
+
+ ```
+
+
+
+To confirm that it works, open your website homepage in edit mode, drag or click
+on the "Images" category and select the image comparison block.
+
+## 4. Set up the before/after image interaction
+
+Now that we have the snippet, we can create its interaction. By convention, we
+add it in the same folder as the `000.scss`.
+
+If you have a similar structure to what I suggested earlier, this is going to be
+a simple interaction: you just need to listen for the slider `input` event and
+update the `--slider-position` CSS custom property.
+
+Once your interaction is running, you will see that it doesn't work in edit
+mode. That's normal: we have a separate registry category for edit interactions.
+If you want exactly the same behavior for the interaction in edit mode as on the
+frontend, just add it to the `public.interactions.edit` registry:
+
+```js
+registry.category("public.interactions.edit").add("awesome_website.image_comparison", {
+ Interaction: ImageComparison,
+});
+```
+
+💡 **Tip**: It's not working? Did you add your file to the manifest?
+
+## 5. Make an edit interaction
+
+What if you want to tweak the interaction in edit mode? Maybe you want to
+prevent an event from firing but still keep some parts of the interaction. Or
+maybe you need to add some features on top of the public ones. We've got you
+covered.
+
+Because in most cases an edit interaction derives from a public interaction, and
+the public interaction could itself extend another interaction (which could have
+its own edit interaction), edit interactions overrides are written as
+**mixins**. That allows the builder to apply all the overrides.
+
+
+ ❓ It's not clear
+
+ | Public | Edit |
+ | --------------- | ------------------ |
+ | Foo | FooEdit (override) |
+ | Bar extends Foo | BarEdit (override) |
+
+ Where `Bar` applies on `.element`.
+ When you edit the page, if `.element` is on the page, it will apply both
+ `FooEdit` and `BarEdit` on `Bar`, in that order.
+
+ **Why not extensions?**
+
+ If we had used extends, it would not have been possible to apply both
+ overrides without copying code.
+
+
+
+Here, for the sake of this demo, we consider that we don't want the slider to be
+actionable in edit*. Create `image_comparison.edit.js` and override your public
+interaction!
+
+🤓☝️ *Well actually, this could be achieved by disabling the interaction
+altogether. But for the sake of the exercise, let's imagine there are other
+features that we want to keep.
+
+Don't forget to add your mixin to the edit interactions registry category, and
+remove that same line from the base interaction file.
+
+```js
+registry.category("public.interactions.edit").add("awesome_website.image_comparison", {
+ Interaction: ImageComparison,
+ mixin: ImageComparisonEdit,
+});
+```
+
+To completely block the slider, we should also set `pointer-events: none`,
+otherwise you can still move the slider thumb. You can either add the Bootstrap
+class `.pe-none` through the edit interaction, or add the rule to the file
+`000.edit.scss`.
+
+💡 **Tip**: the file is already set to override the input's style. That allows
+to click on the images to replace them and have access to all the standard image
+options. Comment out the CSS rules to see the difference.
+
+You must add the edit files to the `website.assets_inside_builder_iframe`
+bundle.
+
+```py
+'website.assets_inside_builder_iframe': [
+ 'awesome_website/static/src/snippets/**/*.edit.*'
+],
+```
+
+
+💡 **Tip**: overriding `dynamicContent` in edit interactions
+- Unless that is exactly what you want, do not override the whole
+`dynamicContent`! Always keep a copy of the original with
+`dynamicContent = { ...this.dynamicContent, { ... } }`, or with
+`patchDynamicContent` (in the setup).
+Prefer `patchDynamicContent` if you add or override an entry of a selector whose
+other entries you want to keep.
+
+
+💡 **Tip**: When are interactions restarted in edition?
+- Through `getConfigurationSnapshot`, the `websiteEditService`'s patch on
+`Interaction` restarts an interaction if it detects a change in the `dataset` or
+in the style of the interaction root element.
+- If you have any other modification (class, child...), it won't restart the
+interaction. This is to avoid stopping and restarting every interaction all the
+time.
+- Then `shouldStop` returns `true` if the snapshot returned was different from
+what it used to be, or if `isImpactedBy` returns `true` (by default it always
+returns `false`)
+
+ If you do need to override that default behavior, you have 3 tools
+ available:
+
+ - override `getConfigurationSnapshot` (you can check existing
+ implementations, e.g. on `DynamicSnippetCarouselEdit`)
+ - override `isImpactedBy`, typically if you should restart depending on a
+ child (present or not), or possibly depending on a class. Note that if you
+ check if a class is present in `isImpactedBy`, the interaction will always
+ restart if the class is present. If you need to restart only when the class
+ is toggled, you should use `getConfigurationSnapshot`.
+ - override `shouldStop`. The most common override on that one is to return
+ `true` all the time, if all your options should trigger a restart of the
+ interaction. Note that it does mean it will restart *all* the time, so try
+ the other solutions first if they're not too heavy.
+
+## 5bis. Make a preview interaction
+
+This part is completely website-specific: when you drop a section snippet on a
+website page (or an HTML field), it opens the snippets preview dialog.
+
+There might be cases when you want the preview to look a very specific way or
+have a limited behavior (compared to its actual, full behavior). Typically, if
+the actual interaction implements a behavior when the user scrolls the page or
+when they click on a button (e.g. carousels), you may want to show a similar
+behavior on hover and on focus of the preview.
+
+💡 **Tip**: when you add `:hover` rules, always consider whether you should also
+add `:focus-visible` to account for keyboard accessibility.
+
+By convention, we suffix those files (`.js`, `.scss`) with `*.preview.*`.
+
+SCSS preview files must be registered in the manifest, in the
+`html_builder.iframe_add_dialog` bundle.
+
+JS preview interactions are, just like edit interactions, mixins and must be
+registered through the registry, in the category `public.interactions.preview`.
+
+️📃 In the case of our comparing slider, we could for instance add an animation
+on hover to move the slider to the left and to the right.
+
+## 6. Add builder options
+
+What we have been calling the "edit mode" since the beginning of this tutorial
+actually depends on the html_builder module. What it does is embedding the
+frontend in an iframe, and rendering options around it (or sometimes on top of
+it, e.g. the editor toolbar when you select some text).
+
+You probably already saw that you have some default options available when you
+click on your snippet. If you paid attention earlier, you remember that we used
+a `` to build our snippet: those default options are the ones that
+appear when the current target is a section.
+
+While interactions are temporary and reset on page load, options apply permanent
+changes that will be saved in the DOM, or sometimes in the database.
+
+Those options are defined as OWL templates with a set of Builder components
+(`BuilderRow`, `BuilderButton`, `BuilderTextInput`, `BuilderColorPicker`, etc.).
+Each individual option is a `BuilderRow` encapsulating one or several other
+component(s).
+
+Those options take an `action` as prop, with optionally some `actionParams` or/
+and some `actionValue`. Actions are defined in JavaScript and extend
+`BuilderAction`.
+
+💡 **Tip**: 4 shorthand actions are available: `classAction`, `styleAction`,
+`attributeAction` and `dataAttributeAction`. Have a look in the codebase to see
+how they're used!
+
+💡 **Tip**: some complex actions may require a deep dive into the way
+`BuilderAction` works. Have a look at its implementation in
+`addons/html_builder/static/src/core/builder_action.js`: it is heavily
+commented / documented.
+
+A plain example would be a checkbox to turn a class on or off:
+
+```xml
+
+
+
+
+
+```
+
+To register the option, you then have to xpath the option into the builder
+options at the place you want it to appear. If you need to add custom logic, you
+can create a `Plugin` and potentially a custom `BuilderAction`.
+
+```xml
+
+
+
+
+
+```
+
+If you need a custom action, you should update the option template:
+```xml
+
+```
+
+
+```js
+class MyToggleOptionPlugin extends Plugin {
+ static id = "awesome_website.MyToggle";
+
+ resources = {
+ builder_actions: { MyToggleAction },
+ };
+}
+
+class MyToggleAction extends BuilderAction {
+ static id = "myToggleAction";
+ apply({ editingElement, params: { mainParam }}) {
+ ...
+ // Handle mainParam (=== "my-param")
+ }
+}
+
+registry.category("website-plugins").add(MyToggleOptionPlugin.id, MyToggleOptionPlugin);
+```
+
+
+ More complex options that rely on a state can also extend
+ `BaseOptionComponent`, but this is not necessary here.
+
+ ```js
+ export class MyToggleOption extends BaseOptionComponent {
+ static id = "my_toggle_option"; // Matches the XML tag name
+ static template = "awesome_website.MyToggleOption"
+
+ setup() {
+ super.setup();
+ this.state = useDomState((editingElement) => {
+ // handle state
+ });
+ // custom logic
+ };
+ }
+
+ registry.category("website-options").add(MyToggleOption.id, MyToggleOption);
+ ```
+
+
+
+️📃 Now it's your turn. Which options do we need for our snippet to shine? Choose
+one or two and try to make them work. Here are some ideas:
+
+- Choose between vertical and horizontal slide
+- Add before / after labels
+- Change the appearance of the corners (rounded, squared, cut...)
+- Set the default value of the slider on some other value (25%, 75%...)
+- Change the color of the handle
+- Adapt the handle size
+- Set a different icon on the slider handle
+- ...
+
+## 7. Use an OWL component
+
+If you need some reusable component and you do not care about it being editable,
+you may want to use an OWL component. You can do that very easily in 2 ways:
+
+- through the Interaction's `t-component` directive.
+- with an `` custom element.
+
+The syntax for `t-component` is: `"t-component": () => [Comp, { ...props }]`,
+where the props are optional.
+
+To be able to use ``, you first have to register your component
+in the `public_components` registry category. The element takes a `name` (the
+one you gave in the registry) and a `props` attribute, which should be formatted
+in JSON.
+
+⚠️ **Important**: these are valid approaches especially outside of the
+boundaries of the HTML builder. But as soon as your component is available on
+the builder, you should absolutely ask yourself whether an OWL component is
+really the way to go! Your component will not be editable, you will not have
+many ways to add website builder options on it, the end user will not have any
+editing right on the text within it... There are always exceptions, but it is
+generally a bad idea.
+
+️📃 Create a basic component with one or two props, and call it through an
+interaction, then using ``.
+
+## 8. Design a weather forecast snippet
+
+## 9. Consolidate your interaction with a service
+
+## 10. Add a test for your interaction
+
+You should always test your new features and your fixes, to make sure that the
+expected behavior keeps working in the future. To do that, we use the Odoo-built
+unit test suite Hoot.
+
+To test an interaction, first create a file that ends in `.test.js` (by
+convention).
+
+You then have to register the interaction you want to test in
+`setupInteractionWhiteList()`, with the ID you gave it for the
+`public.interactions` registry.
+
+Finally, call `await startInteractions("some HTML template")` to run the
+interaction on some given HTML.
+This function returns `{ core }`, which is the public interactions service. In
+tests, we mainly use it in 2 ways:
+- Control the number of interactions started (it will depend on the injected
+HTML and the white-listed interactions):
+`expect(core.interactions).toHaveLength(1);`
+- If you call `startInteractions` with the option `{ waitForStart: false }`, you
+can await `core.isReady` to wait for it to start.
+
+Note that if you need to test things before the interaction starts, in edit mode
+or in translation, the function takes an optional object parameter with 3
+optional boolean keys:
+`{ waitForStart: true, editMode: false, translateMode: false }`.
+
+Don't forget to add your file to the manifest.
+- The `web.assets_unit_tests` bundle registers all the test files.
+- You also need to register the minimum for your interactions to start in the
+`web.assets_unit_tests_setup` bundle. Typically: at least the interaction JS
+definition.
+
+️📃 Take some inspiration on existing modules and create a test for one of your
+interactions. For instance, test the image comparison interaction to make sure
+that the slider position is updated after an input change.
+
+## 11. Add a test for your option
+
+The process is very similar to test your builder options.
+You should know a few helper functions we use to create our tests:
+
+- On website-related options, `defineWebsiteModels()` must be called at the
+start of your file.
+- `setupHTMLBuilder(content, options)`, as its name suggests, sets up a testing
+builder environment. Both parameters are optional. The content is an HTML string
+to start the test with some content already present. The options is an object
+determining:
+ - `editableSelector`: `"#wrapwrap"` by default.
+ - `headerContent`: allows to add some header (before the main content).
+ Empty string by default.
+ - `snippets`: object that defines how test snippets should appear in the
+ sidebar. What group should the test add (`snippet_groups`), what template
+ should the test add as a group snippet and in which group to add it
+ (`snippet_structure`), and what template should it use as an inner snippet
+ (`snippet_content`).
+ - `snippetContent`: array of HTML strings, as a shorthand if you don't need
+ the other keys of `snippets`.
+ - `dropzoneSelectors`: object, or array of objects defining new dropzone
+ selectors (see the `dropzone_selectors` resource).
+ - `styleContent`: CSS style (as a string) to use for the test. By default,
+ the styles of the snippets are not used.
+ - `iframeLangDir`: language direction of the iframe content. `ltr` or `rtl`.
+ `ltr` by default.
+- On website-related options, you should rather use `setupWebsiteBuilder` or
+`setupWebsiteBuilderWithSnippet` if you want to setup a page with existing
+snippets.
+
+️📃 Take some inspiration on existing website builder tests and create a test for
+an option you implemented.
diff --git a/4_customize_fields_views.md b/4_customize_fields_views.md
new file mode 100644
index 00000000000..574c14d3905
--- /dev/null
+++ b/4_customize_fields_views.md
@@ -0,0 +1,114 @@
+# Module 4: Customize Fields and Views
+
+This project is a sequence of (mostly) independant exercises designed to teach
+how to work with fields and views in Odoo.
+
+The setting for this project is an addon that manages a shelter and various kind
+of animals that are adopted. All the code is located in the `awesome_shelter`
+addon.
+
+To get started, you need a running Odoo server and a development environment
+setup. Before getting into the exercises, make sure you have a working setup.
+Start your odoo server with this repository in the addons path, then install the
+`awesome_shelter` addon.
+
+The `awesome_shelter` addon introduces a few useful models:
+
+- `awesome_shelter.animal` represents a specific animal that has been rescued
+- `awesome_shelter.animal_race` is a specific animal race, such as german
+ sheperd.
+- `awesome_shelter.animal_type` is a small model that help us categorize
+ animals, such as "dogs" or "cats"
+
+## Content
+
+- [1. Add a ribbon](#1-add-a-ribbon)
+- [2. Display a custom view banner](#2-display-a-custom-view-banner)
+- [3. Subclass a char field](#3-subclass-a-char-field)
+- [4. Customize a status bar widget](#4-customize-a-status-bar-widget)
+- [5. Display pictogram and type in list view](#5-display-pictogram-and-type-in-list-view)
+- [6. Extend a view](#6-extend-a-view)
+
+## 1. Add a ribbon
+
+In the kanban and form view for an animal, it would be cool to have a visual
+feedback showing that the animal is adopted. Let us do that by adding a ribbon!
+
+This can be done by using an existing widget named `web_ribbon`
+
+1. modify the form view to add a `web_ribbon` widget, which should be visible
+ only when the state is `adopted`
+2. do the same for the kanban view
+
+
+
+## 2. Display a custom view banner
+
+The previous exercise showed how to use a widget. In this exercise, we will
+implement a new widget from scratch.
+
+In the form view for the animal model, we want to display a banner with a
+message when the animal has been adopted for more than 6 months.
+
+1. create a new component named `LongStayBanner`
+2. register it in the `view_widgets` registry
+3. Use it in the form view:
+
+ ```xml
+
+ ```
+
+
+
+## 3. Subclass a char field
+
+Let us now see how to specialize a field.
+
+1. Subclass the charfield component from `@web/views/fields/char/char_field`
+2. register it in the fields registry, and use the field in the form view
+3. create a new template to add a button when the name is not set (this can be
+ done with `xpaths` or using `t-call`)
+4. when the button is clicked, choose a name from a hardcoded list of pet names,
+ and set the value of the field
+
+
+
+
+## 4. Customize a status bar widget
+
+Let us now customize the status bar widget to display a more festive effect when
+an animal is adopted, which usually happens when the shelter staff clicks on the
+`adopted` status.
+
+1. subclass the status bar field
+2. register it in the fields registry, and use the field in the form view
+3. override the `selectItem` to display a celebratory rainbow man when the
+ animal is adopted
+
+
+
+## 5. Display pictogram and type in list view
+
+Animals have a type (cat, dog, etc). Those types can have a pictogram. We want
+an extension of a one2many field widget to display the name and the pictogram a
+little bit like the one2many_avatar, as a single element instead of two columns
+in list view.
+
+1. subclass the many2one field
+2. use it in the list view
+3. modify your field to display a pictogram if it is defined
+
+
+
+## 6. Extend a view
+
+Finally, it sometimes happens that we need to work with views instead of fields.
+
+For this exercise, we want to modify the kanban view to reload its data every 10
+seconds or so. This is because the shelter staff want to display it on a small
+external screen as a visual monitoring tool.
+
+1. Subclass the kanban view
+2. register your kanban view in the `views` registry, and use it (with
+ `js_class` attribute) in the animal kanban view
+3. Modify the code to reload every 10s
diff --git a/5_build_a_clicker_game.md b/5_build_a_clicker_game.md
new file mode 100644
index 00000000000..f5e163f84d1
--- /dev/null
+++ b/5_build_a_clicker_game.md
@@ -0,0 +1,449 @@
+# Module 5: 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
+setup. Before getting into the exercises, make sure you have a working setup.
+Start your odoo server with this repository in the addons path, then install the
+`awesome_clicker` addon.
+
+The code for this addon is currently empty, but we will progressively add
+features to it.
+
+
+
+## Content
+
+- [1. Create a systray item](#1-create-a-systray-item)
+- [2. Count external clicks](#2-count-external-clicks)
+- [3. Create a client action](#3-create-a-client-action)
+- [4. Move the state to a service](#4-move-the-state-to-a-service)
+- [5. Use a custom hook](#5-use-a-custom-hook)
+- [6. Humanize the displayed value](#6-humanize-the-displayed-value)
+- [7. Add a tooltip in ClickValue component](#7-add-a-tooltip-in-clickvalue-component)
+- [8. Buy ClickBots](#8-buy-clickbots)
+- [9. Refactor to a class model](#9-refactor-to-a-class-model)
+- [10. Notify when a milestone is reached](#10-notify-when-a-milestone-is-reached)
+- [11. Add BigBots](#11-add-bigbots)
+- [12. Add a new type of resource: power](#12-add-a-new-type-of-resource-power)
+- [13. Define some random rewards](#13-define-some-random-rewards)
+- [14. Provide a reward when opening a form view](#14-provide-a-reward-when-opening-a-form-view)
+- [15. Add commands in command palette](#15-add-commands-in-command-palette)
+- [16. Add yet another resource: trees](#16-add-yet-another-resource-trees)
+- [17. Use a dropdown menu for the systray item](#17-use-a-dropdown-menu-for-the-systray-item)
+- [18. Use a Notebook component](#18-use-a-notebook-component)
+- [19. Persist the game state](#19-persist-the-game-state)
+
+## 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.
+
+
+
+And voila, we have a completely working clicker game!
+
+#### See also
+
+- [Documentation on the systray registry](https://www.odoo.com/documentation/master/developer/reference/frontend/registries.html#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.
+
+#### See also
+
+- [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.
+
+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"
+}
+```
+
+
+
+**See also:**
+[How to create a client action](https://www.odoo.com/documentation/master/developer/howtos/javascript_client_action.html#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.
+
+
+
+**See also:**
+[Short explanation on services](https://www.odoo.com/documentation/master/developer/tutorials/discover_js_framework/02_build_a_dashboard.html#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. Create and export a `useClicker` hook that encapsulate the `useService` and
+ `useState` in a simple function.
+2. Update all current uses of the clicker service to the new hook:
+
+ ```js
+ this.clicker = useClicker();
+ ```
+
+**See also:**
+[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 displays the value.
+
+Note that Owl allows component that contains just text nodes!
+
+
+
+**See also:**
+[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. The tooltip needs an html element. Change the `ClickValue` to wrap its value
+ in a `` element
+2. Add a dynamic `data-tooltip` attribute to display the exact value.
+
+
+
+**See also:**
+[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 unlock 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.
+
+
+
+# 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.
+2. Rewrite the clicker service to instantiate and export the clicker model
+ class.
+
+To create the `ClickerModel`, you can extend the `Reactive` class from
+`@web/core/utils/reactive`. The `Reactive` class wrap the model into a reactive
+proxy.
+
+**See also:**
+[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.
+
+
+
+#### See also
+
+- [Owl documentation on event bus](https://github.com/odoo/owl/blob/master/doc/reference/utils.md#eventbus)
+- [Documentation on effect service](https://www.odoo.com/documentation/master/developer/reference/frontend/services.html#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 clash 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).
+
+
+
+# 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 `N` click, clickbots now
+provide us with `power*N` clicks).
+
+1. increment level when the number of clicks 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).
+
+## 13. Define some random rewards
+
+We want the user to obtain sometimes bonuses, to reward using Odoo. A reward is
+an object with:
+
+- a `description` string.
+- an `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.
+
+1. Define a list of rewards in a new file `click_rewards.js`. 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.
+
+
+
+#### See also
+
+- [Documentation on patching a class](https://www.odoo.com/documentation/master/developer/reference/frontend/patching_code.html#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/web/static/src/core/utils/patch.js#L71)
+
+## 15. Add commands in command palette
+
+Let us now provide more interesting ways to interact with our game.
+
+1. Add a command `Open Clicker Game` to the command palette.
+2. Add another command: `Buy 1 click bot`.
+
+
+
+**See also:**
+[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.
+
+## 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, ...
+
+
+
+## 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.
+
+
+
+#### See also
+
+- [Odoo: Documentation on Notebook component](https://www.odoo.com/documentation/master/developer/reference/frontend/owl_components.html#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 `@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 in the model and in the UI.
+2. Increment the state version number.
+3. Define a migration.
+
+
diff --git a/6_testing_javascript.md b/6_testing_javascript.md
new file mode 100644
index 00000000000..f27b113a6b0
--- /dev/null
+++ b/6_testing_javascript.md
@@ -0,0 +1,11 @@
+# Module 6: Test javascript code
+
+Short intro
+
+## Content
+
+- [1. Create an interaction](#1-create-an-interaction)
+
+## 1. Create an interaction
+
+todo
\ No newline at end of file
diff --git a/README.md b/README.md
index a0158d919ee..833ed33a880 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,24 @@
-# Odoo tutorials
+
-This repository hosts the code for the bases of the modules used in the
-[official Odoo tutorials](https://www.odoo.com/documentation/latest/developer/tutorials.html).
+# Odoo : Javascript Framework Training
-It has 3 branches for each Odoo version: one for the bases, one for the
-[Discover the JS framework](https://www.odoo.com/documentation/latest/developer/tutorials/discover_js_framework.html)
-tutorial's solutions, and one for the
-[Master the Odoo web framework](https://www.odoo.com/documentation/latest/developer/tutorials/master_odoo_web_framework.html)
-tutorial's solutions. For example, `17.0`, `17.0-discover-js-framework-solutions` and
-`17.0-master-odoo-web-framework-solutions`.
+This branch contains the exercises and supporting code for the Odoo JavaScript Framework Training.
+It provides four addons, each a standalone project that can be completed independently.
+
+To get started:
+
+1. Clone this repository.
+2. Check out this branch.
+2. Launch an Odoo server with this folder included in the addons path.
+
+Much of the theory is covered in the accompanying slides.
+And of course—feel free to ask questions!
+
+| Title | Content |
+| ------------------------------------------------------------------- | -------------------------------------------------------------------------- |
+| [Module 1: Learn Owl 🦉](1_learn_owl.md) | Owl, components, hooks, reactivity, state management, ... |
+| [Module 2: Make a Dashboard](2_make_a_dashboard.md) | assets, basics of odoo framework, rpcs, systray, services, registries, ... |
+| [Module 3: Introduction to the Website](3_intro_to_website.md.md) | interactions, snippets, pages, options, ... |
+| [Module 4: Customize Fields and Views](4_customize_fields_views.md) | fields, views |
+| [Module 5: Build a Clicker Game](5_build_a_clicker_game.md) | advanced framework, state management, ... |
+| [Module 6: Testing Javascript](6_testing_javascript.md.md) | unit tests, integration tests, hoot, tours, ... |
diff --git a/_images/add_ribbon.png b/_images/add_ribbon.png
new file mode 100644
index 00000000000..533811369af
Binary files /dev/null and b/_images/add_ribbon.png differ
diff --git a/_images/autofocus.png b/_images/autofocus.png
new file mode 100644
index 00000000000..eb13a35b67c
Binary files /dev/null and b/_images/autofocus.png differ
diff --git a/_images/banner.png b/_images/banner.png
new file mode 100644
index 00000000000..e5f5246c981
Binary files /dev/null and b/_images/banner.png differ
diff --git a/_images/bigbot.png b/_images/bigbot.png
new file mode 100644
index 00000000000..77ae8dc00ef
Binary files /dev/null and b/_images/bigbot.png differ
diff --git a/_images/builder_add_snippet_search.png b/_images/builder_add_snippet_search.png
new file mode 100644
index 00000000000..4586d1b0a56
Binary files /dev/null and b/_images/builder_add_snippet_search.png differ
diff --git a/_images/builder_snippet_groups.png b/_images/builder_snippet_groups.png
new file mode 100644
index 00000000000..f23bf0eabe6
Binary files /dev/null and b/_images/builder_snippet_groups.png differ
diff --git a/_images/clickbot.png b/_images/clickbot.png
new file mode 100644
index 00000000000..fc721748587
Binary files /dev/null and b/_images/clickbot.png differ
diff --git a/_images/client_action.png b/_images/client_action.png
new file mode 100644
index 00000000000..434a19213df
Binary files /dev/null and b/_images/client_action.png differ
diff --git a/_images/command_palette.png b/_images/command_palette.png
new file mode 100644
index 00000000000..31c3a9ea37e
Binary files /dev/null and b/_images/command_palette.png differ
diff --git a/_images/counter.png b/_images/counter.png
new file mode 100644
index 00000000000..5e2ac1a8f6d
Binary files /dev/null and b/_images/counter.png differ
diff --git a/_images/create_todo.png b/_images/create_todo.png
new file mode 100644
index 00000000000..83ec5c4c532
Binary files /dev/null and b/_images/create_todo.png differ
diff --git a/_images/dashboard_item.png b/_images/dashboard_item.png
new file mode 100644
index 00000000000..1524ca5b0e2
Binary files /dev/null and b/_images/dashboard_item.png differ
diff --git a/_images/delete_todo.png b/_images/delete_todo.png
new file mode 100644
index 00000000000..cc9ff9a85ed
Binary files /dev/null and b/_images/delete_todo.png differ
diff --git a/_images/double_counter.png b/_images/double_counter.png
new file mode 100644
index 00000000000..99e2b88d48e
Binary files /dev/null and b/_images/double_counter.png differ
diff --git a/_images/dropdown.png b/_images/dropdown.png
new file mode 100644
index 00000000000..1cdeddc40cc
Binary files /dev/null and b/_images/dropdown.png differ
diff --git a/_images/final.png b/_images/final.png
new file mode 100644
index 00000000000..f40107a7fc3
Binary files /dev/null and b/_images/final.png differ
diff --git a/_images/generic_card.png b/_images/generic_card.png
new file mode 100644
index 00000000000..ad079e95dde
Binary files /dev/null and b/_images/generic_card.png differ
diff --git a/_images/humanized_number.png b/_images/humanized_number.png
new file mode 100644
index 00000000000..f51cf9c9938
Binary files /dev/null and b/_images/humanized_number.png differ
diff --git a/_images/humanized_tooltip.png b/_images/humanized_tooltip.png
new file mode 100644
index 00000000000..b11e5d52269
Binary files /dev/null and b/_images/humanized_tooltip.png differ
diff --git a/_images/increment_button.png b/_images/increment_button.png
new file mode 100644
index 00000000000..1d96e7aa0a3
Binary files /dev/null and b/_images/increment_button.png differ
diff --git a/_images/items_configuration.png b/_images/items_configuration.png
new file mode 100644
index 00000000000..a828a187b54
Binary files /dev/null and b/_images/items_configuration.png differ
diff --git a/_images/markup.png b/_images/markup.png
new file mode 100644
index 00000000000..bd8fea06f1a
Binary files /dev/null and b/_images/markup.png differ
diff --git a/_images/milestone1.png b/_images/milestone1.png
new file mode 100644
index 00000000000..cc11eda3624
Binary files /dev/null and b/_images/milestone1.png differ
diff --git a/_images/muted_todo.png b/_images/muted_todo.png
new file mode 100644
index 00000000000..e732e4cfae2
Binary files /dev/null and b/_images/muted_todo.png differ
diff --git a/_images/navigation_buttons.png b/_images/navigation_buttons.png
new file mode 100644
index 00000000000..79d0221d217
Binary files /dev/null and b/_images/navigation_buttons.png differ
diff --git a/_images/new_layout.png b/_images/new_layout.png
new file mode 100644
index 00000000000..7bfd8128770
Binary files /dev/null and b/_images/new_layout.png differ
diff --git a/_images/notebook.png b/_images/notebook.png
new file mode 100644
index 00000000000..101be3bd3fd
Binary files /dev/null and b/_images/notebook.png differ
diff --git a/_images/odoo_logo.png b/_images/odoo_logo.png
new file mode 100644
index 00000000000..474f9ff5d66
Binary files /dev/null and b/_images/odoo_logo.png differ
diff --git a/_images/overview_02.png b/_images/overview_02.png
new file mode 100644
index 00000000000..bd618027cee
Binary files /dev/null and b/_images/overview_02.png differ
diff --git a/_images/peach_tree.png b/_images/peach_tree.png
new file mode 100644
index 00000000000..7ccbe51a7f5
Binary files /dev/null and b/_images/peach_tree.png differ
diff --git a/_images/pictogram.png b/_images/pictogram.png
new file mode 100644
index 00000000000..37f1a1b2fb9
Binary files /dev/null and b/_images/pictogram.png differ
diff --git a/_images/pie_chart.png b/_images/pie_chart.png
new file mode 100644
index 00000000000..ba56ded24b8
Binary files /dev/null and b/_images/pie_chart.png differ
diff --git a/_images/rainbowadopted.png b/_images/rainbowadopted.png
new file mode 100644
index 00000000000..52803dbf2b5
Binary files /dev/null and b/_images/rainbowadopted.png differ
diff --git a/_images/reward.png b/_images/reward.png
new file mode 100644
index 00000000000..bdd85e24175
Binary files /dev/null and b/_images/reward.png differ
diff --git a/_images/simple_card.png b/_images/simple_card.png
new file mode 100644
index 00000000000..8cff4e90d01
Binary files /dev/null and b/_images/simple_card.png differ
diff --git a/_images/statistics1.png b/_images/statistics1.png
new file mode 100644
index 00000000000..d05fa928f92
Binary files /dev/null and b/_images/statistics1.png differ
diff --git a/_images/subcharfield.png b/_images/subcharfield.png
new file mode 100644
index 00000000000..e4b321652be
Binary files /dev/null and b/_images/subcharfield.png differ
diff --git a/_images/sum_counter.png b/_images/sum_counter.png
new file mode 100644
index 00000000000..9948016ca23
Binary files /dev/null and b/_images/sum_counter.png differ
diff --git a/_images/systray.png b/_images/systray.png
new file mode 100644
index 00000000000..06b40b440bd
Binary files /dev/null and b/_images/systray.png differ
diff --git a/_images/todo_list.png b/_images/todo_list.png
new file mode 100644
index 00000000000..6d77ee395a5
Binary files /dev/null and b/_images/todo_list.png differ
diff --git a/_images/toggle_card.png b/_images/toggle_card.png
new file mode 100644
index 00000000000..6048b38c738
Binary files /dev/null and b/_images/toggle_card.png differ
diff --git a/_images/toggle_todo.png b/_images/toggle_todo.png
new file mode 100644
index 00000000000..594b6c8aef6
Binary files /dev/null and b/_images/toggle_todo.png differ
diff --git a/_images/trees.png b/_images/trees.png
new file mode 100644
index 00000000000..35b39ce1619
Binary files /dev/null and b/_images/trees.png differ
diff --git a/_images/website_cursor_highlighter.png b/_images/website_cursor_highlighter.png
new file mode 100644
index 00000000000..f62c1a01f95
Binary files /dev/null and b/_images/website_cursor_highlighter.png differ
diff --git a/_images/website_date.png b/_images/website_date.png
new file mode 100644
index 00000000000..d1e6ba097ec
Binary files /dev/null and b/_images/website_date.png differ
diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py
index 56dc2f779b9..f2a57dd1b46 100644
--- a/awesome_clicker/__manifest__.py
+++ b/awesome_clicker/__manifest__.py
@@ -3,11 +3,11 @@
'name': "Awesome Clicker",
'summary': """
- Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game"
+ Companion addon for the Odoo JS Framework Training
""",
'description': """
- Starting module for "Master the Odoo web framework, chapter 1: Build a Clicker game"
+ Companion addon for the Odoo JS Framework Training
""",
'author': "Odoo",
diff --git a/awesome_clicker/static/src/click_rewards.js b/awesome_clicker/static/src/click_rewards.js
new file mode 100644
index 00000000000..6dcacae21f2
--- /dev/null
+++ b/awesome_clicker/static/src/click_rewards.js
@@ -0,0 +1,24 @@
+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,
+ },
+];
diff --git a/awesome_clicker/static/src/clicker_hook.js b/awesome_clicker/static/src/clicker_hook.js
new file mode 100644
index 00000000000..bebbb02b119
--- /dev/null
+++ b/awesome_clicker/static/src/clicker_hook.js
@@ -0,0 +1,6 @@
+import { useService } from "@web/core/utils/hooks";
+import { useState } from "@odoo/owl";
+
+export function useClicker() {
+ return useState(useService("awesome_clicker.clicker"));
+}
diff --git a/awesome_clicker/static/src/clicker_migration.js b/awesome_clicker/static/src/clicker_migration.js
new file mode 100644
index 00000000000..bc9c0c825c7
--- /dev/null
+++ b/awesome_clicker/static/src/clicker_migration.js
@@ -0,0 +1,29 @@
+export const CURRENT_VERSION = 2.0;
+export const migrations = [
+ {
+ fromVersion: 1.0,
+ toVersion: 2.0,
+ apply: (state) => {
+ state.trees.peachTree = {
+ price: 1500000,
+ level: 4,
+ produce: "peach",
+ purchased: 0,
+ };
+ state.fruits.peach = 0;
+ },
+ },
+];
+
+export function migrate(localState) {
+ if (localState?.version < CURRENT_VERSION) {
+ for (const migration of migrations) {
+ if (localState.version === migration.fromVersion) {
+ migration.apply(localState);
+ localState.version = migration.toVersion
+ }
+ }
+ localState.version = CURRENT_VERSION;
+ }
+ return localState;
+}
diff --git a/awesome_clicker/static/src/clicker_model.js b/awesome_clicker/static/src/clicker_model.js
new file mode 100644
index 00000000000..329d3ca7c1f
--- /dev/null
+++ b/awesome_clicker/static/src/clicker_model.js
@@ -0,0 +1,154 @@
+import { Reactive } from "@web/core/utils/reactive";
+import { EventBus } from "@odoo/owl";
+import { rewards } from "./click_rewards";
+import { choose } from "./utils";
+import { CURRENT_VERSION } from "./clicker_migration";
+
+export class ClickerModel extends Reactive {
+ constructor() {
+ super();
+ this.version = CURRENT_VERSION;
+ this.clicks = 0;
+ this.level = 0;
+ this.bus = new EventBus();
+ this.bots = {
+ clickbot: {
+ price: 1000,
+ level: 1,
+ increment: 10,
+ purchased: 0,
+ },
+ bigbot: {
+ price: 5000,
+ level: 2,
+ increment: 100,
+ purchased: 0,
+ }
+ };
+ this.trees = {
+ pearTree: {
+ price: 1000000,
+ level: 4,
+ produce: "pear",
+ purchased: 0,
+ },
+ cherryTree: {
+ price: 1000000,
+ level: 4,
+ produce: "cherry",
+ purchased: 0,
+ },
+ peachTree: {
+ price: 1500000,
+ level: 4,
+ produce: "peach",
+ purchased: 0,
+ },
+ }
+ this.fruits = {
+ pear: 0,
+ cherry: 0,
+ peach: 0,
+ },
+ this.multiplier = 1
+ this.ticks = 0;
+ }
+
+ addClick() {
+ this.increment(1);
+ }
+
+ /**
+ * This method is supposed to be periodically called by outside code, at some
+ * proper interval
+ */
+ tick() {
+ this.ticks++;
+ for (const bot in this.bots) {
+ this.clicks += this.bots[bot].increment * this.bots[bot].purchased * this.multiplier;
+ }
+ if (this.ticks % 3 === 0) {
+ for (const tree in this.trees) {
+ this.fruits[this.trees[tree].produce] += this.trees[tree].purchased;
+ }
+ }
+ }
+
+ buyMultiplier() {
+ if (this.clicks < 50000) {
+ return false;
+ }
+ this.clicks -= 50000;
+ this.multiplier++;
+ }
+
+ increment(inc) {
+ this.clicks += inc;
+ if (
+ this.milestones[this.level] &&
+ this.clicks >= this.milestones[this.level].clicks
+ ) {
+ this.bus.trigger("MILESTONE", this.milestones[this.level]);
+ this.level += 1;
+ }
+ }
+
+ buyBot(name) {
+ if (!Object.keys(this.bots).includes(name)) {
+ throw new Error(`Invalid bot name ${name}`);
+ }
+ if (this.clicks < this.bots[name].price) {
+ return false;
+ }
+
+ this.clicks -= this.bots[name].price;
+ this.bots[name].purchased += 1;
+ }
+
+ giveReward() {
+ const availableReward = [];
+ for (const reward of rewards) {
+ if (reward.minLevel <= this.level || !reward.minLevel) {
+ if (reward.maxLevel >= this.level || !reward.maxLevel) {
+ availableReward.push(reward);
+ }
+ }
+ }
+ const reward = choose(availableReward);
+ this.bus.trigger("REWARD", reward);
+ return choose(availableReward);
+ }
+
+ buyTree(name) {
+ if (!Object.keys(this.trees).includes(name)) {
+ throw new Error(`Invalid tree name ${name}`);
+ }
+ if (this.clicks < this.trees[name].price) {
+ return false;
+ }
+ this.clicks -= this.trees[name].price;
+ this.trees[name].purchased += 1;
+ }
+
+ toJSON() {
+ const json = Object.assign({}, this);
+ delete json["bus"];
+ return json;
+
+ }
+
+ static fromJSON(json) {
+ const clicker = new ClickerModel();
+ const clickerInstance = Object.assign(clicker, json);
+ return clickerInstance;
+ }
+
+ get milestones() {
+ return [
+ { clicks: 1000, unlock: "clickBot" },
+ { clicks: 5000, unlock: "bigBot" },
+ { clicks: 100000, unlock: "power multiplier" },
+ { clicks: 1000000, unlock: "pear tree & cherry tree" },
+ ];
+ }
+}
diff --git a/awesome_clicker/static/src/clicker_provider.js b/awesome_clicker/static/src/clicker_provider.js
new file mode 100644
index 00000000000..a59cc673b31
--- /dev/null
+++ b/awesome_clicker/static/src/clicker_provider.js
@@ -0,0 +1,27 @@
+import { registry } from "@web/core/registry";
+
+const commandProviderRegistry = registry.category("command_provider");
+
+commandProviderRegistry.add("clicker", {
+ provide: (env, options) => {
+ return [
+ {
+ name: "Buy 1 click bot",
+ action() {
+ env.services["awesome_clicker.clicker"].buyBot("clickbot");
+ },
+ },
+ {
+ name: "Open Clicker Game",
+ action() {
+ env.services.action.doAction({
+ type: "ir.actions.client",
+ tag: "awesome_clicker.client_action",
+ target: "new",
+ name: "Clicker Game",
+ });
+ },
+ },
+ ];
+ },
+});
diff --git a/awesome_clicker/static/src/clicker_service.js b/awesome_clicker/static/src/clicker_service.js
new file mode 100644
index 00000000000..e847b39e5f8
--- /dev/null
+++ b/awesome_clicker/static/src/clicker_service.js
@@ -0,0 +1,58 @@
+import { registry } from "@web/core/registry";
+import { ClickerModel } from "./clicker_model";
+import { browser } from "@web/core/browser/browser";
+import { migrate } from "./clicker_migration";
+
+const clickerService = {
+ dependencies: ["action", "effect", "notification"],
+ start(env, services) {
+ const localState = migrate(JSON.parse(browser.localStorage.getItem("clickerState")));
+ const model = localState ? ClickerModel.fromJSON(localState): new ClickerModel();
+
+ document.addEventListener("click", () => model.addClick(), true);
+ setInterval(() => {
+ model.tick();
+ }, 10000);
+
+ setInterval(() => {
+ browser.localStorage.setItem("clickerState", JSON.stringify(model))
+ }, 10000);
+
+ const bus = model.bus;
+ bus.addEventListener("MILESTONE", (ev) => {
+ services.effect.add({
+ message: `Milestone reached! You can now buy ${ev.detail.unlock}`,
+ type: "rainbow_man",
+ });
+ });
+
+ bus.addEventListener("REWARD", (ev) => {
+ const reward = ev.detail;
+ const closeNotification = services.notification.add(
+ `Congrats you won a reward: "${reward.description}"`,
+ {
+ type: "success",
+ sticky: true,
+ buttons: [
+ {
+ name: "Collect",
+ onClick: () => {
+ reward.apply(model);
+ closeNotification();
+ services.action.doAction({
+ type: "ir.actions.client",
+ tag: "awesome_clicker.client_action",
+ target: "new",
+ name: "Clicker Game"
+ });
+ },
+ },
+ ],
+ }
+ );
+ })
+ return model;
+ },
+};
+
+registry.category("services").add("awesome_clicker.clicker", clickerService);
diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js
new file mode 100644
index 00000000000..58940423709
--- /dev/null
+++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js
@@ -0,0 +1,49 @@
+import { Component } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { useClicker } from "../clicker_hook";
+import { ClickerValue } from "../clicker_value/clicker_value";
+import { Dropdown } from "@web/core/dropdown/dropdown";
+import { DropdownItem } from "@web/core/dropdown/dropdown_item";
+
+export class ClickerSystray extends Component {
+ static template = "awesome_clicker.ClickerSystray";
+ static props = {};
+ static components = { ClickerValue, Dropdown, DropdownItem };
+
+ setup() {
+ this.action = useService("action");
+ this.clicker = useClicker();
+ }
+
+ openClientAction() {
+ this.action.doAction({
+ type: "ir.actions.client",
+ tag: "awesome_clicker.client_action",
+ target: "new",
+ name: "Clicker Game"
+ });
+ }
+
+ get numberTrees() {
+ let sum = 0;
+ for (const tree in this.clicker.trees) {
+ sum += this.clicker.trees[tree].purchased;
+ }
+ return sum;
+ }
+
+ get numberFruits() {
+ let sum = 0;
+ for (const fruit in this.clicker.fruits) {
+ sum += this.clicker.fruits[fruit];
+ }
+ return sum;
+ }
+}
+
+export const systrayItem = {
+ Component: ClickerSystray,
+};
+
+registry.category("systray").add("awesome_clicker.ClickerSystray", systrayItem, { sequence: 1000 });
diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml
new file mode 100644
index 00000000000..ed4ede0d902
--- /dev/null
+++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml
@@ -0,0 +1,30 @@
+
+
+
+