Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 29 additions & 1 deletion awesome_dashboard/static/src/dashboard.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@
import { Component } from "@odoo/owl";
import { Component, onWillStart, useState } from "@odoo/owl";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Being extra annoying here, this import is not used 😄

import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";

import { DashboardItem } from "./dashboard_item/dashboard_item";
import { Layout } from "@web/search/layout"
import { PieChart } from "./pie_chart/pie_chart";


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

setup() {
this.action = useService("action");
this.state = useState({
stats: useService("awesome_dashboard.statistics"),
});
}

async openCustomersKanban() {
this.action.doAction('base.action_partner_form');
}

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

registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard);
3 changes: 3 additions & 0 deletions awesome_dashboard/static/src/dashboard.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.o_dashboard {
background-color: gray
}
15 changes: 14 additions & 1 deletion awesome_dashboard/static/src/dashboard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@
<templates xml:space="preserve">

<t t-name="awesome_dashboard.AwesomeDashboard">
hello dashboard
<t t-set-slot="layout-buttons">
<button t-on-click="openCustomersKanban">Customers</button>
<button t-on-click="openLeads">Leads</button>
</t>

<Layout display="{controlPanel: {} }" className="'o_dashboard h-100'">
Comment on lines +5 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Its preferable to declare the layout-buttons slot inside the <Layout> component it belongs to. As written, the buttons are defined before the component that should receive the slot. It doesn't make much a difference but we prefer it that way.

Suggested change
<t t-set-slot="layout-buttons">
<button t-on-click="openCustomersKanban">Customers</button>
<button t-on-click="openLeads">Leads</button>
</t>
<Layout display="{controlPanel: {} }" className="'o_dashboard h-100'">
<Layout display="{ controlPanel: {} }" className="'o_dashboard h-100'">
<t t-set-slot="layout-buttons">

<DashboardItem size="2">
<p>Counter: <t t-esc="this.state.stats.nb_new_orders"/></p>
</DashboardItem>

<DashboardItem size="1">
<PieChart data="this.state.stats.orders_by_size"/>
</DashboardItem>
</Layout>
</t>

</templates>
19 changes: 19 additions & 0 deletions awesome_dashboard/static/src/dashboard_item/dashboard_item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Component} from "@odoo/owl"


export class DashboardItem extends Component {
static template = "awesome_dashboard.dashboard_item"
static props = {
size: {
type: Number,
default: 1,
optional: true,
},
slots: {
type: Object,
shape: {
default: true
}
},
};
}
11 changes: 11 additions & 0 deletions awesome_dashboard/static/src/dashboard_item/dashboard_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<templates xml:space="preserve">

<t t-name="awesome_dashboard.dashboard_item">
<div class="card m-2 border-dark" t-attf-style="width: {{18*props.size}}rem;">
<div class="card-body">
<t t-slot="default"/>
</div>
</div>
</t>

</templates>
54 changes: 54 additions & 0 deletions awesome_dashboard/static/src/pie_chart/pie_chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Component, onMounted, onPatched, onWillStart, onWillUnmount, useRef, useState } from "@odoo/owl"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Currently import but not used 😄

Suggested change
import { Component, onMounted, onPatched, onWillStart, onWillUnmount, useRef, useState } from "@odoo/owl"
import { Component, onMounted, onPatched, onWillStart, onWillUnmount, useRef } from "@odoo/owl"

import { loadJS } from "@web/core/assets";
import { getColor } from "@web/core/colors/colors";


export class PieChart extends Component {
static template = "awesome_dashboard.pie_chart"
static props = {
data: Object,
optional: true
}
Comment on lines +9 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This declares data as a required prop and then adds optional: true as a separate prop definition, which is not the shape Owl expects. That matters here because the dashboard statistics are loaded asynchronously, so orders_by_size can be missing on the first render. The current version can fail before the data arrives, even though the chart is naturally allowed to start empty.

Suggested change
data: Object,
optional: true
}
data: {
type: Object,
optional: true,
},


