diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py
index 56d4a051287..45140aef336 100644
--- a/awesome_dashboard/controllers/controllers.py
+++ b/awesome_dashboard/controllers/controllers.py
@@ -2,6 +2,7 @@
import logging
import random
+import json
from odoo import http
from odoo.http import request
@@ -34,3 +35,24 @@ def get_statistics(self):
'total_amount': random.randint(100, 1000)
}
+ @http.route('/awesome_dashboard/config/get', type='json', auth='user')
+ def get_config(self):
+ key = f"awesome_dashboard.removed_items.user_{request.env.user.id}"
+ value = request.env['ir.config_parameter'].sudo().get_param(key)
+ removed = []
+ if value:
+ try:
+ data = json.loads(value)
+ if isinstance(data, list):
+ removed = data
+ except Exception:
+ removed = []
+ return {"removed": removed}
+
+ @http.route('/awesome_dashboard/config/set', type='json', auth='user')
+ def set_config(self, removed=None):
+ key = f"awesome_dashboard.removed_items.user_{request.env.user.id}"
+ if not isinstance(removed, list):
+ removed = []
+ request.env['ir.config_parameter'].sudo().set_param(key, json.dumps(removed))
+ return {"ok": True}
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.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/cards/number_card.js b/awesome_dashboard/static/src/dashboard/cards/number_card.js
new file mode 100644
index 00000000000..6df307acee1
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/cards/number_card.js
@@ -0,0 +1,11 @@
+/** @odoo-module **/
+
+import { Component } from "@odoo/owl";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.NumberCard";
+ static props = {
+ title: String,
+ value: [String, Number],
+ };
+}
diff --git a/awesome_dashboard/static/src/dashboard/cards/number_card.xml b/awesome_dashboard/static/src/dashboard/cards/number_card.xml
new file mode 100644
index 00000000000..af6cbd78285
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/cards/number_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/cards/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/cards/pie_chart_card.js
new file mode 100644
index 00000000000..52ea607c147
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/cards/pie_chart_card.js
@@ -0,0 +1,14 @@
+/** @odoo-module **/
+
+import { Component } from "@odoo/owl";
+import { PieChart } from "../charts/pie_chart";
+
+export class PieChartCard extends Component {
+ static template = "awesome_dashboard.PieChartCard";
+ static components = { PieChart };
+ static props = {
+ title: String,
+ data: Object,
+ onSliceClick: { type: Function, optional: true },
+ };
+}
diff --git a/awesome_dashboard/static/src/dashboard/cards/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/cards/pie_chart_card.xml
new file mode 100644
index 00000000000..ff64d044319
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/cards/pie_chart_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/charts/pie_chart.js b/awesome_dashboard/static/src/dashboard/charts/pie_chart.js
new file mode 100644
index 00000000000..04b5e24f449
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/charts/pie_chart.js
@@ -0,0 +1,35 @@
+/** @odoo-module **/
+
+import { Component, onMounted, onWillStart, onWillUnmount, useRef } from "@odoo/owl";
+import { loadJS } from "@web/core/assets";
+
+export class PieChart extends Component {
+ static template = "awesome_dashboard.PieChart";
+ static props = { data: Object, onSliceClick: { type: Function, optional: true } };
+
+ setup() {
+ const ref = this.canvasRef = useRef("canvas");
+ let chart, handler;
+
+ onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js"));
+
+ onMounted(() => {
+ const entries = Object.entries(this.props.data);
+ chart = new window.Chart(ref.el.getContext("2d"), {
+ type: "pie",
+ data: {
+ labels: entries.map(([k]) => k),
+ datasets: [{ data: entries.map(([, v]) => v), backgroundColor: ["red", "green", "blue"] }]
+ }
+ });
+
+ handler = e => {
+ const a = chart.getElementAtEvent?.(e) || chart.getElementsAtEventForMode?.(e, "nearest", { intersect: true }, true);
+ if (a?.length) this.props.onSliceClick?.(chart.data.labels[a[0]._index ?? a[0].index], a[0]._index ?? a[0].index);
+ };
+ ref.el.addEventListener("click", handler);
+ });
+
+ onWillUnmount(() => ref.el?.removeEventListener("click", handler));
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/charts/pie_chart.xml b/awesome_dashboard/static/src/dashboard/charts/pie_chart.xml
new file mode 100644
index 00000000000..4ae62f02452
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/charts/pie_chart.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js
new file mode 100644
index 00000000000..c1444b67557
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,93 @@
+/** @odoo-module **/
+
+import { Component, useState } from "@odoo/owl";
+import { Layout } from "@web/search/layout";
+import { DashboardItem } from "./dashboard_item/dashboard_item";
+import { registry } from "@web/core/registry";
+import { useService } from "@web/core/utils/hooks";
+import { DashboardSettingsDialog } from "./settings/settings_dialog";
+import { rpc } from "@web/core/network/rpc";
+import { _t } from "@web/core/l10n/translation";
+import "./statistics/statistics_service";
+import "./dashboard_items";
+
+class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+ static components = { Layout, DashboardItem };
+
+ setup() {
+ const stats = this.statisticsService = useService("awesome_dashboard.statistics");
+ this.statistics = useState(stats.statistics);
+ this.action = useService("action");
+ this.dialog = useService("dialog");
+ this.allItems = registry.category("awesome_dashboard").getAll();
+
+ const LS_KEY = "awesome_dashboard.removed_items";
+ let removed;
+ try { removed = JSON.parse(localStorage.getItem(LS_KEY) || "[]"); } catch { removed = []; }
+ this.config = useState({ removed });
+
+ this.loadServerConfig();
+ window.awesomeDash = this;
+ }
+
+ get items() {
+ const removed = new Set(this.config.removed || []);
+ return this.allItems.filter(it => !removed.has(it.id));
+ }
+
+ openSettings() {
+ (this.env?.services?.dialog || this.dialog).add(DashboardSettingsDialog, {
+ items: this.allItems,
+ removedIds: this.config.removed,
+ title: _t("Dashboard items configuration"),
+ introLabel: _t("Which cards do you wish to see ?"),
+ applyLabel: _t("Apply"),
+ onApply: removed => {
+ this.config.removed = removed;
+ try { localStorage.setItem("awesome_dashboard.removed_items", JSON.stringify(removed)); } catch {}
+ this.saveServerConfig(removed);
+ },
+ });
+ }
+
+ async loadServerConfig() {
+ try {
+ const data = await rpc("/awesome_dashboard/config/get");
+ if (Array.isArray(data?.removed)) this.config.removed = data.removed;
+ } catch {}
+ }
+
+ async saveServerConfig(removed) {
+ try { await rpc("/awesome_dashboard/config/set", { removed }); } catch {}
+ }
+
+ openOrdersBySize(label) {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: _t("Orders: ") + label.toUpperCase(),
+ res_model: "sale.order",
+ views: [[false, "list"], [false, "form"]],
+ domain: ["|", "|",
+ ["name", "ilike", label],
+ ["origin", "ilike", label],
+ ["note", "ilike", label]
+ ],
+ });
+ }
+
+ openCustomer() {
+ 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"]],
+ });
+ }
+}
+
+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..7653e98c6b2
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.scss
@@ -0,0 +1,37 @@
+.o_dashboard {
+ background-color: gray;
+ height: auto;
+ min-height: 100%;
+ overflow-y: auto;
+}
+
+.o_dashboard_content {
+ gap: 0.5rem;
+ overflow-y: auto !important;
+ -webkit-overflow-scrolling: touch;
+ max-height: calc(100vh - 80px);
+}
+
+@media (max-width: 768px) {
+ .o_dashboard_content {
+ display: block;
+ }
+ .o_dashboard_item {
+ width: 100% !important;
+ }
+}
+
+.o_dashboard .o_content {
+ overflow-y: scroll !important;
+ -webkit-overflow-scrolling: touch;
+ min-height: 0;
+ height: auto;
+ max-height: none;
+ flex: 1 1 auto;
+}
+
+.o_dashboard .o_action {
+ min-height: 0 !important;
+ display: flex;
+ flex-direction: column;
+}
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml
new file mode 100644
index 00000000000..6f60b4d03c8
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..b9b3afe811d
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js
@@ -0,0 +1,12 @@
+/** @odoo-module **/
+
+import { Component } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.DashboardItem";
+ static props = {
+ title: { type: String, optional: true },
+ size: { type: Number, optional: true },
+ slots: Object
+ };
+}
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..add15a17742
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..36b619c7434
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js
@@ -0,0 +1,70 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { _t } from "@web/core/l10n/translation";
+import { NumberCard } from "./cards/number_card";
+import { PieChartCard } from "./cards/pie_chart_card";
+
+const awesomeDash = registry.category("awesome_dashboard");
+
+awesomeDash.add("average_quantity", {
+ id: "average_quantity",
+ description: _t("Average amount of t-shirt"),
+ Component: NumberCard,
+ size: 1,
+ props: (statistics) => ({
+ title: _t("Average amount of t-shirt by order this month"),
+ value: statistics.value.average_quantity,
+ }),
+});
+
+awesomeDash.add("average_time", {
+ id: "average_time",
+ description: _t("Average time for an order"),
+ Component: NumberCard,
+ props: (statistics) => ({
+ title: _t("Average time for an order to go from 'New' to 'Sent' or 'Cancelled'"),
+ value: statistics.value.average_time,
+ }),
+});
+
+awesomeDash.add("nb_new_orders", {
+ id: "nb_new_orders",
+ description: _t("Number of new orders this month"),
+ Component: NumberCard,
+ props: (statistics) => ({
+ title: _t("Number of new orders this month"),
+ value: statistics.value.nb_new_orders,
+ }),
+});
+
+awesomeDash.add("nb_cancelled_orders", {
+ id: "nb_cancelled_orders",
+ description: _t("Number of cancelled orders this month"),
+ Component: NumberCard,
+ props: (statistics) => ({
+ title: _t("Number of cancelled orders this month"),
+ value: statistics.value.nb_cancelled_orders,
+ }),
+});
+
+awesomeDash.add("total_amount", {
+ id: "total_amount",
+ description: _t("Total amount of new orders this month"),
+ Component: NumberCard,
+ props: (statistics) => ({
+ title: _t("Total amount of new orders this month"),
+ value: statistics.value.total_amount,
+ }),
+});
+
+awesomeDash.add("orders_by_size", {
+ id: "orders_by_size",
+ description: _t("Shirt orders by size"),
+ Component: PieChartCard,
+ size: 2,
+ props: (statistics) => ({
+ title: _t("Shirt orders by size"),
+ data: statistics.value.orders_by_size,
+ }),
+});
diff --git a/awesome_dashboard/static/src/dashboard/settings/settings_dialog.js b/awesome_dashboard/static/src/dashboard/settings/settings_dialog.js
new file mode 100644
index 00000000000..6f97fc2bcad
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/settings/settings_dialog.js
@@ -0,0 +1,41 @@
+/** @odoo-module **/
+
+import { Component, useState } from "@odoo/owl";
+import { Dialog } from "@web/core/dialog/dialog";
+
+const LS_KEY = "awesome_dashboard.removed_items";
+
+export class DashboardSettingsDialog extends Component {
+ static template = "awesome_dashboard.DashboardSettingsDialog";
+ static components = { Dialog };
+ static props = {
+ items: { type: Array },
+ removedIds: { type: Array },
+ close: { type: Function },
+ onApply: { type: Function, optional: true },
+ };
+
+ setup() {
+ const checkedById = {};
+ const removedSet = new Set(this.props.removedIds || []);
+ for (const item of this.props.items) {
+ checkedById[item.id] = !removedSet.has(item.id);
+ }
+ this.state = useState({ checkedById });
+ }
+
+ toggle(id) {
+ this.state.checkedById[id] ^= 1; // style points ;)
+ }
+
+ apply() {
+ const removed = this.props.items
+ .filter((it) => !this.state.checkedById[it.id])
+ .map((it) => it.id);
+ window.localStorage.setItem(LS_KEY, JSON.stringify(removed));
+ if (this.props.onApply) {
+ this.props.onApply(removed);
+ }
+ this.props.close();
+ }
+}
diff --git a/awesome_dashboard/static/src/dashboard/settings/settings_dialog.xml b/awesome_dashboard/static/src/dashboard/settings/settings_dialog.xml
new file mode 100644
index 00000000000..9853208ad63
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/settings/settings_dialog.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/statistics/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics/statistics_service.js
new file mode 100644
index 00000000000..c3889c28a4e
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/statistics/statistics_service.js
@@ -0,0 +1,21 @@
+import { reactive } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { rpc } from "@web/core/network/rpc";
+
+const statisticsService = {
+ start() {
+ const statistics = reactive({ value: {} });
+
+ async function fetchAndUpdate() {
+ const newData = await rpc("/awesome_dashboard/statistics");
+ statistics.value = newData;
+ }
+
+ fetchAndUpdate();
+ setInterval(fetchAndUpdate, 600000);
+
+ return { statistics, reload: fetchAndUpdate };
+ },
+};
+
+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..8908a02c343
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_loader.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/static/src/card/card.js b/awesome_owl/static/src/card/card.js
new file mode 100644
index 00000000000..02c2922722d
--- /dev/null
+++ b/awesome_owl/static/src/card/card.js
@@ -0,0 +1,18 @@
+import { Component, useState } from "@odoo/owl";
+
+export class Card extends Component {
+ static template = "awesome_owl.Card";
+ static props = {
+ title: String,
+ content: { optional: true },
+ slots: Object,
+ };
+
+ setup() {
+ this.state = useState({ isOpen: true });
+ }
+
+ toggleContent() {
+ this.state.isOpen ^= 1;
+ }
+}
diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml
new file mode 100644
index 00000000000..42049222f7b
--- /dev/null
+++ b/awesome_owl/static/src/card/card.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js
new file mode 100644
index 00000000000..8479af2dda6
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.js
@@ -0,0 +1,5 @@
+import { Component } from "@odoo/owl";
+
+export class Counter extends Component {
+ static template = "awesome_owl.Counter";
+}
diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml
new file mode 100644
index 00000000000..42504519292
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/img/monkey1.jpg b/awesome_owl/static/src/img/monkey1.jpg
new file mode 100644
index 00000000000..8d10b4e6e19
Binary files /dev/null and b/awesome_owl/static/src/img/monkey1.jpg differ
diff --git a/awesome_owl/static/src/img/monkey2.jpg b/awesome_owl/static/src/img/monkey2.jpg
new file mode 100644
index 00000000000..09bdda65aae
Binary files /dev/null and b/awesome_owl/static/src/img/monkey2.jpg differ
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 657fb8b07bb..fa252d163e4 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,7 +1,30 @@
/** @odoo-module **/
-import { Component } from "@odoo/owl";
+import { Component, markup, useState } from "@odoo/owl";
+import { Counter } from "./counter/counter";
+import { Card } from "./card/card";
+import { TodoListView } from "./todo/todo_list";
export class Playground extends Component {
static template = "awesome_owl.playground";
+ static components = {Counter, Card, TodoListView};
+
+ setup() {
+ this.welcomeTxt = "Welcome to Monkey Town"
+ this.content = "Save the monkeys"
+ this.rawHtml = "Raw HTML"
+ this.safeHtml = markup("Medium Rare HTML")
+ this.counters = useState([
+ { value: 0 },
+ { value: 0 },
+ ])
+ }
+
+ increment(index) {
+ this.counters[index].value++
+ }
+
+ total() {
+ return this.counters[0].value + this.counters[1].value
+ }
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..282d1fa5340 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -1,10 +1,35 @@
-
-
-
- hello world
-
-
-
+
+
+
+
+
+
Give me a banana
+
Give me an orange
+
+
Total donations:
+
+
+
+ Thank you for the 🍌s
+
+
+
+ Thank you for the 🍊s
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js
new file mode 100644
index 00000000000..171314578ac
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_item.js
@@ -0,0 +1,12 @@
+// todoItem
+import { Component } from "@odoo/owl";
+import { Todo } from "./todo_model";
+
+export class TodoItem extends Component {
+ static template = "awesome_owl.TodoItem";
+ static props = {
+ todo: Todo,
+ index: Number,
+ onRemove: Function,
+ };
+}
diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml
new file mode 100644
index 00000000000..c1ed67b75a0
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_item.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js
new file mode 100644
index 00000000000..d63f0f91c14
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_list.js
@@ -0,0 +1,29 @@
+// todolist
+import { Component, useState } from "@odoo/owl";
+import { TodoItem } from "./todo_item";
+import { TodoList as TodoListModel } from "./todo_model";
+
+export class TodoListView extends Component {
+ static template = "awesome_owl.TodoListView";
+ static components = { TodoItem };
+
+ setup() {
+ this.model = useState(new TodoListModel());
+ }
+
+ addTodoItem() {
+ this.model.addTodo("New Todo");
+ }
+
+ removeTodoAt(index) {
+ this.model.removeTodo(index);
+ }
+
+ addTodo(ev) {
+ const value = ev.target.value.trim();
+ if (ev.key === "Enter" && value) {
+ this.model.addTodo(value);
+ ev.target.value = "";
+ }
+ }
+}
diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml
new file mode 100644
index 00000000000..0101f55932d
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_list.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/todo/todo_model.js b/awesome_owl/static/src/todo/todo_model.js
new file mode 100644
index 00000000000..bdc82df2865
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_model.js
@@ -0,0 +1,27 @@
+export class Todo {
+ constructor(model, id, title) {
+ this.id = id;
+ this.title = title;
+ this.done = false;
+ this._model = model;
+ }
+
+ toggle() {
+ this.done ^= 1;
+ }
+}
+
+export class TodoList {
+ constructor() {
+ this.todos = [];
+ this._nextId = 1;
+ }
+
+ addTodo(title) {
+ this.todos.push(new Todo(this, this._nextId++, title));
+ }
+
+ removeTodo(index) {
+ this.todos.splice(index, 1);
+ }
+}