[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>
@ -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`
|
||||
|
@ -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/>`_
|
||||
|
@ -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
|
||||
|
@ -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,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>`_
|
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 |
@ -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
|
||||
===================
|
||||
|
@ -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
|
||||
''''''''''''''''''''
|
||||
|
||||
|