From 7ae8cc99e87526160acc06206ca81a2dd12e839e Mon Sep 17 00:00:00 2001 From: Vincent Vallaeys Date: Fri, 21 Aug 2026 13:34:50 +0200 Subject: [PATCH 1/2] [IMP] awesome_owl: Chapter 1 --- awesome_owl/static/src/card/card.js | 22 +++++++++++ awesome_owl/static/src/card/card.xml | 14 +++++++ awesome_owl/static/src/counter/counter.js | 20 ++++++++++ awesome_owl/static/src/counter/counter.xml | 9 +++++ awesome_owl/static/src/playground.js | 17 ++++++++- awesome_owl/static/src/playground.xml | 13 ++++--- awesome_owl/static/src/todolist/TodoItem.js | 25 ++++++++++++ awesome_owl/static/src/todolist/TodoItem.xml | 13 +++++++ awesome_owl/static/src/todolist/TodoList.js | 40 ++++++++++++++++++++ awesome_owl/static/src/todolist/TodoList.xml | 11 ++++++ awesome_owl/static/src/utils.js | 9 +++++ 11 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml create mode 100644 awesome_owl/static/src/todolist/TodoItem.js create mode 100644 awesome_owl/static/src/todolist/TodoItem.xml create mode 100644 awesome_owl/static/src/todolist/TodoList.js create mode 100644 awesome_owl/static/src/todolist/TodoList.xml create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..2d5828f19b3 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,22 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title: String, + slots: { + type: Object, + shape: { + default: true + }, + } + } + + setup() { + this.state = useState({ isOpen: false }) + } + + handleToggle() { + this.state.isOpen = !this.state.isOpen + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..e9839945ea8 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,14 @@ + + + +
+
+
+ +
+ +
+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..f8d7bb893b0 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,20 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.Counter"; + static props = { + onChange: { + type: Function, + optional: true, + }, + } + + setup() { + this.state = useState({ value: 0 }) + } + + increment() { + this.state.value++ + this.props.onChange?.() + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..a48b1858e52 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,9 @@ + + + +
+

Counter:

+ +
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..303d9f9cd21 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,18 @@ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; +import { Card } from "./card/card"; +import { Counter } from "./counter/counter"; +import { TodoList } from "./todolist/TodoList"; export class Playground extends Component { - static template = "awesome_owl.playground"; + static template = "awesome_owl.playground"; + + static components = { Counter, Card, TodoList }; + + setup() { + this.sum = useState({ value: 0 }) + } + + incrementSum() { + this.sum.value++ + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..840789b4424 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,13 @@ - -
- hello world -
+ + + + + + +

Sum:

+
-
diff --git a/awesome_owl/static/src/todolist/TodoItem.js b/awesome_owl/static/src/todolist/TodoItem.js new file mode 100644 index 00000000000..5110d72f9f0 --- /dev/null +++ b/awesome_owl/static/src/todolist/TodoItem.js @@ -0,0 +1,25 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + static props = { + todo: { + type: Object, + shape: { + id: Number, + description: String, + isCompleted: Boolean, + }, + }, + onToggle: Function, + onDelete: Function, + } + + handleToggle() { + this.props.onToggle(this.props.todo.id) + } + + handleDelete() { + this.props.onDelete(this.props.todo.id) + } +} diff --git a/awesome_owl/static/src/todolist/TodoItem.xml b/awesome_owl/static/src/todolist/TodoItem.xml new file mode 100644 index 00000000000..75e33e29883 --- /dev/null +++ b/awesome_owl/static/src/todolist/TodoItem.xml @@ -0,0 +1,13 @@ + + + +
+ + + +
+
+
diff --git a/awesome_owl/static/src/todolist/TodoList.js b/awesome_owl/static/src/todolist/TodoList.js new file mode 100644 index 00000000000..2bcc4b5f7cd --- /dev/null +++ b/awesome_owl/static/src/todolist/TodoList.js @@ -0,0 +1,40 @@ +import { Component, useState } from "@odoo/owl"; +import { useAutofocus } from "../utils"; +import { TodoItem } from "./TodoItem"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem } + + setup() { + this.id = 1 + this.todos = useState([]) + useAutofocus("input") + } + + handleAdd(ev) { + if (ev.keyCode !== 13 || ev.target.value === "") return + + this.todos.push({ + id: this.id++, + description: ev.target.value, + isCompleted: false, + }) + + ev.target.value = "" + } + + handleToggle(todoId) { + const todo = this.todos.find(({ id }) => id === todoId) + if (!todo) return + + todo.isCompleted = !todo.isCompleted + } + + handleDelete(todoId) { + const index = this.todos.findIndex(({ id }) => id === todoId) + if (index === -1) return + + this.todos.splice(index, 1) + } +} diff --git a/awesome_owl/static/src/todolist/TodoList.xml b/awesome_owl/static/src/todolist/TodoList.xml new file mode 100644 index 00000000000..a73e1ba4561 --- /dev/null +++ b/awesome_owl/static/src/todolist/TodoList.xml @@ -0,0 +1,11 @@ + + + + +
+
+ +
+
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..6df7c72bd2b --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,9 @@ +import { onMounted, useRef } from "@odoo/owl"; + +export function useAutofocus(ref) { + if (typeof ref === "string") { + ref = useRef(ref) + } + + onMounted(() => ref.el.focus()) +} From a2be319d161505d8744ceee8ce494df242fc3f65 Mon Sep 17 00:00:00 2001 From: Vincent Vallaeys Date: Tue, 25 Aug 2026 14:26:04 +0200 Subject: [PATCH 2/2] [IMP] awesome_dashboard: Chapter 2 --- awesome_dashboard/__manifest__.py | 38 +++++------ awesome_dashboard/static/src/dashboard.js | 8 --- .../static/src/dashboard/component/number.js | 9 +++ .../static/src/dashboard/component/number.xml | 9 +++ .../src/dashboard/component/pie_chart.js | 35 ++++++++++ .../component/pie_chart.xml} | 6 +- .../static/src/dashboard/dashboard.js | 65 ++++++++++++++++++ .../static/src/dashboard/dashboard.scss | 3 + .../static/src/dashboard/dashboard.xml | 26 +++++++ .../static/src/dashboard/dashboard_item.js | 20 ++++++ .../static/src/dashboard/dashboard_item.xml | 10 +++ .../static/src/dashboard/dashboard_items.js | 68 +++++++++++++++++++ .../src/dashboard/dashboard_settings.js | 33 +++++++++ .../src/dashboard/dashboard_settings.xml | 20 ++++++ .../src/dashboard/dashboard_statistics.js | 21 ++++++ .../static/src/dashboard_loader.js | 13 ++++ 16 files changed, 352 insertions(+), 32 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/component/number.js create mode 100644 awesome_dashboard/static/src/dashboard/component/number.xml create mode 100644 awesome_dashboard/static/src/dashboard/component/pie_chart.js rename awesome_dashboard/static/src/{dashboard.xml => dashboard/component/pie_chart.xml} (51%) create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_settings.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_settings.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_statistics.js create mode 100644 awesome_dashboard/static/src/dashboard_loader.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..e3de7382e60 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -1,30 +1,28 @@ # -*- coding: utf-8 -*- { - 'name': "Awesome Dashboard", - - 'summary': """ + "name": "Awesome Dashboard", + "summary": """ Starting module for "Discover the JS framework, chapter 2: Build a dashboard" """, - - 'description': """ + "description": """ Starting module for "Discover the JS framework, chapter 2: Build a dashboard" """, - - 'author': "Odoo", - 'website': "https://www.odoo.com/", - 'category': 'Tutorials', - 'version': '0.1', - 'application': True, - 'installable': True, - 'depends': ['base', 'web', 'mail', 'crm'], - - 'data': [ - 'views/views.xml', + "author": "Odoo", + "website": "https://www.odoo.com/", + "category": "Tutorials", + "version": "0.1", + "application": True, + "installable": True, + "depends": ["base", "web", "mail", "crm"], + "data": [ + "views/views.xml", ], - 'assets': { - 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + "assets": { + "web.assets_backend": [ + "awesome_dashboard/static/src/**/*", + ("remove", "awesome_dashboard/static/src/dashboard/**/*"), ], + "awesome_dashboard.dashboard": ["awesome_dashboard/static/src/dashboard/**/*"], }, - 'license': 'AGPL-3' + "license": "AGPL-3", } diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index c4fb245621b..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/component/number.js b/awesome_dashboard/static/src/dashboard/component/number.js new file mode 100644 index 00000000000..582e79d462a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/number.js @@ -0,0 +1,9 @@ +import { Component } from "@odoo/owl"; + +export class Number extends Component { + static template = "awesome_dashboard.component.Number" + static props = { + title: String, + value: Number, + } +} diff --git a/awesome_dashboard/static/src/dashboard/component/number.xml b/awesome_dashboard/static/src/dashboard/component/number.xml new file mode 100644 index 00000000000..c0680eee96b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/number.xml @@ -0,0 +1,9 @@ + + + +
+
+

