diff --git a/content/developer/howtos/javascript_client_action.rst b/content/developer/howtos/javascript_client_action.rst index 1b9eab8c0..bbc3238d4 100644 --- a/content/developer/howtos/javascript_client_action.rst +++ b/content/developer/howtos/javascript_client_action.rst @@ -1,4 +1,6 @@ +.. _howtos/javascript_client_action: + ====================== Create a client action ====================== diff --git a/content/developer/reference/frontend/owl_components.rst b/content/developer/reference/frontend/owl_components.rst index 022369a92..865215cf6 100644 --- a/content/developer/reference/frontend/owl_components.rst +++ b/content/developer/reference/frontend/owl_components.rst @@ -1,7 +1,7 @@ .. _frontend/components: ============== -Owl Components +Owl components ============== The Odoo Javascript framework uses a custom component framework called Owl. It diff --git a/content/developer/reference/frontend/patching_code.rst b/content/developer/reference/frontend/patching_code.rst index 99e0a3f77..b0290cab5 100644 --- a/content/developer/reference/frontend/patching_code.rst +++ b/content/developer/reference/frontend/patching_code.rst @@ -97,6 +97,8 @@ Getters and setters are supported too: }, }); +.. _frontend/patching_class: + Patching a javascript class =========================== diff --git a/content/developer/tutorials/discover_js_framework.rst b/content/developer/tutorials/discover_js_framework.rst index 62e06fe15..fe178dd89 100644 --- a/content/developer/tutorials/discover_js_framework.rst +++ b/content/developer/tutorials/discover_js_framework.rst @@ -1,7 +1,7 @@ :show-content: ========================= -Discover the JS Framework +Discover the JS framework ========================= .. toctree:: @@ -10,24 +10,27 @@ Discover the JS Framework discover_js_framework/* -This tutorial is designed to introduce you to the basics of the Odoo Javascript framework. Whether +This two parts tutorial is designed to introduce you to the basics of the Odoo Javascript framework. Whether you are new to the framework or have some prior experience, this tutorial will provide you with a solid foundation for using the Odoo JavaScript framework in your projects. -This tutorial is divided into two parts. The first part covers the basics of Owl components, which +The first part covers the basics of Owl components, which are a key part of the Odoo JS framework. Owl components are reusable UI components that can be used to build complex web interfaces quickly and efficiently. We will explore how to create and use Owl -components in Odoo. - -The second part of the tutorial focuses on creating a dashboard using various features of Odoo. -Dashboards are an essential part of any web application, and provide a nice starting point to use -and interact with the Odoo codebase. +components in Odoo. Then, in the second part of this tutorial, we focus on creating a dashboard using various +features of Odoo. Dashboards are an essential part of any web application, and provide a nice starting +point to use and interact with the Odoo codebase. This tutorial assumes that you have some basic knowledge of development with Odoo in general (models, controllers, QWeb, ...). If you are new to Odoo, we recommend that you start with the :doc:`Getting started ` tutorial before proceeding with this one. +.. note:: + + Each chapter of this tutorial is an independant project. If you feel comfortable with Owl, you can + start directly with chapter 2. + .. _tutorials/discover_js_framework/setup: Setup @@ -35,11 +38,12 @@ Setup #. Clone the `official Odoo tutorials repository `_ and switch to the branch `{CURRENT_MAJOR_BRANCH}`. -#. Add the cloned repository to the :option:`--addons-path `. -#. Start a new Odoo database and install the modules `owl_playground` and `awesome_tshirt`. +#. Add the cloned repository to your :option:`--addons-path `. +#. Start a new Odoo database and install the modules `awesome_owl` (for chapter 1) and `awesome_dashboard` + (for chapter 2). Content ======= - :doc:`discover_js_framework/01_owl_components` -- :doc:`discover_js_framework/02_web_framework` +- :doc:`discover_js_framework/02_build_a_dashboard` diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components.rst b/content/developer/tutorials/discover_js_framework/01_owl_components.rst index 7287690fd..827f07ce0 100644 --- a/content/developer/tutorials/discover_js_framework/01_owl_components.rst +++ b/content/developer/tutorials/discover_js_framework/01_owl_components.rst @@ -1,5 +1,5 @@ ========================= -Chapter 1: Owl Components +Chapter 1: Owl components ========================= This chapter introduces the `Owl framework `_, a tailor-made component @@ -10,27 +10,28 @@ In Owl, every part of user interface is managed by a component: they hold the lo templates that are used to render the user interface. In practice, a component is represented by a small JavaScript class subclassing the `Component` class. -Before getting into the exercises, make sure you have followed all the steps described in this +To get started, you need a running Odoo server and a development environment setup. Before getting +into the exercises, make sure you have followed all the steps described in this :ref:`tutorial introduction `. -.. spoiler:: Solutions - - The solutions for each exercise of the chapter are hosted on the `official Odoo tutorials - repository - `_. It - is recommended to try to solve them first without looking at the solution! - .. tip:: If you use Chrome as your web browser, you can install the `Owl Devtools` extension. This extension provides many features to help you understand and profile any Owl application. `Video: How to use the DevTools `_ -In this chapter, we use the `owl_playground` addon, which provides a simplified environment that +In this chapter, we use the `awesome_owl` addon, which 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. To get started, open the `/owl_playground/playground` route with your browser: it +web client code. To get started, open the `/awesome_owl` route with your browser: it should display an Owl component with the text *hello world*. +.. spoiler:: Solutions + + The solutions for each exercise of the chapter are hosted on the `official Odoo tutorials + repository + `_. It + is recommended to try to solve them first without looking at the solution! + Example: a `Counter` component ============================== @@ -54,8 +55,8 @@ button. } } -The `Counter` component specifies the name of the template to render. The template is written in XML -and defines a part of user interface: +The `Counter` component specifies the name of a template that represents its html. It is written in XML +using the QWeb language: .. code-block:: xml @@ -70,276 +71,468 @@ and defines a part of user interface: 1. Displaying a counter ======================= -As a first exercise, let us implement a counter in the `Playground` component located in -:file:`owl_playground/static/src/`. To see the result, you can go to the `/owl_playground/playground` -route with your browser. - -.. exercise:: - - #. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will - need to use the `useState hook - <{OWL_PATH}/doc/reference/hooks.md#usestate>`_ so that the component is re-rendered - whenever any part of the state object that has been read by this component is modified. - #. In the same component, create an `increment` method. - #. Modify the template in :file:`playground.xml` so that it displays your counter variable. Use - `t-esc <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_ to output the data. - #. Add a button in the template and specify a `t-on-click - <{OWL_PATH}/doc/reference/event_handling.md#event-handling>`_ attribute in the button to - trigger the `increment` method whenever the button is clicked. - .. image:: 01_owl_components/counter.png - :scale: 70% :align: center +As a first exercise, let us modify the `Playground` component located in +:file:`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. + + +#. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will + need to use the `useState hook + <{OWL_PATH}/doc/reference/hooks.md#usestate>`_ so that the component is re-rendered + whenever any part of the state object that has been read by this component is modified. +#. In the same component, create an `increment` method. +#. Modify the template in :file:`playground.xml` so that it displays your counter variable. Use + `t-esc <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_ to output the data. +#. Add a button in the template and specify a `t-on-click + <{OWL_PATH}/doc/reference/event_handling.md#event-handling>`_ attribute in the button to + trigger the `increment` method whenever the button is clicked. + .. 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 :ref:`debug mode with assets ` so that the files are not minified. -2. Extract counter in a component -================================= +This exercise showcases an important feature of Owl: the `reactivity system <{OWL_PATH}/doc/reference/reactivity.md>`_. +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. -For now we have the logic of a counter in the `Playground` component, let us see how to create a -`sub-component <{OWL_PATH}/doc/reference/component.md#sub-components>`_ from it. +2. Extract `Counter` in a sub component +======================================= -.. exercise:: +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 <{OWL_PATH}/doc/reference/component.md#sub-components>`_ from it: - #. Extract the counter code from the `Playground` component into a new `Counter` component. - #. 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`. Make sure - the template is in its own file, with the same name. +#. Extract the counter code from the `Playground` component into a new `Counter` component. +#. 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`. Make sure + the template is in its own file, with the same name. +#. Add two counters in your playground. + +.. image:: 01_owl_components/double_counter.png + :align: center + +.. 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` .. important:: Don't forget :code:`/** @odoo-module **/` in your JavaScript files. More information on this can be found :ref:`here `. -3. A todo component -=================== +.. _tutorials/discover_js_framework/simple_card: -We will create new components in :file:`owl_playground/static/src/` to keep track of a list of -todos. This will be done incrementally in multiple exercises that will introduce various concepts. +3. A simple `Card` component +============================ -.. exercise:: +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 <{OWL_PATH}/doc/reference/props.md>`_). - #. Create a `Todo` component that receive a `todo` object in `props - <{OWL_PATH}/doc/reference/props.md>`_, and display it. It should show something like - **3. buy milk**. - #. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on the task if it is - done. To do that, you can use `dynamic attributes - <{OWL_PATH}/doc/reference/templates.md#dynamic-attributes>`_. - #. Modify :file:`owl_playground/static/src/playground.js` and - :file:`owl_playground/static/src/playground.xml` to display your new `Todo` component with - some hard-coded props to test it first. +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: - .. example:: +.. code-block:: xml - .. code-block:: javascript + - setup() { - ... - this.todo = { id: 3, description: "buy milk", done: false }; - } +The above example should produce some html using bootstrap that look like this: -.. image:: 01_owl_components/todo.png - :scale: 70% +.. code-block:: html + +
+
+
my title
+

+ some content +

+
+
+ +#. Create a `Card` component +#. Import it in `Playground` and display a few cards in its template + +.. image:: 01_owl_components/simple_card.png :align: center -.. seealso:: - `Owl: Dynamic class attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-class-attribute>`_ +4. Using `markup` to display html +================================= -4. Props validation +If you used `t-esc` in the previous exercise, then you may have noticed that Owl will automatically escape +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 directive <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_. + +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. + +#. Update `Card` to use `t-out` +#. Update `Playground` to import `markup`, and use it on some html values +#. 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`. + +.. image:: 01_owl_components/markup.png + :align: center + +5. Props validation =================== -The `Todo` component has an implicit API. It expects to receive in its props the description of a -todo object in a specified format: `id`, `description` and `done`. Let us make that API more +The `Card` component has an implicit API. It expects to receive two strings in its props: 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 <{OWL_PATH}/doc/reference/app.md#dev-mode>`_. You can activate the dev mode in the `App -configuration <{OWL_PATH}/doc/reference/app.md#configuration>`_. +configuration <{OWL_PATH}/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. +It is a good practice to do props validation for every component. -.. exercise:: +#. Add `props validation <{OWL_PATH}/doc/reference/props.md#props-validation>`_ to the `Card` + component. +#. Rename the `title` props into something else in the playground template, then check in the + :guilabel:`Console` tab of your browser's dev tools that you can see an error. - #. Add `props validation <{OWL_PATH}/doc/reference/props.md#props-validation>`_ to the `Todo` - component. - #. Open the :guilabel:`Console` tab of your browser's dev tools and make sure the props - validation passes in dev mode, which is activated by default in `owl_playground`. The dev mode - can be activated and deactivated by modifying the `dev` attribute in the in the `config` - parameter of the `mount <{OWL_PATH}/doc/reference/app.md#mount-helper>`_ function in - :file:`owl_playground/static/src/main.js`. - #. Remove `done` from the props and reload the page. The validation should fail. +6. The sum of two `Counter` +=========================== -5. A list of todos -================== +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. -Now, let us display a list of todos instead of just one todo. For now, we can still hard-code the -list. +This can be done by using a `callback prop <{OWL_PATH}/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. -.. exercise:: +#. Add prop validation to the `Counter` component: it should accept an optional `onChange` + function prop. +#. Update the `Counter` component to call the `onChange` prop (if it exists) whenever it + is incremented. +#. Modify the `Playground` component to maintain a local state value (`sum`), initially + set to 2, and display it in its template +#. Implement an `incrementSum` method in `Playground` +#. Give that method as a prop to two (or more!) sub `Counter` components. - #. Change the code to display a list of todos instead of just one. Create a new `TodoList` - component to hold the `Todo` components and use `t-foreach - <{OWL_PATH}/doc/reference/templates.md#loops>`_ in its template. - #. Think about how it should be keyed with the `t-key` directive. - -.. image:: 01_owl_components/todo_list.png - :scale: 70% +.. image:: 01_owl_components/sum_counter.png :align: center -6. Adding a todo +.. important:: + + There is a subtlety with callback props: they usually should be defined with the `.bind` + suffix. See the `documentation <{OWL_PATH}/doc/reference/props.md#binding-function-props>`_ + +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): + +.. code-block:: js + + { id: 3, description: "buy milk", isCompleted: false } + +#. Create a `TodoList` and a `TodoItem` components +#. The `TodoItem` component should receive a `todo` as a prop, and display its `id` and `description` in a `div`. +#. For now, hardcode the list of todos: + + .. code-block:: js + + // in TodoList + this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]); + +#. Use `t-foreach <{OWL_PATH}/doc/reference/templates.md#loops>`_ to display each todo in a `TodoItem` +#. Display a `TodoList` in the playground +#. Add props validation to `TodoItem` + +.. image:: 01_owl_components/todo_list.png + :align: center + +Note that the `t-foreach` directive is not exactly the same in Owl as the QWeb python implementation: it +requires a `t-key` unique value, so Owl can properly reconciliate each element. + +.. tip:: + + Since the `TodoList` and `TodoItem` components are so tightly coupled, it makes + sense to put them in the same folder + +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 <{OWL_PATH}/doc/reference/templates.md#dynamic-attributes>`_. + +#. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on the `TodoItem` root element + if it is completed. +#. Change the hardcoded `todo` 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 +html properties such as the `value` of an input). + +.. image:: 01_owl_components/muted_todo.png + :align: center + +.. tip:: + + Owl let you combine static class values with dynamic values. The following example will work as expected: + + .. code-block:: xml + +
+ + See also: `Owl: Dynamic class attributes <{OWL_PATH}/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. -.. exercise:: +#. Remove the hardcoded values in the `TodoList` component - #. Add an input above the task list with placeholder *Enter a new task*. - #. Add an `event handler <{OWL_PATH}/doc/reference/event_handling.md>`_ on the `keyup` event - named `addTodo`. - #. Implement `addTodo` to check if enter was pressed (:code:`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. - #. Make sure the todo has a unique id. It can be just a counter that increments at each todo. - #. Wrap the todo list in a `useState` hook to let Owl know that it should update the UI when the - list is modified. - #. Bonus point: don't do anything if the input is empty. + .. code-block:: javascript - .. code-block:: javascript + this.todos = useState([]); + +#. Add an input above the task list with placeholder *Enter a new task*. +#. Add an `event handler <{OWL_PATH}/doc/reference/event_handling.md>`_ on the `keyup` event + named `addTodo`. +#. Implement `addTodo` to check if enter was pressed (:code:`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. +#. Make sure the todo has a unique id. It can be just a counter that increments at each todo. +#. Bonus point: don't do anything if the input is empty. - this.todos = useState([]); .. image:: 01_owl_components/create_todo.png - :scale: 70% :align: center .. seealso:: `Owl: Reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_ -7. Focusing the input -===================== +Theory: Component lifecycle and hooks +===================================== + +So far, we have seen one example of a hook function: `useState`. A `hook <{OWL_PATH}/doc/reference/hooks.md>`_ +is a special function that *hook into* the internals of the component. In the case of +`useState`, it generates a proxy object linked to the current component. This is why +hook functions have to be called in the `setup` method, and no later! + + +.. flowchart LR + +.. classDef hook fill:#ccf + +.. subgraph "creation" +.. direction TB +.. A:::hook +.. B:::hook +.. M:::hook +.. A[setup]-->B +.. B[onWillStart] --> C(render) +.. C --> D("mount (in DOM)") +.. D --> M[onMounted] +.. end + +.. subgraph updates +.. direction TB +.. E:::hook +.. F:::hook +.. H:::hook +.. E["(onWillUpdateProps)"] --> L(render) +.. L --> F[onWillPatch] +.. F --> G(patch DOM) +.. G --> H[onPatched] +.. end + +.. subgraph destruction +.. direction TB +.. I:::hook +.. J:::hook +.. I[onWillUnmount] --> J[onWillDestroy] +.. J --> N(removed from DOM) + +.. end + +.. creation --> updates +.. updates --> destruction + + +.. figure:: 01_owl_components/component_lifecycle.svg + :align: center + :width: 50% + + +An Owl component goes through a lot of phases: it can be instantiated, rendered, +mounted, updated, detached, destroyed, ... This is the `component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_. +The figure above show the most important events in the life of a component (hooks are shown in purple). +Roughly speaking, a component is created, then updated (potentially many times), then is destroyed. + +Owl provides a variety of built-in `hooks functions <{OWL_PATH}/doc/reference/hooks.md>`_. All of them have to be called in +the `setup` function. For example, if you want to execute some code when your component is mounted, you can use the `onMounted` +hook: + +.. code-block:: javascript + + setup() { + onMounted(() => { + // do something here + }); + } + +.. tip:: + + All hook functions start with `use` or `on`. For example: `useState` or `onMounted`. + + +10. Focusing the input +====================== Let's see how we can access the DOM with `t-ref <{OWL_PATH}/doc/reference/refs.md>`_ and `useRef -<{OWL_PATH}/doc/reference/hooks.md#useref>`_. +<{OWL_PATH}/doc/reference/hooks.md#useref>`_. The main idea is that you need to mark +the target element in the component template with a `t-ref`: -.. exercise:: +.. code-block:: xml - #. Focus the `input` from the previous exercise when the dashboard is `mounted - <{OWL_PATH}/doc/reference/component.md#mounted>`_. This this should be done from the - `TodoList` component. - #. Bonus point: extract the code into a specialized `hook <{OWL_PATH}/doc/reference/hooks.md>`_ - `useAutofocus` in a new :file:`owl_playground/utils.js` file. +
hello
-.. seealso:: - `Owl: Component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_ +Then you can access it in the JS with the `useRef hook <{OWL_PATH}/doc/reference/hooks.md#useref>`_. +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` +return an object that contains a `el` (for element) key that is only defined when the +component is mounted. -8. Toggling todos -================= +.. code-block:: js + + setup() { + this.myRef = useRef('some_name'); + onMounted(() => { + console.log(this.myRef.el); + }); + } + + +#. Focus the `input` from the previous exercise. This this should be done from the + `TodoList` component (note that there is a `focus` method on the input html element). +#. Bonus point: extract the code into a specialized `hook <{OWL_PATH}/doc/reference/hooks.md>`_ + `useAutofocus` in a new :file:`awesome_owl/utils.js` file. + +.. image:: 01_owl_components/autofocus.png + :align: center + +.. tip:: + + Refs are usually suffixed by `Ref` to make it obvious that they are special objects: + + .. code-block:: js + + this.inputRef = useRef('refname'); + +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 `Todo` +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 using a `callback prop <{OWL_PATH}/doc/reference/props.md#binding-function-props>`_ `toggleState`. -.. exercise:: +#. Add an input with the attribute :code:`type="checkbox"` before the id of the task, which must + be checked if the state `isCompleted` is true. - #. Add an input with the attribute :code:`type="checkbox"` before the id of the task, which must - be checked if the state `done` is true. + .. tip:: + Owl does not create attributes computed with the `t-att` directive if it evaluates to a + falsy value. - .. tip:: - QWeb does not create attributes computed with the `t-att` directive if it evaluates to a - falsy value. - - #. Add a callback props `toggleState`. - #. Add a `click` event handler on the input in the `Todo` component and make sure it calls the - `toggleState` function with the todo id. - #. Make it work! +#. Add a callback props `toggleState` to `TodoItem`. +#. Add a `click` event handler on the input in the `TodoItem` component and make sure it calls the + `toggleState` function with the todo id. +#. Make it work! .. image:: 01_owl_components/toggle_todo.png - :scale: 70% :align: center -9. Deleting todos -================= +12. Deleting todos +================== The final touch is to let the user delete a todo. -.. exercise:: +#. Add a new callback prop `removeTodo` in `TodoItem`. +#. Insert :code:`` in the template of the `TodoItem` component. +#. Whenever the user clicks on it, it should call the `removeTodo` method. +#. Make it work! - #. Add a new callback prop `removeTodo`. - #. Insert :code:`` in the template of the `Todo` component. - #. Whenever the user clicks on it, it should call the `removeTodo` method. + .. 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. - .. 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. +.. code-block:: - .. code-block:: - - // 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); - } + // 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); + } .. image:: 01_owl_components/delete_todo.png - :scale: 70% :align: center .. _tutorials/discover_js_framework/generic_card: -10. Generic card with slots -=========================== +13. Generic `Card` with slots +============================= -Owl has a powerful `slot <{OWL_PATH}/doc/reference/slots.md>`_ system to allow you to write generic -components. This is useful to factorize the common layout between different parts of the interface. +In a :ref:`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. -.. exercise:: +This is exactly what Owl `slot <{OWL_PATH}/doc/reference/slots.md>`_ system is designed +for: allowing to write generic components. - #. Insert a new `Card` component between the `Counter` and `Todolist` components. Use the - following Bootstrap HTML structure for the card: +Let us modify the `Card` component to use slots: - .. code-block:: html +#. Remove the `content` prop +#. Use the default slot to define the body +#. Insert a few cards with arbitrary content, such as a `Counter` component +#. (bonus) Add prop validation -
- ... -
-
Card title
-

