Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dfc2387
[IMP] awesome_owl: free me from this pain
trcazier Sep 22, 2025
f7b4680
[IMP] awesome_owl: save this counter implementation because I like it…
trcazier Sep 22, 2025
dc2211e
[IMP] awesome_owl: add up to section 7
trcazier Sep 22, 2025
a22285a
[IMP] awesome_owl: apply PR suggestions
trcazier Sep 23, 2025
8611d61
[IMP] awesome_owl: add sections 8-10
trcazier Sep 23, 2025
70e3c05
[IMP] awesome_owl: add section 11
trcazier Sep 23, 2025
aa4125a
[IMP] awesome_owl: replace redundant ternary operator with ||
trcazier Sep 23, 2025
bf99024
[IMP] awesome_owl: replace redundant ternary operator with if
trcazier Sep 23, 2025
03af1db
[IMP] awesome_owl: add missing ;
trcazier Sep 23, 2025
86806ec
[IMP] awesome_owl: add section 12 and missing ;s
trcazier Sep 23, 2025
143eec9
[IMP] awesome_owl: add section 13
trcazier Sep 23, 2025
3156e29
[IMP] awesome_owl: set the starting todo index as 1
trcazier Sep 23, 2025
60483e6
[IMP] awesome_owl: add section 14
trcazier Sep 23, 2025
2209933
[IMP] awesome_dashboard: add section 1
trcazier Sep 23, 2025
adf412a
[IMP] awesome_dashboard: add section 2
trcazier Sep 23, 2025
ac47f9b
[IMP] awesome_dashboard: add sections 3-4
trcazier Sep 23, 2025
71636b1
[IMP] awesome_dashboard: add section 5
trcazier Sep 23, 2025
4115506
[IMP] awesome_dashboard: section 6
trcazier Sep 23, 2025
82f496d
[IMP] awesome_dashboard: add section 7
trcazier Sep 24, 2025
cabcad9
[IMP] awesome_dashboard: add section 8
trcazier Sep 24, 2025
036b648
[IMP] awesome_dashboard: add section 9
trcazier Sep 24, 2025
164bc52
[IMP] awesome_dashboard: remove console logs
trcazier Sep 24, 2025
a502aa5
[IMP] awesome_dashboard: add section 10
trcazier Sep 24, 2025
292c7b8
[IMP] awesome_dashboard: add section 10
trcazier Sep 24, 2025
9f7c018
[MERGE] branch '18.0-web-tutorial-trcaz' of github.com:odoo-dev/tutor…
trcazier Sep 24, 2025
82c0d40
[IMP] awesome_dashboard: move buttons into layout button bar
trcazier Sep 24, 2025
b62f551
[IMP] awesome_dashboard: rewrite xml ids using PascalCase
trcazier Sep 24, 2025
113773f
[IMP] awesome_dashboard: apply PR feedback
trcazier Sep 24, 2025
6f4a297
[IMP] awesome_dashboard: add section 11
trcazier Sep 24, 2025
0ad1da4
[IMP] awesome_dashboard: refactor settings dialog props
trcazier Sep 25, 2025
6aacfbe
[IMP] awesome_dashboard: change var name for clarity
trcazier Sep 25, 2025
3231834
[IMP] awesome_dashboard: add newline to file end
trcazier Sep 25, 2025
44e0d49
[IMP] awesome_dashboard: apply PR suggestion
trcazier Sep 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions awesome_dashboard/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
'web.assets_backend': [
'awesome_dashboard/static/src/**/*',
],
'awesome_dashboard.dashboard': [
'awesome_dashboard/static/src/dashboard/**/*'
]
},
'license': 'AGPL-3'
}
10 changes: 0 additions & 10 deletions awesome_dashboard/static/src/dashboard.js

This file was deleted.

73 changes: 73 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/** @odoo-module **/

import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { Layout } from "@web/search/layout";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { DashboardItem } from "./dashboard_item";
import { PieChart } from "./pie_chart/pie_chart";
import { browser } from "@web/core/browser/browser";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

import { Dialog } from "@web/core/dialog/dialog";
import { CheckBox } from "@web/core/checkbox/checkbox";