+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/component/pie_chart.js b/awesome_dashboard/static/src/dashboard/component/pie_chart.js new file mode 100644 index 00000000000..b3633760080 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/pie_chart.js @@ -0,0 +1,35 @@ +import { Component, onWillStart, useEffect, useRef } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.component.PieChart" + static props = { + title: String, + data: Object, + } + + setup() { + this.chart = null; + this.canvasRef = useRef("canvas"); + + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js") + }) + + useEffect(() => { + this.renderChart() + return () => { + if (this.chart) this.chart.destroy() + } + }) + } + + renderChart() { + const config = { + type: "pie", + data: this.props.data, + } + + this.chart = new Chart(this.canvasRef.el, config) + } +} diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard/component/pie_chart.xml similarity index 51% rename from awesome_dashboard/static/src/dashboard.xml rename to awesome_dashboard/static/src/dashboard/component/pie_chart.xml index 1a2ac9a2fed..d179951d46c 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/component/pie_chart.xml @@ -1,8 +1,6 @@ - - - hello dashboard + + - diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..15529efb7ba --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,65 @@ +import { Component, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { Layout } from "@web/search/layout"; +import { DashboardItem } from "./dashboard_item"; +import { DashboardSettings } from "./dashboard_settings"; +import { browser } from "@web/core/browser/browser"; + +const storageKey = "dashboardDisabled" + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem }; + + setup() { + this.action = useService("action"); + this.dialog = useService("dialog"); + this.stats = useState(useService("awesome_dashboard.statistics")); + + this.items = registry.category("awesome_dashboard").getAll() + this.disabled = useState(browser.localStorage.getItem(storageKey)?.split(",") || []) + } + + handleCustomers() { + this.action.doAction("base.action_partner_form"); + } + + handleLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [[false, "list"], [false, "form"]], + }); + } + + openSettings() { + this.dialog.add(DashboardSettings, { + all: this.items, + disabled: this.disabled, + onToggle: this.handleToggle, + }) + } + + closeSettings() { + this.settings = false + } + + handleToggle(checked, id) { + if (checked) { + const index = this.disabled.indexOf(id); + if (index !== -1) { + this.disabled.splice(index, 1); + } + } else { + if (!this.disabled.includes(id)) { + this.disabled.push(id); + } + } + + browser.localStorage.setItem(storageKey, this.disabled.join(",")) + } +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..6be5e0f83c9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: gray; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..af0e62a2bd7 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + +
+ + + + + + +
+
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js new file mode 100644 index 00000000000..78e2b591712 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -0,0 +1,20 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem" + static props = { + size: { + type: Number, + optional: true, + }, + slots: { + type: Object, + shape: { + default: true + }, + } + } + static defaultProps = { + size: 1, + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item.xml new file mode 100644 index 00000000000..435a662cc09 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js new file mode 100644 index 00000000000..952c01eb4b0 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,68 @@ +import { Number } from "./component/number"; +import { PieChart } from "./component/pie_chart"; +import { registry } from "@web/core/registry"; + +const items = [ + { + id: "average_quality", + description: "Average amount of t-shirts", + Component: Number, + props: (data) => ({ + title: "Average amount of t-shirts by order this month", + value: data.average_quantity + }) + }, + { + id: "average_time", + description: "Average order time", + Component: Number, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: data.average_time + }) + }, + { + id: "new_orders", + description: "New orders", + Component: Number, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders + }) + }, + { + id: "cancelled_orders", + description: "Cancelled orders", + Component: Number, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders + }) + }, + { + id: "total_amount", + description: "New orders total", + Component: Number, + props: (data) => ({ + title: "Total amount of new orders", + value: data.total_amount + }) + }, + { + id: "pie", + description: "Pie", + Component: PieChart, + size: 2, + props: (data) => ({ + title: "Shirt orders by size", + data: { + labels: Object.keys(data.orders_by_size), + datasets: [{ + data: Object.values(data.orders_by_size), + }], + } + }) + }, +] + +items.forEach(i => registry.category("awesome_dashboard").add(i.id, i)) diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings.js b/awesome_dashboard/static/src/dashboard/dashboard_settings.js new file mode 100644 index 00000000000..97918d6f120 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings.js @@ -0,0 +1,33 @@ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; + +export class DashboardSettings extends Component { + static template = "awesome_dashboard.DashboardSettings" + static components = { Dialog, CheckBox } + static props = { + close: Function, + all: { + type: Array, + element: { type: Object, shape: { id: String, description: String, "*": true } } + }, + disabled: { + type: Array, + element: { type: String }, + }, + onToggle: Function + } + + setup() { + this.items = useState(this.props.all.map(item => ({ ...item, enabled: !this.props.disabled.includes(item.id) }))) + } + + handleToggle(checked, item) { + item.enabled = checked + this.props.onToggle(checked, item.id) + } + + handleClose() { + this.props.close() + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings.xml b/awesome_dashboard/static/src/dashboard/dashboard_settings.xml new file mode 100644 index 00000000000..e480d9a9f81 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings.xml @@ -0,0 +1,20 @@ + + + + + + Select the items you want to see + + + + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_statistics.js b/awesome_dashboard/static/src/dashboard/dashboard_statistics.js new file mode 100644 index 00000000000..4093c2a8020 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_statistics.js @@ -0,0 +1,21 @@ +import { reactive } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; + +const statisticsService = { + start() { + const stats = reactive({ isReady: false }); + + async function updateStats() { + const newStats = await rpc("/awesome_dashboard/statistics"); + Object.assign(stats, newStats, { isReady: true }); + } + + setInterval(updateStats, 10 * 60 * 1000); + updateStats(); + + return stats + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js new file mode 100644 index 00000000000..128049b52e1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.js @@ -0,0 +1,13 @@ +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; +import { Component, xml } from "@odoo/owl"; + +class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; + +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader);