diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..fcf77ca1563 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "Python Debugger: Current File with Arguments", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "args": "${command:pickArgs}" + } + ] +} \ No newline at end of file diff --git a/awesome_dashboard/__init__.py b/awesome_dashboard/__init__.py index b0f26a9a602..aa4d0fd63a9 100644 --- a/awesome_dashboard/__init__.py +++ b/awesome_dashboard/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import controllers +from . import models diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..70686a93aba 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -19,12 +19,17 @@ 'depends': ['base', 'web', 'mail', 'crm'], 'data': [ + 'security/ir.model.access.csv', 'views/views.xml', ], 'assets': { 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', + ('remove', 'awesome_dashboard/static/src/dashboard/*'), # Lazy loaded, so remove from main bundle ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/*' # create new bundle to be lazy loaded + ] }, 'license': 'AGPL-3' } diff --git a/awesome_dashboard/controllers/__init__.py b/awesome_dashboard/controllers/__init__.py index 457bae27e11..f296d20d017 100644 --- a/awesome_dashboard/controllers/__init__.py +++ b/awesome_dashboard/controllers/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- -from . import controllers \ No newline at end of file +from . import controllers +from . import awesome_dashboard_controller diff --git a/awesome_dashboard/controllers/awesome_dashboard_controller.py b/awesome_dashboard/controllers/awesome_dashboard_controller.py new file mode 100644 index 00000000000..bf5cbc41712 --- /dev/null +++ b/awesome_dashboard/controllers/awesome_dashboard_controller.py @@ -0,0 +1,27 @@ +from odoo import http +from odoo.http import request + +class AwesomeDashboardController(http.Controller): + + @http.route('/awesome_dashboard/save_settings', type='json', auth='user') + def save_settings(self, new_disabled_items): + current_user = http.request.env.user + user_dashboard_settings = http.request.env['user.dashboard.settings'].sudo().search([('user_id', '=', current_user.id)], limit=1) + disabled_items = ",".join(new_disabled_items) + + if user_dashboard_settings: + user_dashboard_settings.write({'disabled_items': disabled_items}) + else: + http.request.env['user.dashboard.settings'].sudo().create({ + 'user_id': current_user.id, + 'disabled_items': disabled_items + }) + return True + + @http.route('/awesome_dashboard/get_settings', type='json', auth='user') + def get_settings(self): + current_user = http.request.env.user + user_dashboard_settings = http.request.env['user.dashboard.settings'].sudo().search([('user_id', '=', current_user.id)], limit=1) + if user_dashboard_settings and user_dashboard_settings.disabled_items: + return user_dashboard_settings.disabled_items.split(',') + return [] diff --git a/awesome_dashboard/models/__init__.py b/awesome_dashboard/models/__init__.py new file mode 100644 index 00000000000..de10c9a0443 --- /dev/null +++ b/awesome_dashboard/models/__init__.py @@ -0,0 +1 @@ +from . import user_dashboard_settings diff --git a/awesome_dashboard/models/user_dashboard_settings.py b/awesome_dashboard/models/user_dashboard_settings.py new file mode 100644 index 00000000000..7d11bc90030 --- /dev/null +++ b/awesome_dashboard/models/user_dashboard_settings.py @@ -0,0 +1,8 @@ +from odoo import models, fields + +class UserDashboardSettings(models.Model): + _name = "user.dashboard.settings" + _description = "User Dashboard Settings" + + user_id = fields.Many2one('res.users', string="User", required=True) + disabled_items = fields.Char(string='Dashboard Disabled Items') # Add this Field diff --git a/awesome_dashboard/security/ir.model.access.csv b/awesome_dashboard/security/ir.model.access.csv new file mode 100644 index 00000000000..db5675d1408 --- /dev/null +++ b/awesome_dashboard/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_user_dashboard_settings_model,access_user_dashboard_settings_model,model_user_dashboard_settings,base.group_user,1,1,1,1 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/configuration_dialog/configuration_dialog.js b/awesome_dashboard/static/src/dashboard/configuration_dialog/configuration_dialog.js new file mode 100644 index 00000000000..dc6c753ace4 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/configuration_dialog/configuration_dialog.js @@ -0,0 +1,35 @@ +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { rpc } from "@web/core/network/rpc"; +import { Component, useState } from "@odoo/owl"; + +export class ConfigurationDialog extends Component { + static template = "awesome_dashboard.ConfigurationDialog"; + static components = { Dialog, CheckBox }; + + static props = { + items: Array, + disabledItems: Array, + onUpdateConfiguration: Function, + }; + + setup() { + this.checkBoxItems = useState(this.props.items.map((item) => { // Add 'enabled' field to Item -> return Array of Objects + return { + ...item, + enabled: !this.props.disabledItems.includes(item.id), + } + })); + } + + onChange(checked, changedItem) { + changedItem.enabled = checked; + } + + onDone() { + const newDisabledItems = this.checkBoxItems.filter(item => !item.enabled).map(item => item.id); + rpc("/awesome_dashboard/save_settings", { new_disabled_items: newDisabledItems }); + this.props.onUpdateConfiguration(newDisabledItems); + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/configuration_dialog/configuration_dialog.xml b/awesome_dashboard/static/src/dashboard/configuration_dialog/configuration_dialog.xml new file mode 100644 index 00000000000..82eb01340bd --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/configuration_dialog/configuration_dialog.xml @@ -0,0 +1,18 @@ + + + + + Which cards do you whish to see ? + + + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..e6d059a654c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,58 @@ +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { browser } from "@web/core/browser/browser"; +import { PieChart } from "./pie_chart/pie_chart"; +import { DashboardItem } from "./dashboard_item"; +import { ConfigurationDialog } from "./configuration_dialog/configuration_dialog"; +import { Component, useState, onWillStart } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; + + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem, PieChart }; + + setup() { + this.action = useService("action"); + this.dialog = useService("dialog"); + this.statistics = useState(useService("awesome_dashboard.statistics")); // useState() because it's reactive + this.items = registry.category("awesome_dashboard.items").getAll(); + this.state = useState({ + disabledItems: [], // useState() because it's reactive + }); + + onWillStart(async () => { + this.state.disabledItems = await rpc("/awesome_dashboard/get_settings"); + }); + } + + openCustomers() { + this.action.doAction('base.action_partner_form'); + } + + openLeads(){ + this.action.doAction({ // define the action inline + type: 'ir.actions.act_window', + name: 'Leads', + target: 'current', + res_model: 'crm.lead', + views: [ + [false, 'form'], + [false, 'list']], + }); + } + + openConfiguration(){ + this.dialog.add(ConfigurationDialog, { + items: this.items, + disabledItems: this.state.disabledItems, + onUpdateConfiguration: this.updateConfiguration.bind(this), + }); + } + + updateConfiguration(newDisabledItems){ + this.state.disabledItems = newDisabledItems // update the state + } +} +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..136e67a3cbb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,17 @@ +.o_dashboard { + background-color: #f5f5f5; + padding: 20px; +} + +.o_dashboard_buttons { + display: flex; + gap: 0.5rem; +} + +@media (max-width: 767.98px) { + .o_dashboard .card { + width: 100% !important; + max-width: 100% !important; + display: block !important; + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..1c2f903003f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,27 @@ + + + + + +
+
+ + +
+
+

Dashboard

+ +
+
+ + + + + + + +
+
+
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..f0e6bca5d49 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -0,0 +1,9 @@ +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, optional: true }, + }; +} 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..b907c90f762 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.xml @@ -0,0 +1,11 @@ + + + + +
+
+ +
+
+
+
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..6976f907241 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,66 @@ +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"; + +const items = [ + { + id: "average_quantity", + description: _t("Average amount"), + Component: NumberCard, + props: (data) => ({ + title: _t("Average amount of t-shirt by order this month"), + value: data.average_quantity + }), + }, + { + id: "average_time", + description: _t("Average Time"), + Component: NumberCard, + props: (data) => ({ + title: _t("Average Time"), + value: data.average_time + }), + }, + { + id: "nb_new_orders", + description: _t("Number of new orders"), + Component: NumberCard, + props: (data) => ({ + title: _t("Number of new orders"), + 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"), + value: data.nb_cancelled_orders + }), + }, + { + id: "total_amount", + description: _t("Total amount"), + Component: NumberCard, + props: (data) => ({ + title: _t("Total amount"), + value: data.total_amount + }), + }, + { + id: "pie_chart", + description: _t("Shirts orders by size"), + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: _t("Shirts orders by size"), + value: data.orders_by_size + }), + }, +] + +items.forEach((item => { + registry.category("awesome_dashboard.items").add(item.id, item); +})); 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..3296d42cb9b --- /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.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml new file mode 100644 index 00000000000..3a0713623fa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,9 @@ + + + + +
+ +
+
+
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..95df4b89055 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,89 @@ +import { loadJS } from "@web/core/assets"; +import { useService } from "@web/core/utils/hooks"; +import { Component, onWillStart, onWillUnmount, useEffect, useRef } from "@odoo/owl"; + +export class PieChart extends Component { + static template = "awesome_dashboard.pie_chart"; + static props = { + title: String, + data: Object, + }; + + setup() { + this.canvasRef = useRef("canvas") + this.action = useService("action"); + + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + + useEffect(() => { + if (this.pieChart) { + this.pieChart.destroy(); + } + this.renderPieChart(); + }); + onWillUnmount(() => { + if (this.pieChart) { + this.pieChart.destroy(); + } + }); + } + + renderPieChart() { + if (!this.canvasRef.el || !this.props.data) { + return; + } + const key = Object.keys(this.props.data); + const value = Object.values(this.props.data); + const backgroundColor = [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)' + ]; + const borderColor = [ + 'rgba(255, 99, 132, 1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)', + 'rgba(153, 102, 255, 1)', + 'rgba(255, 159, 64, 1)' + ]; + + this.pieChart = new Chart(this.canvasRef.el, { + type: 'pie', + data: { + labels: key, + datasets: [{ + label: this.props.title, + data: value, + backgroundColor: backgroundColor, + borderColor: borderColor, + borderWidth: 1 + }] + }, + options: { + onClick: (event, elements) => { + if(elements.length > 0){ + const index = elements[0].index; + const size = Object.keys(this.props.data)[index]; + this.action.doAction({ + type: "ir.actions.act_window", + name: "Orders List", + res_model: "sale.order", + views: [ + [false, 'list'], + [false, 'form']], + domain: [ + ['order_line.name', 'ilike', size] + ], + }); + } + } + } + }); + } +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..8b6e186221a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -0,0 +1,8 @@ + + + + +

+ + + 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..7af6d6ddf95 --- /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 components = { PieChart } + static props = { + title: String, + value: Object, + } +} 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..c2d1dbdf09d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js new file mode 100644 index 00000000000..fce2dbe2674 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,17 @@ +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; + +const statisticsService = { + start() { + const statistics = reactive({isReady : false}); + async function loadingData() { + const data = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, data, {isReady : true}); + } + loadingData(); + setInterval(loadingData, 1000 * 60 * 10); + return statistics; + }, +}; +registry.category("services").add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..b24476265dc --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.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 index 4cfd7ac3550..a513debda4f 100644 --- a/awesome_owl/static/src/card/card.js +++ b/awesome_owl/static/src/card/card.js @@ -2,4 +2,17 @@ import { Component, useState } from "@odoo/owl"; export class Card extends Component { static template = "awesome_owl.card"; + + static props = { + title: String, + slots: { type: Object, optional: true }, + }; + + setup() { + this.state = useState({contentState: true}); + } + + toggleContent(){ + this.state.contentState = !this.state.contentState; + } } diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml index 39acbf4c9be..8d74d3b4e72 100644 --- a/awesome_owl/static/src/card/card.xml +++ b/awesome_owl/static/src/card/card.xml @@ -1,11 +1,11 @@ -
-
-

+
+ +
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js index 9e8c7b7d081..dc98ffbf90a 100644 --- a/awesome_owl/static/src/counter/counter.js +++ b/awesome_owl/static/src/counter/counter.js @@ -3,11 +3,18 @@ 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++; + if (this.props.onChange) { + this.props.onChange(); // Call the parent callback with the new value + } } } diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index ce6a8be244b..d282ff16f6f 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,12 +1,25 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, useState, markup } from "@odoo/owl"; +import { TodoList } from "./todo/todo_list"; import { Counter } from "./counter/counter"; import { Card } from "./card/card"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList }; - static components = { Counter, Card }; + setup() { + this.state = useState({ sum: 2 }); + } + incrementSum(){ + this.state.sum++; + } + + cards = [ + { title: "Card 1"}, + { title: "Card 2"}, + { title: "Card 3"}, + ]; } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index d4c8bf9ae91..56b8fd2ab14 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,18 +2,32 @@ -
- hello world +

Playground

+

This is a simple playground to test Owl features.

+
+
+

Counters

- - + +
+

Sum:

-
- - - +
+
+

Cards

+
+ + + + + +
+
+
+
+

Todo List

+
- 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..bdf6ac44990 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,25 @@ +import { Component, useState } 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 }, + removeTodo: { type: Function, optional: true }, + }; + + setup() {} + + toggle(event){ + if(this.props.toggleState){ + this.props.toggleState(this.props.todo.id, event.target.checked); + } + } + + remove(){ + if(this.props.removeTodo){ + this.props.removeTodo(this.props.todo.id); + } + } +} 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..75c343a6f2e --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,8 @@ + +
+ + ID: - + + +
+
\ No newline at end of file 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..fa2121e06a2 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,45 @@ +import { Component, useState, onMounted, } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutofocus } from "../utils"; + + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + static components = { TodoItem }; + + static props = {}; + + setup() { + this.todos = useState([ + { id: 1, description: "buy milk", isCompleted: true }, + { id: 2, description: "buy cheese", isCompleted: false }, + { id: 3, description: "buy bread", isCompleted: false }]); + useAutofocus(this, "todoInput"); + } + + addTodo(event) { + if(event.key === "Enter" && event.target.value.trim() !== ""){ + const newId = this.todos.length ? Math.max(...this.todos.map(t => t.id)) +1 : 1; + this.todos.push({ + id: newId, + description: event.target.value.trim(), + isCompleted: false + }); + event.target.value = ""; + } + } + + removeTodo(elemId){ + const index = this.todos.findIndex((elem) => elem.id === elemId); + if(index >= 0){ + this.todos.splice(index, 1); + } + } + + toggleTodoState(id, state){ + const todo = this.todos.find(t => t.id === id); + if (todo) { + todo.isCompleted = state; + } + } +} 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..78b1b4ea789 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,8 @@ + +
+ + + + +
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..e9a152da5ac --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,21 @@ +/** + * useAutofocus - helper to focus an input element + * @param {Component} component - the component instance + * @param {string} refName - the t-ref name of the input element + */ +export function useAutofocus(component, refName) { + // Store original mounted method + const originalMounted = component.mounted; + + component.mounted = function () { + // Call original mounted() if it exists + if (originalMounted) { + originalMounted.call(this); + } + + // Focus the element + if (this.refs && this.refs[refName]) { + this.refs[refName].focus(); + } + }; +} diff --git a/tutorials b/tutorials new file mode 160000 index 00000000000..696a0cc0644 --- /dev/null +++ b/tutorials @@ -0,0 +1 @@ +Subproject commit 696a0cc064431d4b62e2629fc34919d4a9a69ee5