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 + +![A Counter component](_images/counter.png) + +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. + +![Double Counter](_images/double_counter.png) + +**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 + +![Simple Card](_images/simple_card.png) + +## 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. + +![Markup](_images/markup.png) + +**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. + +![Sum of counters](_images/sum_counter.png) + +**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`. + +![Todo List](_images/todo_list.png) + +**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). + +![Muted Todo](_images/muted_todo.png) + +**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. + +![Creating a todo](_images/create_todo.png) + +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. + +![Autofocus](_images/autofocus.png) + +**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! + +![Toggling a todo](_images/toggle_todo.png) + +**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! + +![Deleting a todo](_images/delete_todo.png) + +**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. + +![Generic card](_images/generic_card.png) + +**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 + +![Toggling a card](_images/toggle_card.png) 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! + +![Overview](_images/overview_02.png) + +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. + +![Layout component](_images/new_layout.png) + +#### 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). + +![Navigation buttons](_images/navigation_buttons.png) + +**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. + +![Dashboard items](_images/dashboard_item.png) + +**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’ + +![Statistics](_images/statistics1.png) + +## 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! + +![Pie Chart](_images/pie_chart.png) + +#### 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. + +![Item configurations](_images/items_configuration.png) + +## 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_customize_fields_views.md b/3_customize_fields_views.md new file mode 100644 index 00000000000..0cda469b6e0 --- /dev/null +++ b/3_customize_fields_views.md @@ -0,0 +1,114 @@ +# Module 3: 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 + +![Add a ribbon](_images/add_ribbon.png) + +## 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 + + ``` + +![Custom view banner](_images/banner.png) + +## 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 + +![Charfield subclass](_images/subcharfield.png) + + +## 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 + +![Rainbowman](_images/rainbowadopted.png) + +## 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 + +![Rainbowman](_images/pictogram.png) + +## 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/4_build_a_clicker_game.md b/4_build_a_clicker_game.md new file mode 100644 index 00000000000..87960a76c63 --- /dev/null +++ b/4_build_a_clicker_game.md @@ -0,0 +1,449 @@ +# Module 4: 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. + +![Final Result](_images/final.png) + +## 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. + +![Systray](_images/systray.png) + +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" +} +``` + +![Client action](_images/client_action.png) + +**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. + +![Increment Button](_images/increment_button.png) + +**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! + +![Humanized Number](_images/humanized_number.png) + +**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. + +![Humanized Tooltip](_images/humanized_tooltip.png) + +**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. + +![Clickbot](_images/clickbot.png) + +# 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. + +![Milestones](_images/milestone1.png) + +#### 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). + +![Bigbots](_images/bigbot.png) + +# 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. + +![Rewar](_images/reward.png) + +#### 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`. + +![Command Palette](_images/command_palette.png) + +**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, ... + +![Dropdown](_images/dropdown.png) + +## 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. + +![Notebook](_images/notebook.png) + +#### 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. + +![Peach tree](_images/peach_tree.png) diff --git a/README.md b/README.md index 0c6667bb6f2..11670ba1eec 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,20 @@ -# Odoo tutorials +![Odoo Logo](_images/odoo_logo.png) -This repository hosts the code for the bases and solutions of the -[official Odoo tutorials](https://www.odoo.com/documentation/16.0/developer/howtos.html). +# Odoo : the Javascript Framework Training -It has 2 branches for each Odoo version: one for the bases and one for -the solutions. For example, `16.0` and `16.0-solutions`. The first -contains the code of the modules that serve as base for the tutorials, -and the second contains the code of the same modules with the complete -solution. \ No newline at end of file +This branch contains the exercises and the supporting code for the O doo Javascript +Framework Traning. There are four addons, each corresponding to a +standalone project that can be done independently from the others. + +To get started, clone this repository, checkout this branch, and start an odoo +server with this folder in the addons path. + +Note that a lot of theory is covered in the slides for this event. Also, don't +hesitate 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: Customize Fields and Views](3_customize_fields_views.md) | fields, views | +| [Module 4: Build a Clicker Game](4_build_a_clicker_game.md) | advanced framework, state management, ... | 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/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/awesome_clicker/__init__.py b/awesome_clicker/__init__.py new file mode 100644 index 00000000000..40a96afc6ff --- /dev/null +++ b/awesome_clicker/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py new file mode 100644 index 00000000000..f2a57dd1b46 --- /dev/null +++ b/awesome_clicker/__manifest__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Awesome Clicker", + + 'summary': """ + Companion addon for the Odoo JS Framework Training + """, + + 'description': """ + Companion addon for the Odoo JS Framework Training + """, + + 'author': "Odoo", + 'website': "https://www.odoo.com/", + 'category': 'Tutorials', + 'version': '0.1', + 'application': True, + 'installable': True, + 'depends': ['base', 'web'], + + 'data': [], + 'assets': { + 'web.assets_backend': [ + 'awesome_clicker/static/src/**/*', + ], + + }, + 'license': 'AGPL-3' +} diff --git a/awesome_dashboard/__init__.py b/awesome_dashboard/__init__.py new file mode 100644 index 00000000000..b0f26a9a602 --- /dev/null +++ b/awesome_dashboard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py new file mode 100644 index 00000000000..b575bf4f1be --- /dev/null +++ b/awesome_dashboard/__manifest__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Awesome Dashboard", + + 'summary': """ + Companion addon for the Odoo JS Framework Training + """, + + 'description': """ + Companion addon for the Odoo JS Framework Training + """, + + 'author': "Odoo", + 'website': "https://www.odoo.com/", + 'category': 'Tutorials', + 'version': '0.1', + 'application': True, + 'installable': True, + 'depends': ['base', 'web', 'mail', 'crm'], + + 'data': [ + 'views/views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'awesome_dashboard/static/src/**/*', + ], + }, + 'license': 'AGPL-3' +} diff --git a/awesome_dashboard/controllers/__init__.py b/awesome_dashboard/controllers/__init__.py new file mode 100644 index 00000000000..457bae27e11 --- /dev/null +++ b/awesome_dashboard/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py new file mode 100644 index 00000000000..c46cfc63bfa --- /dev/null +++ b/awesome_dashboard/controllers/controllers.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +import logging +import random + +from odoo import http +from odoo.http import request + +logger = logging.getLogger(__name__) + +class AwesomeDashboard(http.Controller): + @http.route('/awesome_dashboard/statistics', type='jsonrpc', auth='user') + def get_statistics(self): + """ + Returns a dict of statistics about the orders: + 'average_quantity': the average number of t-shirts by order + 'average_time': the average time (in hours) elapsed between the + moment an order is created, and the moment is it sent + 'nb_cancelled_orders': the number of cancelled orders, this month + 'nb_new_orders': the number of new orders, this month + 'total_amount': the total amount of orders, this month + """ + + return { + 'average_quantity': random.randint(4, 12), + 'average_time': random.randint(4, 123), + 'nb_cancelled_orders': random.randint(0, 50), + 'nb_new_orders': random.randint(10, 200), + 'orders_by_size': { + 'm': random.randint(0, 150), + 's': random.randint(0, 150), + 'xl': random.randint(0, 150), + }, + 'total_amount': random.randint(100, 1000) + } diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js new file mode 100644 index 00000000000..c4fb245621b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.js @@ -0,0 +1,8 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml new file mode 100644 index 00000000000..1a2ac9a2fed --- /dev/null +++ b/awesome_dashboard/static/src/dashboard.xml @@ -0,0 +1,8 @@ + + + + + hello dashboard + + + diff --git a/awesome_dashboard/views/views.xml b/awesome_dashboard/views/views.xml new file mode 100644 index 00000000000..47fb2b6f258 --- /dev/null +++ b/awesome_dashboard/views/views.xml @@ -0,0 +1,11 @@ + + + + Dashboard + awesome_dashboard.dashboard + + + + + + diff --git a/awesome_owl/__init__.py b/awesome_owl/__init__.py new file mode 100644 index 00000000000..457bae27e11 --- /dev/null +++ b/awesome_owl/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py new file mode 100644 index 00000000000..242e53ddeb7 --- /dev/null +++ b/awesome_owl/__manifest__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Awesome Owl", + + 'summary': """ + Companion addon for the Odoo JS Framework Training + """, + + 'description': """ + Companion addon for the Odoo JS Framework Training + """, + + 'author': "Odoo", + 'website': "https://www.odoo.com", + + # Categories can be used to filter modules in modules listing + # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml + # for the full list + 'category': 'Tutorials', + 'version': '0.2', + + # any module necessary for this one to work correctly + 'depends': ['base', 'web'], + 'application': True, + 'installable': True, + 'data': [ + 'views/templates.xml', + ], + 'assets': { + 'awesome_owl.assets_playground': [ + ('include', 'web._assets_helpers'), + ('include', 'web._assets_backend_helpers'), + 'web/static/src/scss/pre_variables.scss', + 'web/static/lib/bootstrap/scss/_variables.scss', + 'web/static/lib/bootstrap/scss/_maps.scss', + ('include', 'web._assets_bootstrap'), + ('include', 'web._assets_core'), + 'web/static/src/libs/fontawesome/css/font-awesome.css', + 'awesome_owl/static/src/**/*', + ], + }, + 'license': 'AGPL-3' +} diff --git a/awesome_owl/controllers/__init__.py b/awesome_owl/controllers/__init__.py new file mode 100644 index 00000000000..457bae27e11 --- /dev/null +++ b/awesome_owl/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers \ No newline at end of file diff --git a/awesome_owl/controllers/controllers.py b/awesome_owl/controllers/controllers.py new file mode 100644 index 00000000000..bccfd6fe283 --- /dev/null +++ b/awesome_owl/controllers/controllers.py @@ -0,0 +1,10 @@ +from odoo import http +from odoo.http import request, route + +class OwlPlayground(http.Controller): + @http.route(['/awesome_owl'], type='http', auth='public') + def show_playground(self): + """ + Renders the owl playground page + """ + return request.render('awesome_owl.playground') diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..cd72da2c009 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,14 @@ +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title: String, + slots: { + type: Object, + shape: { + default: true + }, + } + }; +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..5c284e81757 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,13 @@ + + + +
+
+
+

