[IMP] developer: JavaScript tutorial: chapter 3-7

This commit adds the chapter 3,4,5,6 and 7 of the JavaScript web
framework tutorial.

This new tutorial allows people to discover Owl and the building blocks
of the Odoo JavaScript framework.

closes odoo/documentation#3325

X-original-commit: 54628b4f5b
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Co-authored-by: Géry Debongnie <ged@odoo.com>
Co-authored-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
fdardenne 2023-01-12 08:56:31 +00:00
parent cc13f28fd2
commit 8a30786495
46 changed files with 1278 additions and 16 deletions

View File

@ -52,3 +52,8 @@ 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`

View File

@ -57,7 +57,6 @@ intended to quickly understand and practice the basics of Owl.
.. image:: 01_components/overview.png
:scale: 50%
:align: center
:alt: Overview of chapter 1.
.. spoiler:: Solutions
@ -91,7 +90,6 @@ route with your browser.
.. image:: 01_components/counter.png
:scale: 70%
:align: center
:alt: A counter component.
.. seealso::
`Video: How to use the DevTools <https://www.youtube.com/watch?v=IUyQjwnrpzM>`_
@ -143,7 +141,6 @@ todos. This will be done incrementally in multiple exercises that will introduce
.. image:: 01_components/todo.png
:scale: 70%
:align: center
:alt: A Todo component
.. seealso::
`Owl: Dynamic class attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-class-attribute>`_
@ -184,7 +181,6 @@ list.
.. image:: 01_components/todo_list.png
:scale: 70%
:align: center
:alt: A TodoList
6. Adding a todo
================
@ -214,7 +210,6 @@ a todo to the list.
.. image:: 01_components/create_todo.png
:scale: 70%
:align: center
:alt: Creating todos
.. seealso::
`Owl: Reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_
@ -256,7 +251,6 @@ way to do this is by using a `callback prop
.. image:: 01_components/toggle_todo.png
:scale: 70%
:align: center
:alt: Toggling todos
9. Deleting todos
=================
@ -287,7 +281,6 @@ The final touch is to let the user delete a todo.
.. image:: 01_components/delete_todo.png
:scale: 70%
:align: center
:alt: Deleting todos
10. Generic components with slots
=================================
@ -332,7 +325,6 @@ components. This is useful to factorize the common layout between different part
.. image:: 01_components/card.png
:scale: 70%
:align: center
:alt: Creating card with slots
.. seealso::
`Bootstrap: documentation on cards <https://getbootstrap.com/docs/5.2/components/card/>`_

View File

@ -16,11 +16,13 @@ learn how to use the Odoo JavaScript framework which is is built on top of Owl.
.. odoo[Odoo JavaScript framework] --> Owl
.. image:: 02_odoo_web_framework/previously_learned.svg
.. figure:: 02_odoo_web_framework/previously_learned.svg
:align: center
:alt: What we learned in chapter 1
: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.
@ -28,7 +30,6 @@ opportunity to discover many useful features in the Odoo JavaScript framework.
.. image:: 02_odoo_web_framework/overview_02.png
:align: center
:alt: overview
.. spoiler:: Solutions
@ -50,7 +51,6 @@ and a main content zone just below. This is done using a `Layout component
.. image:: 02_odoo_web_framework/new_layout.png
:align: center
:alt: new Layout
.. seealso::
@ -99,7 +99,6 @@ services, and components can import a service with the `useService()` hooks.
.. image:: 02_odoo_web_framework/navigation_buttons.png
:align: center
:alt: buttons for quick navigation
.. seealso::
- `Example: doAction use
@ -153,7 +152,6 @@ Here is a short explanation on the various arguments:
.. image:: 02_odoo_web_framework/statistics.png
:align: center
:alt: statistics cards
.. seealso::
@ -227,7 +225,6 @@ chartjs code every time if they don't need it).
.. image:: 02_odoo_web_framework/pie_chart.png
:align: center
:scale: 50%
:alt: pie chart
.. seealso::
- `Example: lazy loading a js file
@ -251,7 +248,6 @@ Here is a list of some small improvements you could try to do if you have the ti
.. image:: 02_odoo_web_framework/misc.png
:align: center
:scale: 50%
:alt: background color and translation
.. seealso::
- `Example: use of env._t function

View File

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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>`_

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -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%

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,278 @@
=======================================
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
==========================
<<<<<<< HEAD
<<<<<<< HEAD
First step is to create a JavaScript implementation with a simple component.
=======
First step is to create a javascript implementation with a simple component.
>>>>>>> 953fca3a ([IMP] developer: JavaScript tutorial: chapter 6)
=======
The first step is to create a JavaScript implementation with a simple component.
>>>>>>> e1a22ec3 (review chapter 6)
.. 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>`_

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -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>`_

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -286,6 +286,8 @@ codebase in Javascript, it is necessary to test it. In this section, we will
discuss the practice of testing JS code in isolation: these tests stay in the
browser, and are not supposed to reach the server.
.. _reference/testing/qunit:
Qunit test suite
----------------
@ -527,6 +529,7 @@ Tips
is possible to see the state of the widget directly, and even better, to
manipulate the widget by clicking/interacting with it.
.. _reference/testing/integration-testing:
Integration Testing
===================

View File

@ -826,6 +826,8 @@ The template name is an arbitrary string, although when multiple templates
are related (e.g. called sub-templates) it is customary to use dot-separated
names to indicate hierarchical relationships.
.. _reference/qweb/template_inheritance:
Template inheritance
''''''''''''''''''''