class AwesomeDashboard extends Component {
static template = "awesome_dashboard.AwesomeDashboard";
static components = { Layout, DashboardItem, PieChart };

setup() {
this.action = useService("action");
this.stats = useState(useService("awesome_dashboard.statistics"));
this.items = registry.category("awesome_dashboard").getAll();
this.dialog = useService("dialog");
this.state = useState({ itemsNotShown: browser.localStorage.getItem("itemsNotShown")?.split(",") || []});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.state = useState({ itemsNotShown: browser.localStorage.getItem("itemsNotShown")?.split(",") || []});
this.state = useState({ itemsNotShown: localStorage.getItem("itemsNotShown")?.split(",") || []});

just localStorage should be enough. Or maybe I'm dreaming 📦

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk I had no idea how to do it so I just looked it up

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's confusing to have a state variable called state when you already have other state variables (stats). Either put stats in state or take itemsNotShown outside of state

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But isn't state used to pass mutable variables? I put the ones that change in this.state and the others just in this

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never heard a rule like that, did you see it somewhere 👀

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No but it makes more sense to me :)

}

openPartners() {
this.action.doAction("base.action_partner_form");
}

async openLeads() {
this.action.doAction({
type: 'ir.actions.act_window',
name: _t('All Leads'),
res_model: 'crm.lead',
views: [
[false, 'list'],
[false, 'form']
],
});
}

openSettingsDialog() {
this.dialog.add(SettingsDialog, {
items: this.items,
dashboard: this,
});
}
}

registry.category("lazy_components").add("awesome_dashboard.dashboard", AwesomeDashboard);

class SettingsDialog extends Component {
static template = "awesome_dashboard.SettingsDialog";
static components = { Dialog, CheckBox };
static props = ["dashboard", "items"];

setup() {
this.items = useState(this.props.items);
this.items.forEach((item) => { item.shown = !this.props.dashboard.state.itemsNotShown.includes(item.id)});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate everything about this.
Good job 👍

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That way I only have to pass 1 prop: the dashboard, and not: itemsNotShown and 15update methods 😄

}

toggleItem (isChecked, item) {
item.shown = isChecked;

const newItemsNotShown = Object.values(this.items)
.filter((i) => !i.shown)
.map((i) => i.id)

browser.localStorage.setItem("itemsNotShown", newItemsNotShown);

this.props.dashboard.state.itemsNotShown = newItemsNotShown;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uuuhh so you're getting the parent as a prop, then changing the state of the parent in the child. Why don't you handle all of this in the parent. You can create the toggleItem (or similar) function in the parent and pass the function to the child as a prop

Copy link
Author

@trcazier trcazier Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was gonna write a witty answer but instead I'll just commit another solution that will comply entirely with what I was gonna write & will probably irritate you even more

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope you like it

}
}
3 changes: 3 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.o_dashboard {
background-color: purple
}
41 changes: 41 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.AwesomeDashboard">
<Layout className="'o_dashboard h-100'" display="{controlPanel: {} }">
<t t-set-slot="layout-buttons">
<button class="btn btn-primary" t-on-click="() => this.openPartners()">
Customers
</button>
<button class="btn btn-primary" t-on-click="() => this.openLeads()">
Leads
</button>
</t>

<t t-set-slot="control-panel-additional-actions">
<button t-on-click="() => this.openSettingsDialog()" class="">
<i class="fa fa-cog"/>
</button>
</t>

<t t-foreach="items" t-as="item" t-key="item.id">
<DashboardItem size="item.size" t-if="!this.state.itemsNotShown.includes(item.id)">
<t t-set="itemProp" t-value="item.props ? item.props(this.stats) : {'data': this.stats}"/>
<t t-component="item.Component" t-props="itemProp" />
</DashboardItem>
</t>
</Layout>
</t>

<t t-name="awesome_dashboard.SettingsDialog">
<Dialog title="'Configure your dashboard'">
Choose which items you want to display
<t t-foreach="items" t-as="item" t-key="item.id">
<CheckBox value="item.shown" onChange="(ev) => this.toggleItem(ev, item)">
<t t-esc="item.description"/>
</CheckBox>
</t>
</Dialog>
</t>

</templates>
13 changes: 13 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { LazyComponent } from "@web/core/assets";
import { Component, xml, onWillStart, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";


export class AwesomeDashboardLoader extends Component {
static components = { LazyComponent };
static template = xml`
<LazyComponent bundle="'awesome_dashboard.dashboard'" Component="'awesome_dashboard.dashboard'"/>
`;
}

registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader);
13 changes: 13 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Component } from "@odoo/owl";

export class DashboardItem extends Component {
static template = "awesome_dashboard.DashboardItem"
static props = {
size: {type: Number, optional: true},
slots: {optional: true}
};

setup() {
this.size = this.props.size || 1;
}
}
15 changes: 15 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.DashboardItem">
<div class="dashboard_item d-inline-block m-2" t-att-style="'width: ' + (18 * this.size) + 'rem; border: 1px solid black; border-radius: 5px'">
<div class="dashboard_item-body">
<h5 class="dashboard_item-title"><t t-esc="this.title"/></h5>
<p class="dashboard_item-text">
<t t-slot="default"/>
</p>
</div>
</div>
</t>

