diff --git a/.gitignore b/.gitignore index b6e47617de1..86809b7a5fe 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# vscode +.vscode/ diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..18331002cc7 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -12,7 +12,7 @@ 'author': "Odoo", 'website': "https://www.odoo.com/", - 'category': 'Tutorials/AwesomeDashboard', + 'category': 'Tutorials', 'version': '0.1', 'application': True, 'installable': True, @@ -22,8 +22,13 @@ 'views/views.xml', ], 'assets': { + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/*', + 'awesome_dashboard/static/src/dashboard/**/*', + ], 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + 'awesome_dashboard/static/src/dashboard_action.js', + 'awesome_dashboard/static/src/statistics_service.js', ], }, 'license': 'AGPL-3' diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index 637fa4bb972..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @odoo-module **/ - -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/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..f1060e26e61 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,65 @@ +import { Component, reactive, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useBus, useService } from "@web/core/utils/hooks"; +import { Layout } from "@web/search/layout"; +import { DashboardItem } from "./dashboard_item/dashboard_item"; +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; +import { DashboardItemsDialog } from "./dashboard_items_dialog/dashboard_items_dialog"; + +const EXCLUDED_DASHBOARD_ITEMS_LS_KEY = 'excluded_dashboard_items'; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem, DashboardItemsDialog, NumberCard, PieChartCard }; + + setup() { + this.action = useService("action"); + this.dialog = useService("dialog"); + + this.statisticsService = useService("statistics"); + this.state = useState(this.statisticsService); + + this.items = registry.category('dashboard_items').getAll(); + useBus(registry.category('dashboard_items'), 'UPDATE', this.handleDashboardItemsUpdate.bind(this)); + + const initExcludedItems = JSON.parse(localStorage.getItem(EXCLUDED_DASHBOARD_ITEMS_LS_KEY)) ?? []; + const storedStateObj = { excludedItems: initExcludedItems }; + const store = obj => localStorage.setItem(EXCLUDED_DASHBOARD_ITEMS_LS_KEY, JSON.stringify(obj.excludedItems)); + const reactiveStoredState = reactive(storedStateObj, () => store(reactiveStoredState)); + store(reactiveStoredState); + this.storedState = useState(storedStateObj); + } + + handleDashboardItemsUpdate(event) { + this.items = registry.category('dashboard_item').getAll(); + } + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: 'ir.actions.act_window', + name: "Leads", + target: 'current', + res_model: 'crm.lead', + views: [[false, 'form'], [false, 'list']], + }); + } + + handleDashboardItemsConfigChange(excludedItems) { + this.storedState.excludedItems = excludedItems; + } + + openSettings() { + this.removeSettingsDialog = this.dialog.add(DashboardItemsDialog, { + items: this.items, + excludedItems: this.storedState.excludedItems, + onApply: this.handleDashboardItemsConfigChange.bind(this), + }); + } +} + +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..7cc7f864c93 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,14 @@ +.o_dashboard { + background-color: gray; +} + +.stats-name { + font-size: medium; + color: black; +} + +.stats-number { + font-size: larger; + font-weight: bold; + color: green; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..1d0065392e5 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + 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..9563f0c9a94 --- /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 = { + size: { + type: Number, + optional: true, + }, + slots: Object, + }; + static defaultProps = { + size: 1, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss new file mode 100644 index 00000000000..ac0ba69ba42 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss @@ -0,0 +1,7 @@ +.dashboard-item { + background-color: white; + display: inline-block; + margin: 1rem; + padding: 1rem; + text-align: center; +} 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..50d89a029ee --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,9 @@ + + + + +
+ +
+
+
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..b972c85d4f3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,68 @@ +import { _t } from "@web/core/l10n/translation"; +import { registry } from "@web/core/registry"; +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; + +export const dashboardItemRegistry = registry.category("dashboard_items"); + +export const items = [ + { + id: 'average_quantity', + description: _t("Average amount of t-shirt"), + Component: NumberCard, + size: 3, + props: data => ({ + title: _t("Average amount of t-shirt by order this month"), + value: data.average_quantity, + }), + }, + { + id: 'average_time', + description: _t("Average time for an order"), + Component: NumberCard, + props: data => ({ + title: _t("Average time for an order to go from 'new' to 'sent' or 'cancelled'"), + value: data.average_time, + }), + }, + { + id: 'nb_new_orders', + description: _t("Number of new orders"), + Component: NumberCard, + props: data => ({ + title: _t("Number of new orders this month"), + value: data.nb_new_orders, + }), + }, + { + id: 'nb_cancelled_orders', + description: _t("Number of cancelled orders"), + Component: NumberCard, + props: data => ({ + title: _t("Number of cancelled orders this month"), + value: data.nb_cancelled_orders, + }), + }, + { + id: 'total_amount', + description: _t("Total amount of new orders"), + Component: NumberCard, + props: data => ({ + title: _t("Total amount of new orders this month"), + value: data.total_amount, + }), + }, + { + id: 'orders_by_size', + description: _t("Orders by size"), + Component: PieChartCard, + props: data => ({ + title: _t("Shirt orders by size"), + value: data.orders_by_size, + }), + }, +]; + +for (const item of items) { + dashboardItemRegistry.add(item.id, item); +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items_dialog/dashboard_items_dialog.js b/awesome_dashboard/static/src/dashboard/dashboard_items_dialog/dashboard_items_dialog.js new file mode 100644 index 00000000000..d487782ee71 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items_dialog/dashboard_items_dialog.js @@ -0,0 +1,50 @@ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; + +export class DashboardItemsDialog extends Component { + static template = 'awesome_dashboard.DashboardItemsDialog'; + static components = { Dialog }; + static props = { + items: { + type: Array, + element: { + type: Object, + shape: { + id: String, + description: String, + "*": true, + }, + }, + }, + excludedItems: { + type: Array, + element: String, + }, + close: Function, + onApply: Function, + }; + + setup() { + this.state = useState({ + items: this.props.items.map( + item => ({ + ...item, + checked: !this.props.excludedItems.includes(item.id), + }), + ), + }); + } + + toggle(event) { + const index = this.state.items.findIndex(item => item.id === event.target.name); + if (index !== -1) { + this.state.items[index].checked = !this.state.items[index].checked; + } + } + + apply() { + const newExcludedItems = this.state.items.filter(item => !item.checked).map(item => item.id); + this.props.onApply(newExcludedItems); + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items_dialog/dashboard_items_dialog.xml b/awesome_dashboard/static/src/dashboard/dashboard_items_dialog/dashboard_items_dialog.xml new file mode 100644 index 00000000000..bc3db72fb12 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items_dialog/dashboard_items_dialog.xml @@ -0,0 +1,20 @@ + + + + + + +

Dashboard items configuration

+ +
+
+
+
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..79384da6fce --- /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, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.scss b/awesome_dashboard/static/src/dashboard/number_card/number_card.scss new file mode 100644 index 00000000000..e69de29bb2d 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..6b0aa98c379 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,8 @@ + + + + +

+

+
+
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..8780448e298 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,50 @@ +import { Component, onWillStart, onMounted, onWillUnmount, useEffect, useRef } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = 'awesome_dashboard.PieChart'; + static props = { + data: Object, + }; + + setup() { + const canvasRef = useRef('canvas'); + + onWillStart(async () => { + await loadJS('/web/static/lib/Chart/Chart.js'); + }); + + onMounted(() => { + const canvas = canvasRef.el; + const ctx = canvas.getContext('2d'); + this.chart = new Chart(ctx, { + type: 'pie', + data: { + labels: Object.keys(this.props.data), + datasets: [ + { + data: Object.values(this.props.data), + }, + ], + }, + options: {}, + }); + this.chart.update(); + }); + + useEffect( + data => { + this.chart.data.labels = Object.keys(data); + this.chart.data.datasets[0].data = Object.values(data); + this.chart.update(); + }, + () => [this.props.data], + ); + + onWillUnmount(() => { + if (this.chart) { + this.chart.destroy(); + } + }); + } +} diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml similarity index 53% rename from awesome_dashboard/static/src/dashboard.xml rename to awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml index 1a2ac9a2fed..8b1d661047c 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -1,8 +1,7 @@ - - hello dashboard + + - 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..bfa6d20619d --- /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 props = { + title: String, + value: Object, + }; + static components = { PieChart }; +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.scss b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.scss new file mode 100644 index 00000000000..e69de29bb2d 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..b7ffed3da0e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,8 @@ + + + + +

+ +
+
diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..dced3df087f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,12 @@ +import { Component, xml } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; +import { registry } from "@web/core/registry"; + +export class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category('actions').add('awesome_dashboard.dashboard', AwesomeDashboardLoader); diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js new file mode 100644 index 00000000000..a7ec665582f --- /dev/null +++ b/awesome_dashboard/static/src/statistics_service.js @@ -0,0 +1,20 @@ +import { reactive } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; + +export async function loadStatistics() { + return await rpc('/awesome_dashboard/statistics'); +} + +export const statisticsService = { + async start() { + const statisticsBox = reactive({statistics: await loadStatistics()}); + setInterval(async () => { + statisticsBox.statistics = await loadStatistics(); + }, 10 * 60 * 1000); + return statisticsBox; + }, + +} + +registry.category("services").add("statistics", statisticsService); diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index 77abad510ef..50e22de4d15 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -16,7 +16,7 @@ # Categories can be used to filter modules in modules listing # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml # for the full list - 'category': 'Tutorials/AwesomeOwl', + 'category': 'Tutorials', 'version': '0.1', # any module necessary for this one to work correctly diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..11c5705c1c1 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,17 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title: String, + slots: 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..0ff5bc38288 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,11 @@ + + + +
+
+
+

+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..c33f218a965 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,17 @@ +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..257ad908916 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,7 @@ + + + +

Counter:

+ +
+
diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 1af6c827e0b..f495b89f124 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -9,3 +9,4 @@ const config = { // Mount the Playground component when the document.body is ready whenReady(() => mountComponent(Playground, document.body, config)); + diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..08a73def040 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,19 @@ -/** @odoo-module **/ - -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Card } from "./card/card"; +import { Counter } from "./counter/counter"; +import { TodoList } from "./todo_list/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Card, Counter, TodoList }; + content1 = '
some content
'; + content2 = markup`
some content
`; + + setup() { + this.state = useState({ sum: 0 }); + } + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..b4d578a07b8 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -4,7 +4,20 @@
hello world + + + + card content + + +
other card content
+
+ + + + 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..ac4d21de10c --- /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, + }, + }, + toggleState: Function, + delete: Function, + }; + + onChange() { + this.props.toggleState?.(this.props.todo.id); + } + + onDelete() { + this.props.delete(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..61029cbf4e0 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,10 @@ + + + +

+ + . + x +

+
+
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..a22aece9588 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,35 @@ +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 }; + + setup() { + this.todos = useState([]); + this.nextId = useState({value: 0}); + useAutofocus("input"); + } + + addTodo(ev) { + if (ev.keyCode === 13 && ev.target.value) { + this.todos.push({ id: this.nextId.value++, description: ev.target.value, isCompleted: false }); + ev.target.value = ""; + } + } + + toggleState(id) { + const index = this.todos.findIndex(todo => todo.id === id); + if (index !== -1) { + this.todos[index].isCompleted = !this.todos[index].isCompleted; + } + } + + delete(id) { + const index = this.todos.findIndex(todo => todo.id === id); + if (index !== -1) { + 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..c13197d0117 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.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..3c01262eb68 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,8 @@ +import { onMounted, useRef } from "@odoo/owl"; + +export function useAutofocus(refName) { + const ref = useRef(refName); + onMounted(() => { + ref.el.focus(); + }); +}