diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py
index a1cd72893d7..09086732f0f 100644
--- a/awesome_dashboard/__manifest__.py
+++ b/awesome_dashboard/__manifest__.py
@@ -10,7 +10,7 @@
Starting module for "Discover the JS framework, chapter 2: Build a dashboard"
""",
- 'author': "Odoo",
+ 'author': "Odoo S.A.",
'website': "https://www.odoo.com/",
'category': 'Tutorials',
'version': '0.1',
@@ -25,6 +25,9 @@
'web.assets_backend': [
'awesome_dashboard/static/src/**/*',
],
+ 'awesome_dashboard.dashboard': [
+ 'awesome_dashboard/static/src/dashboard/**/*',
+ ],
},
'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.scss b/awesome_dashboard/static/src/dashboard.scss
new file mode 100644
index 00000000000..778961a748e
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard.scss
@@ -0,0 +1,3 @@
+.o_dashboard {
+ background-color: $gray-200;
+}
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/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js
new file mode 100644
index 00000000000..748a496d30c
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,103 @@
+import { Component, useSubEnv, useState } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { Layout } from "@web/search/layout";
+import { useService } from "@web/core/utils/hooks";
+import { DashboardItem } from "./dashboard_item/dashboard_item";
+import { items } from "./dashboard_items";
+import { Dialog } from "@web/core/dialog/dialog";
+import { browser } from "@web/core/browser/browser";
+import { CheckBox } from "@web/core/checkbox/checkbox";
+
+export class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+ static components = { Layout, DashboardItem };
+ static props = { "*": true };
+
+ setup() {
+ useSubEnv({
+ config: {
+ ...this.env.config,
+ breadcrumbs: [{ name: "Dashboard" }],
+ },
+ });
+
+ this.display = { controlPanel: {} };
+ this.action = useService("action");
+ this.stats = useState(useService("awesome_dashboard.statistics"));
+ this.dialog = useService("dialog");
+
+ const stored = browser.localStorage.getItem("disabledDashboardItems");
+ this.state = useState({
+ disabledItems: stored ? JSON.parse(stored) : [],
+ });
+ }
+
+ get items() {
+ const allItems = registry.category("awesome_dashboard").getAll();
+ return allItems.filter((item) => !this.state.disabledItems.includes(item.id));
+ }
+
+ updateConfiguration(newDisabledItems) {
+ this.state.disabledItems = newDisabledItems;
+ }
+
+ openConfiguration() {
+ this.dialog.add(ConfigurationDialog, {
+ items: this.items,
+ disabledItems: this.state.disabledItems,
+ onUpdateConfiguration: this.updateConfiguration.bind(this),
+ })
+ }
+
+ openCustomers() {
+ this.action.doAction("base.action_partner_form");
+ }
+
+ openLeads() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "All leads",
+ res_model: "crm.lead",
+ views: [
+ [false, "list"],
+ [false, "form"],
+ ],
+ });
+ }
+}
+
+class ConfigurationDialog extends Component {
+ static template = "awesome_dashboard.ConfigurationDialog";
+ static components = { Dialog, CheckBox };
+ static props = ["close", "items", "disabledItems", "onUpdateConfiguration"];
+
+ setup() {
+ this.items = useState(this.props.items.map((item) => {
+ return {
+ ...item,
+ enabled: !this.props.disabledItems.includes(item.id),
+ }
+ }));
+ }
+
+ done() {
+ this.props.close();
+ }
+
+ onChange(checked, changedItem) {
+ changedItem.enabled = checked;
+ const newDisabledItems = Object.values(this.items).filter(
+ (item) => !item.enabled
+ ).map((item) => item.id)
+
+ browser.localStorage.setItem(
+ "disabledDashboardItems",
+ newDisabledItems,
+ );
+
+ this.props.onUpdateConfiguration(newDisabledItems);
+ }
+
+}
+
+registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard);
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml
new file mode 100644
index 00000000000..1aed7212803
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js
new file mode 100644
index 00000000000..360e803d5d6
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js
@@ -0,0 +1,15 @@
+import { Component } from '@odoo/owl';
+
+export class DashboardItem extends Component {
+ static template = 'awesome_dashboard.DashboardItem';
+ static props = {
+ slots: Object,
+ size: {
+ type: Number,
+ optional: true
+ },
+ };
+ static defaultProps = {
+ size: 1,
+ };
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml
new file mode 100644
index 00000000000..3b367985b4e
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item/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..c7326e56839
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js
@@ -0,0 +1,66 @@
+import { registry } from "@web/core/registry";
+import { NumberCard } from "./number_card/number_card";
+import { PieChartCard } from "./pie_chart_card/pie_chart_card";
+
+const awesomeDashboardRegistry = registry.category("awesome_dashboard");
+
+awesomeDashboardRegistry.add("average_quantity", {
+ id: "average_quantity",
+ description: "Average amount of t-shirt",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Average amount of t-shirt by order this month",
+ value: data.average_quantity,
+ }),
+});
+
+awesomeDashboardRegistry.add("average_time", {
+ id: "average_time",
+ description: "Average time",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'",
+ value: data.average_time,
+ }),
+});
+
+awesomeDashboardRegistry.add("nb_new_orders", {
+ id: "nb_new_orders",
+ description: "Number of new orders",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Number of new orders this month",
+ value: data.nb_new_orders,
+ }),
+});
+
+awesomeDashboardRegistry.add("nb_cancelled_orders", {
+ id: "nb_cancelled_orders",
+ description: "Number of cancelled orders",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Number of cancelled orders this month",
+ value: data.nb_cancelled_orders,
+ }),
+});
+
+awesomeDashboardRegistry.add("total_amount", {
+ id: "total_amount",
+ description: "Total amount",
+ Component: NumberCard,
+ props: (data) => ({
+ title: "Total amount of new orders this month",
+ value: data.total_amount,
+ }),
+});
+
+awesomeDashboardRegistry.add("orders_by_size", {
+ id: "orders_by_size",
+ description: "Shirt orders by size",
+ Component: PieChartCard,
+ size: 2,
+ props: (data) => ({
+ label: "Shirt orders by size",
+ data: data.orders_by_size,
+ }),
+});
diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js
new file mode 100644
index 00000000000..857b7ca51ab
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js
@@ -0,0 +1,9 @@
+import { Component } from "@odoo/owl";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.NumberCard";
+ static props = {
+ title: String,
+ value: [Number, String],
+ };
+}
diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml
new file mode 100644
index 00000000000..447b6e906c2
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js
new file mode 100644
index 00000000000..2e03ea8d522
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js
@@ -0,0 +1,42 @@
+import { loadJS } from "@web/core/assets";
+import { Component, onWillStart, useRef, onMounted, onWillUnmount, onPatched } from "@odoo/owl";
+
+export class PieChart extends Component {
+ static template = "awesome_dashboard.PieChart";
+ static props = {
+ label: String,
+ data: Object,
+ };
+
+ setup() {
+ this.canvasRef = useRef("canvas");
+ onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js"));
+ onMounted(() => {
+ this.renderChart();
+ });
+ onPatched(() => {
+ this.chart.destroy();
+ this.renderChart();
+ });
+ onWillUnmount(() => {
+ this.chart.destroy();
+ });
+ }
+
+ renderChart() {
+ const labels = Object.keys(this.props.data);
+ const data = Object.values(this.props.data);
+ this.chart = new Chart(this.canvasRef.el, {
+ type: "pie",
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: this.props.label,
+ data: data,
+ },
+ ],
+ },
+ });
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml
new file mode 100644
index 00000000000..8477cace7e0
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
T-Shirt Orders by Size
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js
new file mode 100644
index 00000000000..12822329244
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js
@@ -0,0 +1,11 @@
+import { Component } from "@odoo/owl";
+import { PieChart } from "../pie_chart/pie_chart";
+
+export class PieChartCard extends Component {
+ static template = "awesome_dashboard.PieChartCard";
+ static components = { PieChart };
+ static props = {
+ label: String,
+ data: Object,
+ };
+}
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml
new file mode 100644
index 00000000000..5dbac81fec6
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
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..e34882ca091
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/statistics_service.js
@@ -0,0 +1,21 @@
+import { registry } from "@web/core/registry";
+import { reactive } from "@odoo/owl";
+import { rpc } from "@web/core/network/rpc";
+
+const statisticsService = {
+ start() {
+ const statistics = reactive({ isReady: false });
+
+ async function loadData() {
+ const updates = await rpc("/awesome_dashboard/statistics");
+ Object.assign(statistics, updates, { isReady: true });
+ }
+
+ setInterval(loadData, 5 * 1000);
+ loadData();
+
+ return statistics;
+ },
+};
+
+registry.category("services").add("awesome_dashboard.statistics", statisticsService);
diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js
new file mode 100644
index 00000000000..8908a02c343
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_action.js
@@ -0,0 +1,12 @@
+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);
diff --git a/awesome_owl/controllers/__init__.py b/awesome_owl/controllers/__init__.py
index 457bae27e11..b0f26a9a602 100644
--- a/awesome_owl/controllers/__init__.py
+++ b/awesome_owl/controllers/__init__.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
-from . import controllers
\ No newline at end of file
+from . import controllers
diff --git a/awesome_owl/controllers/controllers.py b/awesome_owl/controllers/controllers.py
index bccfd6fe283..92dfad42a20 100644
--- a/awesome_owl/controllers/controllers.py
+++ b/awesome_owl/controllers/controllers.py
@@ -3,7 +3,7 @@
class OwlPlayground(http.Controller):
@http.route(['/awesome_owl'], type='http', auth='public')
- def show_playground(self):
+ def show_playground(self, **kwargs):
"""
Renders the owl playground page
"""
diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js
new file mode 100644
index 00000000000..c2a3828ddc5
--- /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: Object,
+ },
+ },
+ };
+
+ setup() {
+ this.state = useState({ isOpen: true });
+ }
+
+ toggle() {
+ 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..9488b8bacc3
--- /dev/null
+++ b/awesome_owl/static/src/card/card.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js
new file mode 100644
index 00000000000..b351d71572e
--- /dev/null
+++ b/awesome_owl/static/src/counter/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, optional: true },
+ };
+
+ setup() {
+ this.state = useState({ value: 0 });
+ }
+
+ increment() {
+ this.state.value++;
+ if (this.props.onChange) {
+ 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..0b997366ffd
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ Counter:
+
+
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 4ac769b0aa5..dfa15eba9f9 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,5 +1,21 @@
-import { Component } from "@odoo/owl";
+import { Component, useState, markup } from "@odoo/owl";
+import { Counter } from "./counter/counter";
+import { Card } from "./card/card";
+import { TodoList } from "./todo_list/todo_list";
export class Playground extends Component {
- static template = "awesome_owl.playground";
+ static template = "awesome_owl.Playground";
+ static components = { Counter, Card, TodoList };
+ static props = {};
+
+ setup() {
+ this.state = useState({ sum: 0 });
+
+ this.rawContent = "
some content
";
+ this.htmlContent = markup(this.rawContent);
+ }
+
+ incrementSum() {
+ this.state.sum++;
+ }
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..32fcd1223f2 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -1,10 +1,30 @@
+
+
-
-
- hello world
+
+
+
+
+
+
+ The sum is:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js
new file mode 100644
index 00000000000..37fa70d5920
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_item.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,
+ },
+ },
+ toggleTodo: Function,
+ removeTodo: Function,
+ };
+
+ onToggle() {
+ this.props.toggleTodo(this.props.todo.id);
+ }
+
+ onRemove() {
+ this.props.removeTodo(this.props.todo.id);
+ }
+}
diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml
new file mode 100644
index 00000000000..c38e1d83e00
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_item.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ .
+
+
+
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js
new file mode 100644
index 00000000000..0eef3b20168
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.js
@@ -0,0 +1,40 @@
+import { Component, useState } from "@odoo/owl";
+import { TodoItem } from "./todo_item";
+import { useAutofocus } from "../utils";
+
+export class TodoList extends Component {
+ static template = "awesome_owl.TodoList";
+ static components = { TodoItem };
+ static props = {};
+
+ setup() {
+ this.todos = useState([]);
+ this.id = 1;
+
+ useAutofocus("input");
+ }
+
+ addTodo(event) {
+ if (event.keyCode === 13) {
+ const description = event.target.value.trim();
+ if (description) {
+ this.todos.push({
+ id: this.id++,
+ description: description,
+ isCompleted: false,
+ });
+ event.target.value = "";
+ }
+ }
+ }
+
+ toggleTodo(id) {
+ const todo = this.todos.find((todo) => todo.id === id);
+ if (todo) todo.isCompleted = !todo.isCompleted;
+ }
+
+ removeTodo(id) {
+ const index = this.todos.findIndex((todo) => todo.id === id);
+ if (index >= 0) this.todos.splice(index, 1);
+ }
+}
diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml
new file mode 100644
index 00000000000..eb1de3d2cb2
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js
new file mode 100644
index 00000000000..34b64cce84f
--- /dev/null
+++ b/awesome_owl/static/src/utils.js
@@ -0,0 +1,9 @@
+import { useRef, onMounted } from "@odoo/owl";
+
+export function useAutofocus(nameOfElementRef) {
+ const targetElementRef = useRef(nameOfElementRef);
+ onMounted(() => {
+ const htmlElement = targetElementRef.el;
+ if (htmlElement) htmlElement.focus();
+ });
+}
diff --git a/awesome_owl/views/templates.xml b/awesome_owl/views/templates.xml
index aa54c1a7241..6a673c233c3 100644
--- a/awesome_owl/views/templates.xml
+++ b/awesome_owl/views/templates.xml
@@ -4,6 +4,7 @@
+