[MOV] howtos/discover_js_framework/*: move "Discover the JS Framework" to the tutorials dir
task-2991663
X-original-commit: 9c5ea316ff
Part-of: odoo/documentation#3660
59
content/developer/tutorials/discover_js_framework.rst
Normal file
@@ -0,0 +1,59 @@
|
||||
:show-content:
|
||||
|
||||
=================================
|
||||
Discover the JavaScript Framework
|
||||
=================================
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
:glob:
|
||||
|
||||
discover_js_framework/*
|
||||
|
||||
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.
|
||||
The project is currently a simple kanban view, with a few columns.
|
||||
|
||||
The usual process is as follows: a customer looking for a nice t-shirt can simply order it from the
|
||||
Awesome T-Shirt site and give the url for any image they want. They must also fill in some basic
|
||||
information, such as the desired size and quantity of t-shirts. Once they have confirmed their
|
||||
order, and once the payment has been validated, the system will create a task in our application.
|
||||
|
||||
The big boss of Awesome T-shirt, Bafien Carpink, is unhappy with our implementation. He believes
|
||||
that by micromanaging more, he will be able to get more revenue from his employees. As the IT staff
|
||||
for Awesome T-shirt, we are responsible with improving the system. Various independent tasks must be
|
||||
performed.
|
||||
|
||||
Let us now practice our Odoo skills!
|
||||
|
||||
.. _howtos/discover_js_framework/setup:
|
||||
|
||||
Setup
|
||||
=====
|
||||
|
||||
To follow the training, it is necessary to have basic knowledge on Git and a recent version of Odoo
|
||||
installed. If you have not installed it yet, we recommend installing it from :ref:`source
|
||||
<setup/install/source>` (:dfn:`running Odoo from source code`).
|
||||
|
||||
To setup your development environment, you can also follow the dedicated chapter in :doc:`Getting
|
||||
Started: Development environment setup <../tutorials/getting_started/02_setup>` tutorial.
|
||||
|
||||
The last things to do are:
|
||||
|
||||
- Clone the `official Odoo tutorials repository <https://github.com/odoo/tutorials>`_ and switch to
|
||||
the branch `{BRANCH}`.
|
||||
- Add the cloned repository to the :option:`--addons-path <odoo-bin --addons-path>`.
|
||||
- Start a new Odoo database and install the modules `owl_playground`, `awesome_tshirt`, and
|
||||
`awesome_gallery`.
|
||||
|
||||
Exercises
|
||||
=========
|
||||
|
||||
* :doc:`discover_js_framework/01_components`
|
||||
* :doc:`discover_js_framework/02_odoo_web_framework`
|
||||
* :doc:`discover_js_framework/03_fields_and_views`
|
||||
* :doc:`discover_js_framework/04_miscellaneous`
|
||||
* :doc:`discover_js_framework/05_custom_kanban_view`
|
||||
* :doc:`discover_js_framework/06_creating_view_from_scratch`
|
||||
* :doc:`discover_js_framework/07_testing`
|
||||
@@ -0,0 +1,339 @@
|
||||
=====================
|
||||
Chapter 1: Components
|
||||
=====================
|
||||
|
||||
This chapter introduces the `Owl framework <https://github.com/odoo/owl>`_, a tailor-made component
|
||||
system for Odoo. The main building blocks of OWL are `components
|
||||
<{OWL_PATH}/doc/reference/component.md>`_ and `templates <{OWL_PATH}/doc/reference/templates.md>`_.
|
||||
|
||||
In Owl, every part of user interface is managed by a component: they hold the logic and define the
|
||||
templates that are used to render the user interface. In practice, a component is represented by a
|
||||
small JavaScript class subclassing the `Component` class.
|
||||
|
||||
.. _jstraining/chapter1/intro_example:
|
||||
|
||||
.. example::
|
||||
The `Counter` class implements a component that holds the internal state of a counter and defines
|
||||
how it should be incremented.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
const { Component, useState } = owl;
|
||||
|
||||
class Counter extends Component {
|
||||
static template = "my_module.Counter";
|
||||
|
||||
state = useState({ value: 0 });
|
||||
|
||||
increment() {
|
||||
this.state.value++;
|
||||
}
|
||||
}
|
||||
|
||||
The `Counter` class specifies the name of the template to render. The template is written in XML
|
||||
and defines a part of user interface.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="my_module.Counter" owl="1">
|
||||
<p>Counter: <t t-esc="state.value"/></p>
|
||||
<button class="btn btn-primary" t-on-click="increment">Increment</button>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
You maybe noticed the `owl="1"` temporary attribute, it allows Odoo to differentiate Owl
|
||||
templates from the old JavaScript framework templates.
|
||||
|
||||
Let us take some time to get used to Owl itself. Below, you will find a series of exercises
|
||||
intended to quickly understand and practice the basics of Owl.
|
||||
|
||||
.. todo:: update screenshot
|
||||
|
||||
.. admonition:: Goal
|
||||
|
||||
Here is an overview of what we are going to achieve in this chapter.
|
||||
|
||||
.. image:: 01_components/overview.png
|
||||
:scale: 50%
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the `official Odoo tutorials
|
||||
repository <https://github.com/odoo/tutorials/commits/{BRANCH}-solutions/owl_playground>`_.
|
||||
|
||||
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.
|
||||
|
||||
.. 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 <developer-mode/url>` so that the files are not minified.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Modify :file:`playground.js` so that it acts as a counter like in :ref:`the example above
|
||||
<jstraining/chapter1/intro_example>`. You will need to use the `useState
|
||||
<{OWL_PATH}/doc/reference/hooks.md#usestate>`_ function so that the component is re-rendered
|
||||
whenever any part of the state object 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_components/counter.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`Video: How to use the DevTools <https://www.youtube.com/watch?v=IUyQjwnrpzM>`_
|
||||
|
||||
2. Extract counter in a component
|
||||
=================================
|
||||
|
||||
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.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. 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 file.
|
||||
#. Make sure the template is in its own file, with the same name.
|
||||
|
||||
.. important::
|
||||
Don't forget :code:`/** @odoo-module **/` in your JavaScript files. More information on this can
|
||||
be found :ref:`here <frontend/modules/native_js>`.
|
||||
|
||||
3. A todo component
|
||||
===================
|
||||
|
||||
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.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. 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.
|
||||
|
||||
.. example::
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
setup() {
|
||||
...
|
||||
this.todo = { id: 3, description: "buy milk", done: false };
|
||||
}
|
||||
|
||||
.. image:: 01_components/todo.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`Owl: Dynamic class attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-class-attribute>`_
|
||||
|
||||
4. 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
|
||||
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>`_
|
||||
|
||||
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 `Todo`
|
||||
component.
|
||||
#. Make sure it 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.
|
||||
|
||||
5. A list of todos
|
||||
==================
|
||||
|
||||
Now, let us display a list of todos instead of just one todo. For now, we can still hard-code the
|
||||
list.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Change the code to display a list of todos instead of just one, and use `t-foreach
|
||||
<{OWL_PATH}/doc/reference/templates.md#loops>`_ in the template.
|
||||
#. Think about how it should be keyed with the `t-key` directive.
|
||||
|
||||
.. image:: 01_components/todo_list.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
6. 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::
|
||||
|
||||
#. 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.
|
||||
#. Make sure it has a unique id. It can be just a counter that increments at each todo.
|
||||
#. Then, clear the input of all content.
|
||||
#. Bonus point: don't do anything if the input is empty.
|
||||
|
||||
.. note::
|
||||
Notice that nothing updates in the UI: this is because Owl does not know that it should update
|
||||
the UI. This can be fixed by wrapping the todo list in a `useState` hook.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
this.todos = useState([]);
|
||||
|
||||
.. image:: 01_components/create_todo.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`Owl: Reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_
|
||||
|
||||
7. 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>`_.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Focus the `input` from the previous exercise when the dashboard is `mounted
|
||||
<{OWL_PATH}/doc/reference/component.md#mounted>`_.
|
||||
#. Bonus point: extract the code into a specialized `hook <{OWL_PATH}/doc/reference/hooks.md>`_
|
||||
`useAutofocus`.
|
||||
|
||||
.. seealso::
|
||||
`Owl: Component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_
|
||||
|
||||
8. 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`
|
||||
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 `done` is true.
|
||||
#. 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!
|
||||
|
||||
.. image:: 01_components/toggle_todo.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
9. Deleting todos
|
||||
=================
|
||||
|
||||
The final touch is to let the user delete a todo.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Add a new callback prop `removeTodo`.
|
||||
|
||||
.. 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::
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
#. Insert :code:`<span class="fa fa-remove">` in the template of the `Todo` component.
|
||||
#. Whenever the user clicks on it, it should call the `removeTodo` method.
|
||||
|
||||
.. image:: 01_components/delete_todo.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
10. Generic components 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.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Write a `Card` component using the following Bootstrap HTML structure:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<div class="card" style="width: 18rem;">
|
||||
<img src="..." class="card-img-top" alt="..." />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Card title</h5>
|
||||
<p class="card-text">
|
||||
Some quick example text to build on the card title and make up the bulk
|
||||
of the card's content.
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#. This component should have two slots: one slot for the title, and one for the content (the
|
||||
default slot).
|
||||
|
||||
.. example::
|
||||
Here is how one could use it:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<Card>
|
||||
<t t-set-slot="title">Card title</t>
|
||||
<p class="card-text">Some quick example text...</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</Card>
|
||||
|
||||
#. Bonus point: if the `title` slot is not given, the `h5` should not be rendered at all.
|
||||
|
||||
.. image:: 01_components/card.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`Bootstrap: documentation on cards <https://getbootstrap.com/docs/5.2/components/card/>`_
|
||||
|
||||
11. Go further
|
||||
==============
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. 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.
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,256 @@
|
||||
=============================
|
||||
Chapter 2: Odoo web framework
|
||||
=============================
|
||||
|
||||
In the previous chapter, we learned to use Owl framework and its different concepts. We can now
|
||||
learn how to use the Odoo JavaScript framework which is is built on top of Owl.
|
||||
|
||||
.. graph TD
|
||||
.. subgraph "Owl"
|
||||
.. C[Component]
|
||||
.. T[Template]
|
||||
.. H[Hook]
|
||||
.. S[Slot]
|
||||
.. E[Event]
|
||||
.. end
|
||||
|
||||
.. odoo[Odoo JavaScript framework] --> Owl
|
||||
|
||||
.. figure:: 02_odoo_web_framework/previously_learned.svg
|
||||
:align: center
|
||||
:width: 50%
|
||||
|
||||
This is the progress that we have made in discovering the JavaScript web framework at the end of
|
||||
:doc:`01_components`.
|
||||
|
||||
In the `awesome_tshirt` module, we will build our Awesome dashboard. This will be a good
|
||||
opportunity to discover many useful features in the Odoo JavaScript framework.
|
||||
|
||||
.. admonition:: Goal
|
||||
|
||||
.. image:: 02_odoo_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 <https://github.com/odoo/tutorials/commits/{BRANCH}-solutions/awesome_tshirt>`_.
|
||||
|
||||
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 a `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:`{ "top-right": false, "bottom-right": false }` for the
|
||||
`display` props of the `Layout` component.
|
||||
|
||||
.. image:: 02_odoo_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
|
||||
========================================
|
||||
|
||||
Bafien Carpink want buttons for easy access to common views in Odoo. In order to do that, you will
|
||||
need to use the action service.
|
||||
|
||||
:ref:`Services <frontend/services>` is a notion defined by the Odoo JavaScript framework, it 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 with the `useService()` hooks.
|
||||
|
||||
.. 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
|
||||
<{GITHUB_PATH}/odoo/addons/base/views/res_partner_views.xml#L513>`_).
|
||||
#. A button `New Orders`, which opens a list view with all orders created in the last 7 days.
|
||||
#. A button `Cancelled Order`, which opens a list of all orders created in the last 7 days, but
|
||||
already cancelled.
|
||||
|
||||
.. image:: 02_odoo_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 made in the Owl
|
||||
training) containing a few statistics. There is a route `/awesome_tshirt/statistics` that will
|
||||
perform some computations and return an object containing some useful information.
|
||||
|
||||
Whenever we need to call a specific controller, we need to use the :ref:`rpc service
|
||||
<frontend/services/rpc>`. 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_odoo_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
|
||||
<{GITHUB_PATH}/addons/lunch/static/src/views/search_model.js#L21>`_
|
||||
|
||||
4. Cache network calls, create a service
|
||||
========================================
|
||||
|
||||
If you open your browser dev tools, in the network tabs, you will probably 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 probably 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::
|
||||
|
||||
#. Implements 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.
|
||||
#. Maybe use the `memoize
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/utils/functions.js#L11>`_ utility function from
|
||||
`@web/core/utils/functions`
|
||||
#. 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, which displays the
|
||||
proportions of t-shirts sold for each size: S/M/L/XL/XXL.
|
||||
|
||||
For this exercise, we will use `Chart.js <https://www.chartjs.org/>`_. It is the chart library used
|
||||
by the graph view. However, it is not loaded by default, so we will need to either add it to our
|
||||
assets bundle, or lazy load it (it's 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
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/assets.js#L23>`_ function to load
|
||||
:file:`/web/static/lib/Chart/Chart.js`.
|
||||
#. In a `Card` (from previous exercises), display a `pie chart
|
||||
<https://www.chartjs.org/docs/2.8.0/charts/doughnut.html>`_ 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_odoo_web_framework/pie_chart.png
|
||||
:align: center
|
||||
:scale: 50%
|
||||
|
||||
.. seealso::
|
||||
- `Example: lazy loading a js file
|
||||
<{GITHUB_PATH}/addons/web/static/src/views/graph/graph_renderer.js#L57>`_
|
||||
- `Example: rendering a chart in a component
|
||||
<{GITHUB_PATH}/addons/web/static/src/views/graph/graph_renderer.js#L641>`_
|
||||
|
||||
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 <reference/translations>` (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_odoo_web_framework/misc.png
|
||||
:align: center
|
||||
:scale: 50%
|
||||
|
||||
.. seealso::
|
||||
- `Example: use of env._t function
|
||||
<{GITHUB_PATH}/addons/account/static/src/components/bills_upload/bills_upload.js#L64>`_
|
||||
- `Code: translation code in web/
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/l10n/translation.js#L16>`_
|
||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,423 @@
|
||||
===========================
|
||||
Chapter 3: 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 move on to learning how to create new fields and views.
|
||||
|
||||
.. 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
|
||||
.. end
|
||||
|
||||
.. odoo[Odoo JavaScript framework] --> Owl
|
||||
|
||||
.. figure:: 03_fields_and_views/previously_learned.svg
|
||||
:align: center
|
||||
:width: 60%
|
||||
|
||||
This is the progress that we have made in discovering the JavaScript web framework at the end of
|
||||
:doc:`02_odoo_web_framework`.
|
||||
|
||||
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
|
||||
<frontend/registries>`. 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::
|
||||
Let us discuss a simplified implementation of a `CharField`.
|
||||
|
||||
First, here is the template:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<t t-name="web.CharField" owl="1">
|
||||
<t t-if="props.readonly">
|
||||
<span t-esc="formattedValue" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<input
|
||||
class="o_input"
|
||||
t-att-type="props.isPassword ? 'password' : 'text'"
|
||||
t-att-placeholder="props.placeholder"
|
||||
t-on-change="updateValue"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
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
|
||||
|
||||
<field name="preview_moves" widget="account_resequence_widget"/>
|
||||
|
||||
.. admonition:: Goal
|
||||
|
||||
.. image:: 03_fields_and_views/overview_03.png
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the `official Odoo tutorials
|
||||
repository <https://github.com/odoo/tutorials/commits/{BRANCH}-solutions/awesome_tshirt>`_.
|
||||
|
||||
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 it in the form view.
|
||||
|
||||
For this task, we need to create a new field component `image_preview`. This component is
|
||||
specified as follows: In readonly mode, it is only an image tag with the correct `src` if the field
|
||||
is set; In edit mode, it also behaves like classical `char` fields (you can use the `CharField` in
|
||||
your template by passing it in the props). An `input` should be displayed with the text value of the
|
||||
field, so it can be edited.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Create a new `ImagePreview` component and 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`.
|
||||
#. Register your field in the proper :ref:`registry <frontend/registries>`.
|
||||
#. Update the arch of the form view to use your new field by setting the `widget` attribute.
|
||||
|
||||
.. 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:: 03_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
|
||||
======================================
|
||||
|
||||
.. exercise::
|
||||
|
||||
We want to improve the field of the previous task to help the staff recognize orders for which
|
||||
some action should be done. In particular, we want to display a warning "MISSING TSHIRT DESIGN"
|
||||
in red if there is no image URL specified on the order.
|
||||
|
||||
.. image:: 03_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 task 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 <reference/qweb/template_inheritance>` 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:: 03_fields_and_views/late_field.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Example: A field inheriting another (JS)
|
||||
<{GITHUB_PATH}/addons/account/static/src/components/account_type_selection/account_type_selection.js>`_
|
||||
- `Example: A field inheriting another (XML)
|
||||
<{GITHUB_PATH}/addons/account/static/src/components/account_type_selection/account_type_selection.xml>`_
|
||||
- :ref:`Documentation on xpath <reference/views/inheritance>`
|
||||
|
||||
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 a message/warning box with some
|
||||
information in the form view, 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.
|
||||
|
||||
.. image:: 03_fields_and_views/warning_widget.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Example: Using the tag <widget> in a form view
|
||||
<{GITHUB_PATH}/addons/calendar/views/calendar_views.xml#L197>`_
|
||||
- `Example: Implementation of a widget (JS)
|
||||
<{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days/week_days.js>`_
|
||||
- `Example: Implementation of a widget (XML)
|
||||
<{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days/week_days.xml>`_
|
||||
|
||||
5. Use `markup`
|
||||
===============
|
||||
|
||||
Let's see how we can display raw HTML in a template. Before, there was a `t-raw` directive that
|
||||
would just output anything as HTML. This was unsafe, and has been replaced by a `t-out
|
||||
<{OWL_PATH}/doc/reference/templates.md#outputting-data>`_ directive that acts like a `t-esc` unless
|
||||
the data has been marked explicitly with a `markup` function.
|
||||
|
||||
.. 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`.
|
||||
|
||||
.. note::
|
||||
This is an example of a safe use of `t-out` , since the string is static.
|
||||
|
||||
.. image:: 03_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
|
||||
|
||||
<graph string="Invoices Analysis" type="line" sample="1">
|
||||
<field name="product_categ_id"/>
|
||||
<field name="price_subtotal" type="measure"/>
|
||||
</graph>
|
||||
|
||||
<calendar string="Leads Generation" create="0" mode="month" date_start="activity_date_deadline" color="user_id" hide_time="true" event_limit="5">
|
||||
<field name="expected_revenue"/>
|
||||
<field name="partner_id" avatar_field="avatar_128"/>
|
||||
<field name="user_id" filters="1" invisible="1"/>
|
||||
</calendar>
|
||||
|
||||
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:: 03_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), but then it makes it easy to customize the specific renderer used by a sub view.
|
||||
|
||||
These props will be extended before being given to the Controller. In particular, the search props
|
||||
(domain/context/groupby) will be added.
|
||||
|
||||
Then, 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 form view 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` attribute to the arch of the form view so Odoo will load it.
|
||||
#. Create a new template inheriting from the form controller template to add a button after the
|
||||
create button.
|
||||
#. Add a 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 be disabled if the current order is in `create` mode (i.e., it does not
|
||||
exist yet).
|
||||
#. The button should be displayed as a primary button if the customer is properly set and if the
|
||||
task stage is `printed`. Otherwise, it is displayed as a secondary button.
|
||||
#. Bonus point: clicking twice on the button should not trigger 2 RPCs.
|
||||
|
||||
.. image:: 03_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
|
||||
<{GITHUB_PATH}/addons/mass_mailing/views/mailing_contact_views.xml#L44>`_
|
||||
- `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>`_
|
||||
|
||||
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.
|
||||
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 453 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 334 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,310 @@
|
||||
========================
|
||||
Chapter 4: 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:: 04_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:`03_fields_and_views`.
|
||||
|
||||
.. admonition:: Goal
|
||||
|
||||
.. image:: 04_miscellaneous/kitten_mode.png
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the `official Odoo tutorials
|
||||
repository <https://github.com/odoo/tutorials/commits/{BRANCH}-solutions/awesome_tshirt>`_.
|
||||
|
||||
1. Interacting with the notification system
|
||||
===========================================
|
||||
|
||||
.. note::
|
||||
This task depends on :doc:`the previous exercises <03_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 <frontend/services/notification>` message when the action is
|
||||
completed successfully, and a warning if it failed.
|
||||
#. If it failed, the notification should be permanent.
|
||||
|
||||
.. image:: 04_miscellaneous/notification.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
.. seealso::
|
||||
`Example: Code 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 <frontend/registries/systray>` item is an element that appears in the system tray,
|
||||
which is a small area located on the right-hand side of the navbar. The systray is used to display
|
||||
notifications and provide access to certain features.
|
||||
|
||||
.. 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:: 04_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
|
||||
<{GITHUB_PATH}/addons/barcodes/static/src/barcode_service.js#L5>`_
|
||||
|
||||
3. Real life update
|
||||
===================
|
||||
|
||||
So far, the systray item from above does not update unless the user refreshes the browser. Let us
|
||||
do that by calling periodically (for example, every minute) the server to reload the information.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Modify the systray item code to get its data from the `tshirt` service.
|
||||
#. 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::
|
||||
|
||||
#. 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.
|
||||
#. The systray item can then perform a `useState` on the service return value.
|
||||
#. 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
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/debug/profiling/profiling_service.js#L30>`_
|
||||
|
||||
4. Add a command to the command palette
|
||||
=======================================
|
||||
|
||||
Now, let us see how we can interact with the command palette. The command palette is a feature that
|
||||
allows users to quickly access various commands and functions within the application. It is accessed
|
||||
by pressing `CTRL+K` in the Odoo interface.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Let us modify the image preview field (from a previous exercise) to add a command to the command
|
||||
palette to open the image in a new browser tab (or window).
|
||||
|
||||
Make sure that the command is only active whenever a field preview is visible in the screen.
|
||||
|
||||
.. image:: 04_miscellaneous/new_command.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
- `Example: Using the useCommand hook
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/debug/debug_menu.js#L15>`_
|
||||
- `Code: The command service
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/commands/command_service.js>`_
|
||||
|
||||
5. Monkey patching a component
|
||||
==============================
|
||||
|
||||
Often, it is possible to do what we want by using existing extension points that allow
|
||||
customization, such as registering something in a registry. But it happens that we want to modify
|
||||
something that has no such mechanism. In that case, we have to fall back on a less safe form of
|
||||
customization: monkey patching. Almost everything in Odoo can be monkey patched.
|
||||
|
||||
Bafien, our beloved leader, heard that employees perform better if they are constantly being
|
||||
watched. Since he is not able to be there in person for each and every one of his employees, he
|
||||
tasked you with the following: update 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::
|
||||
|
||||
#. Create the :file:`control_panel_patch.js` file, as well as corresponding CSS and XML files.
|
||||
#. :doc:`Patch </developer/reference/frontend/patching_code>` the `ControlPanel` template to add
|
||||
an icon next to the breadcrumbs. You might want to use the `fa-eye` or `fa-eyes` icons. Make
|
||||
sure it is visible in all views!
|
||||
|
||||
.. tip::
|
||||
There are two ways to inherit a template using XPath: by specifying
|
||||
`t-inherit-mode="primary"`, which creates a new, independent template with the desired
|
||||
modifications, or by using `t-inherit-mode="extension"`, which modifies the original
|
||||
template in place.
|
||||
|
||||
.. 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;
|
||||
}
|
||||
}
|
||||
|
||||
#. Import the ControlPanel component and the `patch` function.
|
||||
#. Update the code to display the message on click by using the dialog service. You can use
|
||||
`ConfirmationDialog`.
|
||||
|
||||
.. image:: 04_miscellaneous/bafien_eye.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
.. image:: 04_miscellaneous/confirmation_dialog.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
.. seealso::
|
||||
- `Code: The patch function <{GITHUB_PATH}/addons/web/static/src/core/utils/patch.js#L16>`_
|
||||
- `Code: The ControlPanel component
|
||||
<{GITHUB_PATH}/addons/web/static/src/search/control_panel/control_panel.js>`_
|
||||
- `The Font Awesome website <https://fontawesome.com/>`_
|
||||
- `Code: The dialog service <{GITHUB_PATH}/addons/web/static/src/core/dialog/dialog_service.js>`_
|
||||
- `Code: ConfirmationDialog
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js>`_
|
||||
- `Example: Using the dialog service
|
||||
<{GITHUB_PATH}/addons/board/static/src/board_controller.js#L88>`_
|
||||
- `Example: XPath with t-inherit-mode="primary"
|
||||
<{GITHUB_PATH}/addons/account/static/src/components/account_move_form/account_move_form_notebook.xml#L4>`_
|
||||
- `Example: XPath with t-inherit-mode="extension"
|
||||
<{GITHUB_PATH}/calendar/static/src/components/activity/activity.xml#L4>`_
|
||||
|
||||
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).
|
||||
#. Import the `AutoComplete` component from `@web/core/autocomplete/autocomplete`.
|
||||
#. Add it to the dashboard, next to the buttons in the control panel.
|
||||
#. Update the code to fetch the list of customers with the tshirt service, and display it in the
|
||||
autocomplete component, filtered by the `fuzzyLookup` method.
|
||||
|
||||
.. image:: 04_miscellaneous/autocomplete.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
.. seealso::
|
||||
- `Code: AutoComplete <{GITHUB_PATH}/addons/web/static/src/core/autocomplete/autocomplete.js>`_
|
||||
- `Code: fuzzyLookup <{GITHUB_PATH}/addons/web/static/src/core/utils/search.js>`_
|
||||
|
||||
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 :file:`kitten_mode.js` file.
|
||||
#. Create a `kitten` service, which should check the content of the active url hash with the
|
||||
help of the :ref:`router service <frontend/services/router>`.
|
||||
#. If `kitten` is set, we are in kitten mode. This should add a class `.o-kitten-mode` on the
|
||||
document body.
|
||||
#. Add the following CSS 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 `.o-kitten-mode` class and update the current URL accordingly.
|
||||
|
||||
.. image:: 04_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, lots of code/styles/templates.
|
||||
Also, suppose that the dashboard is only used by some users in some business flows, so we want 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 <reference/assets_bundle>`
|
||||
`awesome_tshirt.dashboard`.
|
||||
#. Add the awesome dashboard code to this bundle. If needed you can create folders and move
|
||||
files.
|
||||
#. Remove the code from the `web.assets_backend` bundle so it is not loaded twice.
|
||||
|
||||
So far, we removed the dashboard from the main bundle, but it should now be lazily loaded. Right
|
||||
now, there is no client action registered in the action registry.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Create a new file :file:`dashboard_loader.js`.
|
||||
#. Copy the code registering `AwesomeDashboard` to the dashboard loader.
|
||||
#. Register `AwesomeDashboard` as a `LazyComponent`.
|
||||
#. Modify the code in the dashboard loader to use the lazy component `AwesomeDashboard`.
|
||||
|
||||
.. seealso::
|
||||
- :ref:`Documentation on assets <reference/assets>`
|
||||
- `Code: LazyComponent <{GITHUB_PATH}/addons/web/static/src/core/assets.js#L255>`_
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
@@ -0,0 +1,169 @@
|
||||
=============================
|
||||
Chapter 5: 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:`03_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:: 05_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 <https://github.com/odoo/tutorials/commits/{BRANCH}-solutions/awesome_tshirt>`_.
|
||||
|
||||
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:: 05_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:: 05_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:: 05_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:: 05_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:: 05_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
|
||||
<{GITHUB_PATH}/addons/web/static/tests/core/utils/search_test.js#L17>`_
|
||||
|
||||
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 <frontend/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:: 05_custom_kanban_view/customer_pager.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,270 @@
|
||||
=======================================
|
||||
Chapter 6: Creating a view from scratch
|
||||
=======================================
|
||||
|
||||
.. warning::
|
||||
It is highly recommended that you complete :doc:`03_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
|
||||
|
||||
<gallery image_field="some_field"/>
|
||||
|
||||
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:: 06_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 <https://github.com/odoo/tutorials/commits/{BRANCH}-solutions/awesome_gallery>`_.
|
||||
|
||||
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:: 06_creating_view_from_scratch/view_button.png
|
||||
:align: center
|
||||
|
||||
.. image:: 06_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:: 06_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:: 06_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:: 06_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
|
||||
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/action_service.js#L1329>`_
|
||||
|
||||
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
|
||||
|
||||
<gallery image_field="some_field" tooltip_field="some_other_field"/>
|
||||
|
||||
#. 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:: 06_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:: 06_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>`_
|
||||
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 813 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 698 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,79 @@
|
||||
==================
|
||||
Chapter 7: 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 <https://github.com/odoo/tutorials/commits/{BRANCH}-solutions>`_.
|
||||
|
||||
1. Integration testing
|
||||
======================
|
||||
|
||||
To make sure our application works as expected, we can perform :ref:`integration testing
|
||||
<reference/testing/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
|
||||
<reference/testing/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:: 07_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:`06_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:: 07_testing/view_test.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`Example: Testing a list view <{GITHUB_PATH}/addons/web/static/tests/views/list_view_tests.js>`_
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 43 KiB |