+ +

+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..9e9cff76df4 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,19 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.Counter"; + static props = { + onChange: { type: Function, optional: true } + }; + + setup() { + this.state = useState({ value: 1 }); + } + + increment() { + this.state.value = this.state.value + 1; + if (this.props.onChange) { + this.props.onChange(); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..4791fb6cd42 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,9 @@ + + + +
+ Counter: + +
+
+
diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js new file mode 100644 index 00000000000..1aaea902b55 --- /dev/null +++ b/awesome_owl/static/src/main.js @@ -0,0 +1,12 @@ +import { whenReady } from "@odoo/owl"; +import { mountComponent } from "@web/env"; +import { Playground } from "./playground"; + +const config = { + dev: true, + name: "Owl Tutorial" +}; + +// Mount the Playground component when the document.body is ready +whenReady(() => mountComponent(Playground, document.body, config)); + diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js new file mode 100644 index 00000000000..823eacfd2bd --- /dev/null +++ b/awesome_owl/static/src/playground.js @@ -0,0 +1,19 @@ +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todo_list/todo_list"; + +export class Playground extends Component { + static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList }; + + setup() { + this.str1 = "
some content
"; + this.str2 = markup("
some content
"); + this.sum = useState({ value: 2 }); + } + + incrementSum() { + this.sum.value++; + } +} diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml new file mode 100644 index 00000000000..c552a90e934 --- /dev/null +++ b/awesome_owl/static/src/playground.xml @@ -0,0 +1,22 @@ + + + + +
+ hello world + + +
The sum is:
+
+
+ + content of card 1 + + + + +
+ +
+ +
diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js new file mode 100644 index 00000000000..a69b5721607 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,16 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + static props = { + todo: { + type: Object, + shape: { id: Number, description: String, isCompleted: Boolean } + }, + toggleState: Function, + }; + + onChange() { + this.props.toggleState(this.props.todo.id); + } +} diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml new file mode 100644 index 00000000000..9a240566372 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,12 @@ + + + +
+ + +
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js new file mode 100644 index 00000000000..81f9aed7409 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,32 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutofocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem }; + + setup() { + this.nextId = 1; + this.todos = useState([]); + useAutofocus("input") + } + + addTodo(ev) { + if (ev.keyCode === 13 && ev.target.value != "") { + this.todos.push({ + id: this.nextId++, + description: ev.target.value, + isCompleted: false + }); + ev.target.value = ""; + } + } + + toggleTodo(todoId) { + const todo = this.todos.find((todo) => todo.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } +} diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml new file mode 100644 index 00000000000..19ce2ab584b --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,11 @@ + + + +
+ + + + +
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..f452f103aa0 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,8 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(refName) { + const ref = useRef(refName); + onMounted(() => { + ref.el.focus(); + }); +} diff --git a/awesome_owl/views/templates.xml b/awesome_owl/views/templates.xml new file mode 100644 index 00000000000..aa54c1a7241 --- /dev/null +++ b/awesome_owl/views/templates.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/awesome_shelter/__init__.py b/awesome_shelter/__init__.py new file mode 100644 index 00000000000..a0fdc10fe11 --- /dev/null +++ b/awesome_shelter/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/awesome_shelter/__manifest__.py b/awesome_shelter/__manifest__.py new file mode 100644 index 00000000000..5a7af98f925 --- /dev/null +++ b/awesome_shelter/__manifest__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +{ + "name": "Awesome Shelter", + "summary": """ + Companion addon for the Odoo JS Framework Training + """, + "description": """ + Companion addon for the Odoo JS Framework Training + """, + "author": "Odoo", + "website": "https://www.odoo.com/", + "category": "Tutorials", + "version": "0.1", + "application": True, + "installable": True, + "depends": ["base", "web", "contacts"], + "data": [ + "views/views.xml", + "security/ir.model.access.csv", + "data/shelter_data.xml", + ], + "assets": { + "web.assets_backend": [ + "awesome_shelter/static/src/**/*", + ], + }, + "license": "AGPL-3", +} diff --git a/awesome_shelter/data/shelter_data.xml b/awesome_shelter/data/shelter_data.xml new file mode 100644 index 00000000000..1da524ce1a6 --- /dev/null +++ b/awesome_shelter/data/shelter_data.xml @@ -0,0 +1,20 @@ + + + + + Cat + + + + + Dog + + + + + Stray + + + + + \ No newline at end of file diff --git a/awesome_shelter/models/__init__.py b/awesome_shelter/models/__init__.py new file mode 100644 index 00000000000..b90bab0a42a --- /dev/null +++ b/awesome_shelter/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import animal +from . import animal_type +from . import animal_race diff --git a/awesome_shelter/models/animal.py b/awesome_shelter/models/animal.py new file mode 100644 index 00000000000..aa836ca34f7 --- /dev/null +++ b/awesome_shelter/models/animal.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api +from dateutil.relativedelta import relativedelta + + +class Animal(models.Model): + _name = "awesome_shelter.animal" + _description = "Animal" + _inherit = ["mail.thread", "mail.activity.mixin"] + + name = fields.Char(required=True) + picture = fields.Image("Picture") + type_id = fields.Many2one("awesome_shelter.animal_type", "Type", required=True) + pictogram = fields.Image(related="type_id.pictogram") + state = fields.Selection( + selection=[ + ("on_site", "On site"), + ("adopted", "Adopted"), + ], + default="on_site", + required=True, + string="State", + tracking=True, + ) + race_id = fields.Many2one("awesome_shelter.animal_race", "Race") + notes = fields.Html("Notes") + dropper_id = fields.Many2one("res.partner", "Dropper") + drop_date = fields.Date("Drop date", default=fields.Date.today(), required=True) + owner_id = fields.Many2one("res.partner", "Owner", tracking=True) + birth_date = fields.Date("Birth date") + + is_present_for_six_month = fields.Boolean( + compute="_compute_is_present_for_six_month" + ) + + @api.depends("drop_date") + def _compute_is_present_for_six_month(self): + for record in self: + record.is_present_for_six_month = ( + fields.Date.today() + relativedelta(months=-6) < record.drop_date + ) diff --git a/awesome_shelter/models/animal_race.py b/awesome_shelter/models/animal_race.py new file mode 100644 index 00000000000..205906af281 --- /dev/null +++ b/awesome_shelter/models/animal_race.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class AnimalRace(models.Model): + _name = "awesome_shelter.animal_race" + _description = "Race" + + name = fields.Char(required=True) diff --git a/awesome_shelter/models/animal_type.py b/awesome_shelter/models/animal_type.py new file mode 100644 index 00000000000..0decb8b77aa --- /dev/null +++ b/awesome_shelter/models/animal_type.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class AnimalType(models.Model): + _name = "awesome_shelter.animal_type" + _description = "Type" + + name = fields.Char(required=True) + pictogram = fields.Image("Pictogram") diff --git a/awesome_shelter/security/ir.model.access.csv b/awesome_shelter/security/ir.model.access.csv new file mode 100644 index 00000000000..e796e844b32 --- /dev/null +++ b/awesome_shelter/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +awesome_shelter.access_shelter_animal,access_shelter_animal,awesome_shelter.model_awesome_shelter_animal,base.group_user,1,1,1,1 +awesome_shelter.access_shelter_animal_type,access_shelter_animal_type,awesome_shelter.model_awesome_shelter_animal_type,base.group_user,1,1,1,1 +awesome_shelter.access_shelter_animal_race,access_shelter_animal_race,awesome_shelter.model_awesome_shelter_animal_race,base.group_user,1,1,1,1 diff --git a/awesome_shelter/static/img/cat_pictogram.png b/awesome_shelter/static/img/cat_pictogram.png new file mode 100644 index 00000000000..70ba929f084 Binary files /dev/null and b/awesome_shelter/static/img/cat_pictogram.png differ diff --git a/awesome_shelter/static/img/dog_pictogram.png b/awesome_shelter/static/img/dog_pictogram.png new file mode 100644 index 00000000000..7b304821371 Binary files /dev/null and b/awesome_shelter/static/img/dog_pictogram.png differ diff --git a/awesome_shelter/views/views.xml b/awesome_shelter/views/views.xml new file mode 100644 index 00000000000..cb2529c7fec --- /dev/null +++ b/awesome_shelter/views/views.xml @@ -0,0 +1,85 @@ + + + + + Animals + awesome_shelter.animal + list,kanban,form + + + + awesome_shelter.animal.list + awesome_shelter.animal + + + + + + + + + + + + + awesome_shelter.animal.kanban + awesome_shelter.animal + + + + + +
+ + +
+
+ +
+
+
+ +
+ + + awesome_shelter.animal.form + awesome_shelter.animal + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+