- Some quick example text to build on the card title and make up the bulk - of the card's content. -

- Go somewhere -
-
- - #. This component should have two slots: one slot for the title, and one for the content (the - default slot). It should be possible to use the `Card` component as follows: - - .. code-block:: html - - - Card title -

Some quick example text...

- Go somewhere -
- - #. Bonus point: if the `title` slot is not given, the `h5` should not be rendered at all. - -.. image:: 01_owl_components/card.png - :scale: 70% +.. image:: 01_owl_components/generic_card.png :align: center .. seealso:: `Bootstrap: documentation on cards `_ -11. Extensive props validation -============================== +14. Minimizing card content +=========================== -.. exercise:: +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) - #. Add prop validation on the `Card` component. - #. Try to express in the props validation system that it requires a `default` slot, and an - optional `title` slot. +#. Add a state to the `Card` component to track if it is open (the default) or not +#. Add a `t-if` in the template to conditionally render the content +#. Add a button in the header, and modify the code to flip the state when the button is clicked + +.. image:: 01_owl_components/toggle_card.png + :scale: 90% + :align: center diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/autofocus.png b/content/developer/tutorials/discover_js_framework/01_owl_components/autofocus.png new file mode 100644 index 000000000..eb13a35b6 Binary files /dev/null and b/content/developer/tutorials/discover_js_framework/01_owl_components/autofocus.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/card.png b/content/developer/tutorials/discover_js_framework/01_owl_components/card.png deleted file mode 100644 index 7cb843199..000000000 Binary files a/content/developer/tutorials/discover_js_framework/01_owl_components/card.png and /dev/null differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/component_lifecycle.svg b/content/developer/tutorials/discover_js_framework/01_owl_components/component_lifecycle.svg new file mode 100644 index 000000000..e3767e555 --- /dev/null +++ b/content/developer/tutorials/discover_js_framework/01_owl_components/component_lifecycle.svg @@ -0,0 +1 @@ +
destruction
onWillUnmount
onWillDestroy
removed from DOM
updates
(onWillUpdateProps)
render
onWillPatch
patch DOM
onPatched
creation
setup
onWillStart
render
mount (in DOM)
onMounted
\ No newline at end of file diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/counter.png b/content/developer/tutorials/discover_js_framework/01_owl_components/counter.png index cd8eb5f12..5e2ac1a8f 100644 Binary files a/content/developer/tutorials/discover_js_framework/01_owl_components/counter.png and b/content/developer/tutorials/discover_js_framework/01_owl_components/counter.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/create_todo.png b/content/developer/tutorials/discover_js_framework/01_owl_components/create_todo.png index e5d8eadae..83ec5c4c5 100644 Binary files a/content/developer/tutorials/discover_js_framework/01_owl_components/create_todo.png and b/content/developer/tutorials/discover_js_framework/01_owl_components/create_todo.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/delete_todo.png b/content/developer/tutorials/discover_js_framework/01_owl_components/delete_todo.png index 3dbbfbb70..cc9ff9a85 100644 Binary files a/content/developer/tutorials/discover_js_framework/01_owl_components/delete_todo.png and b/content/developer/tutorials/discover_js_framework/01_owl_components/delete_todo.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/double_counter.png b/content/developer/tutorials/discover_js_framework/01_owl_components/double_counter.png new file mode 100644 index 000000000..99e2b88d4 Binary files /dev/null and b/content/developer/tutorials/discover_js_framework/01_owl_components/double_counter.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/generic_card.png b/content/developer/tutorials/discover_js_framework/01_owl_components/generic_card.png new file mode 100644 index 000000000..ad079e95d Binary files /dev/null and b/content/developer/tutorials/discover_js_framework/01_owl_components/generic_card.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/markup.png b/content/developer/tutorials/discover_js_framework/01_owl_components/markup.png new file mode 100644 index 000000000..bd8fea06f Binary files /dev/null and b/content/developer/tutorials/discover_js_framework/01_owl_components/markup.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/muted_todo.png b/content/developer/tutorials/discover_js_framework/01_owl_components/muted_todo.png new file mode 100644 index 000000000..e732e4cfa Binary files /dev/null and b/content/developer/tutorials/discover_js_framework/01_owl_components/muted_todo.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/overview.png b/content/developer/tutorials/discover_js_framework/01_owl_components/overview.png deleted file mode 100644 index d183bffa3..000000000 Binary files a/content/developer/tutorials/discover_js_framework/01_owl_components/overview.png and /dev/null differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/simple_card.png b/content/developer/tutorials/discover_js_framework/01_owl_components/simple_card.png new file mode 100644 index 000000000..8cff4e90d Binary files /dev/null and b/content/developer/tutorials/discover_js_framework/01_owl_components/simple_card.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/sum_counter.png b/content/developer/tutorials/discover_js_framework/01_owl_components/sum_counter.png new file mode 100644 index 000000000..9948016ca Binary files /dev/null and b/content/developer/tutorials/discover_js_framework/01_owl_components/sum_counter.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/todo.png b/content/developer/tutorials/discover_js_framework/01_owl_components/todo.png deleted file mode 100644 index e1733c8e7..000000000 Binary files a/content/developer/tutorials/discover_js_framework/01_owl_components/todo.png and /dev/null differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/todo_list.png b/content/developer/tutorials/discover_js_framework/01_owl_components/todo_list.png index 7de845157..6d77ee395 100644 Binary files a/content/developer/tutorials/discover_js_framework/01_owl_components/todo_list.png and b/content/developer/tutorials/discover_js_framework/01_owl_components/todo_list.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/toggle_card.png b/content/developer/tutorials/discover_js_framework/01_owl_components/toggle_card.png new file mode 100644 index 000000000..6048b38c7 Binary files /dev/null and b/content/developer/tutorials/discover_js_framework/01_owl_components/toggle_card.png differ diff --git a/content/developer/tutorials/discover_js_framework/01_owl_components/toggle_todo.png b/content/developer/tutorials/discover_js_framework/01_owl_components/toggle_todo.png index 913bfdac9..594b6c8ae 100644 Binary files a/content/developer/tutorials/discover_js_framework/01_owl_components/toggle_todo.png and b/content/developer/tutorials/discover_js_framework/01_owl_components/toggle_todo.png differ diff --git a/content/developer/tutorials/discover_js_framework/02_build_a_dashboard.rst b/content/developer/tutorials/discover_js_framework/02_build_a_dashboard.rst new file mode 100644 index 000000000..9f3c21c36 --- /dev/null +++ b/content/developer/tutorials/discover_js_framework/02_build_a_dashboard.rst @@ -0,0 +1,457 @@ +============================ +Chapter 2: Build a dashboard +============================ + +The first part of this tutorial introduced you to most of Owl ideas. It is now time to learn +about the Odoo JavaScript framework in its entirety, as used by the web client. + +.. graph TD +.. subgraph "Owl" +.. C[Component] +.. T[Template] +.. H[Hook] +.. S[Slot] +.. E[Event] +.. end + +.. odoo[Odoo JavaScript framework] --> Owl + +.. figure:: 02_web_framework/previously_learned.svg + :align: center + :width: 50% + +To get started, you need a running Odoo server and a development environment setup. Before getting +into the exercises, make sure you have followed all the steps described in this +:ref:`tutorial introduction `. For this chapter, we will start +from the empty dashboard provided by the `awesome_dashboard` addon. We will progressively add +features to it, using the Odoo JavaScript framework. + +.. admonition:: Goal + + .. image:: 02_web_framework/overview_02.png + :align: center + +.. spoiler:: Solutions + + The solutions for each exercise of the chapter are hosted on the + `official Odoo tutorials repository + `_. + +1. A new 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 +<{GITHUB_PATH}/addons/web/static/src/search/layout.js>`_, available in `@web/search/layout`. + +#. Update the `AwesomeDashboard` component located in :file:`awesome_dashboard/static/src/` to use the + `Layout` component. You can use + :code:`{controlPanel: {} }` for the `display` props of + the `Layout` component. +#. Add a `className` prop to `Layout`: `className="'o_dashboard h-100'"` +#. Add a `dashboard.scss` file in which you set the background-color of `.o_dashboard` to gray (or your + favorite color) + +Open http://localhost:8069/web, then open the :guilabel:`Awesome Dashboard` app, and see the +result. + +.. image:: 02_web_framework/new_layout.png + :align: center + +.. seealso:: + + - `Example: use of Layout in client action + <{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.js>`_ and + `template <{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.xml>`_ + - `Example: use of Layout in kanban view + <{GITHUB_PATH}/addons/web/static/src/views/kanban/kanban_controller.xml>`_ + +.. _tutorials/discover_js_framework/services: + +Theory: Services +================ + +In practice, every component (except the root component) may be destroyed at any time and replaced +(or not) with another component. This means that each component internal state is not persistent. +This is fine in many cases, but there certainly are situations where we want to keep some data around. +For example, all discuss messages should not be reloaded every time we display a channel. + +Also, it may happen that we need to write some code that is not a component. Maybe something that +process all barcodes, or that manages the user configuration (context, ...). + +The Odoo framework defines the idea of a :ref:`service `, which is a persistent +piece of code that exports state and/or functions. Each service can depend on other services, and +components can import a service. + +The following example registers a simple service that displays a notification every 5 seconds: + +.. code-block:: js + + import { registry } from "@web/core/registry"; + + const myService = { + dependencies: ["notification"], + start(env, { notification }) { + let counter = 1; + setInterval(() => { + notification.add(`Tick Tock ${counter++}`); + }, 5000); + }, + }; + + registry.category("services").add("myService", myService); + +Services can be accessed by any component. Imagine that we have a service to maintain some shared +state: + + +.. code-block:: js + + import { registry } from "@web/core/registry"; + + const sharedStateService = { + start(env) { + let state = {}; + + return { + getValue(key) { + return state[key]; + }, + setValue(key, value) { + state[key] = value; + }, + }; + }, + }; + + registry.category("services").add("shared_state", sharedStateService); + +Then, any component can do this: + +.. code-block:: js + + import { useService } from "@web/core/utils/hooks"; + + setup() { + this.sharedState = useService("shared_state"); + const value = this.sharedState.getValue("somekey"); + // do something with value + } + +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: + +.. code-block:: 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: + +#. A button `Customers`, which opens a kanban view with all customers (this action already + exists, so you should use `its xml id + `_). + +#. A button `Leads`, which opens a dynamic action on the `crm.lead` model with a list and a form view. + +.. image:: 02_web_framework/navigation_buttons.png + :align: center + +.. seealso:: + - `Example: doAction use + <{GITHUB_PATH}/addons/account/static/src/components/journal_dashboard_activity + /journal_dashboard_activity.js#L35>`_ + - `Code: action service + <{GITHUB_PATH}/addons/web/static/src/webclient/actions/action_service.js>`_ + +3. Add a DashboardItem +====================== + +Let us now improve our content. + +#. 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`. +#. Add a few cards in the dashboard, with no size and a size of 2. + +.. image:: 02_web_framework/dashboard_item.png + :align: center + +.. seealso:: + - `Owl slot system <{OWL_PATH}/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 :ref:`rpc service `. +It only exports a single function that perform the request: :code:`rpc(route, params, settings)`. +A basic request could look like this: + +.. code-block:: js + + setup() { + this.rpc = useService("rpc"); + onWillStart(async () => { + const result = await this.rpc("/my/controller", {a: 1, b: 2}); + // ... + }); + } + +#. Update `Dashboard` so that it uses the `rpc` service. +#. Call the statistics route `/awesome_dashboard/statistics` in the `onWillStart` hook. +#. 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' + +.. image:: 02_web_framework/statistics.png + :align: center + +.. seealso:: + + - `Code: rpc service <{GITHUB_PATH}/addons/web/static/src/core/network/rpc_service.js>`_ + - `Example: calling a route in onWillStart + `_ + +5. Cache network calls, create a service +======================================== + +If you open the :guilabel:`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! + +#. Register and import a new `awesome_dashboard.statistics` service. +#. It should provide a function `loadStatistics` that, once called, performs the actual rpc, and + always return the same information. +#. Use the `memoize `_ utility function from + `@web/core/utils/functions` that will allow caching the statistics. +#. Use this service in the `Dashboard` component. +#. Check that it works as expected + +.. seealso:: + - `Example: simple service <{GITHUB_PATH}/addons/web/static/src/core/network/http_service.js>`_ + - `Example: service with a dependency + <{GITHUB_PATH}/addons/web/static/src/core/user_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 `_. 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. + +#. Create a `PieChart` component +#. In its `onWillStart` method, load chartjs, you can use the `loadJs + `_ function to load + :file:`/web/static/lib/Chart/Chart.js`. +#. Use the `PieChart` component in a `DashboardItem` to display a `pie chart + `_ that shows the + correct 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 +#. The `PieChart` component will need to render a canvas, and draw on it using `chart.js`. +#. Make it work! + +.. image:: 02_web_framework/pie_chart.png + :align: center + :scale: 80% + +.. seealso:: + - `Example: lazy loading a js file + `_ + - `Example: rendering a chart in a component + `_ + +7. Real life update +=================== + +Since we moved the data loading in a cache, it does not ever 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 dashboard 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. + + +#. Update the dashboard service to reload data every 10 minutes (to test it, use 10s instead!) +#. Modify it to return a `reactive <{OWL_PATH}/doc/reference/reactivity.md#reactive>`_ object. + Reloading data should update the reactive object in place. +#. The `Dashboard` component can now use it with a `useState` + +.. seealso:: + - `Documentation on reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_ + - `Example: Use of reactive in a service + `_ + +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. + +To do that, we will need to create a new bundle containing all our dashboard assets, +then use the `LazyComponent` (located in `@web/core/assets`). + +#. Move all dashboard assets into a sub folder `/dashboard` to make it easier to + add to a bundle. +#. Create a `awesome_dashboard.dashboard` assets bundle containing all content of + the `/dashboard` folder +#. Modify `dashboard.js` to register itself in the `lazy_components` registry, and not + in the `action` registry. +#. Add in `src/` a file `dashboard_action` that import `LazyComponent` and register + it to the `action` 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 some other data. + +So, the next step is then to make our dashboard generic: instead of hardcoding its content +in the template, it can just iterate over a list of dashboard items. But then, many +questions comes 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 folowing structure: + +.. code-block:: 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 choose to add to his 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 the following snippet: + +.. code-block:: 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. + +#. create and implement two components: `NumberCard` and `PieChartCard`, with the corresponding props +#. 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 +#. 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 + + .. code-block:: 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: + +#. Instead of exporting a list, register all dashboard items in a `awesome_dashboard` registry +#. Import all the items of the `awesome_dashboard` registry in the `Dashboard` component + +The dashboard is now easily extensible. Any other odoo addon that want 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 it is persistent, but we don't have to deal +with the server for now. + +The dashboard configuration will be saved as a list of removed item ids. + +#. Add a button in the control panel with a gear icon, to indicate that it is a settings button +#. Clicking on that button should open a dialog +#. In that dialog, we want to see a list of all existing dashboard items, each with a checkbox +#. There should be a `Apply` button in the footer. Clicking on it will build a list of all item ids + that are unchecked +#. We want to store that value in the local storage +#. And modify the `Dashboard` component to filter the current items by removing the ids of items + from the configuration + + +.. image:: 02_web_framework/items_configuration.png + :width: 80% + :align: center + +12. Going further +================= + +Here is a list of some small improvements you could try to do if you have the time: + +#. Make sure your application can be :ref:`translated ` (with + `env._t`). +#. Clicking on a section of the pie chart should open a list view of all orders which have the + corresponding size. +#. Save the content of the dashboard in a user settings on the server! +#. Make it responsive: in mobile mode, each card should take 100% of the width + +.. seealso:: + - `Example: use of env._t function + `_ + - `Code: translation code in web/ + `_ diff --git a/content/developer/tutorials/discover_js_framework/02_web_framework.rst b/content/developer/tutorials/discover_js_framework/02_web_framework.rst deleted file mode 100644 index cc892b9f5..000000000 --- a/content/developer/tutorials/discover_js_framework/02_web_framework.rst +++ /dev/null @@ -1,273 +0,0 @@ -============================= -Chapter 2: Odoo Web Framework -============================= - -The first part of this tutorial introduced you to most of Owl ideas. It is now time to learn -about the Odoo JavaScript framework in its entirety, as used by the web client. - -.. graph TD -.. subgraph "Owl" -.. C[Component] -.. T[Template] -.. H[Hook] -.. S[Slot] -.. E[Event] -.. end - -.. odoo[Odoo JavaScript framework] --> Owl - -.. figure:: 02_web_framework/previously_learned.svg - :align: center - :width: 50% - -For this chapter, we will start from the empty dashboard provided by the `awesome_tshirt` -addon. We will progressively add features to it, using the Odoo JavaScript framework. - -.. admonition:: Goal - - .. image:: 02_web_framework/overview_02.png - :align: center - -.. spoiler:: Solutions - - The solutions for each exercise of the chapter are hosted on the - `official Odoo tutorials repository - `_. - -1. A new 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 -<{GITHUB_PATH}/addons/web/static/src/search/layout.js>`_, available in `@web/search/layout`. - -.. exercise:: - - Update the `AwesomeDashboard` component located in :file:`awesome_tshirt/static/src/` to use the - `Layout` component. You can use - :code:`{controlPanel: { "top-right": false, "bottom-right": false } }` for the `display` props of - the `Layout` component. - - Open http://localhost:8069/web, then open the :guilabel:`Awesome T-Shirts` app, and see the - result. - -.. image:: 02_web_framework/new_layout.png - :align: center - -.. seealso:: - - - `Example: use of Layout in client action - <{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.js>`_ and - `template <{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.xml>`_ - - `Example: use of Layout in kanban view - <{GITHUB_PATH}/addons/web/static/src/views/kanban/kanban_controller.xml>`_ - -2. Add some buttons for quick navigation -======================================== - -Let us now use the action service for an easy access to the common views in Odoo. - -:ref:`Services ` is a notion defined by the Odoo JavaScript framework; it is a -persistent piece of code that exports a state and/or functions. Each service can depend on other -services, and components can import a service with the `useService()` hook. - -.. example:: - - This shows how to open the settings view from a component using the action service. - - .. code-block:: js - - import { useService } from "@web/core/utils/hooks"; - ... - setup() { - this.action = useService("action"); - } - openSettings() { - this.action.doAction("base_setup.action_general_configuration"); - } - ... - -.. exercise:: - - Let us add three buttons in the control panel bottom left zone. - - #. A button `Customers`, which opens a kanban view with all customers (this action already - exists, so you should use `its xml id - `_). - #. A button `New Orders`, which opens a list view with all orders created in the last 7 days. Use - the `Domain `_ helper class to represent the domain. - - .. tip:: - One way to represent the desired domain could be - :code:`[('create_date','>=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]` - - #. A button `Cancelled Order`, which opens a list of all orders created in the last 7 days, but - already cancelled. Rather than defining the action twice, factorize it in a new `openOrders` - method. - -.. image:: 02_web_framework/navigation_buttons.png - :align: center - -.. seealso:: - - `Example: doAction use - <{GITHUB_PATH}/addons/account/static/src/components/journal_dashboard_activity - /journal_dashboard_activity.js#L35>`_ - - `Code: action service - <{GITHUB_PATH}/addons/web/static/src/webclient/actions/action_service.js>`_ - -3. Call the server, add some statistics -======================================= - -Let's improve the dashboard by adding a few cards (see the `Card` component :ref:`made in the -previous chapter `) containing a few statistics. There -is a route `/awesome_tshirt/statistics` that performs some computations and returns an object -containing some useful information. - -Whenever we need to call a specific controller, we need to use the :ref:`rpc service -`. It only exports a single function that perform the request: -:code:`rpc(route, params, settings)` - -Here is a short explanation on the various arguments: - -- `route` is the target route, as a string. For example `/myroute/`. -- `params` is an object that contains all data that will be given to the controller. (optional) -- `settings` are for advanced controls on the request. Make it silent, or using a specific xhr - instance. (optional) - -.. example:: - - A basic request could look like this: - - .. code-block:: js - - setup() { - this.rpc = useService("rpc"); - onWillStart(async () => { - const result = await this.rpc("/my/controller", {a: 1, b: 2}); - // ... - }); - } - -.. exercise:: - #. Change `Dashboard` so that it uses the `rpc` service. - #. Call the statistics route `/awesome_tshirt/statistics` in the `onWillStart` hook. - #. 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' - -.. image:: 02_web_framework/statistics.png - :align: center - -.. seealso:: - - - `Code: rpc service <{GITHUB_PATH}/addons/web/static/src/core/network/rpc_service.js>`_ - - `Example: calling a route in onWillStart - `_ - -4. Cache network calls, create a service -======================================== - -If you open the :guilabel:`Network` tab of your browser's dev tools, you will see that the call to -`/awesome_tshirt/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! - -.. example:: - - The following example registers a simple service that displays a notification every 5 seconds. - - .. code-block:: js - - import { registry } from "@web/core/registry"; - const myService = { - dependencies: ["notification"], - start(env, { notification }) { - let counter = 1; - setInterval(() => { - notification.add(`Tick Tock ${counter++}`); - }, 5000); - }, - }; - registry.category("services").add("myService", myService); - -.. exercise:: - - #. Register and import a new `awesome_tshirt.statistics` service. - #. It should provide a function `loadStatistics` that, once called, performs the actual rpc, and - always return the same information. - #. Use the `memoize `_ utility function from - `@web/core/utils/functions` that will allow caching the statistics. - #. Use this service in the `Dashboard` component. - #. Check that it works as expected - -.. seealso:: - - `Example: simple service <{GITHUB_PATH}/addons/web/static/src/core/network/http_service.js>`_ - - `Example: service with a dependency - <{GITHUB_PATH}/addons/web/static/src/core/user_service.js>`_ - -5. 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 `_. 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. - -.. exercise:: - #. Load chartjs, you can use the `loadJs - `_ function to load - :file:`/web/static/lib/Chart/Chart.js`. - #. In a `Card` (from previous exercises), display a `pie chart - `_ in the dashboard that displays the - correct quantity for each sold t-shirts in each size (that information is available in the - statistics route). - -.. image:: 02_web_framework/pie_chart.png - :align: center - :scale: 50% - -.. seealso:: - - `Example: lazy loading a js file - `_ - - `Example: rendering a chart in a component - `_ - -6. Going further -================ - -Here is a list of some small improvements you could try to do if you have the time: - -.. exercise:: - - #. Make sure your application can be :ref:`translated ` (with - `env._t`). - #. Clicking on a section of the pie chart should open a list view of all orders which have the - corresponding size. - #. Add a SCSS file and see if you can change the background color of the dashboard action. - - .. image:: 02_web_framework/misc.png - :align: center - :scale: 50% - -.. seealso:: - - `Example: use of env._t function - `_ - - `Code: translation code in web/ - `_ diff --git a/content/developer/tutorials/discover_js_framework/02_web_framework/dashboard_item.png b/content/developer/tutorials/discover_js_framework/02_web_framework/dashboard_item.png new file mode 100644 index 000000000..1524ca5b0 Binary files /dev/null and b/content/developer/tutorials/discover_js_framework/02_web_framework/dashboard_item.png differ diff --git a/content/developer/tutorials/discover_js_framework/02_web_framework/items_configuration.png b/content/developer/tutorials/discover_js_framework/02_web_framework/items_configuration.png new file mode 100644 index 000000000..a828a187b Binary files /dev/null and b/content/developer/tutorials/discover_js_framework/02_web_framework/items_configuration.png differ diff --git a/content/developer/tutorials/discover_js_framework/02_web_framework/misc.png b/content/developer/tutorials/discover_js_framework/02_web_framework/misc.png deleted file mode 100644 index 54ad00b0b..000000000 Binary files a/content/developer/tutorials/discover_js_framework/02_web_framework/misc.png and /dev/null differ diff --git a/content/developer/tutorials/discover_js_framework/02_web_framework/navigation_buttons.png b/content/developer/tutorials/discover_js_framework/02_web_framework/navigation_buttons.png index 366d2fa4b..79d0221d2 100644 Binary files a/content/developer/tutorials/discover_js_framework/02_web_framework/navigation_buttons.png and b/content/developer/tutorials/discover_js_framework/02_web_framework/navigation_buttons.png differ diff --git a/content/developer/tutorials/discover_js_framework/02_web_framework/new_layout.png b/content/developer/tutorials/discover_js_framework/02_web_framework/new_layout.png index cf62b05f9..7bfd81287 100644 Binary files a/content/developer/tutorials/discover_js_framework/02_web_framework/new_layout.png and b/content/developer/tutorials/discover_js_framework/02_web_framework/new_layout.png differ diff --git a/content/developer/tutorials/discover_js_framework/02_web_framework/overview_02.png b/content/developer/tutorials/discover_js_framework/02_web_framework/overview_02.png index ae9c2d9b7..bd618027c 100644 Binary files a/content/developer/tutorials/discover_js_framework/02_web_framework/overview_02.png and b/content/developer/tutorials/discover_js_framework/02_web_framework/overview_02.png differ diff --git a/content/developer/tutorials/discover_js_framework/02_web_framework/pie_chart.png b/content/developer/tutorials/discover_js_framework/02_web_framework/pie_chart.png index 1ed070fdb..ba56ded24 100644 Binary files a/content/developer/tutorials/discover_js_framework/02_web_framework/pie_chart.png and b/content/developer/tutorials/discover_js_framework/02_web_framework/pie_chart.png differ diff --git a/content/developer/tutorials/discover_js_framework/02_web_framework/statistics.png b/content/developer/tutorials/discover_js_framework/02_web_framework/statistics.png index 23557c4ba..d05fa928f 100644 Binary files a/content/developer/tutorials/discover_js_framework/02_web_framework/statistics.png and b/content/developer/tutorials/discover_js_framework/02_web_framework/statistics.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework.rst b/content/developer/tutorials/master_odoo_web_framework.rst index 7ea65d030..608c44f69 100644 --- a/content/developer/tutorials/master_odoo_web_framework.rst +++ b/content/developer/tutorials/master_odoo_web_framework.rst @@ -1,7 +1,7 @@ :show-content: ============================= -Master the Odoo Web Framework +Master the Odoo web framework ============================= .. toctree:: @@ -11,32 +11,49 @@ Master the Odoo Web Framework master_odoo_web_framework/* This tutorial is designed for those who have completed the :doc:`discover_js_framework` tutorial and -are looking to deepen their knowledge of the Odoo web framework. +are looking to deepen their knowledge of the Odoo web framework. It is organized in four independant +projects, each focusing on different features of Odoo. -For this training, we will step into the shoes of the IT staff at the fictional company Awesome -T-Shirt, which is dedicated to printing custom t-shirts for online customers. The Awesome T-Shirt -company uses Odoo to manage orders and has created a dedicated Odoo module to manage their workflow. +.. note:: -In this tutorial, we will explore various aspects of the Odoo web framework in detail, including -fields and views, notifications, command palette, and much more. This tutorial will provide you with -the knowledge and skills you need to take full advantage of the Odoo web framework. So, let's get -started! + Each of these chapters can be done independantly, in any order. Also, be aware that some of them + cover a lot of material, so they may be quite long. -.. _howtos/master_odoo_web_framework/setup: +The first project is about building a `clicker game `_. +While working on it, you will learn various aspects of the web framework: systray, command palette, +dialogs, notifications, customizing existing components and much more. + +The second project is focused on an important category of components: fields. Field components +represent the value of a field for a record, they appear in many places in the web client: in form +views, obviously, but also in kanban and list views, and may even be used alone, without a view. +Due to their importance, it makes sense to learn how to create and manipulate such components. + +In the context of the web framework, views usually refers to the javascript implementation of a +component that represents one or many records, depending on a description (`ir.ui.view`). Such +components are actually quite complicated and usually requires various sub systems (a renderer, +a model, a controller, a arch parser, ...). In chapter 3, we create a new view from scratch to +represent a list of images. + +Finally, the last project in chapter 4 is about customizing an existing view (a kanban view) by +adding a search panel on its left. It is interesting to see how one can take existing code, and +modify it to suit our needs. Also, it is a realistic project, that will feature many common issues +that arises while working on Odoo. + + +.. _tutorials/master_odoo_web_framework/setup: Setup ===== #. Clone the `official Odoo tutorials repository `_ and switch to the branch `{CURRENT_MAJOR_BRANCH}`. -#. Add the cloned repository to the :option:`--addons-path `. -#. Start a new Odoo database and install the modules `awesome_tshirt` and `awesome_gallery`. +#. Add the cloned repository to your :option:`--addons-path `. +#. Start a new Odoo database and install the modules for each chapter that you want to work on: + `awesome_clicker` (for chapter 1), `awesome_fields` (for chapter 2), `awesome_gallery` (for chapter 3) or `awesome_kanban` (for chapter 4). Content ======= -- :doc:`master_odoo_web_framework/01_fields_and_views` -- :doc:`master_odoo_web_framework/02_miscellaneous` -- :doc:`master_odoo_web_framework/03_custom_kanban_view` -- :doc:`master_odoo_web_framework/04_creating_view_from_scratch` -- :doc:`master_odoo_web_framework/05_testing` +- :doc:`master_odoo_web_framework/01_build_clicker_game` +- :doc:`master_odoo_web_framework/02_create_gallery_view` +- :doc:`master_odoo_web_framework/03_customize_kanban_view` diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game.rst b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game.rst new file mode 100644 index 000000000..209de2a09 --- /dev/null +++ b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game.rst @@ -0,0 +1,447 @@ +=============================== +Chapter 1: Build a Clicker game +=============================== + +For this project, we will build together a `clicker game `_, +completely integrated with Odoo. In this game, the goal is to accumulate a large number of clicks, and +to automate the system. The interesting part is that we will use the Odoo user interface as our playground. +For example, we will hide bonuses in some random parts of the web client. + +To get started, you need a running Odoo server and a development environment. Before getting +into the exercises, make sure you have followed all the steps described in this +:ref:`tutorial introduction `. + +.. admonition:: Goal + + .. image:: 01_build_clicker_game/final.png + :align: center + +.. spoiler:: Solutions + + The solutions for each exercise of the chapter are hosted on the + `official Odoo tutorials repository + `_. + + +1. Create a systray item +======================== + +To get started, we want to display a counter in the systray. + +#. Create a `clicker_systray_item.js` (and `xml`) file with a hello world Owl component. +#. Register it to the systray registry, and make sure it is visible. +#. Update the content of the item so that it displays the following string: `Clicks: 0`, and + add a button on the right to increment the value. + +.. image:: 01_build_clicker_game/systray.png + :align: center + +And voila, we have a completely working clicker game! + +.. seealso:: + + - :ref:`Documentation on the systray registry ` + - `Example: adding a systray item to the registry + `_ + +2. Count external clicks +======================== + +Well, to be honest, it is not much fun yet. So let us add a new feature: we want all clicks in the +user interface to count, so the user is incentivized to use Odoo as much as possible! But obviously, +the intentional clicks on the main counter should still count more. + +#. Use `useExternalListener` to listen on all clicks on `document.body`. +#. Each of these clicks should increase the counter value by 1. +#. Modify the code so that each click on the counter increased the value by 10 +#. Make sure that a click on the counter does not increase the value by 11! +#. Also additional challenge: make sure the external listener capture the events, so we don't + miss any clicks. + +.. seealso:: + + - `Owl documentation on useExternalListener `_ + - `MDN page on event capture `_ + + +3. Create a client action +========================= + +Currently, the current user interface is quite small: it is just a systray item. We certainly need +more room to display more of our game. To do that, let us create a client action. A client action +is a main action, managed by the web client, that displays a component. + +#. Create a `client_action.js` (and `xml`) file, with a hello world component. +#. Register that client action in the action registry under the name `awesome_clicker.client_action` +#. Add a button on the systray item with the text `Open`. Clicking on it should open the + client action `awesome_clicker.client_action` (use the action service to do that). +#. To avoid disrupting employees' workflow, we prefer the client action to open within a popover + rather than in fullscreen mode. Modify the `doAction` call to open it in a popover. + + .. tip:: + + You can use `target: "new"` in the `doAction` to open the action in a popover: + + .. code-block:: js + + { + type: "ir.actions.client", + tag: "awesome_clicker.client_action", + target: "new", + name: "Clicker" + } + +.. image:: 01_build_clicker_game/client_action.png + :align: center + +.. seealso:: + + - :ref:`How to create a client action ` + +4. Move the state to a service +============================== + +For now, our client action is just a hello world component. We want it to display our game state, but +that state is currently only available in the systray item. So it means that we need to change the +location of our state to make it available for all our components. This is a perfect use case for services. + +#. Create a `clicker_service.js` file with the corresponding service. +#. This service should export a reactive value (the number of clicks) and a few functions to update it: + + .. code-block:: js + + const state = reactive({ clicks: 0 }); + ... + return { + state, + increment(inc) { + state.clicks += inc + } + }; + +#. Access the state in both the systray item and the client action (don't forget to `useState` it). Modify + the systray item to remove its own local state and use it. Also, you can remove the `+10 clicks` button. +#. Display the state in the client action, and add a `+10` clicks button in it. + +.. image:: 01_build_clicker_game/increment_button.png + :align: center + +.. seealso:: + + - :ref:`Short explanation on services ` + +5. Use a custom hook +==================== + +Right now, every part of the code that will need to use our clicker service will have to import `useService` and +`useState`. Since it is quite common, let us use a custom hook. It is also useful to put more emphasis on the +`clicker` part, and less emphasis on the `service` part. + +#. Export a `useClicker` hook. +#. Update all current uses of the clicker service to the new hook: + + .. code-block:: js + + this.clicker = useClicker(); + +.. seealso:: + + - `Documentation on hooks: `_ + +6. Humanize the displayed value +=============================== + +We will in the future display large numbers, so let us get ready for that. There is a `humanNumber` function that +format numbers in a easier to comprehend way: for example, `1234` could be formatted as `1.2k` + +#. Use it to display our counters (both in the systray item and the client action). +#. Create a `ClickValue` component that display the value. + + .. note:: + + Owl allows component that contains just text nodes! + +.. image:: 01_build_clicker_game/humanized_number.png + :align: center + +.. seealso:: + + - `definition of humanNumber function `_ + +7. Add a tooltip in `ClickValue` component +========================================== + +With the `humanNumber` function, we actually lost some precision on our interface. Let us display the real number +as a tooltip. + +#. Tooltip needs an html element. Change the `ClickValue` to wrap the value in a `` element +#. Add a dynamic `data-tooltip` attribute to display the exact value. + +.. image:: 01_build_clicker_game/humanized_tooltip.png + :align: center + +.. seealso:: + + - `Documentation in the tooltip service `_ + +8. Buy ClickBots +================ + +Let us make our game even more interesting: once a player get to 1000 clicks for the first time, the game +should unlock a new feature: the player can buy robots for 1000 clicks. These robots will generate 10 clicks +every 10 seconds. + +#. Add a `level` number to our state. This is a number that will be incremented at some milestones, and + open new features +#. Add a `clickBots` number to our state. It represents the number of robots that have been purchased. +#. Modify the client action to display the number of click bots (only if `level >= 1`), with a `Buy` + button that is enabled if `clicks >= 1000`. The `Buy` button should increment the number of clickbots by 1. +#. Set a 10s interval in the service that will increment the number of clicks by `10*clickBots`. +#. Make sure the Buy button is disabled if the player does not have enough clicks. + +.. image:: 01_build_clicker_game/clickbot.png + :align: center + +9. Refactor to a class model +============================ + +The current code is written in a somewhat functional style. But to do so, we have to export the state and all its +update functions in our clicker object. As this project grows, this may become more and more complex. To make it +simpler, let us split our business logic out of our service and into a class. + +#. Create a `clicker_model` file that exports a reactive class. Move all the state and update functions from + the service into the model. + + .. tip:: + + You can extends the ClickerModel with the `Reactive` class from + :file:`@web/core/utils/reactive`. The `Reactive` class wrap the model into a reactive proxy. + +#. Rewrite the clicker service to instantiate and export the clicker model class. + +.. seealso:: + + - `Example of subclassing Reactive `_ + +10. Notify when a milestone is reached +====================================== + +There is not much feedback that something changed when we reached 1k clicks. Let us use the `effect` service +to communicate that information clearly. The problem is that our click model does not have access to services. +Also, we want to keep as much as possible the UI concern out of the model. So, we can explore a new strategy +for communication: event buses. + +#. Update the clicker model to instantiate a bus, and to trigger a `MILESTONE_1k` event when we reach 1000 clicks + for the first time. +#. Change the clicker service to listen to the same event on the model bus. +#. When that happens, use the `effect` service to display a rainbow man. +#. Add some text to explain that the user can now buy clickbots. + +.. image:: 01_build_clicker_game/milestone.png + :align: center + +.. seealso:: + + - `Owl documentation on event bus `_ + - :ref:`Documentation on effect service ` + +11. Add BigBots +=============== + +Clearly, we need a way to provide the player with more choices. Let us add a new type of clickbot: `BigBots`, +which are just more powerful: they provide with 100 clicks each 10s, but they cost 5000 clicks + +#. increment `level` when it gets to 5k (so it should be 2) +#. Update the state to keep track of bigbots +#. bigbots should be available at `level >=2` +#. Display the corresponding information in the client action + +.. tip:: + + If you need to use `<` or `>` in a template as a javascript expression, be careful since it might class with + the xml parser. To solve that, you can use one of the special aliases: `gt, gte, lt` or `lte`. See the + `Owl documentation page on template expressions `_. + +.. image:: 01_build_clicker_game/bigbot.png + :align: center + +12. Add a new type of resource: power +===================================== + +Now, to add another scaling point, let us add a new type of resource: a power multiplier. This is a number +that can be increased at `level >= 3`, and multiplies the action of the bots (so, instead of providing +one click, clickbots now provide us with `multiplier` clicks). + +#. increment `level` when it gets to 100k (so it should be 3). +#. update the state to keep track of the power (initial value is 1). +#. change bots to use that number as a multiplier. +#. Update the user interface to display and let the user purchase a new power level (costs: 50k). + +.. image:: 01_build_clicker_game/bigbot.png + :align: center + +13. Define some random rewards +============================== + +We want the user to obtain sometimes bonuses, to reward using Odoo. + +#. Define a list of rewards in `click_rewards.js`. A reward is an object with: + - a `description` string. + - a `apply` function that take the game state in argument and can modify it. + - a `minLevel` number (optional) that describes at which unlock level the bonus is available. + - a `maxLevel` number (optional) that describes at which unlock level a bonus is no longer available. + + For example: + + .. code-block:: js + + export const rewards = [ + { + description: "Get 1 click bot", + apply(clicker) { + clicker.increment(1); + }, + maxLevel: 3, + }, + { + description: "Get 10 click bot", + apply(clicker) { + clicker.increment(10); + }, + minLevel: 3, + maxLevel: 4, + }, + { + description: "Increase bot power!", + apply(clicker) { + clicker.multipler += 1; + }, + minLevel: 3, + }, + ]; + + You can add whatever you want to that list! + +#. Define a function `getReward` that will select a random reward from the list of rewards that matches + the current unlock level. +#. Extract the code that choose randomly in an array in a function `choose` that you can move to another `utils.js` file. + +14. Provide a reward when opening a form view +============================================= + +#. Patch the form controller. Each time a form controller is created, it should randomly decides (1% chance) + if a reward should be given. +#. If the answer is yes, call a method `getReward` on the model. +#. That method should choose a reward, send a sticky notification, with a button `Collect` that will + then apply the reward, and finally, it should open the `clicker` client action. + +.. image:: 01_build_clicker_game/reward.png + :align: center + +.. seealso:: + + - :ref:`Documentation on patching a class ` + - `Definition of patch function `_ + - `Example of patching a class `_ + +15. Add commands in command palette +=================================== + +#. Add a command `Open Clicker Game` to the command palette. +#. Add another command: `Buy 1 click bot`. + +.. image:: 01_build_clicker_game/command_palette.png + :align: center + +.. seealso:: + + - `Example of use of command provider registry `_ + +16. Add yet another resource: trees +=================================== + +It is now time to introduce a completely new type of resources. Here is one that should not be too controversial: trees. +We will now allow the user to plant (collect?) fruit trees. A tree costs 1 million clicks, but it will provide us with +fruits (either pears or cherries). + +#. Update the state to keep track of various types of trees: pear/cherries, and their fruits. +#. Add a function that computes the total number of trees and fruits. +#. Define a new unlock level at `clicks >= 1 000 000`. +#. Update the client user interface to display the number of trees and fruits, and also, to buy trees. +#. Increment the fruit number by 1 for each tree every 30s. + +.. image:: 01_build_clicker_game/trees.png + :align: center + +17. Use a dropdown menu for the systray item +============================================ + +Our game starts to become interesting. But for now, the systray only displays the total number of clicks. We +want to see more information: the total number of trees and fruits. Also, it would be useful to have a quick +access to some commands and some more information. Let us use a dropdown menu! + +#. Replace the systray item by a dropdown menu. +#. It should display the numbers of clicks, trees, and fruits, each with a nice icon. +#. Clicking on it should open a dropdown menu that displays more detailed information: each types of trees + and fruits. +#. Also, a few dropdown items with some commands: open the clicker game, buy a clickbot, ... + +.. image:: 01_build_clicker_game/dropdown.png + :align: center + +18. Use a Notebook component +============================ + +We now keep track of a lot more information. Let us improve our client interface by organizing the information +and features in various tabs, with the `Notebook` component: + +#. Use the `Notebook` component. +#. All `click` content should be displayed in one tab. +#. All `tree/fruits` content should be displayed in another tab. + +.. image:: 01_build_clicker_game/notebook.png + :align: center + +.. seealso:: + + - :ref:`Odoo: Documentation on Notebook component ` + - `Owl: Documentation on slots `_ + - `Tests of Notebook component `_ + +19. Persist the game state +=========================== + +You certainly noticed a big flaw in our game: it is transient. The game state is lost each time the user closes the +browser tab. Let us fix that. We will use the local storage to persist the state. + +#. Import `browser` from :file:`@web/core/browser/browser` to access the localstorage. +#. Serialize the state every 10s (in the same interval code) and store it on the local storage. +#. When the `clicker` service is started, it should load the state from the local storage (if any), or initialize itself + otherwise. + +20. Introduce state migration system +==================================== + +Once you persist state somewhere, a new problem arises: what happens when you update your code, so the shape of the state +changes, and the user opens its browser with a state that was created with an old version? Welcome to the world of +migration issues! + +It is probably wise to tackle the problem early. What we will do here is add a version number to the state, and introduce +a system to automatically update the states if it is not up to date. + +#. Add a version number to the state. +#. Define an (empty) list of migrations. A migration is an object with a `fromVersion` number, a `toVersion` number, and a `apply` function. +#. Whenever the code loads the state from the local storage, it should check the version number. If the state is not + uptodate, it should apply all necessary migrations. + +21. Add another type of trees +============================= + +To test our migration system, let us add a new type of trees: peaches. + +#. Add `peach` trees. +#. Increment the state version number. +#. Define a migration. + +.. image:: 01_build_clicker_game/peach_tree.png + :align: center diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/bigbot.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/bigbot.png new file mode 100644 index 000000000..77ae8dc00 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/bigbot.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/clickbot.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/clickbot.png new file mode 100644 index 000000000..fc7217485 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/clickbot.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/client_action.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/client_action.png new file mode 100644 index 000000000..434a19213 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/client_action.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/command_palette.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/command_palette.png new file mode 100644 index 000000000..31c3a9ea3 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/command_palette.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/dropdown.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/dropdown.png new file mode 100644 index 000000000..1cdeddc40 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/dropdown.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/final.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/final.png new file mode 100644 index 000000000..f40107a7f Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/final.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/humanized_number.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/humanized_number.png new file mode 100644 index 000000000..f51cf9c99 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/humanized_number.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/humanized_tooltip.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/humanized_tooltip.png new file mode 100644 index 000000000..b11e5d522 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/humanized_tooltip.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/increment_button.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/increment_button.png new file mode 100644 index 000000000..1d96e7aa0 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/increment_button.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/milestone.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/milestone.png new file mode 100644 index 000000000..cc11eda36 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/milestone.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/new_command.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/new_command.png new file mode 100644 index 000000000..644ca3471 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/new_command.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/notebook.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/notebook.png new file mode 100644 index 000000000..101be3bd3 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/notebook.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/notification.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/notification.png new file mode 100644 index 000000000..f15e49049 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/notification.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/peach_tree.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/peach_tree.png new file mode 100644 index 000000000..7ccbe51a7 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/peach_tree.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/power.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/power.png new file mode 100644 index 000000000..4c5687521 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/power.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/reward.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/reward.png new file mode 100644 index 000000000..bdd85e241 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/reward.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/systray.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/systray.png new file mode 100644 index 000000000..06b40b440 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/systray.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/trees.png b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/trees.png new file mode 100644 index 000000000..35b39ce16 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/01_build_clicker_game/trees.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views.rst b/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views.rst deleted file mode 100644 index 3c5e2310a..000000000 --- a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views.rst +++ /dev/null @@ -1,403 +0,0 @@ -=========================== -Chapter 1: Fields and Views -=========================== - -In the previous chapter, we learned a range of skills, including how to create and use services, -work with the Layout component, make the dashboard translatable, and lazy load a JavaScript library -like Chart.js. Now, let's learn how to create new fields and views. - -.. spoiler:: Solutions - - The solutions for each exercise of the chapter are hosted on the - `official Odoo tutorials repository - `_. It - is recommended not to look at them before trying the exercises. - -Fields and views are among the most important concepts in the Odoo user interface. They are key to -many important user interactions, and should therefore work perfectly. In the context of the -JavaScript framework, fields are components specialized for visualizing/editing a specific field for -a given record. For example, a (Python) model may define a char field, which will be represented by -a field component `CharField`. - -A field component is basically just a component registered in the `fields` :ref:`registry -`. The field component may define some additional static keys (metadata), such -as `displayName` or `supportedTypes`, and the most important one: `extractProps`, which prepare the -base props received by the `CharField`. - -Example: a simple field -======================= - -Let us discuss a simplified implementation of a `CharField`. First, here is the template: - -.. code-block:: xml - - - - - - - - - - -It features a readonly mode and an edit mode, which is an input with a few attributes. Now, here -is the JavaScript code: - -.. code-block:: js - - export class CharField extends Component { - get formattedValue() { - return formatChar(this.props.value, { isPassword: this.props.isPassword }); - } - - updateValue(ev) { - let value = ev.target.value; - if (this.props.shouldTrim) { - value = value.trim(); - } - this.props.update(value); - } - } - - CharField.template = "web.CharField"; - CharField.displayName = _lt("Text"); - CharField.supportedTypes = ["char"]; - - CharField.extractProps = ({ attrs, field }) => { - return { - shouldTrim: field.trim && !archParseBoolean(attrs.password), - maxLength: field.size, - isPassword: archParseBoolean(attrs.password), - placeholder: attrs.placeholder, - }; - }; - - registry.category("fields").add("char", CharField); - -There are a few important things to notice: - -- The `CharField` receives its (raw) value in props. It needs to format it before displaying it. -- It receives an `update` function in its props, which is used by the field to notify the owner of - the state that the value of this field has been changed. Note that the field does not (and should - not) maintain a local state with its value. Whenever the change has been applied, it will come - back (possibly after an onchange) by the way of the props. -- It defines an `extractProps` function. This is a step that translates generic standard props, - specific to a view, to specialized props, useful to the component. This allows the component to - have a better API, and may make it so that it is reusable. - -Fields have to be registered in the `fields` registry. Once it's done, they can be used in some -views (namely: `form`, `list`, `kanban`) by using the `widget` attribute. - -.. example:: - - .. code-block:: xml - - - -.. _tutorials/master_odoo_web_framework/image_preview_field: - -1. An `image_preview` field -=========================== - -Each new order on the website will be created as an `awesome_tshirt.order`. This model has a -`image_url` field (of type `char`), which is currently only visible as a string. We want to be able -to see the image itself in the form view. - -.. exercise:: - - #. Create a new `ImagePreview` component and register it in the proper :ref:`registry - `. Use the `CharField` component in your template. You can use `t-props - <{OWL_PATH}/doc/reference/props.md#dynamic-props>`_ to pass props received by `ImagePreview` - to `CharField`. Update the arch of the form view to use your new field by setting the `widget` - attribute. - #. Change the code of the `ImagePreview` component so that the image is displayed below the URL. - #. When the field is readonly, only the image should be displayed and the URL should be hidden. - -.. note:: - It is possible to solve this exercise by inheriting `CharField`, but the goal of this exercise is - to create a field from scratch. - -.. image:: 01_fields_and_views/image_field.png - :align: center - :scale: 50% - -.. seealso:: - - `Code: CharField <{GITHUB_PATH}/addons/web/static/src/views/fields/char/char_field.js>`_ - -2. Improving the `image_preview` field -====================================== - -We want to improve the field of the previous task to help the staff recognize orders for which some -action should be done. - -.. exercise:: - - Display a warning "MISSING TSHIRT DESIGN" in red if there is no image URL specified on the order. - -.. image:: 01_fields_and_views/missing_image.png - :align: center - -3. Customizing a field component -================================ - -Let's see how to use inheritance to extend an existing component. - -There is a `is_late`, readonly, boolean field on the order model. That would be useful information -to see on the list/kanban/view. Then, let us say that we want to add a red word "Late!" next to it -whenever it is set to `true`. - -.. exercise:: - - #. Create a new `LateOrderBoolean` field inheriting from `BooleanField`. The template of - `LateOrderBoolean` can also :ref:`inherit ` from the - `BooleanField` template. - #. Use it in the list/kanban/form view. - #. Modify it to add a red `Late` next to it, as requested. - -.. image:: 01_fields_and_views/late_field.png - :align: center - -.. seealso:: - - `Example: A field inheriting another - <{GITHUB_PATH}/addons/account/static/src/components/account_type_selection>`_ - - :ref:`Documentation on xpath ` - -4. Message for some customers -============================= - -Odoo form views support a `widget` API, which is like a field, but more generic. It is useful to -insert arbitrary components in the form view. Let us see how we can use it. - -.. exercise:: - - For a super efficient workflow, we would like to display an alert block with specific messages - depending on some conditions: - - - If the `image_url` field is not set, it should display "No image". - - If the `amount` of the order is higher than 100 euros, it should display "Add promotional - material". - - Make sure that your widget is updated in real time. - - .. tip:: - Try to evaluate `props.record` in the :guilabel:`Console` tab of your browser's dev tools. - -.. image:: 01_fields_and_views/warning_widget.png - :align: center - -.. seealso:: - - - `Example: Using the tag in a form view - `_ - - `Example: Implementation of a widget - <{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days>`_ - -5. Use `markup` -=============== - -Let’s see how we can display raw HTML in a template. The `t-out` directive can be used for that -propose. Indeed, `it generally acts like t-esc, unless the data has been marked explicitly with a -markup function <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_. In that case, its value -is injected as HTML. - -.. exercise:: - - #. Modify the previous exercise to put the `image` and `material` words in bold. - #. The warnings should be markuped, and the template should be modified to use `t-out`. - #. Import the `markup` function from Owl and, for each message, replace it with a call of the - function with the message passed as argument. - -.. note:: - This is an example of a safe use of `t-out`, since the string is static. - -.. image:: 01_fields_and_views/warning_widget2.png - :align: center - -6. Add buttons in the control panel -=================================== - -Views are among the most important components in Odoo: they allow users to interact with their -data. Let us discuss how Odoo views are designed. - -The power of Odoo views is that they declare how a particular screen should work with an XML -document (usually named `arch`, short for architecture). This description can be extended/modified -by xpaths serverside. Then, the browser loads that document, parses it (fancy word to say that it -extracts the useful information), and then represents the data accordingly. - -.. example:: - - The `arch` document is view specific. Here is how a `graph` view or a `calendar` view could be - defined: - - .. code-block:: xml - - - - - - - - - - - - -A view is defined in the view registry by an object with a few specific keys. - -- `type`: The (base) type of a view (for example, `form`, `list`...). -- `display_name`: What should be displayed in the tooltip in the view switcher. -- `icon`: Which icon to use in the view switcher. -- `multiRecord`: Whether the view is supposed to manage a single record or a set of records. -- `Controller`: The component that will be used to render the view (the most important information). - -.. example:: - - Here is a minimal `Hello` view, which does not display anything: - - .. code-block:: js - - /** @odoo-module */ - - import { registry } from "@web/core/registry"; - - export const helloView = { - type: "hello", - display_name: "Hello", - icon: "fa fa-picture-o", - multiRecord: true, - Controller: Component, - }; - - registry.category("views").add("hello", helloView); - -Most (or all?) Odoo views share a common architecture: - -.. ```mermaid -.. graph TD -.. subgraph View description -.. V(props function) -.. G(generic props) -.. X(arch parser) -.. S(others ...) -.. V --> X -.. V --> S -.. V --> G -.. end -.. A[Controller] -.. L[Layout] -.. B[Renderer] -.. C[Model] - -.. V == compute props ==> A -.. A --- L -.. L --- B -.. A --- C -.. ``` - -.. image:: 01_fields_and_views/view_architecture.svg - :align: center - :width: 75% - :class: o-no-modal - -The view description can define a `props` function, which receives the standard props, and computes -the base props of the concrete view. The `props` function is executed only once, and can be thought -of as being some kind of factory. It is useful to parse the `arch` XML document, and to allow the -view to be parameterized (for example, it can return a Renderer component that will be used as -Renderer). Then, it is easy to customize the specific renderer used by a sub view. - -The props will be extended before being given to the Controller. In particular, the search props -(domain/context/groupby) will be added. - -Finally, the root component, commonly called the `Controller`, coordinates everything. It uses the -generic `Layout` component (to add a control panel), instantiates a `Model`, and uses a `Renderer` -component in the `Layout` default slot. The `Model` is tasked with loading and updating data, and -the `Renderer` is supposed to handle all rendering work, along with all user interactions. - -In practice, once the t-shirt order is printed, we need to print a label to put on the package. To -do that, let us add a button in the order's form view's control panel, which will call a model -method. - -There is a service dedicated to calling models methods: `orm_service`, located in -`core/orm_service.js`. It provides a way to call common model methods, as well as a generic -`call(model, method, args, kwargs)` method. - -.. example:: - - .. code-block:: js - - setup() { - this.orm = useService("orm"); - onWillStart(async () => { - // will read the fields 'id' and 'descr' from the record with id=3 of my.model - const data = await this.orm.read("my.model", [3], ["id", "descr"]); - // ... - }); - } - -.. exercise:: - - #. Create a customized form view extending the `web` form view and register it as - `awesome_tshirt.order_form_view`. - #. Add a `js_class="awesome_tshirt.order_form_view"` attribute to the arch of the form view so - that Odoo will load it. - #. Create a new template inheriting from the form controller template and add a "Print Label" - button after the "New" button. - #. Clicking on this button should call the method `print_label` from the model - `awesome_tshirt.order` with the proper id. - - .. note:: - `print_label` is a mock method; it only displays a message in the logs. - - #. The button should not be disabled if the current order is in `create` mode (i.e., it does not - exist yet). - - .. tip:: - Log `this.props.resId` and `this.model.root.resId` and compare the two values before and - after entering `create` mode. - - #. The button should be displayed as a primary button if the customer is properly set and if the - task stage is `printed`. Otherwise, it should be displayed as a secondary button. - #. Bonus point: clicking twice on the button should not trigger 2 RPCs. - - .. image:: 01_fields_and_views/form_button.png - :align: center - -.. seealso:: - - `Example: Extending a view (JS) - <{GITHUB_PATH}/addons/mass_mailing/static/src/views/mailing_contact_view_kanban.js>`_ - - `Example: Extending a view (XML) - <{GITHUB_PATH}/addons/mass_mailing/static/src/views/mass_mailing_views.xml>`_ - - `Example: Using a js_class attribute - `_ - - `Code: orm service <{GITHUB_PATH}/addons/web/static/src/core/orm_service.js>`_ - - `Example: Using the orm service - <{GITHUB_PATH}/addons/account/static/src/components/open_move_widget/open_move_widget.js>`_ - - `Code: useDebounced hook - `_ - -7. Auto-reload the kanban view -============================== - -Bafien is upset: he wants to see the kanban view of the tshirt orders on his external monitor, but -the view needs to be up-to-date. He is tired of clicking on the :guilabel:`refresh` icon every 30s, -so he tasked you to find a way to do it automatically. - -Just like the previous exercise, that kind of customization requires creating a new JavaScript view. - -.. exercise:: - - #. Extend the kanban view/controller to reload its data every minute. - #. Register it in the view registry, under `awesome_tshirt.autoreloadkanban`. - #. Use it in the arch of the kanban view (with the `js_class` attribute). - -.. important:: - If you use `setInterval` or something similar, make sure that it is properly canceled when your - component is unmounted. Otherwise, you will introduce a memory leak. diff --git a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/form_button.png b/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/form_button.png deleted file mode 100644 index 5d89c7092..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/form_button.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/form_renderer_fields.svg b/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/form_renderer_fields.svg deleted file mode 100644 index d104ab5f0..000000000 --- a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/form_renderer_fields.svg +++ /dev/null @@ -1 +0,0 @@ -
FormRenderer
Field
BooleanField
Field
Many2OneField
...
\ No newline at end of file diff --git a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/image_field.png b/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/image_field.png deleted file mode 100644 index 50b427fb1..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/image_field.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/late_field.png b/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/late_field.png deleted file mode 100644 index f2d1dcde4..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/late_field.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/missing_image.png b/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/missing_image.png deleted file mode 100644 index 0c8986221..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/missing_image.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/view_architecture.svg b/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/view_architecture.svg deleted file mode 100644 index e7453c9e8..000000000 --- a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/view_architecture.svg +++ /dev/null @@ -1 +0,0 @@ -
View description
compute props
props function
generic props
arch parser
others ...
Controller
Layout
Renderer
Model
\ No newline at end of file diff --git a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/view_component.svg b/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/view_component.svg deleted file mode 100644 index ac36786b4..000000000 --- a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/view_component.svg +++ /dev/null @@ -1 +0,0 @@ -
props
View
KanbanController
\ No newline at end of file diff --git a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/warning_widget.png b/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/warning_widget.png deleted file mode 100644 index c046cda10..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/warning_widget.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/warning_widget2.png b/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/warning_widget2.png deleted file mode 100644 index d224b4363..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/01_fields_and_views/warning_widget2.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view.rst b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view.rst new file mode 100644 index 000000000..9f38f31ab --- /dev/null +++ b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view.rst @@ -0,0 +1,462 @@ +================================ +Chapter 2: Create a Gallery View +================================ + +Let us see how one can create a new view, completely from scratch. In a way, it is not very +difficult to do, but there are no really good resources on how to do it. Note that most situations +should be solved by either customizing an existing view, or with a client action. + +For this exercise, let's assume that we want to create a `gallery` view, which is a view that lets +us represent a set of records with an image field. + +The problem could certainly be solved with a kanban view, but this means that it is not possible to +have our normal kanban view and the gallery view in the same action. + +Let us make a gallery view. Each gallery view will be defined by an `image_field` attribute in its +arch: + +.. code-block:: xml + + + +To complete the tasks in this chapter, you will need to install the awesome_gallery addon. This +addon includes the necessary server files to add a new view. + +.. admonition:: Goal + + .. image:: 02_create_gallery_view/overview.png + :align: center + +.. spoiler:: Solutions + + The solutions for each exercise of the chapter are hosted on the + `official Odoo tutorials repository + `_. + +1. Make a hello world view +========================== + +First step is to create a JavaScript implementation with a simple component. + +#. Create the `gallery_view.js` , `gallery_controller.js` and `gallery_controller.xml` files in + `static/src`. +#. Implement a simple hello world component in `gallery_controller.js`. +#. In `gallery_view.js`, import the controller, create a view object, and register it in the + view registry under the name `gallery`. + + .. example:: + Here is an example on how to define a view object: + + .. code-block:: js + + import { registry } from "@web/core/registry"; + import { MyController } from "./my_controller"; + + export const myView = { + type: "my_view", + display_name: "MyView", + icon: "oi oi-view-list", + multiRecord: true, + Controller: MyController, + }; + + registry.category("views").add("my_controller", myView); + +#. Add `gallery` as one of the view type in the `contacts.action_contacts` action. +#. Make sure that you can see your hello world component when switching to the gallery view. + +.. image:: 02_create_gallery_view/view_button.png + :align: center + +.. image:: 02_create_gallery_view/new_view.png + :align: center + +2. Use the Layout component +=========================== + +So far, our gallery view does not look like a standard view. Let's use the `Layout` component to +have the standard features like other views. + +#. Import the `Layout` component and add it to the `components` of `GalleryController`. +#. Update the template to use `Layout`. It needs a `display` prop, which can be found in + `props.display`. + +.. image:: 02_create_gallery_view/layout.png + :align: center + +3. Parse the arch +================= + +For now, our gallery view does not do much. Let's start by reading the information contained in the +arch of the view. + +The process of parsing an arch is usually done with a `ArchParser`, specific to each view. It +inherits from a generic `XMLParser` class. + +.. example:: + + Here is an example of what an ArchParser might look like: + + .. code-block:: js + + export class MyCustomArchParser { + parse(xmlDoc) { + const myAttribute = xmlDoc.getAttribute("my_attribute") + return { + myAttribute, + } + } + } + +#. Create the `ArchParser` class in its own file. +#. Use it to read the `image_field` information. +#. Update the `gallery` view code to add it to the props received by the controller. + +.. note:: + It is probably a little overkill to do it like that, since we basically only need to read one + attribute from the arch, but it is a design that is used by every other odoo views, since it + lets us extract some upfront processing out of the controller. + +.. seealso:: + `Example: The graph arch parser + <{GITHUB_PATH}/addons/web/static/src/views/graph/graph_arch_parser.js>`_ + +4. Load some data +================= + +Let us now get some real data from the server. For that we must use `webSearchRead` from the orm +service. + +.. example:: + + Here is an example of a `webSearchRead` to get the records from a model: + + .. code-block:: js + + const { length, records } = this.orm.webSearchRead(this.resModel, domain, { + specification: { + [this.fieldToFetch]: {}, + [this.secondFieldToFetch]: {}, + }, + context: { + bin_size: true, + } + }) + +#. Add a :code:`loadImages(domain) {...}` method to the `GalleryController`. It should perform a + `webSearchRead` call from the orm service to fetch records corresponding to the domain, and + use `imageField` received in props. +#. If you didn't include `bin_size` in the context of the call, you will receive the image field + encoded in base64. Make sure to put `bin_size` in the context to receive the size of the image + field. We will display the image later. +#. Modify the `setup` code to call that method in the `onWillStart` and `onWillUpdateProps` + hooks. +#. Modify the template to display the id and the size of each image inside the default slot of + the `Layout` component. + +.. note:: + The loading data code will be moved into a proper model in a next exercise. + +.. image:: 02_create_gallery_view/gallery_data.png + :align: center + +5. Solve the concurrency problem +================================ + +For now, our code is not concurrency proof. If one changes the domain twice, it will trigger the +`loadImages(domain)` twice. We have thus two requests that can arrive at different time depending +on different factors. Receiving the response for the first request after receiving the response +for the second request will lead to an inconsistent state. + +The `KeepLast` primitive from Odoo solves this problem, it manages a list of tasks, and only +keeps the last task active. + +#. Import `KeepLast` from :file:`@web/core/utils/concurrency`. +#. Instanciate a `KeepLast` object in the model. +#. Add the `webSearchRead` call in the `KeepLast` so that only the last call is resolved. + +.. seealso:: + `Example: usage of KeepLast `_ + +6. Reorganize code +================== + +Real views are a little bit more organized. This may be overkill in this example, but it is intended +to learn how to structure code in Odoo. Also, this will scale better with changing requirements. + +#. Move all the model code in its own `GalleryModel` class. +#. Move all the rendering code in a `GalleryRenderer` component. +#. Import `GalleryModel` and `GalleryRenderer` in `GalleryController` to make it work. + +7. Make the view extensible +=========================== + +To extends the view, one could import the gallery view object to modify it to their taste. The +problem is that for the moment, it is not possible to define a custom model or renderer because it +is hardcoded in the controller. + +#. Import `GalleryModel` and `GalleryRenderer` in the gallery view file. +#. Add a `Model` and `Renderer` key to the gallery view object and assign them to `GalleryModel` + and `GalleryRenderer`. Pass `Model` and `Renderer` as props to the controller. +#. Remove the hardcoded import in the controller and get them from the props. +#. Use `t-component + `_ to + have dynamic sub component. + +.. note:: + + This is how someone could now extend the gallery view by modifying the renderer: + + .. code-block:: js + + /** @odoo-module */ + + import { registry } from '@web/core/registry'; + import { galleryView } from '@awesome_gallery/gallery_view'; + import { GalleryRenderer } from '@awesome_gallery/gallery_renderer'; + + export class MyExtendedGalleryRenderer extends GalleryRenderer { + static template = "my_module.MyExtendedGalleryRenderer"; + setup() { + super.setup(); + console.log("my gallery renderer extension"); + } + } + + registry.category("views").add("my_gallery", { + ...galleryView, + Renderer: MyExtendedGalleryRenderer, + }); + +8. Display images +================= + +Update the renderer to display images in a nice way, if the field is set. If `image_field` is +empty, display an empty box instead. + +.. tip:: + + There is a controller that allows to retrieve an image from a record. You can use this + snippet to construct the link: + + .. code-block:: js + + import { url } from "@web/core/utils/urls"; + const url = url("/web/image", { + model: resModel, + id: image_id, + field: imageField, + }); + +.. image:: 02_create_gallery_view/tshirt_images.png + :align: center + +9. Switch to form view on click +=============================== + +Update the renderer to react to a click on an image and switch to a form view. You can use the +`switchView` function from the action service. + +.. seealso:: + `Code: The switchView function `_ + +10. Add an optional tooltip +=========================== + +It is useful to have some additional information on mouse hover. + +#. Update the code to allow an optional additional attribute on the arch: + + .. code-block:: xml + + + +#. On mouse hover, display the content of the tooltip field. It should work if the field is a + char field, a number field or a many2one field. To put a tooltip to an html element, you can + put the string in the `data-tooltip` attribute of the element. +#. Update the customer gallery view arch to add the customer as tooltip field. + +.. image:: 02_create_gallery_view/image_tooltip.png + :align: center + :scale: 50% + +.. seealso:: + `Example: usage of t-att-data-tooltip `_ + +11. Add pagination +================== + +Let's add a pager on the control panel and manage all the pagination like in a normal Odoo view. + +.. image:: 02_create_gallery_view/pagination.png + :align: center + +.. seealso:: + - `Code: The usePager hook <{GITHUB_PATH}/addons/web/static/src/search/pager_hook.js>`_ + - `Example: usePager in list controller `_ + +12. Validating views +===================== + +We have a nice and useful view so far. But in real life, we may have issue with users incorrectly +encoding the `arch` of their Gallery view: it is currently only an unstructured piece of XML. + +Let us add some validation! In Odoo, XML documents can be described with an RN file +:dfn:`(Relax NG file)`, and then validated. + +#. Add an RNG file that describes the current grammar: + + - A mandatory attribute `image_field`. + - An optional attribute: `tooltip_field`. + +#. Add some code to make sure all views are validated against this RNG file. +#. While we are at it, let us make sure that `image_field` and `tooltip_field` are fields from + the current model. + +Since validating an RNG file is not trivial, here is a snippet to help: + +.. code-block:: python + + # -*- coding: utf-8 -*- + import logging + import os + + from lxml import etree + + from odoo.loglevels import ustr + from odoo.tools import misc, view_validation + + _logger = logging.getLogger(__name__) + + _viewname_validator = None + + @view_validation.validate('viewname') + def schema_viewname(arch, **kwargs): + """ Check the gallery view against its schema + + :type arch: etree._Element + """ + global _viewname_validator + + if _viewname_validator is None: + with misc.file_open(os.path.join('modulename', 'rng', 'viewname.rng')) as f: + _viewname_validator = etree.RelaxNG(etree.parse(f)) + + if _viewname_validator.validate(arch): + return True + + for error in _viewname_validator.error_log: + _logger.error(ustr(error)) + return False + +.. seealso:: + `Example: The RNG file of the graph view `_ + +13. Uploading an image +====================== + +Our gallery view does not allow users to upload images. Let us implement that. + +#. Add a button on each image by using the `FileUploader` component. +#. The `FileUploader` component accepts the `onUploaded` props, which is called when the user + uploads an image. Make sure to call `webSave` from the orm service to upload the new image. +#. You maybe noticed that the image is uploaded but it is not re-rendered by the browser. + This is because the image link did not change so the browser do not re-fetch them. Include + the `write_date` from the record to the image url. +#. Make sure that clicking on the upload button does not trigger the switchView. + +.. image:: 02_create_gallery_view/upload_image.png + :align: center + :scale: 50% + +.. seealso:: + + - `Example: usage of FileUploader `_ + - `Odoo: webSave definition `_ + +14. Advanced tooltip template +============================= + +For now we can only specify a tooltip field. But what if we want to allow to write a specific +template for it ? + +.. example:: + + This is an example of a gallery arch view that should work after this exercise. + + .. code-block:: xml + + + awesome_gallery.orders.gallery + res.partner + + + + + +

name:

+

e-mail:

+
+
+
+
+ +#. Replace the `res.partner` gallery arch view in :file:`awesome_gallery/views/views.xml` with + the arch in example above. Don't worry if it does not pass the rng validation. +#. Modify the gallery rng validator to accept the new arch structure. + + .. tip:: + + You can use this rng snippet to validate the tooltip-template tag + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + + + + + + +#. The arch parser should parse the fields and the tooltip template. Import `visitXML` from + :file:`@web/core/utils/xml` and use it to parse field names and the tooltip template. +#. Make sure that the model call the `webSearchRead` by including the parsed field names in the + specification. +#. The renderer (or any sub-component you created for it) should receive the parsed tooltip + template. Manipulate this template to replace the `` element into a `` + element. + + .. tip:: + + The template is an `Element` object so it can be manipulated like a HTML element. + +#. Register the template to Owl thanks to the `xml` function from :file:`@odoo/owl`. +#. Use the `useTooltip` hook from :file:`@web/core/tooltip/tooltip_hook` to display the + tooltips. This hooks take as argument the Owl template and the variable needed by the + template. + +.. image:: 02_create_gallery_view/advanced_tooltip.png + :align: center + :scale: 50% + +.. seealso:: + + - `Example: useTooltip used in Kaban `_ + - `Example: visitXML usage `_ + - `Owl: Inline templates with xml helper function `_ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/advanced_tooltip.png b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/advanced_tooltip.png new file mode 100644 index 000000000..73d599e0b Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/advanced_tooltip.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/gallery_data.png b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/gallery_data.png new file mode 100644 index 000000000..79f757abf Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/gallery_data.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/image_tooltip.png b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/image_tooltip.png new file mode 100644 index 000000000..c5cd690bd Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/image_tooltip.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/layout.png b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/layout.png new file mode 100644 index 000000000..5d5e1bbda Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/layout.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/new_view.png b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/new_view.png new file mode 100644 index 000000000..eeec6fb71 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/new_view.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/overview.png b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/overview.png new file mode 100644 index 000000000..1aa49565c Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/overview.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/pagination.png b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/pagination.png new file mode 100644 index 000000000..a4b70ca6e Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/pagination.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/tshirt_images.png b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/tshirt_images.png new file mode 100644 index 000000000..d3039ee31 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/tshirt_images.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/upload_image.png b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/upload_image.png new file mode 100644 index 000000000..decc5db24 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/upload_image.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/view_button.png b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/view_button.png new file mode 100644 index 000000000..efc2ecd12 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/02_create_gallery_view/view_button.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous.rst b/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous.rst deleted file mode 100644 index 0752e0177..000000000 --- a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous.rst +++ /dev/null @@ -1,299 +0,0 @@ -======================== -Chapter 2: Miscellaneous -======================== - -In the previous task, we learned how to create fields and views. There is still much more to -discover in the feature-rich Odoo web framework, so let's dive in and explore more in this chapter! - -.. graph TD -.. subgraph "Owl" -.. C[Component] -.. T[Template] -.. H[Hook] -.. S[Slot] -.. E[Event] -.. end - -.. subgraph "odoo"[Odoo Javascript framework] -.. Services -.. Translation -.. lazy[Lazy loading libraries] -.. SCSS -.. action --> Services -.. rpc --> Services -.. orm --> Services -.. Fields -.. Views -.. Registries -.. end - -.. odoo[Odoo JavaScript framework] --> Owl - -.. figure:: 02_miscellaneous/previously_learned.svg - :align: center - :width: 70% - - This is the progress that we have made in discovering the JavaScript web framework at the end of - :doc:`01_fields_and_views`. - -.. admonition:: Goal - - .. image:: 02_miscellaneous/kitten_mode.png - :align: center - -.. spoiler:: Solutions - - The solutions for each exercise of the chapter are hosted on the - `official Odoo tutorials repository - `_. - -1. Interacting with the notification system -=========================================== - -.. note:: - This task depends on :doc:`the previous exercises <01_fields_and_views>`. - -After using the :guilabel:`Print Label` button for some t-shirt tasks, it is apparent that there -should be some feedback that the `print_label` action is completed (or failed, for example, the -printer is not connected or ran out of paper). - -.. exercise:: - #. Display a :ref:`notification ` message when the action is - completed successfully, and a warning if it failed. - #. If it failed, the notification should be permanent. - - .. image:: 02_miscellaneous/notification.png - :align: center - :scale: 60% - -.. seealso:: - `Example: Using the notification service - <{GITHUB_PATH}/addons/web/static/src/views/fields/image_url/image_url_field.js>`_ - -2. Add a systray item -===================== - -Our beloved leader wants to keep a close eye on new orders. He wants to see the number of new, -unprocessed orders at all time. Let's do that with a systray item. - -A :ref:`systray ` item is an element that appears in the system tray, -which is a small area located on the right-hand side of the navbar. The systray is used to display -notifications and provide access to certain features. - -.. exercise:: - - #. Create a systray component that connects to the statistics service we made previously. - #. Use it to display the number of new orders. - #. Clicking on it should open a list view with all of those orders. - #. Bonus point: avoid making the initial RPC by adding the information to the session info. The - session info is given to the web client by the server in the initial response. - - .. image:: 02_miscellaneous/systray.png - :align: center - -.. seealso:: - - `Example: Systray item <{GITHUB_PATH}/addons/web/static/src/webclient/user_menu/user_menu.js>`_ - - `Example: Adding some information to the "session info" - <{GITHUB_PATH}/addons/barcodes/models/ir_http.py>`_ - - `Example: Reading the session information - `_ - -3. Real life update -=================== - -So far, the systray item from above does not update unless the user refreshes the browser. Let us -do that by calling periodically (for example, every minute) the server to reload the information. - -.. exercise:: - - #. The `tshirt` service should periodically reload its data. - -Now, the question arises: how is the systray item notified that it should re-render itself? It can -be done in various ways but, for this training, we choose to use the most *declarative* approach: - -.. exercise:: - - 2. Modify the `tshirt` service to return a `reactive - <{OWL_PATH}/doc/reference/reactivity.md#reactive>`_ object. Reloading data should update the - reactive object in place. - 3. The systray item can then perform a `useState - <{OWL_PATH}/doc/reference/reactivity.md#usestate>`_ on the service return value. - 4. This is not really necessary, but you can also *package* the calls to `useService` and - `useState` in a custom hook `useStatistics`. - -.. seealso:: - - `Documentation on reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_ - - `Example: Use of reactive in a service - `_ - -4. Add a command to the command palette -======================================= - -Now, let us see how we can interact with the command palette. The command palette is a feature that -allows users to quickly access various commands and functions within the application. It is accessed -by pressing `CTRL+K` in the Odoo interface. - -.. exercise:: - - Modify the :ref:`image preview field ` - to add a command to the command palette to open the image in a new browser tab (or window). - - Ensure the command is only active whenever a field preview is visible on the screen. - - .. image:: 02_miscellaneous/new_command.png - :align: center - -.. seealso:: - `Example: Using the useCommand hook - `_ - -5. Monkey patching a component -============================== - -Often, we can achieve what we want by using existing extension points that allow for customization, -such as registering something in a registry. Sometimes, however, it happens that we want to modify -something that has no such mechanism. In that case, we must fall back on a less safe form of -customization: monkey patching. Almost everything in Odoo can be monkey patched. - -Bafien, our beloved leader, heard about employees performing better if they are constantly being -watched. Since he cannot be there in person for each of his employees, he tasked you with updating -the user interface to add a blinking red eye in the control panel. Clicking on that eye should open -a dialog with the following message: "Bafien is watching you. This interaction is recorded and may -be used in legal proceedings if necessary. Do you agree to these terms?" - -.. exercise:: - - #. :ref:`Inherit ` the `web.Breadcrumbs` template of the - `ControlPanel component <{GITHUB_PATH}/addons/web/static/src/search/control_panel>`_ to add an - icon next to the breadcrumbs. You might want to use the `fa-eye` or `fa-eyes` icons. - #. :doc:`Patch ` the component to display the - message on click by using `the dialog service - <{GITHUB_PATH}/addons/web/static/src/core/dialog/dialog_service.js>`_. You can use - `ConfirmationDialog - <{GITHUB_PATH}/addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js>`_. - #. Add the CSS class `blink` to the element representing the eye and paste the following code in - a new CSS file located in your patch's directory. - - .. code-block:: css - - .blink { - animation: blink-animation 1s steps(5, start) infinite; - -webkit-animation: blink-animation 1s steps(5, start) infinite; - } - @keyframes blink-animation { - to { - visibility: hidden; - } - } - @-webkit-keyframes blink-animation { - to { - visibility: hidden; - } - } - - .. image:: 02_miscellaneous/bafien_eye.png - :align: center - :scale: 60% - - .. image:: 02_miscellaneous/confirmation_dialog.png - :align: center - :scale: 60% - -.. seealso:: - - `Code: The patch function - `_ - - `The Font Awesome website `_ - - `Example: Using the dialog service - `_ - -6. Fetching orders from a customer -================================== - -Let's see how to use some standard components to build a powerful feature combining autocomplete, -fetching data, and fuzzy lookup. We will add an input in our dashboard to easily search all orders -from a given customer. - -.. exercise:: - - #. Update :file:`tshirt_service.js` to add a `loadCustomers` method, which returns a promise that - returns the list of all customers (and only performs the call once). - #. Add the `AutoComplete component <{GITHUB_PATH}/addons/web/static/src/core/autocomplete>`_ to - the dashboard, next to the buttons in the control panel. - #. Fetch the list of customers with the tshirt service, and display it in the AutoComplete - component, filtered by the `fuzzyLookup - <{GITHUB_PATH}/addons/web/static/src/core/utils/search.js>`_ method. - - .. image:: 02_miscellaneous/autocomplete.png - :align: center - :scale: 60% - -7. Reintroduce Kitten Mode -========================== - -Let us add a special mode to Odoo: whenever the URL contains `kitten=1`, we will display a kitten in -the background of Odoo, because we like kittens. - -.. exercise:: - - #. Create a `kitten` service, which should check the content of the active URL hash with the - help of the :ref:`router service `. If `kitten` is set in the URL, - add the class `o-kitten-mode` to the document body. - #. Add the following SCSS in :file:`kitten_mode.scss`: - - .. code-block:: css - - .o-kitten-mode { - background-image: url(https://upload.wikimedia.org/wikipedia/commons/5/58/Mellow_kitten_%28Unsplash%29.jpg); - background-size: cover; - background-attachment: fixed; - } - - .o-kitten-mode > * { - opacity: 0.9; - } - - #. Add a command to the command palette to toggle the kitten mode. Toggling the kitten mode - should toggle the class `o-kitten-mode` and update the current URL accordingly. - - .. image:: 02_miscellaneous/kitten_mode.png - :align: center - -8. Lazy loading our dashboard -============================= - -This is not really necessary, but the exercise is interesting. Imagine that our awesome dashboard is -a large application with potentially multiple external libraries and lots of code/styles/templates. -Also, suppose that the dashboard is used only by some users in some business flows. It would be -interesting to lazy load it in order to speed up the loading of the web client in most cases. - -So, let us do that! - -.. exercise:: - - #. Modify the manifest to create a new :ref:`bundle ` - `awesome_tshirt.dashboard`. - #. Add the awesome dashboard code to this bundle. Create folders and move files if needed. - #. Remove the code from the `web.assets_backend` bundle so that it is not loaded twice. - -So far, we only removed the dashboard from the main bundle; we now want to lazy load it. Currently, -no client action is registered in the action registry. - -.. exercise:: - - 4. Create a new file :file:`dashboard_loader.js`. - 5. Copy the code registering `AwesomeDashboard` to the dashboard loader. - 6. Register `AwesomeDashboard` as a `LazyComponent - `_. - 7. Modify the code in the dashboard loader to use the lazy component `AwesomeDashboard`. - -If you open the :guilabel:`Network` tab of your browser's dev tools, you should see that -:file:`awesome_tshirt.dashboard.min.js` is now loaded only when the Dashboard is first accessed. - -.. seealso:: - :ref:`Documentation on assets ` diff --git a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/autocomplete.png b/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/autocomplete.png deleted file mode 100644 index 65a7c3da2..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/autocomplete.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/bafien_eye.png b/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/bafien_eye.png deleted file mode 100644 index 6714b0573..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/bafien_eye.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/confirmation_dialog.png b/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/confirmation_dialog.png deleted file mode 100644 index 679edd2cb..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/confirmation_dialog.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/kitten_mode.png b/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/kitten_mode.png deleted file mode 100644 index e0ba81fee..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/kitten_mode.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/new_command.png b/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/new_command.png deleted file mode 100644 index f6094e0c4..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/new_command.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/notification.png b/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/notification.png deleted file mode 100644 index ec7c7d1ae..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/notification.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/previously_learned.svg b/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/previously_learned.svg deleted file mode 100644 index 768d645f2..000000000 --- a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/previously_learned.svg +++ /dev/null @@ -1 +0,0 @@ -
Odoo Javascript framework
Services
action
rpc
orm
Translation
Lazy loading libraries
SCSS
Fields
Views
Registries
Owl
Component
Template
Hook
Slot
Event
\ No newline at end of file diff --git a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/systray.png b/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/systray.png deleted file mode 100644 index b77afc2df..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/02_miscellaneous/systray.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view.rst b/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view.rst deleted file mode 100644 index 09a9f7c52..000000000 --- a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view.rst +++ /dev/null @@ -1,171 +0,0 @@ -============================= -Chapter 3: Custom kanban view -============================= - -.. todo:: It'd be cool to follow the naming convention of the previous chapters: "Chapter N: The concept studied in the chapter" - -.. warning:: - It is highly recommended that you complete :doc:`01_fields_and_views` before starting this - chapter. The concepts introduced in Chapter 3, including views and examples, will be essential - for understanding the material covered in this chapter. - -We have gained an understanding of the numerous capabilities offered by the Odoo web framework. As a -next step, we will customize a kanban view. This is a more complicated project that will showcase -some non trivial aspects of the framework. The goal is to practice composing views, coordinating -various aspects of the UI, and doing it in a maintainable way. - -Bafien had the greatest idea ever: a mix of a kanban view and a list view would be perfect for your -needs! In a nutshell, he wants a list of customers on the left of the task's kanban view. When you -click on a customer on the left sidebar, the kanban view on the right is filtered to only display -orders linked to that customer. - -.. admonition:: Goal - - .. image:: 03_custom_kanban_view/overview.png - :align: center - -.. spoiler:: Solutions - - The solutions for each exercise of the chapter are hosted on the - `official Odoo tutorials repository - `_. - -1. Create a new kanban view -=========================== - -Since we are customizing the kanban view, let us start by extending it and using our extension in -the kanban view for the tshirt orders. - -.. exercise:: - - #. Extend the kanban view by extending the kanban controller and by creating a new view object. - #. Register it in the views registry under `awesome_tshirt.customer_kanban`. - #. Update the kanban arch to use the extended view. This can be done with the `js_class` - attribute. - -2. Create a CustomerList component -================================== - -We will need to display a list of customers, so we might as well create the component. - -.. exercise:: - - #. Create a `CustomerList` component which only displays a `div` with some text for now. - #. It should have a `selectCustomer` prop. - #. Create a new template extending (XPath) the kanban controller template to add the - `CustomerList` next to the kanban renderer. Give it an empty function as `selectCustomer` for - now. - #. Subclass the kanban controller to add `CustomerList` in its sub-components. - #. Make sure you see your component in the kanban view. - - .. image:: 03_custom_kanban_view/customer_list.png - :align: center - :scale: 60% - -3. Load and display data -======================== - -.. exercise:: - - #. Modify the `CustomerList` component to fetch a list of all customers in `onWillStart`. - #. Display the list in the template with a `t-foreach`. - #. Whenever a customer is selected, call the `selectCustomer` function prop. - - .. image:: 03_custom_kanban_view/customer_data.png - :align: center - :scale: 60% - -4. Update the main kanban view -============================== - -.. exercise:: - - #. Implement `selectCustomer` in the kanban controller to add the proper domain. - #. Modify the template to give the real function to the `CustomerList` `selectCustomer` prop. - - Since it is not trivial to interact with the search view, here is a quick snippet to help: - - .. code-block:: js - - selectCustomer(customer_id, customer_name) { - this.env.searchModel.setDomainParts({ - customer: { - domain: [["customer_id", "=", customer_id]], - facetLabel: customer_name, - }, - }); - } - - .. image:: 03_custom_kanban_view/customer_filter.png - :align: center - :scale: 60% - -5. Only display customers which have an active order -==================================================== - -There is a `has_active_order` field on `res.partner`. Let us allow the user to filter results on -customers with an active order. - -.. exercise:: - - #. Add an input of type checkbox in the `CustomerList` component, with a label "Active customers" - next to it. - #. Changing the value of the checkbox should filter the list on customers with an active order. - - .. image:: 03_custom_kanban_view/active_customer.png - :align: center - :scale: 60% - -6. Add a search bar to the customer list -======================================== - -.. exercise:: - - Add an input above the customer list that allows the user to enter a string and to filter the - displayed customers, according to their name. - - .. tip:: - You can use the `fuzzyLookup` function to perform the filter. - - .. image:: 03_custom_kanban_view/customer_search.png - :align: center - :scale: 60% - -.. seealso:: - - - `Code: The fuzzylookup function <{GITHUB_PATH}/addons/web/static/src/core/utils/search.js>`_ - - `Example: Using fuzzyLookup - `_ - -7. Refactor the code to use `t-model` -===================================== - -To solve the previous two exercises, it is likely that you used an event listener on the inputs. Let -us see how we could do it in a more declarative way, with the `t-model -<{OWL_PATH}/doc/reference/input_bindings.md>`_ directive. - -.. exercise:: - - #. Make sure you have a reactive object that represents the fact that the filter is active - (something like - :code:`this.state = useState({ displayActiveCustomers: false, searchString: ''})`). - #. Modify the code to add a getter `displayedCustomers` which returns the currently active list - of customers. - #. Modify the template to use `t-model`. - -8. Paginate customers! -====================== - -.. exercise:: - - #. Add a :ref:`pager ` in the `CustomerList`, and only load/render the first 20 - customers. - #. Whenever the pager is changed, the customer list should update accordingly. - - This is actually pretty hard, in particular in combination with the filtering done in the - previous exercise. There are many edge cases to take into account. - - .. image:: 03_custom_kanban_view/customer_pager.png - :align: center - :scale: 60% diff --git a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/active_customer.png b/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/active_customer.png deleted file mode 100644 index 3c5258b2a..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/active_customer.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_data.png b/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_data.png deleted file mode 100644 index cf9393e15..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_data.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_filter.png b/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_filter.png deleted file mode 100644 index a1999fef1..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_filter.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_list.png b/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_list.png deleted file mode 100644 index 5e56dee5b..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_list.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_pager.png b/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_pager.png deleted file mode 100644 index 1679b1b5d..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_pager.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_search.png b/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_search.png deleted file mode 100644 index 4420ff68f..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/customer_search.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/overview.png b/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/overview.png deleted file mode 100644 index 01ed8f864..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/03_custom_kanban_view/overview.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view.rst b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view.rst new file mode 100644 index 000000000..b67987489 --- /dev/null +++ b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view.rst @@ -0,0 +1,200 @@ +================================== +Chapter 3: Customize a kanban view +================================== + +We have gained an understanding of the numerous capabilities offered by the Odoo web framework. As a +next step, we will customize a kanban view. This is a more complicated project that will showcase +some non trivial aspects of the framework. The goal is to practice composing views, coordinating +various aspects of the UI, and doing it in a maintainable way. + +Bafien had the greatest idea ever: a mix of a kanban view and a list view would be perfect for your +needs! In a nutshell, he wants a list of customers on the left of the CRM kanban view. When you +click on a customer on the left sidebar, the kanban view on the right is filtered to only display +leads linked to that customer. + +.. admonition:: Goal + + .. image:: 03_customize_kanban_view/overview.png + :align: center + +.. spoiler:: Solutions + + The solutions for each exercise of the chapter are hosted on the + `official Odoo tutorials repository + `_. + +1. Create a new kanban view +=========================== + +Since we are customizing the kanban view, let us start by extending it and using our extension in +the kanban view of CRM. + +#. Create a new empty component that extends the `KanbanController` component from + :file:`@web/views/kanban/kanban_controller`. +#. Create a new view object and assign all keys and values from `kanbanView` from + :file:`@web/views/kanban/kanban_view`. Override the Controller key by putting your newly + created controller. +#. Register it in the views registry under `awesome_kanban`. +#. Update the crm kanban arch in :file:`awesome_kanban/views/views.xml` to use the extended view. + This can be done by specifying the `js_class` attribute in the kanban node. + +.. seealso:: + + `Example: Create a new view by extending a pre-existing one `_ + +2. Create a CustomerList component +================================== + +We will need to display a list of customers, so we might as well create the component. + +#. Create a `CustomerList` component which only displays a `div` with some text for now. +#. It should have a `selectCustomer` prop. +#. Create a new template extending (XPath) the kanban controller template `web.KanbanView` to add + the `CustomerList` next to the kanban renderer. Give it an empty function as `selectCustomer` + for now. + + .. tip:: + + You can use this xpath inside the template to add a div before the renderer component. + + .. code-block:: xml + + + ... + + +#. Subclass the kanban controller to add `CustomerList` in its sub-components. +#. Make sure you see your component in the kanban view. + +.. image:: 03_customize_kanban_view/customer_list_component.png + :align: center + +.. seealso:: + + :ref:`Template inheritance ` + +3. Load and display data +======================== + +#. Modify the `CustomerList` component to fetch a list of all customers in `onWillStart`. +#. Display the list in the template with a `t-foreach`. +#. Whenever a customer is selected, call the `selectCustomer` function prop. + +.. image:: 03_customize_kanban_view/customer_data.png + :align: center + +.. seealso:: + + - `Example: fetching records from a model `_ + +4. Update the main kanban view +============================== + +#. Implement `selectCustomer` in the kanban controller to add the proper domain. + + .. tip:: + + Since it is not trivial to interact with the search view, here is a snippet to create a + filter: + + .. code-block:: js + + this.env.searchModel.createNewFilters([{ + description: partner_name, + domain: [["partner_id", "=", partner_id]], + isFromAwesomeKanban: true, // this is a custom key to retrieve our filters later + }]) + +#. By clicking on multiple customers, you can see that the old customer filter is not replaced. + Make sure that by clicking on a customer, the old filter is replaced by the new one. + + .. tip:: + + You can use this snippet to get the customers filters and toggle them. + + .. code-block:: js + + const customerFilters = this.env.searchModel.getSearchItems((searchItem) => + searchItem.isFromAwesomeKanban + ); + + for (const customerFilter of customerFilters) { + if (customerFilter.isActive) { + this.env.searchModel.toggleSearchItem(customerFilter.id); + } + } + +#. Modify the template to give the real function to the `CustomerList` `selectCustomer` prop. + +.. note:: + + You can use `Symbol + `_ + to make sure that the custom `isFromAwesomeKanban` key will not collide with keys any other + code might add to the object. + +.. image:: 03_customize_kanban_view/customer_filter.png + :align: center + +5. Only display customers which have an active order +==================================================== + +There is a `opportunity_ids` field on `res.partner`. Let us allow the user to filter results on +customers with at least one opportunity. + +#. Add an input of type checkbox in the `CustomerList` component, with a label "Active customers" + next to it. +#. Changing the value of the checkbox should filter the list of customers. + +.. image:: 03_customize_kanban_view/active_customer.png + :align: center + :scale: 60% + +6. Add a search bar to the customer list +======================================== + +Add an input above the customer list that allows the user to enter a string and to filter the +displayed customers, according to their name. + +.. tip:: + You can use the `fuzzyLookup` from :file:`@web/core/utils/search` function to perform the + filter. + +.. image:: 03_customize_kanban_view/customer_search.png + :align: center + :scale: 60% + +.. seealso:: + + - `Code: The fuzzylookup function `_ + - `Example: Using fuzzyLookup + `_ + +7. Refactor the code to use `t-model` +===================================== + +To solve the previous two exercises, it is likely that you used an event listener on the inputs. Let +us see how we could do it in a more declarative way, with the `t-model +<{OWL_PATH}/doc/reference/input_bindings.md>`_ directive. + +#. Make sure you have a reactive object that represents the fact that the filter is active + (something like + :code:`this.state = useState({ displayActiveCustomers: false, searchString: ''})`). +#. Modify the code to add a getter `displayedCustomers` which returns the currently active list + of customers. +#. Modify the template to use `t-model`. + +8. Paginate customers! +====================== + +#. Add a :ref:`pager ` in the `CustomerList`, and only load/render the first 20 + customers. +#. Whenever the pager is changed, the customer list should update accordingly. + +This is actually pretty hard, in particular in combination with the filtering done in the +previous exercise. There are many edge cases to take into account. + +.. image:: 03_customize_kanban_view/customer_pager.png + :align: center + :scale: 60% diff --git a/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/active_customer.png b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/active_customer.png new file mode 100644 index 000000000..df7d96b94 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/active_customer.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_data.png b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_data.png new file mode 100644 index 000000000..c6936e284 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_data.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_filter.png b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_filter.png new file mode 100644 index 000000000..370b8fe2f Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_filter.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_list_component.png b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_list_component.png new file mode 100644 index 000000000..a34750f5f Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_list_component.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_pager.png b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_pager.png new file mode 100644 index 000000000..e35628814 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_pager.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_search.png b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_search.png new file mode 100644 index 000000000..46e9bb433 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/customer_search.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/overview.png b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/overview.png new file mode 100644 index 000000000..e610116e1 Binary files /dev/null and b/content/developer/tutorials/master_odoo_web_framework/03_customize_kanban_view/overview.png differ diff --git a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch.rst b/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch.rst deleted file mode 100644 index 25ca7cb78..000000000 --- a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch.rst +++ /dev/null @@ -1,272 +0,0 @@ -======================================= -Chapter 4: Creating a view from scratch -======================================= - -.. warning:: - It is highly recommended that you complete :doc:`01_fields_and_views` before starting this - chapter. The concepts introduced in Chapter 3, including views and examples, will be essential - for understanding the material covered in this chapter. - -Let us see how one can create a new view, completely from scratch. In a way, it is not very -difficult to do, but there are no really good resources on how to do it. Note that most situations -should be solved by either customizing an existing view, or with a client action. - -For this exercise, let's assume that we want to create a `gallery` view, which is a view that lets -us represent a set of records with an image field. In our Awesome Tshirt scenario, we would like to -be able to see a set of t-shirts images. - -The problem could certainly be solved with a kanban view, but this means that it is not possible to -have our normal kanban view and the gallery view in the same action. - -Let us make a gallery view. Each gallery view will be defined by an `image_field` attribute in its -arch: - -.. code-block:: xml - - - -To complete the tasks in this chapter, you will need to install the awesome_gallery addon. This -addon includes the necessary server files to add a new view. - -.. admonition:: Goal - - .. image:: 04_creating_view_from_scratch/overview.png - :align: center - -.. spoiler:: Solutions - - The solutions for each exercise of the chapter are hosted on the - `official Odoo tutorials repository - `_. - -1. Make a hello world view -========================== - -First step is to create a JavaScript implementation with a simple component. - -.. exercise:: - - #. Create the `gallery_view.js` , `gallery_controller.js` and `gallery_controller.xml` files in - `static/src`. - #. Implement a simple hello world component in `gallery_controller.js`. - #. In `gallery_view.js`, import the controller, create a view object, and register it in the - view registry under the name `gallery`. - #. Add `gallery` as one of the view type in the orders action. - #. Make sure that you can see your hello world component when switching to the gallery view. - - .. image:: 04_creating_view_from_scratch/view_button.png - :align: center - - .. image:: 04_creating_view_from_scratch/new_view.png - :align: center - -2. Use the Layout component -=========================== - -So far, our gallery view does not look like a standard view. Let's use the `Layout` component to -have the standard features like other views. - -.. exercise:: - - #. Import the `Layout` component and add it to the `components` of `GalleryController`. - #. Update the template to use `Layout`. It needs a `display` prop, which can be found in - `props.display`. - - .. image:: 04_creating_view_from_scratch/layout.png - :align: center - -3. Parse the arch -================= - -For now, our gallery view does not do much. Let's start by reading the information contained in the -arch of the view. - -The process of parsing an arch is usually done with a `ArchParser`, specific to each view. It -inherits from a generic `XMLParser` class. - -.. example:: - - Here is an example of what an ArchParser might look like: - - .. code-block:: js - - import { XMLParser } from "@web/core/utils/xml"; - - export class GraphArchParser extends XMLParser { - parse(arch, fields) { - const result = {}; - this.visitXML(arch, (node) => { - ... - }); - return result; - } - } - -.. exercise:: - - #. Create the `ArchParser` class in its own file. It can inherit from `XMLParser` in - `@web/core/utils/xml`. - #. Use it to read the `image_field` information. - #. Update the `gallery` view code to add it to the props received by the controller. - - .. note:: - It is probably a little overkill to do it like that, since we basically only need to read one - attribute from the arch, but it is a design that is used by every other odoo views, since it - lets us extract some upfront processing out of the controller. - -.. seealso:: - `Example: The graph arch parser - <{GITHUB_PATH}/addons/web/static/src/views/graph/graph_arch_parser.js>`_ - -4. Load some data -================= - -Let us now get some real data. - -.. exercise:: - - #. Add a :code:`loadImages(domain) {...}` method to the `GalleryController`. It should perform a - `webSearchRead` call from the orm service to fetch records corresponding to the domain, and - use `imageField` received in props. - #. Modify the `setup` code to call that method in the `onWillStart` and `onWillUpdateProps` - hooks. - #. Modify the template to display the data inside the default slot of the `Layout` component. - - .. note:: - The loading data code will be moved into a proper model in the next exercise. - - .. image:: 04_creating_view_from_scratch/gallery_data.png - :align: center - -5. Reorganize code -================== - -Real views are a little bit more organized. This may be overkill in this example, but it is intended -to learn how to structure code in Odoo. Also, this will scale better with changing requirements. - -.. exercise:: - - #. Move all the model code in its own `GalleryModel` class. - #. Move all the rendering code in a `GalleryRenderer` component. - #. Adapt the `GalleryController` and `gallery_view` to make it work. - -6. Display images -================= - -.. exercise:: - - Update the renderer to display images in a nice way, if the field is set. If `image_field` is - empty, display an empty box instead. - - .. image:: 04_creating_view_from_scratch/tshirt_images.png - :align: center - -7. Switch to form view on click -=============================== - -.. exercise:: - - Update the renderer to react to a click on an image and switch to a form view. You can use the - `switchView` function from the action service. - -.. seealso:: - `Code: The switchView function - `_ - -8. Add an optional tooltip -========================== - -It is useful to have some additional information on mouse hover. - -.. exercise:: - - #. Update the code to allow an optional additional attribute on the arch: - - .. code-block:: xml - - - - #. On mouse hover, display the content of the tooltip field. It should work if the field is a - char field, a number field or a many2one field. - #. Update the orders gallery view to add the customer as tooltip field. - - .. image:: 04_creating_view_from_scratch/image_tooltip.png - :align: center - :scale: 60% - -.. seealso:: - `Code: The tooltip hook <{GITHUB_PATH}/addons/web/static/src/core/tooltip/tooltip_hook.js>`_ - -9. Add pagination -================= - -.. exercise:: - - Let's add a pager on the control panel and manage all the pagination like in a normal Odoo view. - Note that it is surprisingly difficult. - - .. image:: 04_creating_view_from_scratch/pagination.png - :align: center - -.. seealso:: - `Code: The usePager hook <{GITHUB_PATH}/addons/web/static/src/search/pager_hook.js>`_ - -10. Validating views -===================== - -We have a nice and useful view so far. But in real life, we may have issue with users incorrectly -encoding the `arch` of their Gallery view: it is currently only an unstructured piece of XML. - -Let us add some validation! In Odoo, XML documents can be described with an RN file -:dfn:`(Relax NG file)`, and then validated. - -.. exercise:: - - #. Add an RNG file that describes the current grammar: - - - A mandatory attribute `image_field`. - - An optional attribute: `tooltip_field`. - - #. Add some code to make sure all views are validated against this RNG file. - #. While we are at it, let us make sure that `image_field` and `tooltip_field` are fields from - the current model. - - Since validating an RNG file is not trivial, here is a snippet to help: - - .. code-block:: python - - # -*- coding: utf-8 -*- - import logging - import os - - from lxml import etree - - from odoo.loglevels import ustr - from odoo.tools import misc, view_validation - - _logger = logging.getLogger(__name__) - - _viewname_validator = None - - @view_validation.validate('viewname') - def schema_viewname(arch, **kwargs): - """ Check the gallery view against its schema - - :type arch: etree._Element - """ - global _viewname_validator - - if _viewname_validator is None: - with misc.file_open(os.path.join('modulename', 'rng', 'viewname.rng')) as f: - _viewname_validator = etree.RelaxNG(etree.parse(f)) - - if _viewname_validator.validate(arch): - return True - - for error in _viewname_validator.error_log: - _logger.error(ustr(error)) - return False - -.. seealso:: - `Example: The RNG file of the graph view <{GITHUB_PATH}/addons/base/rng/graph_view.rng>`_ diff --git a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/gallery_data.png b/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/gallery_data.png deleted file mode 100644 index bae28fc1a..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/gallery_data.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/image_tooltip.png b/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/image_tooltip.png deleted file mode 100644 index 2c06a7d51..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/image_tooltip.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/layout.png b/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/layout.png deleted file mode 100644 index 9acf1fb92..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/layout.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/new_view.png b/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/new_view.png deleted file mode 100644 index 6995b64a5..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/new_view.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/overview.png b/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/overview.png deleted file mode 100644 index cacc0b44b..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/overview.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/pagination.png b/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/pagination.png deleted file mode 100644 index 8b3576163..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/pagination.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/tshirt_images.png b/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/tshirt_images.png deleted file mode 100644 index 979ec9745..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/tshirt_images.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/view_button.png b/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/view_button.png deleted file mode 100644 index eba361a71..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/04_creating_view_from_scratch/view_button.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/05_testing.rst b/content/developer/tutorials/master_odoo_web_framework/05_testing.rst deleted file mode 100644 index 091fc4705..000000000 --- a/content/developer/tutorials/master_odoo_web_framework/05_testing.rst +++ /dev/null @@ -1,80 +0,0 @@ -================== -Chapter 5: Testing -================== - -Automatically testing code is important when working on a codebase. It helps ensure we don't -introduce (too many) bugs or regressions. Let us see how to test our code. - -.. spoiler:: Solutions - - The solutions for each exercise of the chapter are hosted on the - `official Odoo tutorials repository - `_. - -1. Integration testing -====================== - -To make sure our application works as expected, we can perform :ref:`integration testing -` by creating a tour: this is a sequence of steps that we -can execute. Each step wait until some desired DOM state is reached, then performs an action. If, at -some point, it is unable to go to the next step for a long time, the tour fails. - -Let's write a tour to ensure that it is possible to perform an tshirt order from our public route - -.. exercise:: - - #. In the `awesome_tshirt` addon, add a :file:`/static/tests/tours` folder. - #. Add a :file:`/static/tests/tours/order_flow.js` file. - #. Add a tour that performs the following steps: - - #. Open the `/awesome_tshirt/order` route. - #. Fill the order form. - #. Validate it. - #. Navigate to our webclient. - #. Open the list view for the the t-shirt order. - #. Check that our order can be found in the list. - - #. Run the tour manually. - #. Add a Python test to run it programmatically. - #. Run the tour from the terminal. - -2. Unit testing a Component -=========================== - -It is also useful to test independently a component or a piece of code. :ref:`QUnit -` tests are useful to quickly locate an issue. - -.. exercise:: - - #. In the `awesome_tshirt` addon, add a :file:`static/tests/counter_tests.js` file. - #. Add a QUnit test that instantiates a counter, clicks on it, and makes sure it is incremented. - - .. image:: 05_testing/component_test.png - :align: center - -.. seealso:: - - `Example: Testing an Owl component - <{GITHUB_PATH}/addons/web/static/tests/core/checkbox_tests.js>`_ - -3. Unit testing our gallery view -================================ - -Many components need more setup to be tested. In particular, we often need to mock some demo data. -Let us see how to do that. - -.. note:: - This depends on our Gallery View from :doc:`04_creating_view_from_scratch`. - -.. exercise:: - - #. In the `awesome_gallery` addon, add a :file:`/static/tests/gallery_view_tests.js` file. - #. Add a test that instantiates the gallery view with some demo data. - #. Add another test that checks that when the user clicks on an image, it is switched to the form - view of the corresponding order. - - .. image:: 05_testing/view_test.png - :align: center - -.. seealso:: - `Example: Testing a list view <{GITHUB_PATH}/addons/web/static/tests/views/list_view_tests.js>`_ diff --git a/content/developer/tutorials/master_odoo_web_framework/05_testing/component_test.png b/content/developer/tutorials/master_odoo_web_framework/05_testing/component_test.png deleted file mode 100644 index 3f7233dcc..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/05_testing/component_test.png and /dev/null differ diff --git a/content/developer/tutorials/master_odoo_web_framework/05_testing/view_test.png b/content/developer/tutorials/master_odoo_web_framework/05_testing/view_test.png deleted file mode 100644 index fb33175ce..000000000 Binary files a/content/developer/tutorials/master_odoo_web_framework/05_testing/view_test.png and /dev/null differ