[IMP] Rework the JavaScript tutorials
This commit rework the entire JavaScript tutorial series with the following improvements: - Each chapter is now designed to be independent, allowing learners to follow any chapter without the necessity of completing previous ones. - Each chapter has been transformed into a standalone module, enabling learners to create small projects from scratch within each chapter. - The screenshots/text/code have been updated for Odoo 17. Currently we have 5 chapters: - Discover Chapter 1 - Owl Components: This chapter allows to learn the Owl framework in the `awesome_owl` module. - Discover Chapter 2 - Build a dashboard: This chapter allows to grasp the basic of the web framework by building a dashboard in the `awesome_dashboard` module. - Master Chapter 1 - Build a clicker game: This chapter allows to grasp the web framework by building a clicker game in the `awesome_clicker` module. - Master Chapter 2 - Create a gallery view: This chapter allows to learn how to create a new view type. The new view is a gallery of records pictures. It can be done in the `awesome_gallery` module. - Master Chapter 3 - Customize a kanban view: This chapter allows to learn to customize a kanban view by implementing a list of customer in the side of a kanban view. This can be done in the `awesome_kanban` module. The chapter on creating and customizing fields is deleted for now and will be completely rewritten in a near future. The chapter on testing is deleted, how-to guides will be written to cover this subject. The solutions for all exercises has been done for v17, the goal by merging the new tutorial is to have this new branch structure in `odoo/tutorials`: - 16.0 - 16.0-solutions - 17.0 - 17.0-discover-js-framework-solutions - 17.0-master-odoo-web-framework-solutions - master <-- default branch, starting point for all addons - master-discover-js-framework-solutions - master-master-odoo-web-framework-solutions closes odoo/documentation#6876 Task-id: 3623595 Signed-off-by: Géry Debongnie <ged@odoo.com>
@ -1,4 +1,6 @@
|
||||
|
||||
.. _howtos/javascript_client_action:
|
||||
|
||||
======================
|
||||
Create a client action
|
||||
======================
|
||||
|
@ -1,7 +1,7 @@
|
||||
.. _frontend/components:
|
||||
|
||||
==============
|
||||
Owl Components
|
||||
Owl components
|
||||
==============
|
||||
|
||||
The Odoo Javascript framework uses a custom component framework called Owl. It
|
||||
|
@ -97,6 +97,8 @@ Getters and setters are supported too:
|
||||
},
|
||||
});
|
||||
|
||||
.. _frontend/patching_class:
|
||||
|
||||
Patching a javascript class
|
||||
===========================
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
:show-content:
|
||||
|
||||
=========================
|
||||
Discover the JS Framework
|
||||
Discover the JS framework
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
@ -10,24 +10,27 @@ Discover the JS Framework
|
||||
|
||||
discover_js_framework/*
|
||||
|
||||
This tutorial is designed to introduce you to the basics of the Odoo Javascript framework. Whether
|
||||
This two parts tutorial is designed to introduce you to the basics of the Odoo Javascript framework. Whether
|
||||
you are new to the framework or have some prior experience, this tutorial will provide you with a
|
||||
solid foundation for using the Odoo JavaScript framework in your projects.
|
||||
|
||||
This tutorial is divided into two parts. The first part covers the basics of Owl components, which
|
||||
The first part covers the basics of Owl components, which
|
||||
are a key part of the Odoo JS framework. Owl components are reusable UI components that can be used
|
||||
to build complex web interfaces quickly and efficiently. We will explore how to create and use Owl
|
||||
components in Odoo.
|
||||
|
||||
The second part of the tutorial focuses on creating a dashboard using various features of Odoo.
|
||||
Dashboards are an essential part of any web application, and provide a nice starting point to use
|
||||
and interact with the Odoo codebase.
|
||||
components in Odoo. Then, in the second part of this tutorial, we focus on creating a dashboard using various
|
||||
features of Odoo. Dashboards are an essential part of any web application, and provide a nice starting
|
||||
point to use and interact with the Odoo codebase.
|
||||
|
||||
This tutorial assumes that you have some basic knowledge of development with Odoo in general
|
||||
(models, controllers, QWeb, ...). If you are new to Odoo, we recommend that you start with the
|
||||
:doc:`Getting started </developer/tutorials/getting_started>` tutorial before proceeding with this
|
||||
one.
|
||||
|
||||
.. note::
|
||||
|
||||
Each chapter of this tutorial is an independant project. If you feel comfortable with Owl, you can
|
||||
start directly with chapter 2.
|
||||
|
||||
.. _tutorials/discover_js_framework/setup:
|
||||
|
||||
Setup
|
||||
@ -35,11 +38,12 @@ Setup
|
||||
|
||||
#. Clone the `official Odoo tutorials repository <https://github.com/odoo/tutorials>`_ and switch to
|
||||
the branch `{CURRENT_MAJOR_BRANCH}`.
|
||||
#. Add the cloned repository to the :option:`--addons-path <odoo-bin --addons-path>`.
|
||||
#. Start a new Odoo database and install the modules `owl_playground` and `awesome_tshirt`.
|
||||
#. Add the cloned repository to your :option:`--addons-path <odoo-bin --addons-path>`.
|
||||
#. Start a new Odoo database and install the modules `awesome_owl` (for chapter 1) and `awesome_dashboard`
|
||||
(for chapter 2).
|
||||
|
||||
Content
|
||||
=======
|
||||
|
||||
- :doc:`discover_js_framework/01_owl_components`
|
||||
- :doc:`discover_js_framework/02_web_framework`
|
||||
- :doc:`discover_js_framework/02_build_a_dashboard`
|
||||
|
@ -1,5 +1,5 @@
|
||||
=========================
|
||||
Chapter 1: Owl Components
|
||||
Chapter 1: Owl components
|
||||
=========================
|
||||
|
||||
This chapter introduces the `Owl framework <https://github.com/odoo/owl>`_, a tailor-made component
|
||||
@ -10,27 +10,28 @@ In Owl, every part of user interface is managed by a component: they hold the lo
|
||||
templates that are used to render the user interface. In practice, a component is represented by a
|
||||
small JavaScript class subclassing the `Component` class.
|
||||
|
||||
Before getting into the exercises, make sure you have followed all the steps described in this
|
||||
To get started, you need a running Odoo server and a development environment setup. Before getting
|
||||
into the exercises, make sure you have followed all the steps described in this
|
||||
:ref:`tutorial introduction <tutorials/discover_js_framework/setup>`.
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the `official Odoo tutorials
|
||||
repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/owl_playground>`_. It
|
||||
is recommended to try to solve them first without looking at the solution!
|
||||
|
||||
.. tip::
|
||||
If you use Chrome as your web browser, you can install the `Owl Devtools` extension. This
|
||||
extension provides many features to help you understand and profile any Owl application.
|
||||
|
||||
`Video: How to use the DevTools <https://www.youtube.com/watch?v=IUyQjwnrpzM>`_
|
||||
|
||||
In this chapter, we use the `owl_playground` addon, which provides a simplified environment that
|
||||
In this chapter, we use the `awesome_owl` addon, which provides a simplified environment that
|
||||
only contains Owl and a few other files. The goal is to learn Owl itself, without relying on Odoo
|
||||
web client code. To get started, open the `/owl_playground/playground` route with your browser: it
|
||||
web client code. To get started, open the `/awesome_owl` route with your browser: it
|
||||
should display an Owl component with the text *hello world*.
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the `official Odoo tutorials
|
||||
repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-discover-js-framework-solutions/awesome_owl>`_. It
|
||||
is recommended to try to solve them first without looking at the solution!
|
||||
|
||||
Example: a `Counter` component
|
||||
==============================
|
||||
|
||||
@ -54,8 +55,8 @@ button.
|
||||
}
|
||||
}
|
||||
|
||||
The `Counter` component specifies the name of the template to render. The template is written in XML
|
||||
and defines a part of user interface:
|
||||
The `Counter` component specifies the name of a template that represents its html. It is written in XML
|
||||
using the QWeb language:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
@ -70,11 +71,13 @@ and defines a part of user interface:
|
||||
1. Displaying a counter
|
||||
=======================
|
||||
|
||||
As a first exercise, let us implement a counter in the `Playground` component located in
|
||||
:file:`owl_playground/static/src/`. To see the result, you can go to the `/owl_playground/playground`
|
||||
route with your browser.
|
||||
.. image:: 01_owl_components/counter.png
|
||||
:align: center
|
||||
|
||||
As a first exercise, let us modify the `Playground` component located in
|
||||
:file:`awesome_owl/static/src/` to turn it into a counter. To see the result, you can go to the
|
||||
`/awesome_owl` route with your browser.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Modify :file:`playground.js` so that it acts as a counter like in the example above. You will
|
||||
need to use the `useState hook
|
||||
@ -87,112 +90,226 @@ route with your browser.
|
||||
<{OWL_PATH}/doc/reference/event_handling.md#event-handling>`_ attribute in the button to
|
||||
trigger the `increment` method whenever the button is clicked.
|
||||
|
||||
.. image:: 01_owl_components/counter.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
.. tip::
|
||||
The Odoo JavaScript files downloaded by the browser are minified. For debugging purpose, it's
|
||||
easier when the files are not minified. Switch to
|
||||
:ref:`debug mode with assets <developer-mode/url>` so that the files are not minified.
|
||||
|
||||
2. Extract counter in a component
|
||||
=================================
|
||||
This exercise showcases an important feature of Owl: the `reactivity system <{OWL_PATH}/doc/reference/reactivity.md>`_.
|
||||
The `useState` function wraps a value in a proxy so Owl can keep track of which component
|
||||
needs which part of the state, so it can be updated whenever a value has been changed. Try
|
||||
removing the `useState` function and see what happens.
|
||||
|
||||
For now we have the logic of a counter in the `Playground` component, let us see how to create a
|
||||
`sub-component <{OWL_PATH}/doc/reference/component.md#sub-components>`_ from it.
|
||||
2. Extract `Counter` in a sub component
|
||||
=======================================
|
||||
|
||||
.. exercise::
|
||||
For now we have the logic of a counter in the `Playground` component, but it is not reusable. Let us
|
||||
see how to create a `sub-component <{OWL_PATH}/doc/reference/component.md#sub-components>`_ from it:
|
||||
|
||||
#. Extract the counter code from the `Playground` component into a new `Counter` component.
|
||||
#. You can do it in the same file first, but once it's done, update your code to move the
|
||||
`Counter` in its own folder and file. Import it relatively from `./counter/counter`. Make sure
|
||||
the template is in its own file, with the same name.
|
||||
#. Add two counters in your playground.
|
||||
|
||||
.. image:: 01_owl_components/double_counter.png
|
||||
:align: center
|
||||
|
||||
.. tip::
|
||||
By convention, most components code, template and css should have the same snake-cased name
|
||||
as the component. For example, if we have a `TodoList` component, its code should be in
|
||||
`todo_list.js`, `todo_list.xml` and if necessary, `todo_list.scss`
|
||||
|
||||
.. important::
|
||||
Don't forget :code:`/** @odoo-module **/` in your JavaScript files. More information on this can
|
||||
be found :ref:`here <frontend/modules/native_js>`.
|
||||
|
||||
3. A todo component
|
||||
===================
|
||||
.. _tutorials/discover_js_framework/simple_card:
|
||||
|
||||
We will create new components in :file:`owl_playground/static/src/` to keep track of a list of
|
||||
todos. This will be done incrementally in multiple exercises that will introduce various concepts.
|
||||
3. A simple `Card` component
|
||||
============================
|
||||
|
||||
.. exercise::
|
||||
Components are really the most natural way to divide a complicated user interface into multiple
|
||||
reusable pieces. But to make them truly useful, it is necessary to be able to communicate
|
||||
some information between them. Let us see how a parent component can provide information to a
|
||||
sub component by using attributes (most commonly known as `props <{OWL_PATH}/doc/reference/props.md>`_).
|
||||
|
||||
#. Create a `Todo` component that receive a `todo` object in `props
|
||||
<{OWL_PATH}/doc/reference/props.md>`_, and display it. It should show something like
|
||||
**3. buy milk**.
|
||||
#. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on the task if it is
|
||||
done. To do that, you can use `dynamic attributes
|
||||
<{OWL_PATH}/doc/reference/templates.md#dynamic-attributes>`_.
|
||||
#. Modify :file:`owl_playground/static/src/playground.js` and
|
||||
:file:`owl_playground/static/src/playground.xml` to display your new `Todo` component with
|
||||
some hard-coded props to test it first.
|
||||
The goal of this exercise is to create a `Card` component, that takes two props: `title` and `content`.
|
||||
For example, here is how it could be used:
|
||||
|
||||
.. example::
|
||||
.. code-block:: xml
|
||||
|
||||
.. code-block:: javascript
|
||||
<Card title="'my title'" content="'some content'"/>
|
||||
|
||||
setup() {
|
||||
...
|
||||
this.todo = { id: 3, description: "buy milk", done: false };
|
||||
}
|
||||
The above example should produce some html using bootstrap that look like this:
|
||||
|
||||
.. image:: 01_owl_components/todo.png
|
||||
:scale: 70%
|
||||
.. code-block:: html
|
||||
|
||||
<div class="card d-inline-block m-2" style="width: 18rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">my title</h5>
|
||||
<p class="card-text">
|
||||
some content
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#. Create a `Card` component
|
||||
#. Import it in `Playground` and display a few cards in its template
|
||||
|
||||
.. image:: 01_owl_components/simple_card.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`Owl: Dynamic class attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-class-attribute>`_
|
||||
4. Using `markup` to display html
|
||||
=================================
|
||||
|
||||
4. Props validation
|
||||
If you used `t-esc` in the previous exercise, then you may have noticed that Owl will automatically escape
|
||||
its content. For example, if you try to display some html like this: `<Card title="'my title'" content="this.html"/>`
|
||||
with `this.html = "<div>some content</div>""`,
|
||||
the resulting output will simply display the html as a string.
|
||||
|
||||
In this case, since the `Card` component may be used to display any kind of content, it makes sense
|
||||
to allow the user to display some html. This is done with the
|
||||
`t-out directive <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_.
|
||||
|
||||
However, displaying arbitrary content as html is dangerous, it could be used to inject malicious code, so
|
||||
by default, Owl will always escape a string unless it has been explicitely marked as safe with the `markup`
|
||||
function.
|
||||
|
||||
#. Update `Card` to use `t-out`
|
||||
#. Update `Playground` to import `markup`, and use it on some html values
|
||||
#. Make sure that you see that normal strings are always escaped, unlike markuped strings.
|
||||
|
||||
.. note::
|
||||
|
||||
The `t-esc` directive can still be used in Owl templates. It is slightly faster than `t-out`.
|
||||
|
||||
.. image:: 01_owl_components/markup.png
|
||||
:align: center
|
||||
|
||||
5. Props validation
|
||||
===================
|
||||
|
||||
The `Todo` component has an implicit API. It expects to receive in its props the description of a
|
||||
todo object in a specified format: `id`, `description` and `done`. Let us make that API more
|
||||
The `Card` component has an implicit API. It expects to receive two strings in its props: the `title`
|
||||
and the `content`. Let us make that API more
|
||||
explicit. We can add a props definition that will let Owl perform a validation step in `dev mode
|
||||
<{OWL_PATH}/doc/reference/app.md#dev-mode>`_. You can activate the dev mode in the `App
|
||||
configuration <{OWL_PATH}/doc/reference/app.md#configuration>`_.
|
||||
configuration <{OWL_PATH}/doc/reference/app.md#configuration>`_ (but it is activated by default
|
||||
on the `awesome_owl` playground).
|
||||
|
||||
It is a good practice to do props validation for every component.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Add `props validation <{OWL_PATH}/doc/reference/props.md#props-validation>`_ to the `Todo`
|
||||
#. Add `props validation <{OWL_PATH}/doc/reference/props.md#props-validation>`_ to the `Card`
|
||||
component.
|
||||
#. Open the :guilabel:`Console` tab of your browser's dev tools and make sure the props
|
||||
validation passes in dev mode, which is activated by default in `owl_playground`. The dev mode
|
||||
can be activated and deactivated by modifying the `dev` attribute in the in the `config`
|
||||
parameter of the `mount <{OWL_PATH}/doc/reference/app.md#mount-helper>`_ function in
|
||||
:file:`owl_playground/static/src/main.js`.
|
||||
#. Remove `done` from the props and reload the page. The validation should fail.
|
||||
#. Rename the `title` props into something else in the playground template, then check in the
|
||||
:guilabel:`Console` tab of your browser's dev tools that you can see an error.
|
||||
|
||||
5. A list of todos
|
||||
==================
|
||||
6. The sum of two `Counter`
|
||||
===========================
|
||||
|
||||
Now, let us display a list of todos instead of just one todo. For now, we can still hard-code the
|
||||
list.
|
||||
We saw in a previous exercise that `props` can be used to provide information from a parent
|
||||
to a child component. Now, let us see how we can communicate information in the opposite
|
||||
direction: in this exercise, we want to display two `Counter` components, and below them, the sum of
|
||||
their values. So, the parent component (`Playground`) need to be informed whenever one of
|
||||
the `Counter` value is changed.
|
||||
|
||||
.. exercise::
|
||||
This can be done by using a `callback prop <{OWL_PATH}/doc/reference/props.md#binding-function-props>`_:
|
||||
a prop that is a function meant to be called back. The child component can choose to call
|
||||
that function with any argument. In our case, we will simply add an optional `onChange` prop that will
|
||||
be called whenever the `Counter` component is incremented.
|
||||
|
||||
#. Change the code to display a list of todos instead of just one. Create a new `TodoList`
|
||||
component to hold the `Todo` components and use `t-foreach
|
||||
<{OWL_PATH}/doc/reference/templates.md#loops>`_ in its template.
|
||||
#. Think about how it should be keyed with the `t-key` directive.
|
||||
#. Add prop validation to the `Counter` component: it should accept an optional `onChange`
|
||||
function prop.
|
||||
#. Update the `Counter` component to call the `onChange` prop (if it exists) whenever it
|
||||
is incremented.
|
||||
#. Modify the `Playground` component to maintain a local state value (`sum`), initially
|
||||
set to 2, and display it in its template
|
||||
#. Implement an `incrementSum` method in `Playground`
|
||||
#. Give that method as a prop to two (or more!) sub `Counter` components.
|
||||
|
||||
.. image:: 01_owl_components/todo_list.png
|
||||
:scale: 70%
|
||||
.. image:: 01_owl_components/sum_counter.png
|
||||
:align: center
|
||||
|
||||
6. Adding a todo
|
||||
.. important::
|
||||
|
||||
There is a subtlety with callback props: they usually should be defined with the `.bind`
|
||||
suffix. See the `documentation <{OWL_PATH}/doc/reference/props.md#binding-function-props>`_
|
||||
|
||||
7. A todo list
|
||||
==============
|
||||
|
||||
Let us now discover various features of Owl by creating a todo list. We need two components: a
|
||||
`TodoList` component that will display a list of `TodoItem` components. The list of todos is a
|
||||
state that should be maintained by the `TodoList`.
|
||||
|
||||
For this tutorial, a `todo` is an object that contains three values: an `id` (number), a `description`
|
||||
(string) and a flag `isCompleted` (boolean):
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
{ id: 3, description: "buy milk", isCompleted: false }
|
||||
|
||||
#. Create a `TodoList` and a `TodoItem` components
|
||||
#. The `TodoItem` component should receive a `todo` as a prop, and display its `id` and `description` in a `div`.
|
||||
#. For now, hardcode the list of todos:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
// in TodoList
|
||||
this.todos = useState([{ id: 3, description: "buy milk", isCompleted: false }]);
|
||||
|
||||
#. Use `t-foreach <{OWL_PATH}/doc/reference/templates.md#loops>`_ to display each todo in a `TodoItem`
|
||||
#. Display a `TodoList` in the playground
|
||||
#. Add props validation to `TodoItem`
|
||||
|
||||
.. image:: 01_owl_components/todo_list.png
|
||||
:align: center
|
||||
|
||||
Note that the `t-foreach` directive is not exactly the same in Owl as the QWeb python implementation: it
|
||||
requires a `t-key` unique value, so Owl can properly reconciliate each element.
|
||||
|
||||
.. tip::
|
||||
|
||||
Since the `TodoList` and `TodoItem` components are so tightly coupled, it makes
|
||||
sense to put them in the same folder
|
||||
|
||||
8. Use dynamic attributes
|
||||
=========================
|
||||
|
||||
For now, the `TodoItem` component does not visually show if the `todo` is completed. Let us do that by
|
||||
using a `dynamic attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-attributes>`_.
|
||||
|
||||
#. Add the Bootstrap classes `text-muted` and `text-decoration-line-through` on the `TodoItem` root element
|
||||
if it is completed.
|
||||
#. Change the hardcoded `todo` value to check that it is properly displayed.
|
||||
|
||||
Even though the directive is named `t-att` (for attribute), it can be used to set a `class` value (and
|
||||
html properties such as the `value` of an input).
|
||||
|
||||
.. image:: 01_owl_components/muted_todo.png
|
||||
:align: center
|
||||
|
||||
.. tip::
|
||||
|
||||
Owl let you combine static class values with dynamic values. The following example will work as expected:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<div class="a" t-att-class="someExpression"/>
|
||||
|
||||
See also: `Owl: Dynamic class attributes <{OWL_PATH}/doc/reference/templates.md#dynamic-class-attribute>`_
|
||||
|
||||
9. Adding a todo
|
||||
================
|
||||
|
||||
So far, the todos in our list are hard-coded. Let us make it more useful by allowing the user to add
|
||||
a todo to the list.
|
||||
|
||||
.. exercise::
|
||||
#. Remove the hardcoded values in the `TodoList` component
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
this.todos = useState([]);
|
||||
|
||||
#. Add an input above the task list with placeholder *Enter a new task*.
|
||||
#. Add an `event handler <{OWL_PATH}/doc/reference/event_handling.md>`_ on the `keyup` event
|
||||
@ -201,75 +318,167 @@ a todo to the list.
|
||||
case, create a new todo with the current content of the input as the description and clear the
|
||||
input of all content.
|
||||
#. Make sure the todo has a unique id. It can be just a counter that increments at each todo.
|
||||
#. Wrap the todo list in a `useState` hook to let Owl know that it should update the UI when the
|
||||
list is modified.
|
||||
#. Bonus point: don't do anything if the input is empty.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
this.todos = useState([]);
|
||||
|
||||
.. image:: 01_owl_components/create_todo.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`Owl: Reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_
|
||||
|
||||
7. Focusing the input
|
||||
=====================
|
||||
Theory: Component lifecycle and hooks
|
||||
=====================================
|
||||
|
||||
So far, we have seen one example of a hook function: `useState`. A `hook <{OWL_PATH}/doc/reference/hooks.md>`_
|
||||
is a special function that *hook into* the internals of the component. In the case of
|
||||
`useState`, it generates a proxy object linked to the current component. This is why
|
||||
hook functions have to be called in the `setup` method, and no later!
|
||||
|
||||
|
||||
.. flowchart LR
|
||||
|
||||
.. classDef hook fill:#ccf
|
||||
|
||||
.. subgraph "creation"
|
||||
.. direction TB
|
||||
.. A:::hook
|
||||
.. B:::hook
|
||||
.. M:::hook
|
||||
.. A[setup]-->B
|
||||
.. B[onWillStart] --> C(render)
|
||||
.. C --> D("mount (in DOM)")
|
||||
.. D --> M[onMounted]
|
||||
.. end
|
||||
|
||||
.. subgraph updates
|
||||
.. direction TB
|
||||
.. E:::hook
|
||||
.. F:::hook
|
||||
.. H:::hook
|
||||
.. E["(onWillUpdateProps)"] --> L(render)
|
||||
.. L --> F[onWillPatch]
|
||||
.. F --> G(patch DOM)
|
||||
.. G --> H[onPatched]
|
||||
.. end
|
||||
|
||||
.. subgraph destruction
|
||||
.. direction TB
|
||||
.. I:::hook
|
||||
.. J:::hook
|
||||
.. I[onWillUnmount] --> J[onWillDestroy]
|
||||
.. J --> N(removed from DOM)
|
||||
|
||||
.. end
|
||||
|
||||
.. creation --> updates
|
||||
.. updates --> destruction
|
||||
|
||||
|
||||
.. figure:: 01_owl_components/component_lifecycle.svg
|
||||
:align: center
|
||||
:width: 50%
|
||||
|
||||
|
||||
An Owl component goes through a lot of phases: it can be instantiated, rendered,
|
||||
mounted, updated, detached, destroyed, ... This is the `component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_.
|
||||
The figure above show the most important events in the life of a component (hooks are shown in purple).
|
||||
Roughly speaking, a component is created, then updated (potentially many times), then is destroyed.
|
||||
|
||||
Owl provides a variety of built-in `hooks functions <{OWL_PATH}/doc/reference/hooks.md>`_. All of them have to be called in
|
||||
the `setup` function. For example, if you want to execute some code when your component is mounted, you can use the `onMounted`
|
||||
hook:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
// do something here
|
||||
});
|
||||
}
|
||||
|
||||
.. tip::
|
||||
|
||||
All hook functions start with `use` or `on`. For example: `useState` or `onMounted`.
|
||||
|
||||
|
||||
10. Focusing the input
|
||||
======================
|
||||
|
||||
Let's see how we can access the DOM with `t-ref <{OWL_PATH}/doc/reference/refs.md>`_ and `useRef
|
||||
<{OWL_PATH}/doc/reference/hooks.md#useref>`_.
|
||||
<{OWL_PATH}/doc/reference/hooks.md#useref>`_. The main idea is that you need to mark
|
||||
the target element in the component template with a `t-ref`:
|
||||
|
||||
.. exercise::
|
||||
.. code-block:: xml
|
||||
|
||||
#. Focus the `input` from the previous exercise when the dashboard is `mounted
|
||||
<{OWL_PATH}/doc/reference/component.md#mounted>`_. This this should be done from the
|
||||
`TodoList` component.
|
||||
<div t-ref="some_name">hello</div>
|
||||
|
||||
Then you can access it in the JS with the `useRef hook <{OWL_PATH}/doc/reference/hooks.md#useref>`_.
|
||||
However, there is a problem if you think about it: the actual html element for a
|
||||
component does not exist when the component is created. It only exists when the
|
||||
component is mounted. But hooks have to be called in the `setup` method. So, `useRef`
|
||||
return an object that contains a `el` (for element) key that is only defined when the
|
||||
component is mounted.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
setup() {
|
||||
this.myRef = useRef('some_name');
|
||||
onMounted(() => {
|
||||
console.log(this.myRef.el);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#. Focus the `input` from the previous exercise. This this should be done from the
|
||||
`TodoList` component (note that there is a `focus` method on the input html element).
|
||||
#. Bonus point: extract the code into a specialized `hook <{OWL_PATH}/doc/reference/hooks.md>`_
|
||||
`useAutofocus` in a new :file:`owl_playground/utils.js` file.
|
||||
`useAutofocus` in a new :file:`awesome_owl/utils.js` file.
|
||||
|
||||
.. seealso::
|
||||
`Owl: Component lifecycle <{OWL_PATH}/doc/reference/component.md#lifecycle>`_
|
||||
.. image:: 01_owl_components/autofocus.png
|
||||
:align: center
|
||||
|
||||
8. Toggling todos
|
||||
=================
|
||||
.. tip::
|
||||
|
||||
Refs are usually suffixed by `Ref` to make it obvious that they are special objects:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
this.inputRef = useRef('refname');
|
||||
|
||||
11. Toggling todos
|
||||
==================
|
||||
|
||||
Now, let's add a new feature: mark a todo as completed. This is actually trickier than one might
|
||||
think. The owner of the state is not the same as the component that displays it. So, the `Todo`
|
||||
think. The owner of the state is not the same as the component that displays it. So, the `TodoItem`
|
||||
component needs to communicate to its parent that the todo state needs to be toggled. One classic
|
||||
way to do this is by using a `callback prop
|
||||
<{OWL_PATH}/doc/reference/props.md#binding-function-props>`_ `toggleState`.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Add an input with the attribute :code:`type="checkbox"` before the id of the task, which must
|
||||
be checked if the state `done` is true.
|
||||
be checked if the state `isCompleted` is true.
|
||||
|
||||
.. tip::
|
||||
QWeb does not create attributes computed with the `t-att` directive if it evaluates to a
|
||||
Owl does not create attributes computed with the `t-att` directive if it evaluates to a
|
||||
falsy value.
|
||||
|
||||
#. Add a callback props `toggleState`.
|
||||
#. Add a `click` event handler on the input in the `Todo` component and make sure it calls the
|
||||
#. Add a callback props `toggleState` to `TodoItem`.
|
||||
#. Add a `click` event handler on the input in the `TodoItem` component and make sure it calls the
|
||||
`toggleState` function with the todo id.
|
||||
#. Make it work!
|
||||
|
||||
.. image:: 01_owl_components/toggle_todo.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
9. Deleting todos
|
||||
=================
|
||||
12. Deleting todos
|
||||
==================
|
||||
|
||||
The final touch is to let the user delete a todo.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Add a new callback prop `removeTodo`.
|
||||
#. Insert :code:`<span class="fa fa-remove"/>` in the template of the `Todo` component.
|
||||
#. Add a new callback prop `removeTodo` in `TodoItem`.
|
||||
#. Insert :code:`<span class="fa fa-remove"/>` in the template of the `TodoItem` component.
|
||||
#. Whenever the user clicks on it, it should call the `removeTodo` method.
|
||||
#. Make it work!
|
||||
|
||||
.. tip::
|
||||
If you're using an array to store your todo list, you can use the JavaScript `splice`
|
||||
@ -285,61 +494,45 @@ The final touch is to let the user delete a todo.
|
||||
}
|
||||
|
||||
.. image:: 01_owl_components/delete_todo.png
|
||||
:scale: 70%
|
||||
:align: center
|
||||
|
||||
.. _tutorials/discover_js_framework/generic_card:
|
||||
|
||||
10. Generic card with slots
|
||||
===========================
|
||||
13. Generic `Card` with slots
|
||||
=============================
|
||||
|
||||
Owl has a powerful `slot <{OWL_PATH}/doc/reference/slots.md>`_ system to allow you to write generic
|
||||
components. This is useful to factorize the common layout between different parts of the interface.
|
||||
In a :ref:`previous exercise <tutorials/discover_js_framework/simple_card>`, we built
|
||||
a simple `Card` component. But it is honestly quite limited. What if we want
|
||||
to display some arbitrary content inside a card, such as a sub component? Well,
|
||||
it does not work, since the content of the card is described by a string. It would
|
||||
however be very convenient if we could describe the content as a piece of template.
|
||||
|
||||
.. exercise::
|
||||
This is exactly what Owl `slot <{OWL_PATH}/doc/reference/slots.md>`_ system is designed
|
||||
for: allowing to write generic components.
|
||||
|
||||
#. Insert a new `Card` component between the `Counter` and `Todolist` components. Use the
|
||||
following Bootstrap HTML structure for the card:
|
||||
Let us modify the `Card` component to use slots:
|
||||
|
||||
.. code-block:: html
|
||||
#. Remove the `content` prop
|
||||
#. Use the default slot to define the body
|
||||
#. Insert a few cards with arbitrary content, such as a `Counter` component
|
||||
#. (bonus) Add prop validation
|
||||
|
||||
<div class="card" style="width: 18rem;">
|
||||
<img src="..." class="card-img-top" alt="..." />
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Card title</h5>
|
||||
<p class="card-text">
|
||||
Some quick example text to build on the card title and make up the bulk
|
||||
of the card's content.
|
||||
</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#. This component should have two slots: one slot for the title, and one for the content (the
|
||||
default slot). It should be possible to use the `Card` component as follows:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<Card>
|
||||
<t t-set-slot="title">Card title</t>
|
||||
<p class="card-text">Some quick example text...</p>
|
||||
<a href="#" class="btn btn-primary">Go somewhere</a>
|
||||
</Card>
|
||||
|
||||
#. Bonus point: if the `title` slot is not given, the `h5` should not be rendered at all.
|
||||
|
||||
.. image:: 01_owl_components/card.png
|
||||
:scale: 70%
|
||||
.. image:: 01_owl_components/generic_card.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`Bootstrap: documentation on cards <https://getbootstrap.com/docs/5.2/components/card/>`_
|
||||
|
||||
11. Extensive props validation
|
||||
==============================
|
||||
14. Minimizing card content
|
||||
===========================
|
||||
|
||||
.. exercise::
|
||||
Finally, let's add a feature to the `Card` component, to make it more interesting: we
|
||||
want a button to toggle its content (show it or hide it)
|
||||
|
||||
#. Add prop validation on the `Card` component.
|
||||
#. Try to express in the props validation system that it requires a `default` slot, and an
|
||||
optional `title` slot.
|
||||
#. Add a state to the `Card` component to track if it is open (the default) or not
|
||||
#. Add a `t-if` in the template to conditionally render the content
|
||||
#. Add a button in the header, and modify the code to flip the state when the button is clicked
|
||||
|
||||
.. image:: 01_owl_components/toggle_card.png
|
||||
:scale: 90%
|
||||
:align: center
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 2.8 KiB |
@ -0,0 +1,457 @@
|
||||
============================
|
||||
Chapter 2: Build a dashboard
|
||||
============================
|
||||
|
||||
The first part of this tutorial introduced you to most of Owl ideas. It is now time to learn
|
||||
about the Odoo JavaScript framework in its entirety, as used by the web client.
|
||||
|
||||
.. graph TD
|
||||
.. subgraph "Owl"
|
||||
.. C[Component]
|
||||
.. T[Template]
|
||||
.. H[Hook]
|
||||
.. S[Slot]
|
||||
.. E[Event]
|
||||
.. end
|
||||
|
||||
.. odoo[Odoo JavaScript framework] --> Owl
|
||||
|
||||
.. figure:: 02_web_framework/previously_learned.svg
|
||||
:align: center
|
||||
:width: 50%
|
||||
|
||||
To get started, you need a running Odoo server and a development environment setup. Before getting
|
||||
into the exercises, make sure you have followed all the steps described in this
|
||||
:ref:`tutorial introduction <tutorials/discover_js_framework/setup>`. For this chapter, we will start
|
||||
from the empty dashboard provided by the `awesome_dashboard` addon. We will progressively add
|
||||
features to it, using the Odoo JavaScript framework.
|
||||
|
||||
.. admonition:: Goal
|
||||
|
||||
.. image:: 02_web_framework/overview_02.png
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the
|
||||
`official Odoo tutorials repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-discover-js-framework-solutions/awesome_dashboard>`_.
|
||||
|
||||
1. A new Layout
|
||||
===============
|
||||
|
||||
Most screens in the Odoo web client uses a common layout: a control panel on top, with some buttons,
|
||||
and a main content zone just below. This is done using the `Layout component
|
||||
<{GITHUB_PATH}/addons/web/static/src/search/layout.js>`_, available in `@web/search/layout`.
|
||||
|
||||
#. Update the `AwesomeDashboard` component located in :file:`awesome_dashboard/static/src/` to use the
|
||||
`Layout` component. You can use
|
||||
:code:`{controlPanel: {} }` for the `display` props of
|
||||
the `Layout` component.
|
||||
#. Add a `className` prop to `Layout`: `className="'o_dashboard h-100'"`
|
||||
#. Add a `dashboard.scss` file in which you set the background-color of `.o_dashboard` to gray (or your
|
||||
favorite color)
|
||||
|
||||
Open http://localhost:8069/web, then open the :guilabel:`Awesome Dashboard` app, and see the
|
||||
result.
|
||||
|
||||
.. image:: 02_web_framework/new_layout.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Example: use of Layout in client action
|
||||
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.js>`_ and
|
||||
`template <{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.xml>`_
|
||||
- `Example: use of Layout in kanban view
|
||||
<{GITHUB_PATH}/addons/web/static/src/views/kanban/kanban_controller.xml>`_
|
||||
|
||||
.. _tutorials/discover_js_framework/services:
|
||||
|
||||
Theory: Services
|
||||
================
|
||||
|
||||
In practice, every component (except the root component) may be destroyed at any time and replaced
|
||||
(or not) with another component. This means that each component internal state is not persistent.
|
||||
This is fine in many cases, but there certainly are situations where we want to keep some data around.
|
||||
For example, all discuss messages should not be reloaded every time we display a channel.
|
||||
|
||||
Also, it may happen that we need to write some code that is not a component. Maybe something that
|
||||
process all barcodes, or that manages the user configuration (context, ...).
|
||||
|
||||
The Odoo framework defines the idea of a :ref:`service <frontend/services>`, which is a persistent
|
||||
piece of code that exports state and/or functions. Each service can depend on other services, and
|
||||
components can import a service.
|
||||
|
||||
The following example registers a simple service that displays a notification every 5 seconds:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const myService = {
|
||||
dependencies: ["notification"],
|
||||
start(env, { notification }) {
|
||||
let counter = 1;
|
||||
setInterval(() => {
|
||||
notification.add(`Tick Tock ${counter++}`);
|
||||
}, 5000);
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("myService", myService);
|
||||
|
||||
Services can be accessed by any component. Imagine that we have a service to maintain some shared
|
||||
state:
|
||||
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const sharedStateService = {
|
||||
start(env) {
|
||||
let state = {};
|
||||
|
||||
return {
|
||||
getValue(key) {
|
||||
return state[key];
|
||||
},
|
||||
setValue(key, value) {
|
||||
state[key] = value;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("shared_state", sharedStateService);
|
||||
|
||||
Then, any component can do this:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
setup() {
|
||||
this.sharedState = useService("shared_state");
|
||||
const value = this.sharedState.getValue("somekey");
|
||||
// do something with value
|
||||
}
|
||||
|
||||
2. Add some buttons for quick navigation
|
||||
========================================
|
||||
|
||||
One important service provided by Odoo is the `action` service: it can execute
|
||||
all kind of standard actions defined by Odoo. For example, here is how one
|
||||
component could execute an action by its xml id:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
...
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
}
|
||||
openSettings() {
|
||||
this.action.doAction("base_setup.action_general_configuration");
|
||||
}
|
||||
...
|
||||
|
||||
Let us now add two buttons to our control panel:
|
||||
|
||||
#. A button `Customers`, which opens a kanban view with all customers (this action already
|
||||
exists, so you should use `its xml id
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/odoo/addons/base/views/res_partner_views.xml#L510>`_).
|
||||
|
||||
#. A button `Leads`, which opens a dynamic action on the `crm.lead` model with a list and a form view.
|
||||
|
||||
.. image:: 02_web_framework/navigation_buttons.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
- `Example: doAction use
|
||||
<{GITHUB_PATH}/addons/account/static/src/components/journal_dashboard_activity
|
||||
/journal_dashboard_activity.js#L35>`_
|
||||
- `Code: action service
|
||||
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/action_service.js>`_
|
||||
|
||||
3. Add a DashboardItem
|
||||
======================
|
||||
|
||||
Let us now improve our content.
|
||||
|
||||
#. Create a generic `DashboardItem` component that display its default slot in a nice card layout
|
||||
It should take an optional `size` number props, that default to `1`
|
||||
The width should be hardcoded to `(18*size)rem`.
|
||||
#. Add a few cards in the dashboard, with no size and a size of 2.
|
||||
|
||||
.. image:: 02_web_framework/dashboard_item.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
- `Owl slot system <{OWL_PATH}/doc/reference/slots.md>`_
|
||||
|
||||
4. Call the server, add some statistics
|
||||
=======================================
|
||||
|
||||
Let's improve the dashboard by adding a few dashboard items to display *real* business data.
|
||||
The *awesome_dashboard* addon provides a `/awesome_dashboard/statistics` route that is meant
|
||||
to return some interesting information.
|
||||
|
||||
To call a specific controller, we need to use the :ref:`rpc service <frontend/services/rpc>`.
|
||||
It only exports a single function that perform the request: :code:`rpc(route, params, settings)`.
|
||||
A basic request could look like this:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
onWillStart(async () => {
|
||||
const result = await this.rpc("/my/controller", {a: 1, b: 2});
|
||||
// ...
|
||||
});
|
||||
}
|
||||
|
||||
#. Update `Dashboard` so that it uses the `rpc` service.
|
||||
#. Call the statistics route `/awesome_dashboard/statistics` in the `onWillStart` hook.
|
||||
#. Display a few cards in the dashboard containing:
|
||||
|
||||
- Number of new orders this month
|
||||
- Total amount of new orders this month
|
||||
- Average amount of t-shirt by order this month
|
||||
- Number of cancelled orders this month
|
||||
- Average time for an order to go from 'new' to 'sent' or 'cancelled'
|
||||
|
||||
.. image:: 02_web_framework/statistics.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Code: rpc service <{GITHUB_PATH}/addons/web/static/src/core/network/rpc_service.js>`_
|
||||
- `Example: calling a route in onWillStart
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/lunch/static/src/views/search_model.js#L21>`_
|
||||
|
||||
5. Cache network calls, create a service
|
||||
========================================
|
||||
|
||||
If you open the :guilabel:`Network` tab of your browser's dev tools, you will see that the call to
|
||||
`/awesome_dashboard/statistics` is done every time the client action is displayed. This is because the
|
||||
`onWillStart` hook is called each time the `Dashboard` component is mounted. But in this case, we
|
||||
would prefer to do it only the first time, so we actually need to maintain some state outside of the
|
||||
`Dashboard` component. This is a nice use case for a service!
|
||||
|
||||
#. Register and import a new `awesome_dashboard.statistics` service.
|
||||
#. It should provide a function `loadStatistics` that, once called, performs the actual rpc, and
|
||||
always return the same information.
|
||||
#. Use the `memoize <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/utils/functions.js#L11>`_ utility function from
|
||||
`@web/core/utils/functions` that will allow caching the statistics.
|
||||
#. Use this service in the `Dashboard` component.
|
||||
#. Check that it works as expected
|
||||
|
||||
.. seealso::
|
||||
- `Example: simple service <{GITHUB_PATH}/addons/web/static/src/core/network/http_service.js>`_
|
||||
- `Example: service with a dependency
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/user_service.js>`_
|
||||
|
||||
6. Display a pie chart
|
||||
======================
|
||||
|
||||
Everyone likes charts (!), so let us add a pie chart in our dashboard. It will display the
|
||||
proportions of t-shirts sold for each size: S/M/L/XL/XXL.
|
||||
|
||||
For this exercise, we will use `Chart.js <https://www.chartjs.org/>`_. It is the chart library used
|
||||
by the graph view. However, it is not loaded by default, so we will need to either add it to our
|
||||
assets bundle, or lazy load it. Lazy loading is usually better since our users will not have to load
|
||||
the chartjs code every time if they don't need it.
|
||||
|
||||
#. Create a `PieChart` component
|
||||
#. In its `onWillStart` method, load chartjs, you can use the `loadJs
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/assets.js#L23>`_ function to load
|
||||
:file:`/web/static/lib/Chart/Chart.js`.
|
||||
#. Use the `PieChart` component in a `DashboardItem` to display a `pie chart
|
||||
<https://www.chartjs.org/docs/2.8.0/charts/doughnut.html>`_ that shows the
|
||||
correct quantity for each sold t-shirts in each size (that information is available in the
|
||||
statistics route). Note that you can use the `size` property to make it look larger
|
||||
#. The `PieChart` component will need to render a canvas, and draw on it using `chart.js`.
|
||||
#. Make it work!
|
||||
|
||||
.. image:: 02_web_framework/pie_chart.png
|
||||
:align: center
|
||||
:scale: 80%
|
||||
|
||||
.. seealso::
|
||||
- `Example: lazy loading a js file
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/views/graph/graph_renderer.js#L57>`_
|
||||
- `Example: rendering a chart in a component
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/views/graph/graph_renderer.js#L618>`_
|
||||
|
||||
7. Real life update
|
||||
===================
|
||||
|
||||
Since we moved the data loading in a cache, it does not ever updates. But let us say that we
|
||||
are looking at fast moving data, so we want to periodically (for example, every 10min) reload
|
||||
fresh data.
|
||||
|
||||
This is quite simple to implement, with a `setTimeout` or `setInterval` in the dashboard service.
|
||||
However, here is the tricky part: if the dashboard is currently being displayed, it should be
|
||||
updated immediately.
|
||||
|
||||
To do that, one can use a `reactive` object: it is just like the proxy returned by `useState`,
|
||||
but not linked to any component. A component can then do a `useState` on it to subscribe to its
|
||||
changes.
|
||||
|
||||
|
||||
#. Update the dashboard service to reload data every 10 minutes (to test it, use 10s instead!)
|
||||
#. Modify it to return a `reactive <{OWL_PATH}/doc/reference/reactivity.md#reactive>`_ object.
|
||||
Reloading data should update the reactive object in place.
|
||||
#. The `Dashboard` component can now use it with a `useState`
|
||||
|
||||
.. seealso::
|
||||
- `Documentation on reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_
|
||||
- `Example: Use of reactive in a service
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/debug/profiling/profiling_service.js#L30>`_
|
||||
|
||||
8. Lazy loading the dashboard
|
||||
=============================
|
||||
|
||||
Let us imagine that our dashboard is getting quite big, and is only of interest to some
|
||||
of our users. In that case, it could make sense to lazy load our dashboard, and all
|
||||
related assets, so we only pay the cost of loading the code when we actually want to
|
||||
look at it.
|
||||
|
||||
To do that, we will need to create a new bundle containing all our dashboard assets,
|
||||
then use the `LazyComponent` (located in `@web/core/assets`).
|
||||
|
||||
#. Move all dashboard assets into a sub folder `/dashboard` to make it easier to
|
||||
add to a bundle.
|
||||
#. Create a `awesome_dashboard.dashboard` assets bundle containing all content of
|
||||
the `/dashboard` folder
|
||||
#. Modify `dashboard.js` to register itself in the `lazy_components` registry, and not
|
||||
in the `action` registry.
|
||||
#. Add in `src/` a file `dashboard_action` that import `LazyComponent` and register
|
||||
it to the `action` registry
|
||||
|
||||
9. Making our dashboard generic
|
||||
===============================
|
||||
|
||||
So far, we have a nice working dashboard. But it is currently hardcoded in the dashboard
|
||||
template. What if we want to customize our dashboard? Maybe some users have different
|
||||
needs, and want to see some other data.
|
||||
|
||||
So, the next step is then to make our dashboard generic: instead of hardcoding its content
|
||||
in the template, it can just iterate over a list of dashboard items. But then, many
|
||||
questions comes up: how to represent a dashboard item, how to register it, what data
|
||||
should it receive, and so on. There are many different ways to design such a system,
|
||||
with different trade offs.
|
||||
|
||||
For this tutorial, we will say that a dashboard item is an object with the folowing structure:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
const item = {
|
||||
id: "average_quantity",
|
||||
description: "Average amount of t-shirt",
|
||||
Component: StandardItem,
|
||||
// size and props are optionals
|
||||
size: 3,
|
||||
props: (data) => ({
|
||||
title: "Average amount of t-shirt by order this month",
|
||||
value: data.average_quantity
|
||||
}),
|
||||
};
|
||||
|
||||
The `description` value will be useful in a later exercise to show the name of items that the
|
||||
user can choose to add to his dashboard. The `size` number is optional, and simply describes
|
||||
the size of the dashboard item that will be displayed. Finally, the `props` function is optional.
|
||||
If not given, we will simply give the `statistics` object as data. But if it is defined, it will
|
||||
be used to compute specific props for the component.
|
||||
|
||||
The goal is to replace the content of the dashboard with the following snippet:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<t t-foreach="items" t-as="item" t-key="item.id">
|
||||
<DashboardItem size="item.size || 1">
|
||||
<t t-set="itemProp" t-value="item.props ? item.props(statistics) : {'data': statistics}"/>
|
||||
<t t-component="item.Component" t-props="itemProp" />
|
||||
</DashboardItem>
|
||||
</t>
|
||||
|
||||
Note that the above example features two advanced features of Owl: dynamic components, and dynamic props.
|
||||
|
||||
We currently have two kinds of item components: number cards, with a title and a number, and pie cards, with
|
||||
some label and a pie chart.
|
||||
|
||||
#. create and implement two components: `NumberCard` and `PieChartCard`, with the corresponding props
|
||||
#. create a file `dashboard_items.js` in which you define and export a list of items, using `NumberCard`
|
||||
and `PieChartCard` to recreate our current dashboard
|
||||
#. import that list of items in our `Dashboard` component, add it to the component, and update the template
|
||||
to use a `t-foreach` like shown above
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
setup() {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
And now, our dashboard template is generic!
|
||||
|
||||
10. Making our dashboard extensible
|
||||
===================================
|
||||
|
||||
However, the content of our item list is still hardcoded. Let us fix that by using a registry:
|
||||
|
||||
#. Instead of exporting a list, register all dashboard items in a `awesome_dashboard` registry
|
||||
#. Import all the items of the `awesome_dashboard` registry in the `Dashboard` component
|
||||
|
||||
The dashboard is now easily extensible. Any other odoo addon that want to register a new item to the
|
||||
dashboard can just add it to the registry.
|
||||
|
||||
11. Add and remove dashboard items
|
||||
==================================
|
||||
|
||||
Let us see how we can make our dashboard customizable. To make it simple, we will save the user
|
||||
dashboard configuration in the local storage, so it is persistent, but we don't have to deal
|
||||
with the server for now.
|
||||
|
||||
The dashboard configuration will be saved as a list of removed item ids.
|
||||
|
||||
#. Add a button in the control panel with a gear icon, to indicate that it is a settings button
|
||||
#. Clicking on that button should open a dialog
|
||||
#. In that dialog, we want to see a list of all existing dashboard items, each with a checkbox
|
||||
#. There should be a `Apply` button in the footer. Clicking on it will build a list of all item ids
|
||||
that are unchecked
|
||||
#. We want to store that value in the local storage
|
||||
#. And modify the `Dashboard` component to filter the current items by removing the ids of items
|
||||
from the configuration
|
||||
|
||||
|
||||
.. image:: 02_web_framework/items_configuration.png
|
||||
:width: 80%
|
||||
:align: center
|
||||
|
||||
12. Going further
|
||||
=================
|
||||
|
||||
Here is a list of some small improvements you could try to do if you have the time:
|
||||
|
||||
#. Make sure your application can be :ref:`translated <reference/translations>` (with
|
||||
`env._t`).
|
||||
#. Clicking on a section of the pie chart should open a list view of all orders which have the
|
||||
corresponding size.
|
||||
#. Save the content of the dashboard in a user settings on the server!
|
||||
#. Make it responsive: in mobile mode, each card should take 100% of the width
|
||||
|
||||
.. seealso::
|
||||
- `Example: use of env._t function
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/account/static/src/components/bills_upload/bills_upload.js#L64>`_
|
||||
- `Code: translation code in web/
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/l10n/translation.js#L16>`_
|
@ -1,273 +0,0 @@
|
||||
=============================
|
||||
Chapter 2: Odoo Web Framework
|
||||
=============================
|
||||
|
||||
The first part of this tutorial introduced you to most of Owl ideas. It is now time to learn
|
||||
about the Odoo JavaScript framework in its entirety, as used by the web client.
|
||||
|
||||
.. graph TD
|
||||
.. subgraph "Owl"
|
||||
.. C[Component]
|
||||
.. T[Template]
|
||||
.. H[Hook]
|
||||
.. S[Slot]
|
||||
.. E[Event]
|
||||
.. end
|
||||
|
||||
.. odoo[Odoo JavaScript framework] --> Owl
|
||||
|
||||
.. figure:: 02_web_framework/previously_learned.svg
|
||||
:align: center
|
||||
:width: 50%
|
||||
|
||||
For this chapter, we will start from the empty dashboard provided by the `awesome_tshirt`
|
||||
addon. We will progressively add features to it, using the Odoo JavaScript framework.
|
||||
|
||||
.. admonition:: Goal
|
||||
|
||||
.. image:: 02_web_framework/overview_02.png
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the
|
||||
`official Odoo tutorials repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_tshirt>`_.
|
||||
|
||||
1. A new Layout
|
||||
===============
|
||||
|
||||
Most screens in the Odoo web client uses a common layout: a control panel on top, with some buttons,
|
||||
and a main content zone just below. This is done using the `Layout component
|
||||
<{GITHUB_PATH}/addons/web/static/src/search/layout.js>`_, available in `@web/search/layout`.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Update the `AwesomeDashboard` component located in :file:`awesome_tshirt/static/src/` to use the
|
||||
`Layout` component. You can use
|
||||
:code:`{controlPanel: { "top-right": false, "bottom-right": false } }` for the `display` props of
|
||||
the `Layout` component.
|
||||
|
||||
Open http://localhost:8069/web, then open the :guilabel:`Awesome T-Shirts` app, and see the
|
||||
result.
|
||||
|
||||
.. image:: 02_web_framework/new_layout.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Example: use of Layout in client action
|
||||
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.js>`_ and
|
||||
`template <{GITHUB_PATH}/addons/web/static/src/webclient/actions/reports/report_action.xml>`_
|
||||
- `Example: use of Layout in kanban view
|
||||
<{GITHUB_PATH}/addons/web/static/src/views/kanban/kanban_controller.xml>`_
|
||||
|
||||
2. Add some buttons for quick navigation
|
||||
========================================
|
||||
|
||||
Let us now use the action service for an easy access to the common views in Odoo.
|
||||
|
||||
:ref:`Services <frontend/services>` is a notion defined by the Odoo JavaScript framework; it is a
|
||||
persistent piece of code that exports a state and/or functions. Each service can depend on other
|
||||
services, and components can import a service with the `useService()` hook.
|
||||
|
||||
.. example::
|
||||
|
||||
This shows how to open the settings view from a component using the action service.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
...
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
}
|
||||
openSettings() {
|
||||
this.action.doAction("base_setup.action_general_configuration");
|
||||
}
|
||||
...
|
||||
|
||||
.. exercise::
|
||||
|
||||
Let us add three buttons in the control panel bottom left zone.
|
||||
|
||||
#. A button `Customers`, which opens a kanban view with all customers (this action already
|
||||
exists, so you should use `its xml id
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
odoo/addons/base/views/res_partner_views.xml#L525>`_).
|
||||
#. A button `New Orders`, which opens a list view with all orders created in the last 7 days. Use
|
||||
the `Domain <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
/addons/web/static/src/core/domain.js#L19>`_ helper class to represent the domain.
|
||||
|
||||
.. tip::
|
||||
One way to represent the desired domain could be
|
||||
:code:`[('create_date','>=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]`
|
||||
|
||||
#. A button `Cancelled Order`, which opens a list of all orders created in the last 7 days, but
|
||||
already cancelled. Rather than defining the action twice, factorize it in a new `openOrders`
|
||||
method.
|
||||
|
||||
.. image:: 02_web_framework/navigation_buttons.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
- `Example: doAction use
|
||||
<{GITHUB_PATH}/addons/account/static/src/components/journal_dashboard_activity
|
||||
/journal_dashboard_activity.js#L35>`_
|
||||
- `Code: action service
|
||||
<{GITHUB_PATH}/addons/web/static/src/webclient/actions/action_service.js>`_
|
||||
|
||||
3. Call the server, add some statistics
|
||||
=======================================
|
||||
|
||||
Let's improve the dashboard by adding a few cards (see the `Card` component :ref:`made in the
|
||||
previous chapter <tutorials/discover_js_framework/generic_card>`) containing a few statistics. There
|
||||
is a route `/awesome_tshirt/statistics` that performs some computations and returns an object
|
||||
containing some useful information.
|
||||
|
||||
Whenever we need to call a specific controller, we need to use the :ref:`rpc service
|
||||
<frontend/services/rpc>`. It only exports a single function that perform the request:
|
||||
:code:`rpc(route, params, settings)`
|
||||
|
||||
Here is a short explanation on the various arguments:
|
||||
|
||||
- `route` is the target route, as a string. For example `/myroute/`.
|
||||
- `params` is an object that contains all data that will be given to the controller. (optional)
|
||||
- `settings` are for advanced controls on the request. Make it silent, or using a specific xhr
|
||||
instance. (optional)
|
||||
|
||||
.. example::
|
||||
|
||||
A basic request could look like this:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
setup() {
|
||||
this.rpc = useService("rpc");
|
||||
onWillStart(async () => {
|
||||
const result = await this.rpc("/my/controller", {a: 1, b: 2});
|
||||
// ...
|
||||
});
|
||||
}
|
||||
|
||||
.. exercise::
|
||||
#. Change `Dashboard` so that it uses the `rpc` service.
|
||||
#. Call the statistics route `/awesome_tshirt/statistics` in the `onWillStart` hook.
|
||||
#. Display a few cards in the dashboard containing:
|
||||
|
||||
- Number of new orders this month
|
||||
- Total amount of new orders this month
|
||||
- Average amount of t-shirt by order this month
|
||||
- Number of cancelled orders this month
|
||||
- Average time for an order to go from 'new' to 'sent' or 'cancelled'
|
||||
|
||||
.. image:: 02_web_framework/statistics.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Code: rpc service <{GITHUB_PATH}/addons/web/static/src/core/network/rpc_service.js>`_
|
||||
- `Example: calling a route in onWillStart
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/lunch/static/src/views/search_model.js#L21>`_
|
||||
|
||||
4. Cache network calls, create a service
|
||||
========================================
|
||||
|
||||
If you open the :guilabel:`Network` tab of your browser's dev tools, you will see that the call to
|
||||
`/awesome_tshirt/statistics` is done every time the client action is displayed. This is because the
|
||||
`onWillStart` hook is called each time the `Dashboard` component is mounted. But in this case, we
|
||||
would prefer to do it only the first time, so we actually need to maintain some state outside of the
|
||||
`Dashboard` component. This is a nice use case for a service!
|
||||
|
||||
.. example::
|
||||
|
||||
The following example registers a simple service that displays a notification every 5 seconds.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
const myService = {
|
||||
dependencies: ["notification"],
|
||||
start(env, { notification }) {
|
||||
let counter = 1;
|
||||
setInterval(() => {
|
||||
notification.add(`Tick Tock ${counter++}`);
|
||||
}, 5000);
|
||||
},
|
||||
};
|
||||
registry.category("services").add("myService", myService);
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Register and import a new `awesome_tshirt.statistics` service.
|
||||
#. It should provide a function `loadStatistics` that, once called, performs the actual rpc, and
|
||||
always return the same information.
|
||||
#. Use the `memoize <https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/utils/functions.js#L11>`_ utility function from
|
||||
`@web/core/utils/functions` that will allow caching the statistics.
|
||||
#. Use this service in the `Dashboard` component.
|
||||
#. Check that it works as expected
|
||||
|
||||
.. seealso::
|
||||
- `Example: simple service <{GITHUB_PATH}/addons/web/static/src/core/network/http_service.js>`_
|
||||
- `Example: service with a dependency
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/user_service.js>`_
|
||||
|
||||
5. Display a pie chart
|
||||
======================
|
||||
|
||||
Everyone likes charts (!), so let us add a pie chart in our dashboard. It will display the
|
||||
proportions of t-shirts sold for each size: S/M/L/XL/XXL.
|
||||
|
||||
For this exercise, we will use `Chart.js <https://www.chartjs.org/>`_. It is the chart library used
|
||||
by the graph view. However, it is not loaded by default, so we will need to either add it to our
|
||||
assets bundle, or lazy load it. Lazy loading is usually better since our users will not have to load
|
||||
the chartjs code every time if they don't need it.
|
||||
|
||||
.. exercise::
|
||||
#. Load chartjs, you can use the `loadJs
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/assets.js#L23>`_ function to load
|
||||
:file:`/web/static/lib/Chart/Chart.js`.
|
||||
#. In a `Card` (from previous exercises), display a `pie chart
|
||||
<https://www.chartjs.org/docs/2.8.0/charts/doughnut.html>`_ in the dashboard that displays the
|
||||
correct quantity for each sold t-shirts in each size (that information is available in the
|
||||
statistics route).
|
||||
|
||||
.. image:: 02_web_framework/pie_chart.png
|
||||
:align: center
|
||||
:scale: 50%
|
||||
|
||||
.. seealso::
|
||||
- `Example: lazy loading a js file
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/views/graph/graph_renderer.js#L57>`_
|
||||
- `Example: rendering a chart in a component
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/views/graph/graph_renderer.js#L618>`_
|
||||
|
||||
6. Going further
|
||||
================
|
||||
|
||||
Here is a list of some small improvements you could try to do if you have the time:
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Make sure your application can be :ref:`translated <reference/translations>` (with
|
||||
`env._t`).
|
||||
#. Clicking on a section of the pie chart should open a list view of all orders which have the
|
||||
corresponding size.
|
||||
#. Add a SCSS file and see if you can change the background color of the dashboard action.
|
||||
|
||||
.. image:: 02_web_framework/misc.png
|
||||
:align: center
|
||||
:scale: 50%
|
||||
|
||||
.. seealso::
|
||||
- `Example: use of env._t function
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/account/static/src/components/bills_upload/bills_upload.js#L64>`_
|
||||
- `Code: translation code in web/
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/l10n/translation.js#L16>`_
|
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 16 KiB |
@ -1,7 +1,7 @@
|
||||
:show-content:
|
||||
|
||||
=============================
|
||||
Master the Odoo Web Framework
|
||||
Master the Odoo web framework
|
||||
=============================
|
||||
|
||||
.. toctree::
|
||||
@ -11,32 +11,49 @@ Master the Odoo Web Framework
|
||||
master_odoo_web_framework/*
|
||||
|
||||
This tutorial is designed for those who have completed the :doc:`discover_js_framework` tutorial and
|
||||
are looking to deepen their knowledge of the Odoo web framework.
|
||||
are looking to deepen their knowledge of the Odoo web framework. It is organized in four independant
|
||||
projects, each focusing on different features of Odoo.
|
||||
|
||||
For this training, we will step into the shoes of the IT staff at the fictional company Awesome
|
||||
T-Shirt, which is dedicated to printing custom t-shirts for online customers. The Awesome T-Shirt
|
||||
company uses Odoo to manage orders and has created a dedicated Odoo module to manage their workflow.
|
||||
.. note::
|
||||
|
||||
In this tutorial, we will explore various aspects of the Odoo web framework in detail, including
|
||||
fields and views, notifications, command palette, and much more. This tutorial will provide you with
|
||||
the knowledge and skills you need to take full advantage of the Odoo web framework. So, let's get
|
||||
started!
|
||||
Each of these chapters can be done independantly, in any order. Also, be aware that some of them
|
||||
cover a lot of material, so they may be quite long.
|
||||
|
||||
.. _howtos/master_odoo_web_framework/setup:
|
||||
The first project is about building a `clicker game <https://en.wikipedia.org/wiki/Incremental_game>`_.
|
||||
While working on it, you will learn various aspects of the web framework: systray, command palette,
|
||||
dialogs, notifications, customizing existing components and much more.
|
||||
|
||||
The second project is focused on an important category of components: fields. Field components
|
||||
represent the value of a field for a record, they appear in many places in the web client: in form
|
||||
views, obviously, but also in kanban and list views, and may even be used alone, without a view.
|
||||
Due to their importance, it makes sense to learn how to create and manipulate such components.
|
||||
|
||||
In the context of the web framework, views usually refers to the javascript implementation of a
|
||||
component that represents one or many records, depending on a description (`ir.ui.view`). Such
|
||||
components are actually quite complicated and usually requires various sub systems (a renderer,
|
||||
a model, a controller, a arch parser, ...). In chapter 3, we create a new view from scratch to
|
||||
represent a list of images.
|
||||
|
||||
Finally, the last project in chapter 4 is about customizing an existing view (a kanban view) by
|
||||
adding a search panel on its left. It is interesting to see how one can take existing code, and
|
||||
modify it to suit our needs. Also, it is a realistic project, that will feature many common issues
|
||||
that arises while working on Odoo.
|
||||
|
||||
|
||||
.. _tutorials/master_odoo_web_framework/setup:
|
||||
|
||||
Setup
|
||||
=====
|
||||
|
||||
#. Clone the `official Odoo tutorials repository <https://github.com/odoo/tutorials>`_ and switch to
|
||||
the branch `{CURRENT_MAJOR_BRANCH}`.
|
||||
#. Add the cloned repository to the :option:`--addons-path <odoo-bin --addons-path>`.
|
||||
#. Start a new Odoo database and install the modules `awesome_tshirt` and `awesome_gallery`.
|
||||
#. Add the cloned repository to your :option:`--addons-path <odoo-bin --addons-path>`.
|
||||
#. Start a new Odoo database and install the modules for each chapter that you want to work on:
|
||||
`awesome_clicker` (for chapter 1), `awesome_fields` (for chapter 2), `awesome_gallery` (for chapter 3) or `awesome_kanban` (for chapter 4).
|
||||
|
||||
Content
|
||||
=======
|
||||
|
||||
- :doc:`master_odoo_web_framework/01_fields_and_views`
|
||||
- :doc:`master_odoo_web_framework/02_miscellaneous`
|
||||
- :doc:`master_odoo_web_framework/03_custom_kanban_view`
|
||||
- :doc:`master_odoo_web_framework/04_creating_view_from_scratch`
|
||||
- :doc:`master_odoo_web_framework/05_testing`
|
||||
- :doc:`master_odoo_web_framework/01_build_clicker_game`
|
||||
- :doc:`master_odoo_web_framework/02_create_gallery_view`
|
||||
- :doc:`master_odoo_web_framework/03_customize_kanban_view`
|
||||
|
@ -0,0 +1,447 @@
|
||||
===============================
|
||||
Chapter 1: Build a Clicker game
|
||||
===============================
|
||||
|
||||
For this project, we will build together a `clicker game <https://en.wikipedia.org/wiki/Incremental_game>`_,
|
||||
completely integrated with Odoo. In this game, the goal is to accumulate a large number of clicks, and
|
||||
to automate the system. The interesting part is that we will use the Odoo user interface as our playground.
|
||||
For example, we will hide bonuses in some random parts of the web client.
|
||||
|
||||
To get started, you need a running Odoo server and a development environment. Before getting
|
||||
into the exercises, make sure you have followed all the steps described in this
|
||||
:ref:`tutorial introduction <tutorials/master_odoo_web_framework/setup>`.
|
||||
|
||||
.. admonition:: Goal
|
||||
|
||||
.. image:: 01_build_clicker_game/final.png
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the
|
||||
`official Odoo tutorials repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-master-odoo-web-framework-solutions/awesome_clicker>`_.
|
||||
|
||||
|
||||
1. Create a systray item
|
||||
========================
|
||||
|
||||
To get started, we want to display a counter in the systray.
|
||||
|
||||
#. Create a `clicker_systray_item.js` (and `xml`) file with a hello world Owl component.
|
||||
#. Register it to the systray registry, and make sure it is visible.
|
||||
#. Update the content of the item so that it displays the following string: `Clicks: 0`, and
|
||||
add a button on the right to increment the value.
|
||||
|
||||
.. image:: 01_build_clicker_game/systray.png
|
||||
:align: center
|
||||
|
||||
And voila, we have a completely working clicker game!
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :ref:`Documentation on the systray registry <frontend/registries/systray>`
|
||||
- `Example: adding a systray item to the registry
|
||||
<https://github.com/odoo/odoo/blob/c4fb9c92d7826ddbc183d38b867ca4446b2fb709/addons/web/static/src/webclient/user_menu/user_menu.js#L41-L42>`_
|
||||
|
||||
2. Count external clicks
|
||||
========================
|
||||
|
||||
Well, to be honest, it is not much fun yet. So let us add a new feature: we want all clicks in the
|
||||
user interface to count, so the user is incentivized to use Odoo as much as possible! But obviously,
|
||||
the intentional clicks on the main counter should still count more.
|
||||
|
||||
#. Use `useExternalListener` to listen on all clicks on `document.body`.
|
||||
#. Each of these clicks should increase the counter value by 1.
|
||||
#. Modify the code so that each click on the counter increased the value by 10
|
||||
#. Make sure that a click on the counter does not increase the value by 11!
|
||||
#. Also additional challenge: make sure the external listener capture the events, so we don't
|
||||
miss any clicks.
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Owl documentation on useExternalListener <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md#useexternallistener>`_
|
||||
- `MDN page on event capture <https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#event_capture>`_
|
||||
|
||||
|
||||
3. Create a client action
|
||||
=========================
|
||||
|
||||
Currently, the current user interface is quite small: it is just a systray item. We certainly need
|
||||
more room to display more of our game. To do that, let us create a client action. A client action
|
||||
is a main action, managed by the web client, that displays a component.
|
||||
|
||||
#. Create a `client_action.js` (and `xml`) file, with a hello world component.
|
||||
#. Register that client action in the action registry under the name `awesome_clicker.client_action`
|
||||
#. Add a button on the systray item with the text `Open`. Clicking on it should open the
|
||||
client action `awesome_clicker.client_action` (use the action service to do that).
|
||||
#. To avoid disrupting employees' workflow, we prefer the client action to open within a popover
|
||||
rather than in fullscreen mode. Modify the `doAction` call to open it in a popover.
|
||||
|
||||
.. tip::
|
||||
|
||||
You can use `target: "new"` in the `doAction` to open the action in a popover:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
{
|
||||
type: "ir.actions.client",
|
||||
tag: "awesome_clicker.client_action",
|
||||
target: "new",
|
||||
name: "Clicker"
|
||||
}
|
||||
|
||||
.. image:: 01_build_clicker_game/client_action.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :ref:`How to create a client action <howtos/javascript_client_action>`
|
||||
|
||||
4. Move the state to a service
|
||||
==============================
|
||||
|
||||
For now, our client action is just a hello world component. We want it to display our game state, but
|
||||
that state is currently only available in the systray item. So it means that we need to change the
|
||||
location of our state to make it available for all our components. This is a perfect use case for services.
|
||||
|
||||
#. Create a `clicker_service.js` file with the corresponding service.
|
||||
#. This service should export a reactive value (the number of clicks) and a few functions to update it:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
const state = reactive({ clicks: 0 });
|
||||
...
|
||||
return {
|
||||
state,
|
||||
increment(inc) {
|
||||
state.clicks += inc
|
||||
}
|
||||
};
|
||||
|
||||
#. Access the state in both the systray item and the client action (don't forget to `useState` it). Modify
|
||||
the systray item to remove its own local state and use it. Also, you can remove the `+10 clicks` button.
|
||||
#. Display the state in the client action, and add a `+10` clicks button in it.
|
||||
|
||||
.. image:: 01_build_clicker_game/increment_button.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :ref:`Short explanation on services <tutorials/discover_js_framework/services>`
|
||||
|
||||
5. Use a custom hook
|
||||
====================
|
||||
|
||||
Right now, every part of the code that will need to use our clicker service will have to import `useService` and
|
||||
`useState`. Since it is quite common, let us use a custom hook. It is also useful to put more emphasis on the
|
||||
`clicker` part, and less emphasis on the `service` part.
|
||||
|
||||
#. Export a `useClicker` hook.
|
||||
#. Update all current uses of the clicker service to the new hook:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
this.clicker = useClicker();
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Documentation on hooks: <https://github.com/odoo/owl/blob/master/doc/reference/hooks.md>`_
|
||||
|
||||
6. Humanize the displayed value
|
||||
===============================
|
||||
|
||||
We will in the future display large numbers, so let us get ready for that. There is a `humanNumber` function that
|
||||
format numbers in a easier to comprehend way: for example, `1234` could be formatted as `1.2k`
|
||||
|
||||
#. Use it to display our counters (both in the systray item and the client action).
|
||||
#. Create a `ClickValue` component that display the value.
|
||||
|
||||
.. note::
|
||||
|
||||
Owl allows component that contains just text nodes!
|
||||
|
||||
.. image:: 01_build_clicker_game/humanized_number.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `definition of humanNumber function <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/utils/numbers.js#L119>`_
|
||||
|
||||
7. Add a tooltip in `ClickValue` component
|
||||
==========================================
|
||||
|
||||
With the `humanNumber` function, we actually lost some precision on our interface. Let us display the real number
|
||||
as a tooltip.
|
||||
|
||||
#. Tooltip needs an html element. Change the `ClickValue` to wrap the value in a `<span/>` element
|
||||
#. Add a dynamic `data-tooltip` attribute to display the exact value.
|
||||
|
||||
.. image:: 01_build_clicker_game/humanized_tooltip.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Documentation in the tooltip service <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/tooltip/tooltip_service.js#L17>`_
|
||||
|
||||
8. Buy ClickBots
|
||||
================
|
||||
|
||||
Let us make our game even more interesting: once a player get to 1000 clicks for the first time, the game
|
||||
should unlock a new feature: the player can buy robots for 1000 clicks. These robots will generate 10 clicks
|
||||
every 10 seconds.
|
||||
|
||||
#. Add a `level` number to our state. This is a number that will be incremented at some milestones, and
|
||||
open new features
|
||||
#. Add a `clickBots` number to our state. It represents the number of robots that have been purchased.
|
||||
#. Modify the client action to display the number of click bots (only if `level >= 1`), with a `Buy`
|
||||
button that is enabled if `clicks >= 1000`. The `Buy` button should increment the number of clickbots by 1.
|
||||
#. Set a 10s interval in the service that will increment the number of clicks by `10*clickBots`.
|
||||
#. Make sure the Buy button is disabled if the player does not have enough clicks.
|
||||
|
||||
.. image:: 01_build_clicker_game/clickbot.png
|
||||
:align: center
|
||||
|
||||
9. Refactor to a class model
|
||||
============================
|
||||
|
||||
The current code is written in a somewhat functional style. But to do so, we have to export the state and all its
|
||||
update functions in our clicker object. As this project grows, this may become more and more complex. To make it
|
||||
simpler, let us split our business logic out of our service and into a class.
|
||||
|
||||
#. Create a `clicker_model` file that exports a reactive class. Move all the state and update functions from
|
||||
the service into the model.
|
||||
|
||||
.. tip::
|
||||
|
||||
You can extends the ClickerModel with the `Reactive` class from
|
||||
:file:`@web/core/utils/reactive`. The `Reactive` class wrap the model into a reactive proxy.
|
||||
|
||||
#. Rewrite the clicker service to instantiate and export the clicker model class.
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Example of subclassing Reactive <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/model/relational_model/datapoint.js#L32>`_
|
||||
|
||||
10. Notify when a milestone is reached
|
||||
======================================
|
||||
|
||||
There is not much feedback that something changed when we reached 1k clicks. Let us use the `effect` service
|
||||
to communicate that information clearly. The problem is that our click model does not have access to services.
|
||||
Also, we want to keep as much as possible the UI concern out of the model. So, we can explore a new strategy
|
||||
for communication: event buses.
|
||||
|
||||
#. Update the clicker model to instantiate a bus, and to trigger a `MILESTONE_1k` event when we reach 1000 clicks
|
||||
for the first time.
|
||||
#. Change the clicker service to listen to the same event on the model bus.
|
||||
#. When that happens, use the `effect` service to display a rainbow man.
|
||||
#. Add some text to explain that the user can now buy clickbots.
|
||||
|
||||
.. image:: 01_build_clicker_game/milestone.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Owl documentation on event bus <https://github.com/odoo/owl/blob/master/doc/reference/utils.md#eventbus>`_
|
||||
- :ref:`Documentation on effect service <frontend/services/effect>`
|
||||
|
||||
11. Add BigBots
|
||||
===============
|
||||
|
||||
Clearly, we need a way to provide the player with more choices. Let us add a new type of clickbot: `BigBots`,
|
||||
which are just more powerful: they provide with 100 clicks each 10s, but they cost 5000 clicks
|
||||
|
||||
#. increment `level` when it gets to 5k (so it should be 2)
|
||||
#. Update the state to keep track of bigbots
|
||||
#. bigbots should be available at `level >=2`
|
||||
#. Display the corresponding information in the client action
|
||||
|
||||
.. tip::
|
||||
|
||||
If you need to use `<` or `>` in a template as a javascript expression, be careful since it might class with
|
||||
the xml parser. To solve that, you can use one of the special aliases: `gt, gte, lt` or `lte`. See the
|
||||
`Owl documentation page on template expressions <https://github.com/odoo/owl/blob/master/doc/reference/templates.md#expression-evaluation>`_.
|
||||
|
||||
.. image:: 01_build_clicker_game/bigbot.png
|
||||
:align: center
|
||||
|
||||
12. Add a new type of resource: power
|
||||
=====================================
|
||||
|
||||
Now, to add another scaling point, let us add a new type of resource: a power multiplier. This is a number
|
||||
that can be increased at `level >= 3`, and multiplies the action of the bots (so, instead of providing
|
||||
one click, clickbots now provide us with `multiplier` clicks).
|
||||
|
||||
#. increment `level` when it gets to 100k (so it should be 3).
|
||||
#. update the state to keep track of the power (initial value is 1).
|
||||
#. change bots to use that number as a multiplier.
|
||||
#. Update the user interface to display and let the user purchase a new power level (costs: 50k).
|
||||
|
||||
.. image:: 01_build_clicker_game/bigbot.png
|
||||
:align: center
|
||||
|
||||
13. Define some random rewards
|
||||
==============================
|
||||
|
||||
We want the user to obtain sometimes bonuses, to reward using Odoo.
|
||||
|
||||
#. Define a list of rewards in `click_rewards.js`. A reward is an object with:
|
||||
- a `description` string.
|
||||
- a `apply` function that take the game state in argument and can modify it.
|
||||
- a `minLevel` number (optional) that describes at which unlock level the bonus is available.
|
||||
- a `maxLevel` number (optional) that describes at which unlock level a bonus is no longer available.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
export const rewards = [
|
||||
{
|
||||
description: "Get 1 click bot",
|
||||
apply(clicker) {
|
||||
clicker.increment(1);
|
||||
},
|
||||
maxLevel: 3,
|
||||
},
|
||||
{
|
||||
description: "Get 10 click bot",
|
||||
apply(clicker) {
|
||||
clicker.increment(10);
|
||||
},
|
||||
minLevel: 3,
|
||||
maxLevel: 4,
|
||||
},
|
||||
{
|
||||
description: "Increase bot power!",
|
||||
apply(clicker) {
|
||||
clicker.multipler += 1;
|
||||
},
|
||||
minLevel: 3,
|
||||
},
|
||||
];
|
||||
|
||||
You can add whatever you want to that list!
|
||||
|
||||
#. Define a function `getReward` that will select a random reward from the list of rewards that matches
|
||||
the current unlock level.
|
||||
#. Extract the code that choose randomly in an array in a function `choose` that you can move to another `utils.js` file.
|
||||
|
||||
14. Provide a reward when opening a form view
|
||||
=============================================
|
||||
|
||||
#. Patch the form controller. Each time a form controller is created, it should randomly decides (1% chance)
|
||||
if a reward should be given.
|
||||
#. If the answer is yes, call a method `getReward` on the model.
|
||||
#. That method should choose a reward, send a sticky notification, with a button `Collect` that will
|
||||
then apply the reward, and finally, it should open the `clicker` client action.
|
||||
|
||||
.. image:: 01_build_clicker_game/reward.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :ref:`Documentation on patching a class <frontend/patching_class>`
|
||||
- `Definition of patch function <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/utils/patch.js#L71>`_
|
||||
- `Example of patching a class <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/pos_mercury/static/src/app/screens/receipt_screen/receipt_screen.js#L6>`_
|
||||
|
||||
15. Add commands in command palette
|
||||
===================================
|
||||
|
||||
#. Add a command `Open Clicker Game` to the command palette.
|
||||
#. Add another command: `Buy 1 click bot`.
|
||||
|
||||
.. image:: 01_build_clicker_game/command_palette.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Example of use of command provider registry <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/src/core/debug/debug_providers.js#L10>`_
|
||||
|
||||
16. Add yet another resource: trees
|
||||
===================================
|
||||
|
||||
It is now time to introduce a completely new type of resources. Here is one that should not be too controversial: trees.
|
||||
We will now allow the user to plant (collect?) fruit trees. A tree costs 1 million clicks, but it will provide us with
|
||||
fruits (either pears or cherries).
|
||||
|
||||
#. Update the state to keep track of various types of trees: pear/cherries, and their fruits.
|
||||
#. Add a function that computes the total number of trees and fruits.
|
||||
#. Define a new unlock level at `clicks >= 1 000 000`.
|
||||
#. Update the client user interface to display the number of trees and fruits, and also, to buy trees.
|
||||
#. Increment the fruit number by 1 for each tree every 30s.
|
||||
|
||||
.. image:: 01_build_clicker_game/trees.png
|
||||
:align: center
|
||||
|
||||
17. Use a dropdown menu for the systray item
|
||||
============================================
|
||||
|
||||
Our game starts to become interesting. But for now, the systray only displays the total number of clicks. We
|
||||
want to see more information: the total number of trees and fruits. Also, it would be useful to have a quick
|
||||
access to some commands and some more information. Let us use a dropdown menu!
|
||||
|
||||
#. Replace the systray item by a dropdown menu.
|
||||
#. It should display the numbers of clicks, trees, and fruits, each with a nice icon.
|
||||
#. Clicking on it should open a dropdown menu that displays more detailed information: each types of trees
|
||||
and fruits.
|
||||
#. Also, a few dropdown items with some commands: open the clicker game, buy a clickbot, ...
|
||||
|
||||
.. image:: 01_build_clicker_game/dropdown.png
|
||||
:align: center
|
||||
|
||||
18. Use a Notebook component
|
||||
============================
|
||||
|
||||
We now keep track of a lot more information. Let us improve our client interface by organizing the information
|
||||
and features in various tabs, with the `Notebook` component:
|
||||
|
||||
#. Use the `Notebook` component.
|
||||
#. All `click` content should be displayed in one tab.
|
||||
#. All `tree/fruits` content should be displayed in another tab.
|
||||
|
||||
.. image:: 01_build_clicker_game/notebook.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :ref:`Odoo: Documentation on Notebook component <frontend/owl/notebook>`
|
||||
- `Owl: Documentation on slots <https://github.com/odoo/owl/blob/master/doc/reference/slots.md>`_
|
||||
- `Tests of Notebook component <https://github.com/odoo/odoo/blob/c638913df191dfcc5547f90b8b899e7738c386f1/addons/web/static/tests/core/notebook_tests.js#L27>`_
|
||||
|
||||
19. Persist the game state
|
||||
===========================
|
||||
|
||||
You certainly noticed a big flaw in our game: it is transient. The game state is lost each time the user closes the
|
||||
browser tab. Let us fix that. We will use the local storage to persist the state.
|
||||
|
||||
#. Import `browser` from :file:`@web/core/browser/browser` to access the localstorage.
|
||||
#. Serialize the state every 10s (in the same interval code) and store it on the local storage.
|
||||
#. When the `clicker` service is started, it should load the state from the local storage (if any), or initialize itself
|
||||
otherwise.
|
||||
|
||||
20. Introduce state migration system
|
||||
====================================
|
||||
|
||||
Once you persist state somewhere, a new problem arises: what happens when you update your code, so the shape of the state
|
||||
changes, and the user opens its browser with a state that was created with an old version? Welcome to the world of
|
||||
migration issues!
|
||||
|
||||
It is probably wise to tackle the problem early. What we will do here is add a version number to the state, and introduce
|
||||
a system to automatically update the states if it is not up to date.
|
||||
|
||||
#. Add a version number to the state.
|
||||
#. Define an (empty) list of migrations. A migration is an object with a `fromVersion` number, a `toVersion` number, and a `apply` function.
|
||||
#. Whenever the code loads the state from the local storage, it should check the version number. If the state is not
|
||||
uptodate, it should apply all necessary migrations.
|
||||
|
||||
21. Add another type of trees
|
||||
=============================
|
||||
|
||||
To test our migration system, let us add a new type of trees: peaches.
|
||||
|
||||
#. Add `peach` trees.
|
||||
#. Increment the state version number.
|
||||
#. Define a migration.
|
||||
|
||||
.. image:: 01_build_clicker_game/peach_tree.png
|
||||
:align: center
|
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 101 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 54 KiB |
@ -1,403 +0,0 @@
|
||||
===========================
|
||||
Chapter 1: Fields and Views
|
||||
===========================
|
||||
|
||||
In the previous chapter, we learned a range of skills, including how to create and use services,
|
||||
work with the Layout component, make the dashboard translatable, and lazy load a JavaScript library
|
||||
like Chart.js. Now, let's learn how to create new fields and views.
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the
|
||||
`official Odoo tutorials repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_tshirt>`_. It
|
||||
is recommended not to look at them before trying the exercises.
|
||||
|
||||
Fields and views are among the most important concepts in the Odoo user interface. They are key to
|
||||
many important user interactions, and should therefore work perfectly. In the context of the
|
||||
JavaScript framework, fields are components specialized for visualizing/editing a specific field for
|
||||
a given record. For example, a (Python) model may define a char field, which will be represented by
|
||||
a field component `CharField`.
|
||||
|
||||
A field component is basically just a component registered in the `fields` :ref:`registry
|
||||
<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: a simple field
|
||||
=======================
|
||||
|
||||
Let us discuss a simplified implementation of a `CharField`. First, here is the template:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<t t-name="web.CharField">
|
||||
<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"/>
|
||||
|
||||
.. _tutorials/master_odoo_web_framework/image_preview_field:
|
||||
|
||||
1. An `image_preview` field
|
||||
===========================
|
||||
|
||||
Each new order on the website will be created as an `awesome_tshirt.order`. This model has a
|
||||
`image_url` field (of type `char`), which is currently only visible as a string. We want to be able
|
||||
to see the image itself in the form view.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Create a new `ImagePreview` component and register it in the proper :ref:`registry
|
||||
<frontend/registries>`. Use the `CharField` component in your template. You can use `t-props
|
||||
<{OWL_PATH}/doc/reference/props.md#dynamic-props>`_ to pass props received by `ImagePreview`
|
||||
to `CharField`. Update the arch of the form view to use your new field by setting the `widget`
|
||||
attribute.
|
||||
#. Change the code of the `ImagePreview` component so that the image is displayed below the URL.
|
||||
#. When the field is readonly, only the image should be displayed and the URL should be hidden.
|
||||
|
||||
.. note::
|
||||
It is possible to solve this exercise by inheriting `CharField`, but the goal of this exercise is
|
||||
to create a field from scratch.
|
||||
|
||||
.. image:: 01_fields_and_views/image_field.png
|
||||
:align: center
|
||||
:scale: 50%
|
||||
|
||||
.. seealso::
|
||||
|
||||
`Code: CharField <{GITHUB_PATH}/addons/web/static/src/views/fields/char/char_field.js>`_
|
||||
|
||||
2. Improving the `image_preview` field
|
||||
======================================
|
||||
|
||||
We want to improve the field of the previous task to help the staff recognize orders for which some
|
||||
action should be done.
|
||||
|
||||
.. exercise::
|
||||
|
||||
Display a warning "MISSING TSHIRT DESIGN" in red if there is no image URL specified on the order.
|
||||
|
||||
.. image:: 01_fields_and_views/missing_image.png
|
||||
:align: center
|
||||
|
||||
3. Customizing a field component
|
||||
================================
|
||||
|
||||
Let's see how to use inheritance to extend an existing component.
|
||||
|
||||
There is a `is_late`, readonly, boolean field on the order model. That would be useful information
|
||||
to see on the list/kanban/view. Then, let us say that we want to add a red word "Late!" next to it
|
||||
whenever it is set to `true`.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Create a new `LateOrderBoolean` field inheriting from `BooleanField`. The template of
|
||||
`LateOrderBoolean` can also :ref:`inherit <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:: 01_fields_and_views/late_field.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
- `Example: A field inheriting another
|
||||
<{GITHUB_PATH}/addons/account/static/src/components/account_type_selection>`_
|
||||
- :ref:`Documentation on xpath <reference/view_records/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 an alert block with specific messages
|
||||
depending on some conditions:
|
||||
|
||||
- If the `image_url` field is not set, it should display "No image".
|
||||
- If the `amount` of the order is higher than 100 euros, it should display "Add promotional
|
||||
material".
|
||||
- Make sure that your widget is updated in real time.
|
||||
|
||||
.. tip::
|
||||
Try to evaluate `props.record` in the :guilabel:`Console` tab of your browser's dev tools.
|
||||
|
||||
.. image:: 01_fields_and_views/warning_widget.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Example: Using the tag <widget> in a form view
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/calendar/views/calendar_views.xml#L197>`_
|
||||
- `Example: Implementation of a widget
|
||||
<{GITHUB_PATH}/addons/web/static/src/views/widgets/week_days>`_
|
||||
|
||||
5. Use `markup`
|
||||
===============
|
||||
|
||||
Let’s see how we can display raw HTML in a template. The `t-out` directive can be used for that
|
||||
propose. Indeed, `it generally acts like t-esc, unless the data has been marked explicitly with a
|
||||
markup function <{OWL_PATH}/doc/reference/templates.md#outputting-data>`_. In that case, its value
|
||||
is injected as HTML.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Modify the previous exercise to put the `image` and `material` words in bold.
|
||||
#. The warnings should be markuped, and the template should be modified to use `t-out`.
|
||||
#. Import the `markup` function from Owl and, for each message, replace it with a call of the
|
||||
function with the message passed as argument.
|
||||
|
||||
.. note::
|
||||
This is an example of a safe use of `t-out`, since the string is static.
|
||||
|
||||
.. image:: 01_fields_and_views/warning_widget2.png
|
||||
:align: center
|
||||
|
||||
6. Add buttons in the control panel
|
||||
===================================
|
||||
|
||||
Views are among the most important components in Odoo: they allow users to interact with their
|
||||
data. Let us discuss how Odoo views are designed.
|
||||
|
||||
The power of Odoo views is that they declare how a particular screen should work with an XML
|
||||
document (usually named `arch`, short for architecture). This description can be extended/modified
|
||||
by xpaths serverside. Then, the browser loads that document, parses it (fancy word to say that it
|
||||
extracts the useful information), and then represents the data accordingly.
|
||||
|
||||
.. example::
|
||||
|
||||
The `arch` document is view specific. Here is how a `graph` view or a `calendar` view could be
|
||||
defined:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<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:: 01_fields_and_views/view_architecture.svg
|
||||
:align: center
|
||||
:width: 75%
|
||||
:class: o-no-modal
|
||||
|
||||
The view description can define a `props` function, which receives the standard props, and computes
|
||||
the base props of the concrete view. The `props` function is executed only once, and can be thought
|
||||
of as being some kind of factory. It is useful to parse the `arch` XML document, and to allow the
|
||||
view to be parameterized (for example, it can return a Renderer component that will be used as
|
||||
Renderer). Then, it is easy to customize the specific renderer used by a sub view.
|
||||
|
||||
The props will be extended before being given to the Controller. In particular, the search props
|
||||
(domain/context/groupby) will be added.
|
||||
|
||||
Finally, the root component, commonly called the `Controller`, coordinates everything. It uses the
|
||||
generic `Layout` component (to add a control panel), instantiates a `Model`, and uses a `Renderer`
|
||||
component in the `Layout` default slot. The `Model` is tasked with loading and updating data, and
|
||||
the `Renderer` is supposed to handle all rendering work, along with all user interactions.
|
||||
|
||||
In practice, once the t-shirt order is printed, we need to print a label to put on the package. To
|
||||
do that, let us add a button in the order's form view's control panel, which will call a model
|
||||
method.
|
||||
|
||||
There is a service dedicated to calling models methods: `orm_service`, located in
|
||||
`core/orm_service.js`. It provides a way to call common model methods, as well as a generic
|
||||
`call(model, method, args, kwargs)` method.
|
||||
|
||||
.. example::
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
onWillStart(async () => {
|
||||
// will read the fields 'id' and 'descr' from the record with id=3 of my.model
|
||||
const data = await this.orm.read("my.model", [3], ["id", "descr"]);
|
||||
// ...
|
||||
});
|
||||
}
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Create a customized form view extending the `web` form view and register it as
|
||||
`awesome_tshirt.order_form_view`.
|
||||
#. Add a `js_class="awesome_tshirt.order_form_view"` attribute to the arch of the form view so
|
||||
that Odoo will load it.
|
||||
#. Create a new template inheriting from the form controller template and add a "Print Label"
|
||||
button after the "New" button.
|
||||
#. Clicking on this button should call the method `print_label` from the model
|
||||
`awesome_tshirt.order` with the proper id.
|
||||
|
||||
.. note::
|
||||
`print_label` is a mock method; it only displays a message in the logs.
|
||||
|
||||
#. The button should not be disabled if the current order is in `create` mode (i.e., it does not
|
||||
exist yet).
|
||||
|
||||
.. tip::
|
||||
Log `this.props.resId` and `this.model.root.resId` and compare the two values before and
|
||||
after entering `create` mode.
|
||||
|
||||
#. The button should be displayed as a primary button if the customer is properly set and if the
|
||||
task stage is `printed`. Otherwise, it should be displayed as a secondary button.
|
||||
#. Bonus point: clicking twice on the button should not trigger 2 RPCs.
|
||||
|
||||
.. image:: 01_fields_and_views/form_button.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
- `Example: Extending a view (JS)
|
||||
<{GITHUB_PATH}/addons/mass_mailing/static/src/views/mailing_contact_view_kanban.js>`_
|
||||
- `Example: Extending a view (XML)
|
||||
<{GITHUB_PATH}/addons/mass_mailing/static/src/views/mass_mailing_views.xml>`_
|
||||
- `Example: Using a js_class attribute
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
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>`_
|
||||
- `Code: useDebounced hook
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/utils/timing.js#L117>`_
|
||||
|
||||
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.
|
Before Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 453 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 17 KiB |
@ -0,0 +1,462 @@
|
||||
================================
|
||||
Chapter 2: Create a Gallery View
|
||||
================================
|
||||
|
||||
Let us see how one can create a new view, completely from scratch. In a way, it is not very
|
||||
difficult to do, but there are no really good resources on how to do it. Note that most situations
|
||||
should be solved by either customizing an existing view, or with a client action.
|
||||
|
||||
For this exercise, let's assume that we want to create a `gallery` view, which is a view that lets
|
||||
us represent a set of records with an image field.
|
||||
|
||||
The problem could certainly be solved with a kanban view, but this means that it is not possible to
|
||||
have our normal kanban view and the gallery view in the same action.
|
||||
|
||||
Let us make a gallery view. Each gallery view will be defined by an `image_field` attribute in its
|
||||
arch:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<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:: 02_create_gallery_view/overview.png
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the
|
||||
`official Odoo tutorials repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-master-odoo-web-framework-solutions/awesome_gallery>`_.
|
||||
|
||||
1. Make a hello world view
|
||||
==========================
|
||||
|
||||
First step is to create a JavaScript implementation with a simple component.
|
||||
|
||||
#. Create the `gallery_view.js` , `gallery_controller.js` and `gallery_controller.xml` files in
|
||||
`static/src`.
|
||||
#. Implement a simple hello world component in `gallery_controller.js`.
|
||||
#. In `gallery_view.js`, import the controller, create a view object, and register it in the
|
||||
view registry under the name `gallery`.
|
||||
|
||||
.. example::
|
||||
Here is an example on how to define a view object:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { MyController } from "./my_controller";
|
||||
|
||||
export const myView = {
|
||||
type: "my_view",
|
||||
display_name: "MyView",
|
||||
icon: "oi oi-view-list",
|
||||
multiRecord: true,
|
||||
Controller: MyController,
|
||||
};
|
||||
|
||||
registry.category("views").add("my_controller", myView);
|
||||
|
||||
#. Add `gallery` as one of the view type in the `contacts.action_contacts` action.
|
||||
#. Make sure that you can see your hello world component when switching to the gallery view.
|
||||
|
||||
.. image:: 02_create_gallery_view/view_button.png
|
||||
:align: center
|
||||
|
||||
.. image:: 02_create_gallery_view/new_view.png
|
||||
:align: center
|
||||
|
||||
2. Use the Layout component
|
||||
===========================
|
||||
|
||||
So far, our gallery view does not look like a standard view. Let's use the `Layout` component to
|
||||
have the standard features like other views.
|
||||
|
||||
#. Import the `Layout` component and add it to the `components` of `GalleryController`.
|
||||
#. Update the template to use `Layout`. It needs a `display` prop, which can be found in
|
||||
`props.display`.
|
||||
|
||||
.. image:: 02_create_gallery_view/layout.png
|
||||
:align: center
|
||||
|
||||
3. Parse the arch
|
||||
=================
|
||||
|
||||
For now, our gallery view does not do much. Let's start by reading the information contained in the
|
||||
arch of the view.
|
||||
|
||||
The process of parsing an arch is usually done with a `ArchParser`, specific to each view. It
|
||||
inherits from a generic `XMLParser` class.
|
||||
|
||||
.. example::
|
||||
|
||||
Here is an example of what an ArchParser might look like:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
export class MyCustomArchParser {
|
||||
parse(xmlDoc) {
|
||||
const myAttribute = xmlDoc.getAttribute("my_attribute")
|
||||
return {
|
||||
myAttribute,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#. Create the `ArchParser` class in its own file.
|
||||
#. Use it to read the `image_field` information.
|
||||
#. Update the `gallery` view code to add it to the props received by the controller.
|
||||
|
||||
.. note::
|
||||
It is probably a little overkill to do it like that, since we basically only need to read one
|
||||
attribute from the arch, but it is a design that is used by every other odoo views, since it
|
||||
lets us extract some upfront processing out of the controller.
|
||||
|
||||
.. seealso::
|
||||
`Example: The graph arch parser
|
||||
<{GITHUB_PATH}/addons/web/static/src/views/graph/graph_arch_parser.js>`_
|
||||
|
||||
4. Load some data
|
||||
=================
|
||||
|
||||
Let us now get some real data from the server. For that we must use `webSearchRead` from the orm
|
||||
service.
|
||||
|
||||
.. example::
|
||||
|
||||
Here is an example of a `webSearchRead` to get the records from a model:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
const { length, records } = this.orm.webSearchRead(this.resModel, domain, {
|
||||
specification: {
|
||||
[this.fieldToFetch]: {},
|
||||
[this.secondFieldToFetch]: {},
|
||||
},
|
||||
context: {
|
||||
bin_size: true,
|
||||
}
|
||||
})
|
||||
|
||||
#. Add a :code:`loadImages(domain) {...}` method to the `GalleryController`. It should perform a
|
||||
`webSearchRead` call from the orm service to fetch records corresponding to the domain, and
|
||||
use `imageField` received in props.
|
||||
#. If you didn't include `bin_size` in the context of the call, you will receive the image field
|
||||
encoded in base64. Make sure to put `bin_size` in the context to receive the size of the image
|
||||
field. We will display the image later.
|
||||
#. Modify the `setup` code to call that method in the `onWillStart` and `onWillUpdateProps`
|
||||
hooks.
|
||||
#. Modify the template to display the id and the size of each image inside the default slot of
|
||||
the `Layout` component.
|
||||
|
||||
.. note::
|
||||
The loading data code will be moved into a proper model in a next exercise.
|
||||
|
||||
.. image:: 02_create_gallery_view/gallery_data.png
|
||||
:align: center
|
||||
|
||||
5. Solve the concurrency problem
|
||||
================================
|
||||
|
||||
For now, our code is not concurrency proof. If one changes the domain twice, it will trigger the
|
||||
`loadImages(domain)` twice. We have thus two requests that can arrive at different time depending
|
||||
on different factors. Receiving the response for the first request after receiving the response
|
||||
for the second request will lead to an inconsistent state.
|
||||
|
||||
The `KeepLast` primitive from Odoo solves this problem, it manages a list of tasks, and only
|
||||
keeps the last task active.
|
||||
|
||||
#. Import `KeepLast` from :file:`@web/core/utils/concurrency`.
|
||||
#. Instanciate a `KeepLast` object in the model.
|
||||
#. Add the `webSearchRead` call in the `KeepLast` so that only the last call is resolved.
|
||||
|
||||
.. seealso::
|
||||
`Example: usage of KeepLast <https://github.com/odoo/odoo/blob/ebf646b44f747567ff8788c884f7f18dffd453e0/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js#L164>`_
|
||||
|
||||
6. Reorganize code
|
||||
==================
|
||||
|
||||
Real views are a little bit more organized. This may be overkill in this example, but it is intended
|
||||
to learn how to structure code in Odoo. Also, this will scale better with changing requirements.
|
||||
|
||||
#. Move all the model code in its own `GalleryModel` class.
|
||||
#. Move all the rendering code in a `GalleryRenderer` component.
|
||||
#. Import `GalleryModel` and `GalleryRenderer` in `GalleryController` to make it work.
|
||||
|
||||
7. Make the view extensible
|
||||
===========================
|
||||
|
||||
To extends the view, one could import the gallery view object to modify it to their taste. The
|
||||
problem is that for the moment, it is not possible to define a custom model or renderer because it
|
||||
is hardcoded in the controller.
|
||||
|
||||
#. Import `GalleryModel` and `GalleryRenderer` in the gallery view file.
|
||||
#. Add a `Model` and `Renderer` key to the gallery view object and assign them to `GalleryModel`
|
||||
and `GalleryRenderer`. Pass `Model` and `Renderer` as props to the controller.
|
||||
#. Remove the hardcoded import in the controller and get them from the props.
|
||||
#. Use `t-component
|
||||
<https://github.com/odoo/owl/blob/master/doc/reference/component.md#dynamic-sub-components>`_ to
|
||||
have dynamic sub component.
|
||||
|
||||
.. note::
|
||||
|
||||
This is how someone could now extend the gallery view by modifying the renderer:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { galleryView } from '@awesome_gallery/gallery_view';
|
||||
import { GalleryRenderer } from '@awesome_gallery/gallery_renderer';
|
||||
|
||||
export class MyExtendedGalleryRenderer extends GalleryRenderer {
|
||||
static template = "my_module.MyExtendedGalleryRenderer";
|
||||
setup() {
|
||||
super.setup();
|
||||
console.log("my gallery renderer extension");
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("views").add("my_gallery", {
|
||||
...galleryView,
|
||||
Renderer: MyExtendedGalleryRenderer,
|
||||
});
|
||||
|
||||
8. Display images
|
||||
=================
|
||||
|
||||
Update the renderer to display images in a nice way, if the field is set. If `image_field` is
|
||||
empty, display an empty box instead.
|
||||
|
||||
.. tip::
|
||||
|
||||
There is a controller that allows to retrieve an image from a record. You can use this
|
||||
snippet to construct the link:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
import { url } from "@web/core/utils/urls";
|
||||
const url = url("/web/image", {
|
||||
model: resModel,
|
||||
id: image_id,
|
||||
field: imageField,
|
||||
});
|
||||
|
||||
.. image:: 02_create_gallery_view/tshirt_images.png
|
||||
:align: center
|
||||
|
||||
9. Switch to form view on click
|
||||
===============================
|
||||
|
||||
Update the renderer to react to a click on an image and switch to a form view. You can use the
|
||||
`switchView` function from the action service.
|
||||
|
||||
.. seealso::
|
||||
`Code: The switchView function <https://github.com/odoo/odoo/blob/db2092d8d389fdd285f54e9b34a5a99cc9523d27/addons/web/static/src/webclient/actions/action_service.js#L1064>`_
|
||||
|
||||
10. Add an optional tooltip
|
||||
===========================
|
||||
|
||||
It is useful to have some additional information on mouse hover.
|
||||
|
||||
#. Update the code to allow an optional additional attribute on the arch:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<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. To put a tooltip to an html element, you can
|
||||
put the string in the `data-tooltip` attribute of the element.
|
||||
#. Update the customer gallery view arch to add the customer as tooltip field.
|
||||
|
||||
.. image:: 02_create_gallery_view/image_tooltip.png
|
||||
:align: center
|
||||
:scale: 50%
|
||||
|
||||
.. seealso::
|
||||
`Example: usage of t-att-data-tooltip <https://github.com/odoo/odoo/blob/145fe958c212ddef9fab56a232c8b2d3db635c8e/addons/survey/static/src/views/widgets/survey_question_trigger/survey_question_trigger.xml#L8>`_
|
||||
|
||||
11. Add pagination
|
||||
==================
|
||||
|
||||
Let's add a pager on the control panel and manage all the pagination like in a normal Odoo view.
|
||||
|
||||
.. image:: 02_create_gallery_view/pagination.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
- `Code: The usePager hook <{GITHUB_PATH}/addons/web/static/src/search/pager_hook.js>`_
|
||||
- `Example: usePager in list controller <https://github.com/odoo/odoo/blob/48ef812a635f70571b395f82ffdb2969ce99da9e/addons/web/static/src/views/list/list_controller.js#L109-L128>`_
|
||||
|
||||
12. Validating views
|
||||
=====================
|
||||
|
||||
We have a nice and useful view so far. But in real life, we may have issue with users incorrectly
|
||||
encoding the `arch` of their Gallery view: it is currently only an unstructured piece of XML.
|
||||
|
||||
Let us add some validation! In Odoo, XML documents can be described with an RN file
|
||||
:dfn:`(Relax NG file)`, and then validated.
|
||||
|
||||
#. Add an RNG file that describes the current grammar:
|
||||
|
||||
- A mandatory attribute `image_field`.
|
||||
- An optional attribute: `tooltip_field`.
|
||||
|
||||
#. Add some code to make sure all views are validated against this RNG file.
|
||||
#. While we are at it, let us make sure that `image_field` and `tooltip_field` are fields from
|
||||
the current model.
|
||||
|
||||
Since validating an RNG file is not trivial, here is a snippet to help:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import os
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo.loglevels import ustr
|
||||
from odoo.tools import misc, view_validation
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
_viewname_validator = None
|
||||
|
||||
@view_validation.validate('viewname')
|
||||
def schema_viewname(arch, **kwargs):
|
||||
""" Check the gallery view against its schema
|
||||
|
||||
:type arch: etree._Element
|
||||
"""
|
||||
global _viewname_validator
|
||||
|
||||
if _viewname_validator is None:
|
||||
with misc.file_open(os.path.join('modulename', 'rng', 'viewname.rng')) as f:
|
||||
_viewname_validator = etree.RelaxNG(etree.parse(f))
|
||||
|
||||
if _viewname_validator.validate(arch):
|
||||
return True
|
||||
|
||||
for error in _viewname_validator.error_log:
|
||||
_logger.error(ustr(error))
|
||||
return False
|
||||
|
||||
.. seealso::
|
||||
`Example: The RNG file of the graph view <https://github.com/odoo/odoo/blob/70942e4cfb7a8993904b4d142e3b1749a40db806/odoo/addons/base/rng/graph_view.rng>`_
|
||||
|
||||
13. Uploading an image
|
||||
======================
|
||||
|
||||
Our gallery view does not allow users to upload images. Let us implement that.
|
||||
|
||||
#. Add a button on each image by using the `FileUploader` component.
|
||||
#. The `FileUploader` component accepts the `onUploaded` props, which is called when the user
|
||||
uploads an image. Make sure to call `webSave` from the orm service to upload the new image.
|
||||
#. You maybe noticed that the image is uploaded but it is not re-rendered by the browser.
|
||||
This is because the image link did not change so the browser do not re-fetch them. Include
|
||||
the `write_date` from the record to the image url.
|
||||
#. Make sure that clicking on the upload button does not trigger the switchView.
|
||||
|
||||
.. image:: 02_create_gallery_view/upload_image.png
|
||||
:align: center
|
||||
:scale: 50%
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Example: usage of FileUploader <https://github.com/odoo/odoo/blob/7710c3331ebd22f8396870bd0731f8c1152d9c41/addons/mail/static/src/web/activity/activity.xml#L48-L52>`_
|
||||
- `Odoo: webSave definition <https://github.com/odoo/odoo/blob/ebd538a1942c532bcf1c9deeab3c25efe23b6893/addons/web/static/src/core/orm_service.js#L312>`_
|
||||
|
||||
14. Advanced tooltip template
|
||||
=============================
|
||||
|
||||
For now we can only specify a tooltip field. But what if we want to allow to write a specific
|
||||
template for it ?
|
||||
|
||||
.. example::
|
||||
|
||||
This is an example of a gallery arch view that should work after this exercise.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<record id="contacts_gallery_view" model="ir.ui.view">
|
||||
<field name="name">awesome_gallery.orders.gallery</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<gallery image_field="image_1920" tooltip_field="name">
|
||||
<field name="email"/> <!-- Specify to the model that email should be fetched -->
|
||||
<field name="name"/> <!-- Specify to the model that name should be fetched -->
|
||||
<tooltip-template> <!-- Specify the owl template for the tooltip -->
|
||||
<p class="m-0">name: <field name="name"/></p> <!-- field is compiled into a t-esc-->
|
||||
<p class="m-0">e-mail: <field name="email"/></p>
|
||||
</tooltip-template>
|
||||
</gallery>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
#. Replace the `res.partner` gallery arch view in :file:`awesome_gallery/views/views.xml` with
|
||||
the arch in example above. Don't worry if it does not pass the rng validation.
|
||||
#. Modify the gallery rng validator to accept the new arch structure.
|
||||
|
||||
.. tip::
|
||||
|
||||
You can use this rng snippet to validate the tooltip-template tag
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<rng:define name="tooltip-template">
|
||||
<rng:element name="tooltip-template">
|
||||
<rng:zeroOrMore>
|
||||
<rng:text/>
|
||||
<rng:ref name="any"/>
|
||||
</rng:zeroOrMore>
|
||||
</rng:element>
|
||||
</rng:define>
|
||||
|
||||
<rng:define name="any">
|
||||
<rng:element>
|
||||
<rng:anyName/>
|
||||
<rng:zeroOrMore>
|
||||
<rng:choice>
|
||||
<rng:attribute>
|
||||
<rng:anyName/>
|
||||
</rng:attribute>
|
||||
<rng:text/>
|
||||
<rng:ref name="any"/>
|
||||
</rng:choice>
|
||||
</rng:zeroOrMore>
|
||||
</rng:element>
|
||||
</rng:define>
|
||||
#. The arch parser should parse the fields and the tooltip template. Import `visitXML` from
|
||||
:file:`@web/core/utils/xml` and use it to parse field names and the tooltip template.
|
||||
#. Make sure that the model call the `webSearchRead` by including the parsed field names in the
|
||||
specification.
|
||||
#. The renderer (or any sub-component you created for it) should receive the parsed tooltip
|
||||
template. Manipulate this template to replace the `<field>` element into a `<t t-esc="x">`
|
||||
element.
|
||||
|
||||
.. tip::
|
||||
|
||||
The template is an `Element` object so it can be manipulated like a HTML element.
|
||||
|
||||
#. Register the template to Owl thanks to the `xml` function from :file:`@odoo/owl`.
|
||||
#. Use the `useTooltip` hook from :file:`@web/core/tooltip/tooltip_hook` to display the
|
||||
tooltips. This hooks take as argument the Owl template and the variable needed by the
|
||||
template.
|
||||
|
||||
.. image:: 02_create_gallery_view/advanced_tooltip.png
|
||||
:align: center
|
||||
:scale: 50%
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Example: useTooltip used in Kaban <https://github.com/odoo/odoo/blob/0e6481f359e2e4dd4f5b5147a1754bb3cca57311/addons/web/static/src/views/kanban/kanban_record.js#L189-L192>`_
|
||||
- `Example: visitXML usage <https://github.com/odoo/odoo/blob/48ef812a635f70571b395f82ffdb2969ce99da9e/addons/web/static/src/views/list/list_arch_parser.js#L19>`_
|
||||
- `Owl: Inline templates with xml helper function <https://github.com/odoo/owl/blob/master/doc/reference/templates.md#inline-templates>`_
|
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 608 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 905 KiB |
After Width: | Height: | Size: 125 KiB |
After Width: | Height: | Size: 15 KiB |
@ -1,299 +0,0 @@
|
||||
========================
|
||||
Chapter 2: Miscellaneous
|
||||
========================
|
||||
|
||||
In the previous task, we learned how to create fields and views. There is still much more to
|
||||
discover in the feature-rich Odoo web framework, so let's dive in and explore more in this chapter!
|
||||
|
||||
.. graph TD
|
||||
.. subgraph "Owl"
|
||||
.. C[Component]
|
||||
.. T[Template]
|
||||
.. H[Hook]
|
||||
.. S[Slot]
|
||||
.. E[Event]
|
||||
.. end
|
||||
|
||||
.. subgraph "odoo"[Odoo Javascript framework]
|
||||
.. Services
|
||||
.. Translation
|
||||
.. lazy[Lazy loading libraries]
|
||||
.. SCSS
|
||||
.. action --> Services
|
||||
.. rpc --> Services
|
||||
.. orm --> Services
|
||||
.. Fields
|
||||
.. Views
|
||||
.. Registries
|
||||
.. end
|
||||
|
||||
.. odoo[Odoo JavaScript framework] --> Owl
|
||||
|
||||
.. figure:: 02_miscellaneous/previously_learned.svg
|
||||
:align: center
|
||||
:width: 70%
|
||||
|
||||
This is the progress that we have made in discovering the JavaScript web framework at the end of
|
||||
:doc:`01_fields_and_views`.
|
||||
|
||||
.. admonition:: Goal
|
||||
|
||||
.. image:: 02_miscellaneous/kitten_mode.png
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the
|
||||
`official Odoo tutorials repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_tshirt>`_.
|
||||
|
||||
1. Interacting with the notification system
|
||||
===========================================
|
||||
|
||||
.. note::
|
||||
This task depends on :doc:`the previous exercises <01_fields_and_views>`.
|
||||
|
||||
After using the :guilabel:`Print Label` button for some t-shirt tasks, it is apparent that there
|
||||
should be some feedback that the `print_label` action is completed (or failed, for example, the
|
||||
printer is not connected or ran out of paper).
|
||||
|
||||
.. exercise::
|
||||
#. Display a :ref:`notification <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:: 02_miscellaneous/notification.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
.. seealso::
|
||||
`Example: Using the notification service
|
||||
<{GITHUB_PATH}/addons/web/static/src/views/fields/image_url/image_url_field.js>`_
|
||||
|
||||
2. Add a systray item
|
||||
=====================
|
||||
|
||||
Our beloved leader wants to keep a close eye on new orders. He wants to see the number of new,
|
||||
unprocessed orders at all time. Let's do that with a systray item.
|
||||
|
||||
A :ref:`systray <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:: 02_miscellaneous/systray.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
- `Example: Systray item <{GITHUB_PATH}/addons/web/static/src/webclient/user_menu/user_menu.js>`_
|
||||
- `Example: Adding some information to the "session info"
|
||||
<{GITHUB_PATH}/addons/barcodes/models/ir_http.py>`_
|
||||
- `Example: Reading the session information
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
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::
|
||||
|
||||
#. The `tshirt` service should periodically reload its data.
|
||||
|
||||
Now, the question arises: how is the systray item notified that it should re-render itself? It can
|
||||
be done in various ways but, for this training, we choose to use the most *declarative* approach:
|
||||
|
||||
.. exercise::
|
||||
|
||||
2. Modify the `tshirt` service to return a `reactive
|
||||
<{OWL_PATH}/doc/reference/reactivity.md#reactive>`_ object. Reloading data should update the
|
||||
reactive object in place.
|
||||
3. The systray item can then perform a `useState
|
||||
<{OWL_PATH}/doc/reference/reactivity.md#usestate>`_ on the service return value.
|
||||
4. This is not really necessary, but you can also *package* the calls to `useService` and
|
||||
`useState` in a custom hook `useStatistics`.
|
||||
|
||||
.. seealso::
|
||||
- `Documentation on reactivity <{OWL_PATH}/doc/reference/reactivity.md>`_
|
||||
- `Example: Use of reactive in a service
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
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::
|
||||
|
||||
Modify the :ref:`image preview field <tutorials/master_odoo_web_framework/image_preview_field>`
|
||||
to add a command to the command palette to open the image in a new browser tab (or window).
|
||||
|
||||
Ensure the command is only active whenever a field preview is visible on the screen.
|
||||
|
||||
.. image:: 02_miscellaneous/new_command.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`Example: Using the useCommand hook
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/debug/debug_menu.js#L15>`_
|
||||
|
||||
5. Monkey patching a component
|
||||
==============================
|
||||
|
||||
Often, we can achieve what we want by using existing extension points that allow for customization,
|
||||
such as registering something in a registry. Sometimes, however, it happens that we want to modify
|
||||
something that has no such mechanism. In that case, we must fall back on a less safe form of
|
||||
customization: monkey patching. Almost everything in Odoo can be monkey patched.
|
||||
|
||||
Bafien, our beloved leader, heard about employees performing better if they are constantly being
|
||||
watched. Since he cannot be there in person for each of his employees, he tasked you with updating
|
||||
the user interface to add a blinking red eye in the control panel. Clicking on that eye should open
|
||||
a dialog with the following message: "Bafien is watching you. This interaction is recorded and may
|
||||
be used in legal proceedings if necessary. Do you agree to these terms?"
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. :ref:`Inherit <reference/qweb/template_inheritance>` the `web.Breadcrumbs` template of the
|
||||
`ControlPanel component <{GITHUB_PATH}/addons/web/static/src/search/control_panel>`_ to add an
|
||||
icon next to the breadcrumbs. You might want to use the `fa-eye` or `fa-eyes` icons.
|
||||
#. :doc:`Patch </developer/reference/frontend/patching_code>` the component to display the
|
||||
message on click by using `the dialog service
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/dialog/dialog_service.js>`_. You can use
|
||||
`ConfirmationDialog
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/confirmation_dialog/confirmation_dialog.js>`_.
|
||||
#. Add the CSS class `blink` to the element representing the eye and paste the following code in
|
||||
a new CSS file located in your patch's directory.
|
||||
|
||||
.. code-block:: css
|
||||
|
||||
.blink {
|
||||
animation: blink-animation 1s steps(5, start) infinite;
|
||||
-webkit-animation: blink-animation 1s steps(5, start) infinite;
|
||||
}
|
||||
@keyframes blink-animation {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes blink-animation {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.. image:: 02_miscellaneous/bafien_eye.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
.. image:: 02_miscellaneous/confirmation_dialog.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
.. seealso::
|
||||
- `Code: The patch function
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/utils/patch.js#L16>`_
|
||||
- `The Font Awesome website <https://fontawesome.com/>`_
|
||||
- `Example: Using the dialog service
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/board/static/src/board_controller.js#L88>`_
|
||||
|
||||
6. Fetching orders from a customer
|
||||
==================================
|
||||
|
||||
Let's see how to use some standard components to build a powerful feature combining autocomplete,
|
||||
fetching data, and fuzzy lookup. We will add an input in our dashboard to easily search all orders
|
||||
from a given customer.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Update :file:`tshirt_service.js` to add a `loadCustomers` method, which returns a promise that
|
||||
returns the list of all customers (and only performs the call once).
|
||||
#. Add the `AutoComplete component <{GITHUB_PATH}/addons/web/static/src/core/autocomplete>`_ to
|
||||
the dashboard, next to the buttons in the control panel.
|
||||
#. Fetch the list of customers with the tshirt service, and display it in the AutoComplete
|
||||
component, filtered by the `fuzzyLookup
|
||||
<{GITHUB_PATH}/addons/web/static/src/core/utils/search.js>`_ method.
|
||||
|
||||
.. image:: 02_miscellaneous/autocomplete.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
7. Reintroduce Kitten Mode
|
||||
==========================
|
||||
|
||||
Let us add a special mode to Odoo: whenever the URL contains `kitten=1`, we will display a kitten in
|
||||
the background of Odoo, because we like kittens.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Create a `kitten` service, which should check the content of the active URL hash with the
|
||||
help of the :ref:`router service <frontend/services/router>`. If `kitten` is set in the URL,
|
||||
add the class `o-kitten-mode` to the document body.
|
||||
#. Add the following SCSS in :file:`kitten_mode.scss`:
|
||||
|
||||
.. code-block:: css
|
||||
|
||||
.o-kitten-mode {
|
||||
background-image: url(https://upload.wikimedia.org/wikipedia/commons/5/58/Mellow_kitten_%28Unsplash%29.jpg);
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.o-kitten-mode > * {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
#. Add a command to the command palette to toggle the kitten mode. Toggling the kitten mode
|
||||
should toggle the class `o-kitten-mode` and update the current URL accordingly.
|
||||
|
||||
.. image:: 02_miscellaneous/kitten_mode.png
|
||||
:align: center
|
||||
|
||||
8. Lazy loading our dashboard
|
||||
=============================
|
||||
|
||||
This is not really necessary, but the exercise is interesting. Imagine that our awesome dashboard is
|
||||
a large application with potentially multiple external libraries and lots of code/styles/templates.
|
||||
Also, suppose that the dashboard is used only by some users in some business flows. It would be
|
||||
interesting to lazy load it in order to speed up the loading of the web client in most cases.
|
||||
|
||||
So, let us do that!
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Modify the manifest to create a new :ref:`bundle <reference/assets_bundle>`
|
||||
`awesome_tshirt.dashboard`.
|
||||
#. Add the awesome dashboard code to this bundle. Create folders and move files if needed.
|
||||
#. Remove the code from the `web.assets_backend` bundle so that it is not loaded twice.
|
||||
|
||||
So far, we only removed the dashboard from the main bundle; we now want to lazy load it. Currently,
|
||||
no client action is registered in the action registry.
|
||||
|
||||
.. exercise::
|
||||
|
||||
4. Create a new file :file:`dashboard_loader.js`.
|
||||
5. Copy the code registering `AwesomeDashboard` to the dashboard loader.
|
||||
6. Register `AwesomeDashboard` as a `LazyComponent
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
addons/web/static/src/core/assets.js#L265-L282>`_.
|
||||
7. Modify the code in the dashboard loader to use the lazy component `AwesomeDashboard`.
|
||||
|
||||
If you open the :guilabel:`Network` tab of your browser's dev tools, you should see that
|
||||
:file:`awesome_tshirt.dashboard.min.js` is now loaded only when the Dashboard is first accessed.
|
||||
|
||||
.. seealso::
|
||||
:ref:`Documentation on assets <reference/assets>`
|
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 2.0 MiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 8.7 KiB |
@ -1,171 +0,0 @@
|
||||
=============================
|
||||
Chapter 3: Custom kanban view
|
||||
=============================
|
||||
|
||||
.. todo:: It'd be cool to follow the naming convention of the previous chapters: "Chapter N: The concept studied in the chapter"
|
||||
|
||||
.. warning::
|
||||
It is highly recommended that you complete :doc:`01_fields_and_views` before starting this
|
||||
chapter. The concepts introduced in Chapter 3, including views and examples, will be essential
|
||||
for understanding the material covered in this chapter.
|
||||
|
||||
We have gained an understanding of the numerous capabilities offered by the Odoo web framework. As a
|
||||
next step, we will customize a kanban view. This is a more complicated project that will showcase
|
||||
some non trivial aspects of the framework. The goal is to practice composing views, coordinating
|
||||
various aspects of the UI, and doing it in a maintainable way.
|
||||
|
||||
Bafien had the greatest idea ever: a mix of a kanban view and a list view would be perfect for your
|
||||
needs! In a nutshell, he wants a list of customers on the left of the task's kanban view. When you
|
||||
click on a customer on the left sidebar, the kanban view on the right is filtered to only display
|
||||
orders linked to that customer.
|
||||
|
||||
.. admonition:: Goal
|
||||
|
||||
.. image:: 03_custom_kanban_view/overview.png
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the
|
||||
`official Odoo tutorials repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_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:: 03_custom_kanban_view/customer_list.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
3. Load and display data
|
||||
========================
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Modify the `CustomerList` component to fetch a list of all customers in `onWillStart`.
|
||||
#. Display the list in the template with a `t-foreach`.
|
||||
#. Whenever a customer is selected, call the `selectCustomer` function prop.
|
||||
|
||||
.. image:: 03_custom_kanban_view/customer_data.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
4. Update the main kanban view
|
||||
==============================
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Implement `selectCustomer` in the kanban controller to add the proper domain.
|
||||
#. Modify the template to give the real function to the `CustomerList` `selectCustomer` prop.
|
||||
|
||||
Since it is not trivial to interact with the search view, here is a quick snippet to help:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
selectCustomer(customer_id, customer_name) {
|
||||
this.env.searchModel.setDomainParts({
|
||||
customer: {
|
||||
domain: [["customer_id", "=", customer_id]],
|
||||
facetLabel: customer_name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
.. image:: 03_custom_kanban_view/customer_filter.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
5. Only display customers which have an active order
|
||||
====================================================
|
||||
|
||||
There is a `has_active_order` field on `res.partner`. Let us allow the user to filter results on
|
||||
customers with an active order.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Add an input of type checkbox in the `CustomerList` component, with a label "Active customers"
|
||||
next to it.
|
||||
#. Changing the value of the checkbox should filter the list on customers with an active order.
|
||||
|
||||
.. image:: 03_custom_kanban_view/active_customer.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
6. Add a search bar to the customer list
|
||||
========================================
|
||||
|
||||
.. exercise::
|
||||
|
||||
Add an input above the customer list that allows the user to enter a string and to filter the
|
||||
displayed customers, according to their name.
|
||||
|
||||
.. tip::
|
||||
You can use the `fuzzyLookup` function to perform the filter.
|
||||
|
||||
.. image:: 03_custom_kanban_view/customer_search.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Code: The fuzzylookup function <{GITHUB_PATH}/addons/web/static/src/core/utils/search.js>`_
|
||||
- `Example: Using fuzzyLookup
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
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:: 03_custom_kanban_view/customer_pager.png
|
||||
:align: center
|
||||
:scale: 60%
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 22 KiB |
@ -0,0 +1,200 @@
|
||||
==================================
|
||||
Chapter 3: Customize a kanban view
|
||||
==================================
|
||||
|
||||
We have gained an understanding of the numerous capabilities offered by the Odoo web framework. As a
|
||||
next step, we will customize a kanban view. This is a more complicated project that will showcase
|
||||
some non trivial aspects of the framework. The goal is to practice composing views, coordinating
|
||||
various aspects of the UI, and doing it in a maintainable way.
|
||||
|
||||
Bafien had the greatest idea ever: a mix of a kanban view and a list view would be perfect for your
|
||||
needs! In a nutshell, he wants a list of customers on the left of the CRM kanban view. When you
|
||||
click on a customer on the left sidebar, the kanban view on the right is filtered to only display
|
||||
leads linked to that customer.
|
||||
|
||||
.. admonition:: Goal
|
||||
|
||||
.. image:: 03_customize_kanban_view/overview.png
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the
|
||||
`official Odoo tutorials repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-master-odoo-web-framework-solutions/awesome_kanban>`_.
|
||||
|
||||
1. Create a new kanban view
|
||||
===========================
|
||||
|
||||
Since we are customizing the kanban view, let us start by extending it and using our extension in
|
||||
the kanban view of CRM.
|
||||
|
||||
#. Create a new empty component that extends the `KanbanController` component from
|
||||
:file:`@web/views/kanban/kanban_controller`.
|
||||
#. Create a new view object and assign all keys and values from `kanbanView` from
|
||||
:file:`@web/views/kanban/kanban_view`. Override the Controller key by putting your newly
|
||||
created controller.
|
||||
#. Register it in the views registry under `awesome_kanban`.
|
||||
#. Update the crm kanban arch in :file:`awesome_kanban/views/views.xml` to use the extended view.
|
||||
This can be done by specifying the `js_class` attribute in the kanban node.
|
||||
|
||||
.. seealso::
|
||||
|
||||
`Example: Create a new view by extending a pre-existing one <https://github.com/odoo/odoo/blob/0a59f37e7dd73daff2e9926542312195b3de4154/addons/todo/static/src/views/todo_conversion_form/todo_conversion_form_view.js>`_
|
||||
|
||||
2. Create a CustomerList component
|
||||
==================================
|
||||
|
||||
We will need to display a list of customers, so we might as well create the component.
|
||||
|
||||
#. Create a `CustomerList` component which only displays a `div` with some text for now.
|
||||
#. It should have a `selectCustomer` prop.
|
||||
#. Create a new template extending (XPath) the kanban controller template `web.KanbanView` to add
|
||||
the `CustomerList` next to the kanban renderer. Give it an empty function as `selectCustomer`
|
||||
for now.
|
||||
|
||||
.. tip::
|
||||
|
||||
You can use this xpath inside the template to add a div before the renderer component.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<xpath expr="//t[@t-component='props.Renderer']" position="before">
|
||||
...
|
||||
</xpath>
|
||||
|
||||
#. Subclass the kanban controller to add `CustomerList` in its sub-components.
|
||||
#. Make sure you see your component in the kanban view.
|
||||
|
||||
.. image:: 03_customize_kanban_view/customer_list_component.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`Template inheritance <reference/qweb/template_inheritance>`
|
||||
|
||||
3. Load and display data
|
||||
========================
|
||||
|
||||
#. Modify the `CustomerList` component to fetch a list of all customers in `onWillStart`.
|
||||
#. Display the list in the template with a `t-foreach`.
|
||||
#. Whenever a customer is selected, call the `selectCustomer` function prop.
|
||||
|
||||
.. image:: 03_customize_kanban_view/customer_data.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Example: fetching records from a model <https://github.com/odoo/odoo/blob/986c00c1bd1b3ca16a04ab25f5a2504108136112/addons/project/static/src/views/burndown_chart/burndown_chart_model.js#L26-L31>`_
|
||||
|
||||
4. Update the main kanban view
|
||||
==============================
|
||||
|
||||
#. Implement `selectCustomer` in the kanban controller to add the proper domain.
|
||||
|
||||
.. tip::
|
||||
|
||||
Since it is not trivial to interact with the search view, here is a snippet to create a
|
||||
filter:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
this.env.searchModel.createNewFilters([{
|
||||
description: partner_name,
|
||||
domain: [["partner_id", "=", partner_id]],
|
||||
isFromAwesomeKanban: true, // this is a custom key to retrieve our filters later
|
||||
}])
|
||||
|
||||
#. By clicking on multiple customers, you can see that the old customer filter is not replaced.
|
||||
Make sure that by clicking on a customer, the old filter is replaced by the new one.
|
||||
|
||||
.. tip::
|
||||
|
||||
You can use this snippet to get the customers filters and toggle them.
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
const customerFilters = this.env.searchModel.getSearchItems((searchItem) =>
|
||||
searchItem.isFromAwesomeKanban
|
||||
);
|
||||
|
||||
for (const customerFilter of customerFilters) {
|
||||
if (customerFilter.isActive) {
|
||||
this.env.searchModel.toggleSearchItem(customerFilter.id);
|
||||
}
|
||||
}
|
||||
|
||||
#. Modify the template to give the real function to the `CustomerList` `selectCustomer` prop.
|
||||
|
||||
.. note::
|
||||
|
||||
You can use `Symbol
|
||||
<https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol>`_
|
||||
to make sure that the custom `isFromAwesomeKanban` key will not collide with keys any other
|
||||
code might add to the object.
|
||||
|
||||
.. image:: 03_customize_kanban_view/customer_filter.png
|
||||
:align: center
|
||||
|
||||
5. Only display customers which have an active order
|
||||
====================================================
|
||||
|
||||
There is a `opportunity_ids` field on `res.partner`. Let us allow the user to filter results on
|
||||
customers with at least one opportunity.
|
||||
|
||||
#. Add an input of type checkbox in the `CustomerList` component, with a label "Active customers"
|
||||
next to it.
|
||||
#. Changing the value of the checkbox should filter the list of customers.
|
||||
|
||||
.. image:: 03_customize_kanban_view/active_customer.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
6. Add a search bar to the customer list
|
||||
========================================
|
||||
|
||||
Add an input above the customer list that allows the user to enter a string and to filter the
|
||||
displayed customers, according to their name.
|
||||
|
||||
.. tip::
|
||||
You can use the `fuzzyLookup` from :file:`@web/core/utils/search` function to perform the
|
||||
filter.
|
||||
|
||||
.. image:: 03_customize_kanban_view/customer_search.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
.. seealso::
|
||||
|
||||
- `Code: The fuzzylookup function <https://github.com/odoo/odoo/blob/235fc69280a18a5805d8eb84d76ada91ba49fe67/addons/web/static/src/core/utils/search.js#L41-L54>`_
|
||||
- `Example: Using fuzzyLookup
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
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.
|
||||
|
||||
#. Make sure you have a reactive object that represents the fact that the filter is active
|
||||
(something like
|
||||
:code:`this.state = useState({ displayActiveCustomers: false, searchString: ''})`).
|
||||
#. Modify the code to add a getter `displayedCustomers` which returns the currently active list
|
||||
of customers.
|
||||
#. Modify the template to use `t-model`.
|
||||
|
||||
8. Paginate customers!
|
||||
======================
|
||||
|
||||
#. Add a :ref:`pager <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:: 03_customize_kanban_view/customer_pager.png
|
||||
:align: center
|
||||
:scale: 60%
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 95 KiB |
@ -1,272 +0,0 @@
|
||||
=======================================
|
||||
Chapter 4: Creating a view from scratch
|
||||
=======================================
|
||||
|
||||
.. warning::
|
||||
It is highly recommended that you complete :doc:`01_fields_and_views` before starting this
|
||||
chapter. The concepts introduced in Chapter 3, including views and examples, will be essential
|
||||
for understanding the material covered in this chapter.
|
||||
|
||||
Let us see how one can create a new view, completely from scratch. In a way, it is not very
|
||||
difficult to do, but there are no really good resources on how to do it. Note that most situations
|
||||
should be solved by either customizing an existing view, or with a client action.
|
||||
|
||||
For this exercise, let's assume that we want to create a `gallery` view, which is a view that lets
|
||||
us represent a set of records with an image field. In our Awesome Tshirt scenario, we would like to
|
||||
be able to see a set of t-shirts images.
|
||||
|
||||
The problem could certainly be solved with a kanban view, but this means that it is not possible to
|
||||
have our normal kanban view and the gallery view in the same action.
|
||||
|
||||
Let us make a gallery view. Each gallery view will be defined by an `image_field` attribute in its
|
||||
arch:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<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:: 04_creating_view_from_scratch/overview.png
|
||||
:align: center
|
||||
|
||||
.. spoiler:: Solutions
|
||||
|
||||
The solutions for each exercise of the chapter are hosted on the
|
||||
`official Odoo tutorials repository
|
||||
<https://github.com/odoo/tutorials/commits/{CURRENT_MAJOR_BRANCH}-solutions/awesome_gallery>`_.
|
||||
|
||||
1. Make a hello world view
|
||||
==========================
|
||||
|
||||
First step is to create a JavaScript implementation with a simple component.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Create the `gallery_view.js` , `gallery_controller.js` and `gallery_controller.xml` files in
|
||||
`static/src`.
|
||||
#. Implement a simple hello world component in `gallery_controller.js`.
|
||||
#. In `gallery_view.js`, import the controller, create a view object, and register it in the
|
||||
view registry under the name `gallery`.
|
||||
#. Add `gallery` as one of the view type in the orders action.
|
||||
#. Make sure that you can see your hello world component when switching to the gallery view.
|
||||
|
||||
.. image:: 04_creating_view_from_scratch/view_button.png
|
||||
:align: center
|
||||
|
||||
.. image:: 04_creating_view_from_scratch/new_view.png
|
||||
:align: center
|
||||
|
||||
2. Use the Layout component
|
||||
===========================
|
||||
|
||||
So far, our gallery view does not look like a standard view. Let's use the `Layout` component to
|
||||
have the standard features like other views.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Import the `Layout` component and add it to the `components` of `GalleryController`.
|
||||
#. Update the template to use `Layout`. It needs a `display` prop, which can be found in
|
||||
`props.display`.
|
||||
|
||||
.. image:: 04_creating_view_from_scratch/layout.png
|
||||
:align: center
|
||||
|
||||
3. Parse the arch
|
||||
=================
|
||||
|
||||
For now, our gallery view does not do much. Let's start by reading the information contained in the
|
||||
arch of the view.
|
||||
|
||||
The process of parsing an arch is usually done with a `ArchParser`, specific to each view. It
|
||||
inherits from a generic `XMLParser` class.
|
||||
|
||||
.. example::
|
||||
|
||||
Here is an example of what an ArchParser might look like:
|
||||
|
||||
.. code-block:: js
|
||||
|
||||
import { XMLParser } from "@web/core/utils/xml";
|
||||
|
||||
export class GraphArchParser extends XMLParser {
|
||||
parse(arch, fields) {
|
||||
const result = {};
|
||||
this.visitXML(arch, (node) => {
|
||||
...
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Create the `ArchParser` class in its own file. It can inherit from `XMLParser` in
|
||||
`@web/core/utils/xml`.
|
||||
#. Use it to read the `image_field` information.
|
||||
#. Update the `gallery` view code to add it to the props received by the controller.
|
||||
|
||||
.. note::
|
||||
It is probably a little overkill to do it like that, since we basically only need to read one
|
||||
attribute from the arch, but it is a design that is used by every other odoo views, since it
|
||||
lets us extract some upfront processing out of the controller.
|
||||
|
||||
.. seealso::
|
||||
`Example: The graph arch parser
|
||||
<{GITHUB_PATH}/addons/web/static/src/views/graph/graph_arch_parser.js>`_
|
||||
|
||||
4. Load some data
|
||||
=================
|
||||
|
||||
Let us now get some real data.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Add a :code:`loadImages(domain) {...}` method to the `GalleryController`. It should perform a
|
||||
`webSearchRead` call from the orm service to fetch records corresponding to the domain, and
|
||||
use `imageField` received in props.
|
||||
#. Modify the `setup` code to call that method in the `onWillStart` and `onWillUpdateProps`
|
||||
hooks.
|
||||
#. Modify the template to display the data inside the default slot of the `Layout` component.
|
||||
|
||||
.. note::
|
||||
The loading data code will be moved into a proper model in the next exercise.
|
||||
|
||||
.. image:: 04_creating_view_from_scratch/gallery_data.png
|
||||
:align: center
|
||||
|
||||
5. Reorganize code
|
||||
==================
|
||||
|
||||
Real views are a little bit more organized. This may be overkill in this example, but it is intended
|
||||
to learn how to structure code in Odoo. Also, this will scale better with changing requirements.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Move all the model code in its own `GalleryModel` class.
|
||||
#. Move all the rendering code in a `GalleryRenderer` component.
|
||||
#. Adapt the `GalleryController` and `gallery_view` to make it work.
|
||||
|
||||
6. Display images
|
||||
=================
|
||||
|
||||
.. exercise::
|
||||
|
||||
Update the renderer to display images in a nice way, if the field is set. If `image_field` is
|
||||
empty, display an empty box instead.
|
||||
|
||||
.. image:: 04_creating_view_from_scratch/tshirt_images.png
|
||||
:align: center
|
||||
|
||||
7. Switch to form view on click
|
||||
===============================
|
||||
|
||||
.. exercise::
|
||||
|
||||
Update the renderer to react to a click on an image and switch to a form view. You can use the
|
||||
`switchView` function from the action service.
|
||||
|
||||
.. seealso::
|
||||
`Code: The switchView function
|
||||
<https://github.com/odoo/odoo/blob/1f4e583ba20a01f4c44b0a4ada42c4d3bb074273/
|
||||
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:: 04_creating_view_from_scratch/image_tooltip.png
|
||||
:align: center
|
||||
:scale: 60%
|
||||
|
||||
.. seealso::
|
||||
`Code: The tooltip hook <{GITHUB_PATH}/addons/web/static/src/core/tooltip/tooltip_hook.js>`_
|
||||
|
||||
9. Add pagination
|
||||
=================
|
||||
|
||||
.. exercise::
|
||||
|
||||
Let's add a pager on the control panel and manage all the pagination like in a normal Odoo view.
|
||||
Note that it is surprisingly difficult.
|
||||
|
||||
.. image:: 04_creating_view_from_scratch/pagination.png
|
||||
:align: center
|
||||
|
||||
.. seealso::
|
||||
`Code: The usePager hook <{GITHUB_PATH}/addons/web/static/src/search/pager_hook.js>`_
|
||||
|
||||
10. Validating views
|
||||
=====================
|
||||
|
||||
We have a nice and useful view so far. But in real life, we may have issue with users incorrectly
|
||||
encoding the `arch` of their Gallery view: it is currently only an unstructured piece of XML.
|
||||
|
||||
Let us add some validation! In Odoo, XML documents can be described with an RN file
|
||||
:dfn:`(Relax NG file)`, and then validated.
|
||||
|
||||
.. exercise::
|
||||
|
||||
#. Add an RNG file that describes the current grammar:
|
||||
|
||||
- A mandatory attribute `image_field`.
|
||||
- An optional attribute: `tooltip_field`.
|
||||
|
||||
#. Add some code to make sure all views are validated against this RNG file.
|
||||
#. While we are at it, let us make sure that `image_field` and `tooltip_field` are fields from
|
||||
the current model.
|
||||
|
||||
Since validating an RNG file is not trivial, here is a snippet to help:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import os
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo.loglevels import ustr
|
||||
from odoo.tools import misc, view_validation
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
_viewname_validator = None
|
||||
|
||||
@view_validation.validate('viewname')
|
||||
def schema_viewname(arch, **kwargs):
|
||||
""" Check the gallery view against its schema
|
||||
|
||||
:type arch: etree._Element
|
||||
"""
|
||||
global _viewname_validator
|
||||
|
||||
if _viewname_validator is None:
|
||||
with misc.file_open(os.path.join('modulename', 'rng', 'viewname.rng')) as f:
|
||||
_viewname_validator = etree.RelaxNG(etree.parse(f))
|
||||
|
||||
if _viewname_validator.validate(arch):
|
||||
return True
|
||||
|
||||
for error in _viewname_validator.error_log:
|
||||
_logger.error(ustr(error))
|
||||
return False
|
||||
|
||||
.. seealso::
|
||||
`Example: The RNG file of the graph view <{GITHUB_PATH}/addons/base/rng/graph_view.rng>`_
|
Before Width: | Height: | Size: 69 KiB |