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