diff --git a/content/developer/howtos/discover_js_framework.rst b/content/developer/howtos/discover_js_framework.rst index d6d8f7835..743a54383 100644 --- a/content/developer/howtos/discover_js_framework.rst +++ b/content/developer/howtos/discover_js_framework.rst @@ -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` diff --git a/content/developer/howtos/discover_js_framework/01_components.rst b/content/developer/howtos/discover_js_framework/01_components.rst index 22dc9cfd1..1efce009b 100644 --- a/content/developer/howtos/discover_js_framework/01_components.rst +++ b/content/developer/howtos/discover_js_framework/01_components.rst @@ -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 `_ @@ -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 `_ diff --git a/content/developer/howtos/discover_js_framework/02_odoo_web_framework.rst b/content/developer/howtos/discover_js_framework/02_odoo_web_framework.rst index 4c4eb672f..934c16de7 100644 --- a/content/developer/howtos/discover_js_framework/02_odoo_web_framework.rst +++ b/content/developer/howtos/discover_js_framework/02_odoo_web_framework.rst @@ -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 diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views.rst b/content/developer/howtos/discover_js_framework/03_fields_and_views.rst new file mode 100644 index 000000000..2fbf74de3 --- /dev/null +++ b/content/developer/howtos/discover_js_framework/03_fields_and_views.rst @@ -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 +`. 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 + + + + + + + + + + + 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 + + + +.. 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 `_. + +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 `. + #. 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 ` 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 ` + +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 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 + + + + + + + + + + + + +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. diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/form_button.png b/content/developer/howtos/discover_js_framework/03_fields_and_views/form_button.png new file mode 100644 index 000000000..5d89c7092 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/03_fields_and_views/form_button.png differ diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/form_renderer_fields.svg b/content/developer/howtos/discover_js_framework/03_fields_and_views/form_renderer_fields.svg new file mode 100644 index 000000000..d104ab5f0 --- /dev/null +++ b/content/developer/howtos/discover_js_framework/03_fields_and_views/form_renderer_fields.svg @@ -0,0 +1 @@ +
FormRenderer
Field
BooleanField
Field
Many2OneField
...
\ No newline at end of file diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/image_field.png b/content/developer/howtos/discover_js_framework/03_fields_and_views/image_field.png new file mode 100644 index 000000000..50b427fb1 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/03_fields_and_views/image_field.png differ diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/late_field.png b/content/developer/howtos/discover_js_framework/03_fields_and_views/late_field.png new file mode 100644 index 000000000..f2d1dcde4 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/03_fields_and_views/late_field.png differ diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/missing_image.png b/content/developer/howtos/discover_js_framework/03_fields_and_views/missing_image.png new file mode 100644 index 000000000..0c8986221 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/03_fields_and_views/missing_image.png differ diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/overview_03.png b/content/developer/howtos/discover_js_framework/03_fields_and_views/overview_03.png new file mode 100644 index 000000000..a221c28d0 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/03_fields_and_views/overview_03.png differ diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/previously_learned.svg b/content/developer/howtos/discover_js_framework/03_fields_and_views/previously_learned.svg new file mode 100644 index 000000000..5d3370211 --- /dev/null +++ b/content/developer/howtos/discover_js_framework/03_fields_and_views/previously_learned.svg @@ -0,0 +1 @@ +
Odoo Javascript framework
Services
action
rpc
Translation
Lazy loading libraries
SCSS
Owl
Component
Template
Hook
Slot
Event
\ No newline at end of file diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/view_architecture.svg b/content/developer/howtos/discover_js_framework/03_fields_and_views/view_architecture.svg new file mode 100644 index 000000000..e7453c9e8 --- /dev/null +++ b/content/developer/howtos/discover_js_framework/03_fields_and_views/view_architecture.svg @@ -0,0 +1 @@ +
View description
compute props
props function
generic props
arch parser
others ...
Controller
Layout
Renderer
Model
\ No newline at end of file diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/view_component.svg b/content/developer/howtos/discover_js_framework/03_fields_and_views/view_component.svg new file mode 100644 index 000000000..ac36786b4 --- /dev/null +++ b/content/developer/howtos/discover_js_framework/03_fields_and_views/view_component.svg @@ -0,0 +1 @@ +
props
View
KanbanController
\ No newline at end of file diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/warning_widget.png b/content/developer/howtos/discover_js_framework/03_fields_and_views/warning_widget.png new file mode 100644 index 000000000..c046cda10 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/03_fields_and_views/warning_widget.png differ diff --git a/content/developer/howtos/discover_js_framework/03_fields_and_views/warning_widget2.png b/content/developer/howtos/discover_js_framework/03_fields_and_views/warning_widget2.png new file mode 100644 index 000000000..d224b4363 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/03_fields_and_views/warning_widget2.png differ diff --git a/content/developer/howtos/discover_js_framework/04_miscellaneous.rst b/content/developer/howtos/discover_js_framework/04_miscellaneous.rst new file mode 100644 index 000000000..29528b538 --- /dev/null +++ b/content/developer/howtos/discover_js_framework/04_miscellaneous.rst @@ -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 `_. + +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 ` 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 ` 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 ` 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 `_ + - `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 `. + #. 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 ` + `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 ` + - `Code: LazyComponent <{GITHUB_PATH}/addons/web/static/src/core/assets.js#L255>`_ diff --git a/content/developer/howtos/discover_js_framework/04_miscellaneous/autocomplete.png b/content/developer/howtos/discover_js_framework/04_miscellaneous/autocomplete.png new file mode 100644 index 000000000..65a7c3da2 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/04_miscellaneous/autocomplete.png differ diff --git a/content/developer/howtos/discover_js_framework/04_miscellaneous/bafien_eye.png b/content/developer/howtos/discover_js_framework/04_miscellaneous/bafien_eye.png new file mode 100644 index 000000000..6714b0573 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/04_miscellaneous/bafien_eye.png differ diff --git a/content/developer/howtos/discover_js_framework/04_miscellaneous/confirmation_dialog.png b/content/developer/howtos/discover_js_framework/04_miscellaneous/confirmation_dialog.png new file mode 100644 index 000000000..679edd2cb Binary files /dev/null and b/content/developer/howtos/discover_js_framework/04_miscellaneous/confirmation_dialog.png differ diff --git a/content/developer/howtos/discover_js_framework/04_miscellaneous/kitten_mode.png b/content/developer/howtos/discover_js_framework/04_miscellaneous/kitten_mode.png new file mode 100644 index 000000000..e0ba81fee Binary files /dev/null and b/content/developer/howtos/discover_js_framework/04_miscellaneous/kitten_mode.png differ diff --git a/content/developer/howtos/discover_js_framework/04_miscellaneous/new_command.png b/content/developer/howtos/discover_js_framework/04_miscellaneous/new_command.png new file mode 100644 index 000000000..f6094e0c4 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/04_miscellaneous/new_command.png differ diff --git a/content/developer/howtos/discover_js_framework/04_miscellaneous/notification.png b/content/developer/howtos/discover_js_framework/04_miscellaneous/notification.png new file mode 100644 index 000000000..ec7c7d1ae Binary files /dev/null and b/content/developer/howtos/discover_js_framework/04_miscellaneous/notification.png differ diff --git a/content/developer/howtos/discover_js_framework/04_miscellaneous/previously_learned.svg b/content/developer/howtos/discover_js_framework/04_miscellaneous/previously_learned.svg new file mode 100644 index 000000000..768d645f2 --- /dev/null +++ b/content/developer/howtos/discover_js_framework/04_miscellaneous/previously_learned.svg @@ -0,0 +1 @@ +
Odoo Javascript framework
Services
action
rpc
orm
Translation
Lazy loading libraries
SCSS
Fields
Views
Registries
Owl
Component
Template
Hook
Slot
Event
\ No newline at end of file diff --git a/content/developer/howtos/discover_js_framework/04_miscellaneous/systray.png b/content/developer/howtos/discover_js_framework/04_miscellaneous/systray.png new file mode 100644 index 000000000..b77afc2df Binary files /dev/null and b/content/developer/howtos/discover_js_framework/04_miscellaneous/systray.png differ diff --git a/content/developer/howtos/discover_js_framework/05_custom_kanban_view.rst b/content/developer/howtos/discover_js_framework/05_custom_kanban_view.rst new file mode 100644 index 000000000..6722e1e6a --- /dev/null +++ b/content/developer/howtos/discover_js_framework/05_custom_kanban_view.rst @@ -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 `_. + +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 ` 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% diff --git a/content/developer/howtos/discover_js_framework/05_custom_kanban_view/active_customer.png b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/active_customer.png new file mode 100644 index 000000000..3c5258b2a Binary files /dev/null and b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/active_customer.png differ diff --git a/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_data.png b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_data.png new file mode 100644 index 000000000..cf9393e15 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_data.png differ diff --git a/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_filter.png b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_filter.png new file mode 100644 index 000000000..a1999fef1 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_filter.png differ diff --git a/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_list.png b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_list.png new file mode 100644 index 000000000..5e56dee5b Binary files /dev/null and b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_list.png differ diff --git a/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_pager.png b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_pager.png new file mode 100644 index 000000000..1679b1b5d Binary files /dev/null and b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_pager.png differ diff --git a/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_search.png b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_search.png new file mode 100644 index 000000000..4420ff68f Binary files /dev/null and b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/customer_search.png differ diff --git a/content/developer/howtos/discover_js_framework/05_custom_kanban_view/overview.png b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/overview.png new file mode 100644 index 000000000..01ed8f864 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/05_custom_kanban_view/overview.png differ diff --git a/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch.rst b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch.rst new file mode 100644 index 000000000..43fb6785f --- /dev/null +++ b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch.rst @@ -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 + + + +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 `_. + +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 + + + + #. 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>`_ diff --git a/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/gallery_data.png b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/gallery_data.png new file mode 100644 index 000000000..bae28fc1a Binary files /dev/null and b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/gallery_data.png differ diff --git a/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/image_tooltip.png b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/image_tooltip.png new file mode 100644 index 000000000..2c06a7d51 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/image_tooltip.png differ diff --git a/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/layout.png b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/layout.png new file mode 100644 index 000000000..9acf1fb92 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/layout.png differ diff --git a/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/new_view.png b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/new_view.png new file mode 100644 index 000000000..6995b64a5 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/new_view.png differ diff --git a/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/overview.png b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/overview.png new file mode 100644 index 000000000..cacc0b44b Binary files /dev/null and b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/overview.png differ diff --git a/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/pagination.png b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/pagination.png new file mode 100644 index 000000000..8b3576163 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/pagination.png differ diff --git a/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/tshirt_images.png b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/tshirt_images.png new file mode 100644 index 000000000..979ec9745 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/tshirt_images.png differ diff --git a/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/view_button.png b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/view_button.png new file mode 100644 index 000000000..eba361a71 Binary files /dev/null and b/content/developer/howtos/discover_js_framework/06_creating_view_from_scratch/view_button.png differ diff --git a/content/developer/howtos/discover_js_framework/07_testing.rst b/content/developer/howtos/discover_js_framework/07_testing.rst new file mode 100644 index 000000000..4ade6f91f --- /dev/null +++ b/content/developer/howtos/discover_js_framework/07_testing.rst @@ -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 `_. + +1. Integration testing +====================== + +To make sure our application works as expected, we can perform :ref:`integration testing +` by creating a tour: this is a sequence of steps that we +can execute. Each step wait until some desired DOM state is reached, then performs an action. If, at +some point, it is unable to go to the next step for a long time, the tour fails. + +Let's write a tour to ensure that it is possible to perform an tshirt order from our public route + +.. exercise:: + + #. In the `awesome_tshirt` addon, add a :file:`/static/tests/tours` folder. + #. Add a :file:`/static/tests/tours/order_flow.js` file. + #. Add a tour that performs the following steps: + + #. Open the `/awesome_tshirt/order` route. + #. Fill the order form. + #. Validate it. + #. Navigate to our webclient. + #. Open the list view for the the t-shirt order. + #. Check that our order can be found in the list. + + #. Run the tour manually. + #. Add a Python test to run it programmatically. + #. Run the tour from the terminal. + +2. Unit testing a Component +=========================== + +It is also useful to test independently a component or a piece of code. :ref:`QUnit +` tests are useful to quickly locate an issue. + +.. exercise:: + + #. In the `awesome_tshirt` addon, add a :file:`static/tests/counter_tests.js` file. + #. Add a QUnit test that instantiates a counter, clicks on it, and makes sure it is incremented. + + .. image:: 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>`_ diff --git a/content/developer/howtos/discover_js_framework/07_testing/component_test.png b/content/developer/howtos/discover_js_framework/07_testing/component_test.png new file mode 100644 index 000000000..3f7233dcc Binary files /dev/null and b/content/developer/howtos/discover_js_framework/07_testing/component_test.png differ diff --git a/content/developer/howtos/discover_js_framework/07_testing/view_test.png b/content/developer/howtos/discover_js_framework/07_testing/view_test.png new file mode 100644 index 000000000..fb33175ce Binary files /dev/null and b/content/developer/howtos/discover_js_framework/07_testing/view_test.png differ diff --git a/content/developer/reference/backend/testing.rst b/content/developer/reference/backend/testing.rst index bb1678993..f74ea1f82 100644 --- a/content/developer/reference/backend/testing.rst +++ b/content/developer/reference/backend/testing.rst @@ -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 =================== diff --git a/content/developer/reference/frontend/qweb.rst b/content/developer/reference/frontend/qweb.rst index 706d94a00..49e80886f 100644 --- a/content/developer/reference/frontend/qweb.rst +++ b/content/developer/reference/frontend/qweb.rst @@ -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 ''''''''''''''''''''