</templates>
65 changes: 65 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_items.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { NumberCard } from "./number_card/number_card"
import { PieChartCard } from "./pie_chart_card/pie_chart_card"
import { registry } from "@web/core/registry";

const items = [
{
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,
})
},
{
id: "average_time",
description: "Average time for an order",
Component: NumberCard,
props: (data) => ({
title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'",
value: data.average_time,
})
},
{
id: "number_new_orders",
description: "New orders this month",
Component: NumberCard,
props: (data) => ({
title: "Number of new orders this month",
value: data.nb_new_orders,
})
},
{
id: "cancelled_orders",
description: "Cancelled orders this month",
Component: NumberCard,
props: (data) => ({
title: "Number of cancelled orders this month",
value: data.nb_cancelled_orders,
})
},
{
id: "amount_new_orders",
description: "amount orders this month",
Component: NumberCard,
props: (data) => ({
title: "Total amount of new orders this month",
value: data.total_amount,
})
},
{
id: "pie_chart",
description: "Shirt orders by size",
Component: PieChartCard,
size: 2,
props: (data) => ({
title: "Shirt orders by size",
values: data.orders_by_size,
})
}
]

items.forEach(i => {
registry.category("awesome_dashboard").add(i.id, i);
});
28 changes: 28 additions & 0 deletions awesome_dashboard/static/src/dashboard/dashboard_service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { registry } from "@web/core/registry";
import { memoize } from "@web/core/utils/functions";
import { rpc } from "@web/core/network/rpc";
import { reactive } from "@odoo/owl";

const dashboardService = {
start() {
let stats = reactive({
average_quantity: 0,
average_time: 0,
nb_cancelled_orders: 0,
nb_new_orders: 0,
orders_by_size: {},
total_amount: 0
});
async function loadData() {
const newStats = await rpc("/awesome_dashboard/statistics");
Object.assign(stats, newStats);
}
setInterval(loadData, 2*1000);
loadData();

return stats;
},
};


registry.category("services").add("awesome_dashboard.statistics", dashboardService);
Original file line number Diff line number Diff line change
@@ -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,
};
}
12 changes: 12 additions & 0 deletions awesome_dashboard/static/src/dashboard/number_card/number_card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>

<templates xml:space="preserve">

<t t-name="awesome_dashboard.NumberCard" owl="1">
<t t-esc="props.title"/>
<div class="fs-1 fw-bold text-success text-center">
<t t-esc="props.value"/>
</div>
</t>

</templates>
37 changes: 37 additions & 0 deletions awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Component, onMounted, onWillUnmount, onWillStart, useEffect, useRef } from "@odoo/owl";
import { loadJS } from "@web/core/assets";

export class PieChart extends Component {
static template = "awesome_dashboard.PieChart";
static props = { data: {optional: false}};

setup() {
this.canvasRef = useRef("canvas");

onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js"));

useEffect(() => {
if (this.pieChart) {
this.pieChart.destroy();
}
Comment on lines +14 to +16

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (this.pieChart) {
this.pieChart.destroy();
}
this.pieChart?.destroy();

this.data = Object.values(this.props.data);
this.labels = Object.keys(this.props.data);

this.buildChart();
});

onWillUnmount(() => { if (this.pieChart) {this.pieChart.destroy()}; });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
onWillUnmount(() => { if (this.pieChart) {this.pieChart.destroy()}; });
onWillUnmount(() => {this.pieChart?.destroy(); });

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum hum...

#967 (comment)

}

buildChart() {
this.pieChart = new Chart(this.canvasRef.el, {
type: "pie",
data: {
labels: this.labels,
datasets: [{
data: this.data,
}],
},
});
}
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.AwesomeDashboard">
hello dashboard
<t t-name="awesome_dashboard.PieChart">
<canvas t-ref="canvas"/>
</t>

</templates>
Original file line number Diff line number Diff line change
@@ -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 = {
title: String,
values: {optional: false},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>

<templates xml:space="preserve">

<t t-name="awesome_dashboard.PieChartCard" owl="1">
<t t-esc="props.title"/>
<PieChart data="props.values" label="''"/>
</t>

</templates>
19 changes: 19 additions & 0 deletions awesome_owl/static/src/card/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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({open: true});
this.title = this.props.title;
}
}
Loading