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 @@
+
+
+
+
+
+
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``;
+ 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
+
+
+
+
+
+
+
+
+
-
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());
+}