setup() {
onWillStart(async () => {
await loadJS("/web/static/lib/Chart/Chart.js")
})

this.canvasRef = useRef("canvas");

if (this.props.data) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Oh but actually it won't happend since you only reach that if (this.props.data). I think that's another isue. Your hooks should be assigned anytime and then it's the job of renderChart to not do anything if props is empty. Does that make sense ? The lifecycle hooks should be always registered in the setup. 👍

Suggested change
if (this.props.data) {

onMounted(() => {
this.renderChart()
})

onPatched(() => {
this.chart.destroy()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

onPatched can run before a chart instance exists, especially sine data is optional and the first render may be empty 👍

Suggested change
this.chart.destroy()
this.chart?.destroy();

this.renderChart()
})

onWillUnmount(() => {
this.chart.destroy()
});
}
}

renderChart() {
const keys = Object.keys(this.props.data)
const values = Object.values(this.props.data)
Comment on lines +37 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Because data is loaded asynchronously, this.props.data can be undefined when the chart first renders. The chart component should handle that calmly and render an empty chart state instead of throwing. This is a useful tutorial lesson: components that receive async service data should be defensive at their boundary. What I would do instead is normalize the prop to an empty object before reading its keys and values. Does that make sense ? :)

Suggested change
const keys = Object.keys(this.props.data)
const values = Object.values(this.props.data)
const data = this.props.data || {};
const keys = Object.keys(data)
const values = Object.values(data)

const colors = keys.map((_, index) => getColor(index));

this.chart = new Chart(this.canvasRef.el, {
type: "pie",
data: {
labels: keys,
datasets: [
{
backgroundColor: colors,
data: values,
},
]
}
});
}
}
8 changes: 8 additions & 0 deletions awesome_dashboard/static/src/pie_chart/pie_chart.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_dashboard.pie_chart">
<canvas t-ref="canvas" />
</t>

</templates>
22 changes: 22 additions & 0 deletions awesome_dashboard/static/src/statistics_service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
import { reactive } from "@odoo/owl";


const statisticsService = {
start() {
let stats = reactive({})

async function getStats() {
let updates = await rpc("/awesome_dashboard/statistics");
Object.assign(stats, updates);
}

getStats()
setInterval(getStats, 10*1000)

return stats
}
}

registry.category("services").add("awesome_dashboard.statistics", statisticsService);
25 changes: 25 additions & 0 deletions awesome_owl/static/src/card/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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({
state: false,
})
}

toggleState() {
this.state.state = !this.state.state;
}
}
17 changes: 17 additions & 0 deletions awesome_owl/static/src/card/card.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<templates xml:space="preserve">

<t t-name="awesome_owl.card">
<div class="card d-inline-block m-2" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">
<t t-out="props.title"/>
<button t-on-click="toggleState">toggle show</button>
</h5>
<p class="card-text" t-if="state.state">
<t t-slot="default"/>
</p>
</div>
</div>
</t>

</templates>
21 changes: 21 additions & 0 deletions awesome_owl/static/src/counter/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Component, useState } from "@odoo/owl";


