From 4a9a4ea845186aa38ee762223f2fbcabc88db642 Mon Sep 17 00:00:00 2001 From: "Julien Piron (jupir)" Date: Fri, 22 May 2026 11:06:53 +0200 Subject: [PATCH] [ADD] awesome_owl, awesome_dashboard: allow to keep track of todos and t-shirt sales Problem: Projects is to complex for handling simple todo lists. There was no dashboard for t-shirt selling KPIs. Solution: Creation of a simple todo app and a dashboard task-6237921 --- awesome_dashboard/__manifest__.py | 7 +-- awesome_dashboard/static/src/dashboard.js | 8 --- awesome_dashboard/static/src/dashboard.xml | 8 --- .../static/src/dashboard/config_dialog.js | 43 +++++++++++++++ .../static/src/dashboard/dashboard.js | 51 +++++++++++++++++ .../static/src/dashboard/dashboard.scss | 5 ++ .../static/src/dashboard/dashboard.xml | 26 +++++++++ .../static/src/dashboard/dashboard_item.js | 19 +++++++ .../static/src/dashboard/dashboard_items.js | 49 +++++++++++++++++ .../static/src/dashboard/number_card.js | 8 +++ .../static/src/dashboard/pie_chart.js | 42 ++++++++++++++ .../static/src/dashboard/pie_chart_card.js | 10 ++++ .../src/dashboard/statistics_service.js | 29 ++++++++++ .../static/src/dashboard/use_local_storage.js | 9 +++ .../static/src/dashboard_action.js | 10 ++++ awesome_owl/static/src/Card.js | 17 ++++++ awesome_owl/static/src/Card.xml | 16 ++++++ awesome_owl/static/src/Counter.js | 19 +++++++ awesome_owl/static/src/Counter.xml | 7 +++ awesome_owl/static/src/TodoItem.js | 33 +++++++++++ awesome_owl/static/src/TodoList.js | 55 +++++++++++++++++++ awesome_owl/static/src/playground.js | 15 ++++- awesome_owl/static/src/playground.xml | 35 ++++++++++-- awesome_owl/static/src/utils.js | 6 ++ 24 files changed, 501 insertions(+), 26 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js delete mode 100644 awesome_dashboard/static/src/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/config_dialog.js 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_items.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/statistics_service.js create mode 100644 awesome_dashboard/static/src/dashboard/use_local_storage.js create mode 100644 awesome_dashboard/static/src/dashboard_action.js create mode 100644 awesome_owl/static/src/Card.js create mode 100644 awesome_owl/static/src/Card.xml create mode 100644 awesome_owl/static/src/Counter.js create mode 100644 awesome_owl/static/src/Counter.xml create mode 100644 awesome_owl/static/src/TodoItem.js create mode 100644 awesome_owl/static/src/TodoList.js create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..fcb45b2a436 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -1,15 +1,12 @@ # -*- coding: utf-8 -*- { 'name': "Awesome Dashboard", - 'summary': """ Starting module for "Discover the JS framework, chapter 2: Build a dashboard" """, - 'description': """ Starting module for "Discover the JS framework, chapter 2: Build a dashboard" """, - 'author': "Odoo", 'website': "https://www.odoo.com/", 'category': 'Tutorials', @@ -17,7 +14,6 @@ 'application': True, 'installable': True, 'depends': ['base', 'web', 'mail', 'crm'], - 'data': [ 'views/views.xml', ], @@ -25,6 +21,7 @@ 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', ], + '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.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/config_dialog.js b/awesome_dashboard/static/src/dashboard/config_dialog.js new file mode 100644 index 00000000000..a1fed44b84d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/config_dialog.js @@ -0,0 +1,43 @@ +import { Component, xml } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { registry } from "@web/core/registry"; + +export class ConfigDialog extends Component { + static template = xml` + + + + + + + + + `; + static components = { Dialog, CheckBox }; + static props = { + close: Function, + hiddenItems: Array, + }; + + setup() { + this.items = registry.category("awesome_dashboard").getAll(); + } + + onChange(event) { + if (!event.target.checked) { + this.props.hiddenItems.push(event.target.id); + } else { + const itemIndex = this.props.hiddenItems.findIndex((item) => item === event.target.id); + if (itemIndex !== -1) { + this.props.hiddenItems.splice(itemIndex, 1); + } + } + } + + onClose() { + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..bc5a64c835d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,51 @@ +import { Component, useState } from "@odoo/owl"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; + +import { DashboardItem } from "./dashboard_item"; +import { ConfigDialog } from "./config_dialog"; +import { useLocalStorage } from "./use_local_storage"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { ConfigDialog, DashboardItem, Layout }; + + setup() { + this.display = { + controlPanel: {}, + }; + this.action = useService("action"); + + this.statistics = useState(useService("awesome_dashboard.statistics")); + + this.items = registry.category("awesome_dashboard").getAll(); + this.hiddenItems = useLocalStorage("awesome_dashboard.hidden_items", []); + + this.dialogService = useService("dialog"); + } + + openCustomersView() { + this.action.doAction("base.action_partner_form"); + } + + openLeadsView() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [ + [false, "kanban"], + [false, "form"], + ], + }); + } + + openConfigDialog() { + this.dialogService.add(ConfigDialog, { + hiddenItems: this.hiddenItems, + }); + } +} + +registry.category("actions").add("awesome_dashboard.dashboard", 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..faf6f2bcf02 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,5 @@ +.o_dashboard { + background-color: gray; + padding: 4rem; + min-height: 100%; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..55c2dbadd5e --- /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..cb1a2accde5 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -0,0 +1,19 @@ +import { Component, xml } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = xml`
+
+ +
+
`; + static props = { + size: { + type: Number, + optional: true, + }, + slots: { type: Object, optional: true }, + }; + static defaultProps = { + size: 1, + }; +} 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..32013d01d90 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,49 @@ +import { registry } from "@web/core/registry"; + +import { NumberCard } from "./number_card"; +import { PieChartCard } from "./pie_chart_card"; + +const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + size: 3, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity, + }), + }, + { + id: "average_time", + description: "Average time between order creation and shipment", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Average time between order creation and shipment", + value: data.average_time, + }), + }, + { + id: "nb_cancelled_orders", + description: "Average time between order creation and shipment", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Average time between order creation and shipment", + value: data.nb_cancelled_orders, + }), + }, + { + id: "orders_by_size", + description: "Graph of t-shirt sizes ordered", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "Graph of t-shirt sizes ordered", + value: data.orders_by_size, + }), + }, +]; + +items.forEach((item) => registry.category("awesome_dashboard").add(item.id, item)); diff --git a/awesome_dashboard/static/src/dashboard/number_card.js b/awesome_dashboard/static/src/dashboard/number_card.js new file mode 100644 index 00000000000..7dc431dc885 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card.js @@ -0,0 +1,8 @@ +import { Component, xml } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = xml`
+