export class Counter extends Component {
static template = "awesome_owl.counter";
static prop = {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

😉

Suggested change
static prop = {
static props = {

onChange: {
type: Function,
optional: true,
},
}

setup() {
this.state = useState({ value: 0 });
}

increment() {
this.state.value++;
if (this.props.onChange) { this.props.onChange(); }
}
}
10 changes: 10 additions & 0 deletions awesome_owl/static/src/counter/counter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<templates xml:space="preserve">

<t t-name="awesome_owl.counter">
<div class="m-2 p-2 border d-inline-block">
<p>Counter: <t t-esc="state.value"/></p>
<button class="btn btn-primary" t-on-click="increment">Increment</button>
</div>
</t>

</templates>
1 change: 0 additions & 1 deletion awesome_owl/static/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ const config = {

// Mount the Playground component when the document.body is ready
whenReady(() => mountComponent(Playground, document.body, config));

14 changes: 13 additions & 1 deletion awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { Component } from "@odoo/owl";
import {Component, useState} from "@odoo/owl";
import { Card } from "./card/card";
import { Counter } from "./counter/counter";
import { TodoList } from "./todo/todo_list";

export class Playground extends Component {
static template = "awesome_owl.playground";
static components = { Counter, Card, TodoList };

setup() {
this.sum = useState({ value: 0 });
}

incrementSum() {
this.sum.value++
}
}
12 changes: 12 additions & 0 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@
<t t-name="awesome_owl.playground">
<div class="p-3">
hello world
<Counter onChange.bind="incrementSum"/>
<Counter onChange.bind="incrementSum"/>
<div>The sum is: <t t-esc="sum.value"/></div>

<Card title="'not marked up'">
<Counter/>
</Card>
<Card title="'marked up'">
<div class='text-primary'>some content</div>
</Card>

<TodoList/>
</div>
</t>

Expand Down
37 changes: 37 additions & 0 deletions awesome_owl/static/src/todo/todo_item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Component } from "@odoo/owl";


export class TodoItem extends Component {
static template = "awesome_owl.todo_item";
static props = {
todo: {
type: Object,
shape: {
id: Number,
description: String,
isCompleted: Boolean,
}
},
toggleState: {
type: Function,
optional: true
},

deleteTodo: {
type: Function,
optional: true
}
};

onChange() {
if (this.props.toggleState) {
this.props.toggleState(this.props.todo.id);
}
}

onRemove() {
if (this.props.deleteTodo) {
this.props.deleteTodo(this.props.todo.id)
}
}
}
15 changes: 15 additions & 0 deletions awesome_owl/static/src/todo/todo_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_owl.todo_item">
<div class="form-check">
<input class="form-check-input" type="checkbox" t-on-change="onChange"/>
<label t-att-class="props.todo.isCompleted ? 'text-decoration-line-through text-muted' : '' ">
<t t-esc="props.todo.id"/>.
<t t-esc="props.todo.description"/>
<span class="fa fa-remove" t-on-click="onRemove"/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Okay so I already said this to Sreedev so I guess this is the way they show it in the tutorial but being an clickable action, this should be a button not a span. It works great with the mouse but keyboard users and assistive technologies, don't forget to make this kind of action a button in the future 😄

Suggested change
<span class="fa fa-remove" t-on-click="onRemove"/>
<button type="button" class="btn btn-link p-0" t-on-click="onRemove">
<i class="fa fa-remove"/>
</button>

</label>
</div>
</t>

</templates>
41 changes: 41 additions & 0 deletions awesome_owl/static/src/todo/todo_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Component, useState } from "@odoo/owl"
import { TodoItem } from "./todo_item";
import { Autofocus } from "../utils";


export class TodoList extends Component {
static template = "awesome_owl.todo_list";
static components = { TodoItem };

setup() {
this.nextId = 0;
this.todos = useState([]);
Autofocus("input")
}

addTodo(event) {
if (event.key === 'Enter' && event.target.value !== '') {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This only rejects an actually empty string, so a todo containing only spaces can still be added. It's up to you really but I would consider this a small bug in the feature. Your call 😄

Suggested change
if (event.key === 'Enter' && event.target.value !== '') {
if (event.key === "Enter" && event.target.value.trim() !== "") {

this.todos.push({
id: this.nextId++,
description: event.target.value,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Then if you validate the trimmed value, you should save the trimmed value. Still your call 😄

Suggested change
description: event.target.value,
description: event.target.value.trim(),

isCompleted: false
});

event.target.value = '';
}
}

listToggleComplete(itemId) {
const todoItem = this.todos.find(item => item.id === itemId);
if (todoItem) {
todoItem.isCompleted = !todoItem.isCompleted;
}
}

removeTodo(itemId) {
const todoIndex = this.todos.findIndex((todo) => todo.id === itemId);
if (todoIndex >= 0) {
this.todos.splice(todoIndex, 1);
}
}
}
Loading