+

+
`; +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart.js new file mode 100644 index 00000000000..34f67e6524f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart.js @@ -0,0 +1,42 @@ +import { Component, onWillStart, useEffect, useRef, xml } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = xml`
`; + static props = { + data: Object, + }; + + chartRef = useRef("piechart"); + + setup() { + this.chart = null; + + onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"])); + useEffect( + () => { + if (!this.chartRef.el) { + return; + } + this.chart = new Chart(this.chartRef.el, { + type: "pie", + data: { + labels: Object.keys(this.props.data), + datasets: [ + { + data: Object.values(this.props.data), + }, + ], + }, + }); + + return () => { + if (this.chart) { + this.chart.destroy(); + } + }; + }, + () => [this.chartRef, this.props.data] + ); + } +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card.js new file mode 100644 index 00000000000..26393da8aeb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card.js @@ -0,0 +1,10 @@ +import { Component, xml } from "@odoo/owl"; +import { PieChart } from "./pie_chart"; + +export class PieChartCard extends Component { + static template = xml`
+

+ +
`; + static components = { PieChart }; +} diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js new file mode 100644 index 00000000000..38c0a897687 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,29 @@ +import { reactive } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; + +async function loadStatistics() { + return rpc("/awesome_dashboard/statistics"); +} + +const cachedStatsServices = { + start() { + const statistics = reactive({ value: null }); + + loadStatistics().then((value) => { + statistics.value = value; + }); + + setInterval( + () => + loadStatistics().then((value) => { + statistics.value = value; + }), + 600_000 + ); + + return statistics; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", cachedStatsServices); diff --git a/awesome_dashboard/static/src/dashboard/use_local_storage.js b/awesome_dashboard/static/src/dashboard/use_local_storage.js new file mode 100644 index 00000000000..cf34210e01c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/use_local_storage.js @@ -0,0 +1,9 @@ +import { reactive, useState } from "@odoo/owl"; + +export function useLocalStorage(key, initialState) { + const state = JSON.parse(localStorage.getItem(key)) || initialState; + const store = (obj) => localStorage.setItem(key, JSON.stringify(obj)); + const reactiveState = reactive(state, () => store(reactiveState)); + store(reactiveState); + return useState(state); +} diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..8b563e09580 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,10 @@ +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +export class DashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml``; +} + +registry.category("actions").add("awesome_dashboard.dashboard_action", DashboardLoader); diff --git a/awesome_owl/static/src/Card.js b/awesome_owl/static/src/Card.js new file mode 100644 index 00000000000..b801437283d --- /dev/null +++ b/awesome_owl/static/src/Card.js @@ -0,0 +1,17 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + + setup() { + this.state = useState({ minimized: false }); + this.toggle = this.toggle.bind(this); + } + + toggle() { + console.log("toggle"); + console.log(this.state.minimized); + this.state.minimized = !this.state.minimized; + console.log(this.state.minimized); + } +} diff --git a/awesome_owl/static/src/Card.xml b/awesome_owl/static/src/Card.xml new file mode 100644 index 00000000000..a6be4b1d1cc --- /dev/null +++ b/awesome_owl/static/src/Card.xml @@ -0,0 +1,16 @@ + + + +
+
+ +
+
+

+ Card +

+ +
+
+
+
diff --git a/awesome_owl/static/src/Counter.js b/awesome_owl/static/src/Counter.js new file mode 100644 index 00000000000..a9a51da4e64 --- /dev/null +++ b/awesome_owl/static/src/Counter.js @@ -0,0 +1,19 @@ +import { Component, useState } from "@odoo/owl" + +export class Counter extends Component { + static template = "awesome_owl.Counter" + static props = { + onChange: { + type: Function + } + } + + setup() { + this.state = useState({ value: 0 }) + } + + increment() { + this.state.value++ + this.props.onChange() + } +} diff --git a/awesome_owl/static/src/Counter.xml b/awesome_owl/static/src/Counter.xml new file mode 100644 index 00000000000..fdd4f4252e0 --- /dev/null +++ b/awesome_owl/static/src/Counter.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/awesome_owl/static/src/TodoItem.js b/awesome_owl/static/src/TodoItem.js new file mode 100644 index 00000000000..32b94a4161c --- /dev/null +++ b/awesome_owl/static/src/TodoItem.js @@ -0,0 +1,33 @@ +import { Component, xml } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = xml`
  • + + - + +
  • `; + + static props = { + todo: { + type: Object, + shape: { + id: Number, + description: String, + isCompleted: Boolean, + }, + }, + toggle: { + type: Function, + }, + delete: { + type: Function, + }, + }; + + _onChecked(event) { + this.props.toggle(this.props.todo.id, event.target.checked); + } + _onDelete() { + this.props.delete(this.props.todo.id); + } +} diff --git a/awesome_owl/static/src/TodoList.js b/awesome_owl/static/src/TodoList.js new file mode 100644 index 00000000000..7d0ab64fac5 --- /dev/null +++ b/awesome_owl/static/src/TodoList.js @@ -0,0 +1,55 @@ +import { Component, useState, xml } from "@odoo/owl"; +import { TodoItem } from "./TodoItem"; +import { useAutofocus } from "./utils"; + +export class TodoList extends Component { + static template = xml`
    +

    TodoList

    + +
      + +
    +
    `; + static components = { TodoItem }; + + setup() { + this.state = useState({ + todos: [], + }); + useAutofocus("inputRef"); + } + + _addTodo(event) { + if (event.key === "Enter" && event.target.value) { + this.state.todos.push({ + id: this.state.todos.length + 1, + description: event.target.value, + isCompleted: false, + }); + event.target.value = ""; + } + } + + _toggle(id, isCompleted) { + const todoIndex = this.state.todos.findIndex((todo) => todo.id === id); + + if (todoIndex == -1) { + throw new Error("Cannot find the todo"); + } + + this.state.todos[todoIndex] = { + ...this.state.todos[todoIndex], + isCompleted, + }; + } + + _delete(id) { + const todoIndex = this.state.todos.findIndex((todo) => todo.id === id); + + if (todoIndex == -1) { + throw new Error("Cannot find the todo"); + } + + this.state.todos.splice(todoIndex, 1); + } +} diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..b610a048c9d 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 { Counter } from './Counter' +import { Card } from "./Card" +import { TodoList } from "./TodoList"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Card, Counter, TodoList } + + setup() { + this.state = useState({ sum: 0 }) + this.incrementSum = this.incrementSum.bind(this) + } + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..cb8ab868936 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,37 @@ - -
    - hello world + +
    +
    +
    + + Counter 1 + +
    +
    + + Counter 2 + + + +
    +
    + + Sum +

    The sum is

    +
    +
    +
    +
    + +
    - diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..558cc3ef886 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,6 @@ +import { onMounted, useRef } from "@odoo/owl"; + +export function useAutofocus(refName) { + const ref = useRef(refName); + onMounted(() => ref.el.focus()); +}