From 962dedec98c253c4a5f6fe5f186b8c1a0a19af17 Mon Sep 17 00:00:00 2001 From: sawer Date: Mon, 27 Oct 2025 11:24:07 +0100 Subject: [PATCH 1/5] [ADD] Chapter 1: Owl components - sawer --- awesome_owl/static/src/card/card.js | 25 +++++++++++++ awesome_owl/static/src/card/card.xml | 18 +++++++++ awesome_owl/static/src/counter/counter.js | 21 +++++++++++ awesome_owl/static/src/counter/counter.xml | 13 +++++++ awesome_owl/static/src/playground.js | 18 ++++++++- awesome_owl/static/src/playground.xml | 22 ++++++++++- awesome_owl/static/src/todolist/todoitem.js | 30 +++++++++++++++ awesome_owl/static/src/todolist/todoitem.xml | 15 ++++++++ awesome_owl/static/src/todolist/todolist.js | 39 ++++++++++++++++++++ awesome_owl/static/src/todolist/todolist.xml | 16 ++++++++ awesome_owl/static/src/utils.js | 9 +++++ 11 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 awesome_owl/static/src/card/card.js create mode 100644 awesome_owl/static/src/card/card.xml create mode 100644 awesome_owl/static/src/counter/counter.js create mode 100644 awesome_owl/static/src/counter/counter.xml create mode 100644 awesome_owl/static/src/todolist/todoitem.js create mode 100644 awesome_owl/static/src/todolist/todoitem.xml create mode 100644 awesome_owl/static/src/todolist/todolist.js create mode 100644 awesome_owl/static/src/todolist/todolist.xml create mode 100644 awesome_owl/static/src/utils.js diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..1751661fb08 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,25 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: {type: String}, + slots: { + type: Object, + shape: { + default: true + } + } + } + + setup() { + this.state = useState({ isOpen: false }); + } + + 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..f81b33ab3cc --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,18 @@ + + + + +
+
+
+ + +
+
+

+ +

+
+
+ +
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..f7fdbd0627c --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,21 @@ +/** @odoo-module **/ + +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: 1 }); + } + + increment() { + this.state.value++; + if (this.props.onChange) { + 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..183bfef03f3 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,13 @@ + + + + +
+ +

Counter:

+
+ +
+
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..acfab6d4bfe 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,23 @@ /** @odoo-module **/ +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todolist/todolist"; -import { Component } from "@odoo/owl"; +import { Component, useState, markup } from "@odoo/owl"; export class Playground extends Component { static template = "awesome_owl.playground"; + + static components = { Counter, Card, TodoList }; + + content1 = "
some text 1
"; + content2 = markup("
some text 2
"); + + setup() { + this.state = useState({ sum: 2 }); + } + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..944b5f9f9d4 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,28 @@ + -
- hello world + +
+ + +

The sum is:

+
+
+
+ + + + + Content of card 2 +
+
+
+ +
+ diff --git a/awesome_owl/static/src/todolist/todoitem.js b/awesome_owl/static/src/todolist/todoitem.js new file mode 100644 index 00000000000..d2467f8ddf9 --- /dev/null +++ b/awesome_owl/static/src/todolist/todoitem.js @@ -0,0 +1,30 @@ +/** @odoo-module **/ + +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 + } + }, + removeTodo: { + type: Function + } + } + + toggleState() { + this.props.todo.isCompleted = !this.props.todo.isCompleted; + } + + onDelete() { + this.props.removeTodo(this.props.todo.id); + } + +} diff --git a/awesome_owl/static/src/todolist/todoitem.xml b/awesome_owl/static/src/todolist/todoitem.xml new file mode 100644 index 00000000000..6d9cd6ce411 --- /dev/null +++ b/awesome_owl/static/src/todolist/todoitem.xml @@ -0,0 +1,15 @@ + + + + + +
+ + + +
+
+ +
diff --git a/awesome_owl/static/src/todolist/todolist.js b/awesome_owl/static/src/todolist/todolist.js new file mode 100644 index 00000000000..9460a905a3c --- /dev/null +++ b/awesome_owl/static/src/todolist/todolist.js @@ -0,0 +1,39 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todoitem"; +import { useAutofocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.todolist"; + + static components = { TodoItem }; + + setup() { + this.todos = useState([ + { id: 1, description: "take a coffe", isCompleted: true }, + { id: 2, description: "write tutorial", isCompleted: false }, + { id: 3, description: "buy milk", isCompleted: false } + ]); + + useAutofocus("addTodo-input"); + } + + addTodo(ev) { + if(ev.keyCode === 13 && ev.target.value !== "") { + this.todos.push({ id: this.todos.length + 1, description: ev.target.value.replace(/\n/g, ''), isCompleted: false }); + ev.target.value = ""; + } + } + + removeTodo(idToRemove) { + const index = this.todos.findIndex((elem) => elem.id === idToRemove); + if (index >= 0) { + this.todos.splice(index, 1); + for(let i=index; i + + + + + +
+ +

+ +

+
+ +
+ +
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..3a7d572da73 --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,9 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(name) { + const addTodoInputRef = useRef(name); + onMounted(() => { + addTodoInputRef.el.focus(); + console.log(addTodoInputRef.el); + }); +} From 95317a89be5c0d36e2ed542431f1e6d8bdc624f0 Mon Sep 17 00:00:00 2001 From: sawer Date: Mon, 27 Oct 2025 16:54:31 +0100 Subject: [PATCH 2/5] [ADD] Chapter 2: Build a dashboard - points 1 to 4 [ADD] Chapter 2: Build a dashboard - points 5 to 11 --- awesome_dashboard/static/src/dashboard.js | 10 --- .../static/src/dashboard/dashboard.js | 89 +++++++++++++++++++ .../static/src/dashboard/dashboard.scss | 3 + .../static/src/dashboard/dashboard.xml | 47 ++++++++++ .../dashboard_item/dashboard_item.js | 24 +++++ .../dashboard_item/dashboard_item.xml | 14 +++ .../static/src/dashboard/dashboard_items.js | 73 +++++++++++++++ .../src/dashboard/number_card/number_card.js | 13 +++ .../src/dashboard/number_card/number_card.xml | 11 +++ .../pie_chart_card/pie_chart_card.js | 16 ++++ .../pie_chart_card/pie_chart_card.xml | 9 ++ .../static/src/dashboard/piechart/piechart.js | 34 +++++++ .../piechart/piechart.xml} | 4 +- .../static/src/dashboard/statistics.js | 21 +++++ .../static/src/dashboard_action.js | 15 ++++ 15 files changed, 371 insertions(+), 12 deletions(-) delete mode 100644 awesome_dashboard/static/src/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.scss create mode 100644 awesome_dashboard/static/src/dashboard/dashboard.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml create mode 100644 awesome_dashboard/static/src/dashboard/dashboard_items.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.js create mode 100644 awesome_dashboard/static/src/dashboard/number_card/number_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js create mode 100644 awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml create mode 100644 awesome_dashboard/static/src/dashboard/piechart/piechart.js rename awesome_dashboard/static/src/{dashboard.xml => dashboard/piechart/piechart.xml} (56%) create mode 100644 awesome_dashboard/static/src/dashboard/statistics.js create mode 100644 awesome_dashboard/static/src/dashboard_action.js 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..56678b17f25 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,89 @@ +/** @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 { DashboardItem } from "./dashboard_item/dashboard_item"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; +import { _t } from "@web/core/l10n/translation"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + + static components = { Layout, DashboardItem }; + + setup() { + this.action = useService("action"); + this.statistics = useState(useService("awesome_dashboard.statistics")); + this.items = registry.category("awesome_dashboard").getAll(); + this.dialog = useService("dialog"); + this.state = useState({ + disabledItems: browser.localStorage.getItem("disabledDashboardItems")?.split(",") || [] + }); + } + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: 'ir.actions.act_window', + name: 'All Leads', + target: 'current', + res_model: 'crm.lead', + views: [[false, 'list'], [false, 'form']], + }); + } + + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.items, + disabledItems: this.state.disabledItems, + onUpdateConfiguration: this.updateConfiguration.bind(this), + }) + } + + updateConfiguration(newDisabledItems) { + this.state.disabledItems = newDisabledItems; + } +} + +class ConfigurationDialog extends Component { + static template = "awesome_dashboard.ConfigurationDialog"; + static components = { Dialog, CheckBox }; + static props = ["close", "items", "disabledItems", "onUpdateConfiguration"]; + + setup() { + this.items = useState(this.props.items.map((item) => { + return { + ...item, + enabled: !this.props.disabledItems.includes(item.id), + } + })); + } + + done() { + this.props.close(); + } + + onChange(checked, changedItem) { + changedItem.enabled = checked; + const newDisabledItems = Object.values(this.items).filter( + (item) => !item.enabled + ).map((item) => item.id) + + browser.localStorage.setItem( + "disabledDashboardItems", + newDisabledItems, + ); + + this.props.onUpdateConfiguration(newDisabledItems); + } + +} + +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..7bf58f67a2e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: rgb(147, 121, 172); +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..72b96a8e18d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + +
+ + + + + + +
+ +
+ +
+ + + + Which item do you want to display ? + + + + + + + + + + + +
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..af807045517 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,24 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + static props = { + size: { + type: Number, + optional: true + }, + slots: { + type: Object, + shape: { + default: Object + } + } + }; + static defaultProps = { + size: 1, + }; +} 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..9e6f41fef4f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,14 @@ + + + + + +
+
+ +
+
+ +
+ +
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..aaab3bc5edb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,73 @@ +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; +import { registry } from "@web/core/registry"; +import { _t } from "@web/core/l10n/translation"; + +export const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + // size and props are optionals + size: 1.2, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity + }), + }, + { + id: "total_amount", + description: "Total amount of new orders", + Component: NumberCard, + // size and props are optional + props: (data) => ({ + title: "Total amount of new orders this month:", + value: data.total_amount + }), + }, + { + id: "nb_new_orders", + description: "Number of new orders", + Component: NumberCard, + // size and props are optionals + props: (data) => ({ + title: "Number of new orders this month:", + value: data.nb_new_orders + }), + }, + { + id: "nb_cancelled_orders", + description: "Number of cancelled orders", + Component: NumberCard, + // size and props are optionals + props: (data) => ({ + title: "Number of cancelled orders this month:", + value: data.nb_cancelled_orders + }), + }, + { + id: "average_time", + description: "Average time", + Component: NumberCard, + // size and props are optionals + size: 2, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled':", + value: data.average_time + }), + }, + { + id: "pie_chart", + description: "Shirt orders by size", + Component: PieChartCard, + // size and props are optionals + props: (data) => ({ + title: "Shirt orders by size:", + values: data.orders_by_size + }) + } +] + +items.forEach(item => { + registry.category("awesome_dashboard").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..315d5c4dabf --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,13 @@ +/** @odoo-module */ + +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..ba90222d0b0 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,11 @@ + + + + + +
+ +
+
+ +
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..236b3a0ab05 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js @@ -0,0 +1,16 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; +import { PieChart } from "../piechart/piechart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + + static components = { PieChart } + + static props = { + title: String, + values: 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..37f5cd0a295 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/piechart/piechart.js b/awesome_dashboard/static/src/dashboard/piechart/piechart.js new file mode 100644 index 00000000000..a383e03d7aa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.js @@ -0,0 +1,34 @@ +/** @odoo-module **/ + +import { Component, onWillStart, useRef, onMounted } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + + static props = { + data: Object, + label: String + } + + setup() { + this.canva = useRef("canva") + onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"])); + onMounted(() => this.renderChart()); + } + + renderChart() { + this.myPieChart = new Chart(this.canva.el, { + type: 'pie', + data: { + labels: Object.keys(this.props.data), + datasets: [ + { + label: this.props.label, + data: Object.values(this.props.data) + }, + ], + }, + }); + } +} diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml similarity index 56% rename from awesome_dashboard/static/src/dashboard.xml rename to awesome_dashboard/static/src/dashboard/piechart/piechart.xml index 1a2ac9a2fed..7b73cb45b7a 100644 --- a/awesome_dashboard/static/src/dashboard.xml +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml @@ -1,8 +1,8 @@ - - hello dashboard + + diff --git a/awesome_dashboard/static/src/dashboard/statistics.js b/awesome_dashboard/static/src/dashboard/statistics.js new file mode 100644 index 00000000000..009a027b7cd --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics.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( {changed: false} ) + + async function loadStatistics() { + const updates = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, updates, { changed: true }); + }; + + setInterval(loadStatistics, 10000); + loadStatistics(); + + 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..35296129c1f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,15 @@ +/** @odoo-module */ + +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); From 01980815d9ff32a309f32451ed76e0fc5dfea058 Mon Sep 17 00:00:00 2001 From: sawer Date: Wed, 29 Oct 2025 10:39:50 +0100 Subject: [PATCH 3/5] [ADD] Chapter 1: Build a Clicker game - points 1 to 10 [ADD] Chapter 1: Build a Clicker game - points 11 to 21 --- awesome_clicker/static/src/click_rewards.js | 54 +++++++ .../static/src/click_value/click_value.js | 15 ++ .../static/src/click_value/click_value.xml | 10 ++ awesome_clicker/static/src/clicker_hook.js | 6 + .../static/src/clicker_migration.js | 37 +++++ awesome_clicker/static/src/clicker_model.js | 142 ++++++++++++++++++ .../static/src/clicker_providers.js | 27 ++++ awesome_clicker/static/src/clicker_service.js | 57 +++++++ .../clicker_systray_item.js | 54 +++++++ .../clicker_systray_item.xml | 44 ++++++ .../static/src/client_action/client_action.js | 20 +++ .../src/client_action/client_action.xml | 105 +++++++++++++ .../src/form_controller/form_controller.js | 13 ++ awesome_clicker/static/src/utils.js | 4 + 14 files changed, 588 insertions(+) create mode 100644 awesome_clicker/static/src/click_rewards.js create mode 100644 awesome_clicker/static/src/click_value/click_value.js create mode 100644 awesome_clicker/static/src/click_value/click_value.xml create mode 100644 awesome_clicker/static/src/clicker_hook.js create mode 100644 awesome_clicker/static/src/clicker_migration.js create mode 100644 awesome_clicker/static/src/clicker_model.js create mode 100644 awesome_clicker/static/src/clicker_providers.js create mode 100644 awesome_clicker/static/src/clicker_service.js create mode 100644 awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js create mode 100644 awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml create mode 100644 awesome_clicker/static/src/client_action/client_action.js create mode 100644 awesome_clicker/static/src/client_action/client_action.xml create mode 100644 awesome_clicker/static/src/form_controller/form_controller.js create mode 100644 awesome_clicker/static/src/utils.js diff --git a/awesome_clicker/static/src/click_rewards.js b/awesome_clicker/static/src/click_rewards.js new file mode 100644 index 00000000000..59ced766413 --- /dev/null +++ b/awesome_clicker/static/src/click_rewards.js @@ -0,0 +1,54 @@ +export const rewards = [ + { + description: "Get 1 click bot", + apply(clicker) { + clicker.bots["clickBots"].number += 1; + }, + maxLevel: 2, + }, + { + description: "Get 10 click bot", + apply(clicker) { + clicker.bots["clickBots"].number += 10; + }, + minLevel: 1, + maxLevel: 3, + }, + { + description: "Get 5 big bot", + apply(clicker) { + clicker.bots["bigBots"].number += 5; + }, + minLevel: 2, + maxLevel: 4, + }, + { + description: "Increase bot power!", + apply(clicker) { + clicker.power += 1; + }, + minLevel: 3, + }, + { + description: "Increase bot power by 100!", + apply(clicker) { + clicker.power += 100; + }, + minLevel: 4, + }, + { + description: "Get a Cherry Tree", + apply(clicker) { + clicker.trees["cherryTrees"].number += 1; + }, + minLevel: 4, + }, + { + description: "Get a Pear Tree", + apply(clicker) { + clicker.trees["pearTrees"].number += 1; + }, + minLevel: 4, + }, + +]; diff --git a/awesome_clicker/static/src/click_value/click_value.js b/awesome_clicker/static/src/click_value/click_value.js new file mode 100644 index 00000000000..271060c75f6 --- /dev/null +++ b/awesome_clicker/static/src/click_value/click_value.js @@ -0,0 +1,15 @@ +import { Component } from "@odoo/owl"; +import { humanNumber } from "@web/core/utils/numbers"; + +export class ClickValue extends Component { + static template = "awesome_clicker.ClickValue"; + + static props = { + value: Number + } + + get display() { + return humanNumber(this.props.value, { decimals: 1, }) + } + +} diff --git a/awesome_clicker/static/src/click_value/click_value.xml b/awesome_clicker/static/src/click_value/click_value.xml new file mode 100644 index 00000000000..96c23778561 --- /dev/null +++ b/awesome_clicker/static/src/click_value/click_value.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/awesome_clicker/static/src/clicker_hook.js b/awesome_clicker/static/src/clicker_hook.js new file mode 100644 index 00000000000..7920029fd9b --- /dev/null +++ b/awesome_clicker/static/src/clicker_hook.js @@ -0,0 +1,6 @@ +import { useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +export function useClicker() { + return useState(useService("awesome_clicker.clicker")); +} diff --git a/awesome_clicker/static/src/clicker_migration.js b/awesome_clicker/static/src/clicker_migration.js new file mode 100644 index 00000000000..94aeeb0f74a --- /dev/null +++ b/awesome_clicker/static/src/clicker_migration.js @@ -0,0 +1,37 @@ +export const CURRENT_VERSION = 2.0; +export const migrations = [ + { + fromVersion: 1.0, + toVersion: 1.5, + apply: (state) => { + console.log("Nothing to do, you are uptodate dude!"); + } + }, + { + fromVersion: 1.5, + toVersion: 2.0, + apply: (state) => { + console.log("New tree available: peach tree!"); + state.trees.peachTree = { + price: 1000000, + number: 0, + level: 4, + fruit: "peaches" + } + state.fruits.peaches = 0; + } + } +]; + +export function migrate(localState) { + if (localState?.version_number < CURRENT_VERSION) { + for (const migration of migrations) { + if (localState.version_number === migration.fromVersion) { + migration.apply(localState); + localState.version_number = migration.toVersion + } + } + localState.version_number = CURRENT_VERSION; + } + return localState; +} diff --git a/awesome_clicker/static/src/clicker_model.js b/awesome_clicker/static/src/clicker_model.js new file mode 100644 index 00000000000..182c83dadc0 --- /dev/null +++ b/awesome_clicker/static/src/clicker_model.js @@ -0,0 +1,142 @@ + +import { Reactive } from "@web/core/utils/reactive"; +import { EventBus } from "@odoo/owl"; +import { rewards } from "./click_rewards"; +import { randomChoice } from "./utils"; +import { CURRENT_VERSION } from "./clicker_migration"; + +export class ClickerModel extends Reactive { + + constructor() { + super(); + this.value = 0; + this.level = 0; + this.bus = new EventBus(); + this.bots = { + clickBots : { + price: 1000, + number: 0, + level: 1, + clicks: 10 + }, + bigBots : { + price: 5000, + number: 0, + level: 2, + clicks: 100 + } + } + this.power = 1; + this.fruits = { + cherries: 0, + pears: 0, + peaches: 0 + }; + this.trees = { + cherryTrees: { + price: 1000000, + number: 0, + level: 4, + fruit: "cherries" + }, + pearTrees: { + price: 1000000, + number: 0, + level: 4, + fruit: "pears" + }, + peachTree: { + price: 1500000, + level: 4, + produce: "peaches", + purchased: 0, + } + }; + this.version_number = CURRENT_VERSION; + } + + increment(inc) { + this.value += inc; + if (this.milestones[this.level] && this.value >= this.milestones[this.level].clicks) { + this.bus.trigger("MILESTONE", this.milestones[this.level]); + this.level++; + } + } + + buyBot(botName) { + const bot = this.bots[botName]; + const clickBotPrice = bot.price; + if (this.value < clickBotPrice) { + return false; + } + bot.number++; + this.value-=clickBotPrice; + }; + + clickBotsAction() { + for(const bot in this.bots) { + this.value += ( this.bots[bot].clicks * this.bots[bot].number ) * this.power; + } + }; + + buyPower() { + const powerPrice = 50000; + if (this.value < powerPrice) { + return false; + } + this.power++; + this.value-=powerPrice; + }; + + getReward() { + const availableReward = []; + for (const reward of rewards) { + if (reward.minLevel <= this.level || !reward.minLevel) { + if (reward.maxLevel >= this.level || !reward.maxLevel) { + availableReward.push(reward); + } + } + } + const reward = randomChoice(availableReward); + this.bus.trigger("REWARD", reward); + return reward; + } + + buyTree(treeName) { + const tree = this.trees[treeName]; + const treePrice = tree.price; + if (this.value < treePrice) { + return false; + } + tree.number++; + this.value-=treePrice; + }; + + treesAction() { + for(const tree in this.trees) { + this.fruits[this.trees[tree].fruit] += this.trees[tree].number; + } + }; + + toJSON() { + const json = Object.assign({}, this); + delete json["bus"]; + return json; + } + + static fromJSON(json) { + const clicker = new ClickerModel(); + const clickerInstance = Object.assign(clicker, json); + return clickerInstance; + } + + get milestones() { + return [ + { clicks: 1000, bot: "ClickBot" }, + { clicks: 5000, bot: "BigBot" }, + { clicks: 100000, bot: "Power" }, + { clicks: 1000000, bot: "Trees and Fruits" } + ] + } + +} diff --git a/awesome_clicker/static/src/clicker_providers.js b/awesome_clicker/static/src/clicker_providers.js new file mode 100644 index 00000000000..e4b3d0ba69d --- /dev/null +++ b/awesome_clicker/static/src/clicker_providers.js @@ -0,0 +1,27 @@ +import { registry } from "@web/core/registry"; + +const commandProviderRegistry = registry.category("command_provider"); + +commandProviderRegistry.add("clicker", { + provide: (env, options) => { + return [ + { + name: "Open Clicker Game", + action() { + env.services.action.doAction({ + type: "ir.actions.client", + tag: "awesome_clicker.ClientAction", + target: "new", + name: "Clicker Game" + }); + } + }, + { + name: "Buy 1 click bot", + action() { + env.services["awesome_clicker.clicker"].buyBot("clickBots"); + } + } + ] + } +}); diff --git a/awesome_clicker/static/src/clicker_service.js b/awesome_clicker/static/src/clicker_service.js new file mode 100644 index 00000000000..368e696d3b0 --- /dev/null +++ b/awesome_clicker/static/src/clicker_service.js @@ -0,0 +1,57 @@ +import { registry } from "@web/core/registry"; +import { ClickerModel } from "./clicker_model"; +import { browser } from "@web/core/browser/browser"; +import { migrate } from "./clicker_migration"; + +const clickerService = { + dependencies: ["action", "effect", "notification"], + start(env, services) { + const localState = migrate(JSON.parse(browser.localStorage.getItem("clickerState"))); + const model = localState ? ClickerModel.fromJSON(localState): new ClickerModel(); + + document.addEventListener("click", () => model.increment(1), true); + + setInterval(() => model.clickBotsAction(), 10000); + setInterval(() => model.treesAction(), 30000); + setInterval(() => { + browser.localStorage.setItem("clickerState", JSON.stringify(model)) + }, 10000); + + const bus = model.bus; + bus.addEventListener("MILESTONE", (ev) => { + services.effect.add({ + message: `Milestone reached! You can now buy ${ev.detail.bot}`, + type: "rainbow_man", + }); + }); + + bus.addEventListener("REWARD", (ev) => { + const closeNotification = services.notification.add( + `Congrats you won a reward: "${ev.detail.description}"`, + { + type: "success", + sticky: true, + buttons: [ + { + name: "Collect", + onClick: () => { + ev.detail.apply(model); + closeNotification(); + services.action.doAction({ + type: "ir.actions.client", + tag: "awesome_clicker.ClientAction", + target: "new", + name: "Clicker Game" + }); + }, + }, + ], + } + ); + }); + + return model + } +} + +registry.category("services").add("awesome_clicker.clicker", clickerService); diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js new file mode 100644 index 00000000000..7e87f7b4c99 --- /dev/null +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.js @@ -0,0 +1,54 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useClicker } from "../clicker_hook"; +import { useService } from "@web/core/utils/hooks"; +import { ClickValue } from "../click_value/click_value"; +import { Dropdown } from "@web/core/dropdown/dropdown"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; + +export class ClickerSystrayItem extends Component { + static template = "awesome_clicker.ClickerSystrayItem"; + + static components = { ClickValue, Dropdown, DropdownItem } + + static props = {} + + setup() { + this.clickerService = useClicker(); + this.action = useService("action"); + } + + openClientAction() { + this.action.doAction( + { + type: "ir.actions.client", + tag: "awesome_clicker.ClientAction", + target: "new", + name: "Clicker Game" + } + ) + } + + get totalFruits() { + let totalFruits = 0; + for(const fruit in this.clickerService.fruits) { + totalFruits += this.clickerService.fruits[fruit]; + } + return totalFruits; + } + + get totalTrees() { + let totalTrees = 0; + for(const tree in this.clickerService.trees) { + totalTrees += this.clickerService.trees[tree].number; + } + return totalTrees; + } + +} + +export const systrayItem = { + Component: ClickerSystrayItem, +}; + +registry.category("systray").add("awesome_clicker.ClickerSystrayItem", systrayItem); diff --git a/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml new file mode 100644 index 00000000000..02187d01fef --- /dev/null +++ b/awesome_clicker/static/src/clicker_systray_item/clicker_systray_item.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + x + + + + + x + + + + + +
+ + +
+ +
+ +
diff --git a/awesome_clicker/static/src/client_action/client_action.js b/awesome_clicker/static/src/client_action/client_action.js new file mode 100644 index 00000000000..f27cc8e8e9b --- /dev/null +++ b/awesome_clicker/static/src/client_action/client_action.js @@ -0,0 +1,20 @@ +import { Component } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useClicker } from "../clicker_hook"; +import { ClickValue } from "../click_value/click_value"; +import { Notebook } from "@web/core/notebook/notebook" + +export class ClientAction extends Component { + static template = "awesome_clicker.ClientAction"; + + static components = { ClickValue, Notebook } + + static props = ["*"]; + + setup() { + this.clickerService = useClicker(); + } + +} + +registry.category("actions").add("awesome_clicker.ClientAction", ClientAction); diff --git a/awesome_clicker/static/src/client_action/client_action.xml b/awesome_clicker/static/src/client_action/client_action.xml new file mode 100644 index 00000000000..f6d1dc9e0a7 --- /dev/null +++ b/awesome_clicker/static/src/client_action/client_action.xml @@ -0,0 +1,105 @@ + + + + + +
+ Clicks: + + +
+ + + + + +
+

Bots

+ +
+ +
+
+ x ( clicks/10seconds) + +
+
+ +
+
+
+
+ +
+ +
+

Power multiplier

+ +
+
+
+ x Power + +
+
+ +
+
+
+ +
+
+ + + +
+

Trees

+ +
+ +
+
+ x (1x /30seconds) + +
+
+ +
+
+
+
+ +
+ +
+

Fruits

+ +
+ +
+
+ x +
+
+
+
+ +
+ +
+ +
+ +
+ +
diff --git a/awesome_clicker/static/src/form_controller/form_controller.js b/awesome_clicker/static/src/form_controller/form_controller.js new file mode 100644 index 00000000000..a17311a4408 --- /dev/null +++ b/awesome_clicker/static/src/form_controller/form_controller.js @@ -0,0 +1,13 @@ +import { FormController } from "@web/views/form/form_controller"; +import { patch } from "@web/core/utils/patch"; +import { useClicker } from "../clicker_hook"; + +patch(FormController.prototype, { + setup() { + super.setup(...arguments); + if (Math.random() < 0.05) { + const clicker = useClicker(); + clicker.getReward(); + } + }, +}); diff --git a/awesome_clicker/static/src/utils.js b/awesome_clicker/static/src/utils.js new file mode 100644 index 00000000000..85bf0b17fcd --- /dev/null +++ b/awesome_clicker/static/src/utils.js @@ -0,0 +1,4 @@ +export function randomChoice(list) { + const idx = Math.floor(Math.random() * list.length); + return list[idx]; +} From 390d5bf50fbfff1e857a5265d5c319727f14d886 Mon Sep 17 00:00:00 2001 From: sawer Date: Thu, 30 Oct 2025 09:15:39 +0100 Subject: [PATCH 4/5] [ADD] Chapter 2: Create a gallery view - point 1 [ADD] Chapter 2: Create a galler view - points 2 to 14 [FIX] runbot correction in validation.py [FIX] real fix in validation.py --- .../static/src/clicker_migration.js | 2 +- awesome_gallery/__init__.py | 1 + awesome_gallery/models/ir_ui_view.py | 6 ++ awesome_gallery/rng/gallery_view.rng | 59 ++++++++++++++++++ .../static/src/gallery_arch_parser.js | 24 ++++++++ .../static/src/gallery_controller.js | 61 +++++++++++++++++++ .../static/src/gallery_controller.xml | 14 +++++ awesome_gallery/static/src/gallery_image.js | 51 ++++++++++++++++ awesome_gallery/static/src/gallery_image.xml | 26 ++++++++ awesome_gallery/static/src/gallery_model.js | 55 +++++++++++++++++ .../static/src/gallery_renderer.js | 37 +++++++++++ .../static/src/gallery_renderer.xml | 17 ++++++ awesome_gallery/static/src/gallery_view.js | 34 ++++++++++- awesome_gallery/validation.py | 25 ++++++++ awesome_gallery/views/views.xml | 24 +++++++- .../static/src/kanban_controller.js | 0 16 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 awesome_gallery/rng/gallery_view.rng create mode 100644 awesome_gallery/static/src/gallery_arch_parser.js create mode 100644 awesome_gallery/static/src/gallery_controller.js create mode 100644 awesome_gallery/static/src/gallery_controller.xml create mode 100644 awesome_gallery/static/src/gallery_image.js create mode 100644 awesome_gallery/static/src/gallery_image.xml create mode 100644 awesome_gallery/static/src/gallery_model.js create mode 100644 awesome_gallery/static/src/gallery_renderer.js create mode 100644 awesome_gallery/static/src/gallery_renderer.xml create mode 100644 awesome_gallery/validation.py create mode 100644 awesome_kanban/static/src/kanban_controller.js diff --git a/awesome_clicker/static/src/clicker_migration.js b/awesome_clicker/static/src/clicker_migration.js index 94aeeb0f74a..47dd78c1eb0 100644 --- a/awesome_clicker/static/src/clicker_migration.js +++ b/awesome_clicker/static/src/clicker_migration.js @@ -13,7 +13,7 @@ export const migrations = [ apply: (state) => { console.log("New tree available: peach tree!"); state.trees.peachTree = { - price: 1000000, + price: 1500000, number: 0, level: 4, fruit: "peaches" diff --git a/awesome_gallery/__init__.py b/awesome_gallery/__init__.py index a0fdc10fe11..9724f5e7f8d 100644 --- a/awesome_gallery/__init__.py +++ b/awesome_gallery/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- from . import models +from . import validation diff --git a/awesome_gallery/models/ir_ui_view.py b/awesome_gallery/models/ir_ui_view.py index 0c11b8298ac..429ec33a4d8 100644 --- a/awesome_gallery/models/ir_ui_view.py +++ b/awesome_gallery/models/ir_ui_view.py @@ -6,3 +6,9 @@ class View(models.Model): _inherit = 'ir.ui.view' type = fields.Selection(selection_add=[('gallery', "Awesome Gallery")]) + + def _is_qweb_based_view(self, view_type): + return view_type == "gallery" or super()._is_qweb_based_view(view_type) + + def _get_view_info(self): + return {'gallery': {'icon': 'fa fa-picture-o'}} | super()._get_view_info() diff --git a/awesome_gallery/rng/gallery_view.rng b/awesome_gallery/rng/gallery_view.rng new file mode 100644 index 00000000000..b59dc2723e6 --- /dev/null +++ b/awesome_gallery/rng/gallery_view.rng @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/awesome_gallery/static/src/gallery_arch_parser.js b/awesome_gallery/static/src/gallery_arch_parser.js new file mode 100644 index 00000000000..1390a91de76 --- /dev/null +++ b/awesome_gallery/static/src/gallery_arch_parser.js @@ -0,0 +1,24 @@ +import { visitXML } from "@web/core/utils/xml"; + +export class galleryArchParser { + parse(xmlDoc) { + const imageField = xmlDoc.getAttribute("image_field"); + const limit = xmlDoc.getAttribute("limit") || 80; + const fieldsForTooltip = []; + let tooltipTemplate = undefined; + visitXML(xmlDoc, (node) => { + if (node.tagName === "field") { + fieldsForTooltip.push(node.getAttribute("name")); + } + if (node.tagName === "tooltip-template") { + tooltipTemplate = node; + } + }) + return { + imageField, + limit, + fieldsForTooltip, + tooltipTemplate, + } + } +} diff --git a/awesome_gallery/static/src/gallery_controller.js b/awesome_gallery/static/src/gallery_controller.js new file mode 100644 index 00000000000..c1ff336a012 --- /dev/null +++ b/awesome_gallery/static/src/gallery_controller.js @@ -0,0 +1,61 @@ +/** @odoo-module */ + +import { Component, onWillStart, onWillUpdateProps, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { standardViewProps } from "@web/views/standard_view_props"; +import { Layout } from "@web/search/layout"; +import { usePager } from "@web/search/pager_hook"; + +export class GalleryController extends Component { + static template = "awesome_gallery.GalleryController"; + + static props = { + ...standardViewProps, + archInfo: Object, + Model: Function, + Renderer: Function, + }; + + static components = { Layout } + + setup() { + this.orm = useService("orm"); + this.model = useState( + new this.props.Model( + this.orm, + this.props.resModel, + this.props.archInfo, + this.props.fields, + ) + ); + + onWillStart(async () => { + await this.model.loadImages(this.props.domain); + }); + + onWillUpdateProps(async (nextProps) => { + if (JSON.stringify(nextProps.domain) !== JSON.stringify(this.props.domain)) { + await this.model.loadImages(nextProps.domain); + } + }); + + usePager(() => { + const gallery = this.model.pager + return { + offset: gallery.offset, + limit: gallery.limit, + total: this.model.recordsLength, + onUpdate: async ({ offset, limit }) => { + gallery.offset = offset; + gallery.limit = limit; + await this.model.loadImages(this.props.domain); + }, + }; + }); + } + + async onImageUpload(record_id, image_binary) { + this.model.uploadImage(record_id, image_binary, this.props.domain); + } + +} diff --git a/awesome_gallery/static/src/gallery_controller.xml b/awesome_gallery/static/src/gallery_controller.xml new file mode 100644 index 00000000000..8a1832b5e3a --- /dev/null +++ b/awesome_gallery/static/src/gallery_controller.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/awesome_gallery/static/src/gallery_image.js b/awesome_gallery/static/src/gallery_image.js new file mode 100644 index 00000000000..a4e13953905 --- /dev/null +++ b/awesome_gallery/static/src/gallery_image.js @@ -0,0 +1,51 @@ +/** @odoo-module */ + +import { Component, useState } from "@odoo/owl"; +import { GalleryModel } from "./gallery_model"; +import { url } from "@web/core/utils/urls"; +import { useService } from "@web/core/utils/hooks"; +import { FileUploader } from "@web/views/fields/file_handler"; +import { useTooltip } from "@web/core/tooltip/tooltip_hook"; + +export class GalleryImage extends Component { + static template = "awesome_gallery.GalleryImage"; + static props = { + record: Object, + model: GalleryModel, + onImageUpload: Function, + tooltipTemplate: { + optional: true, + type: String, + }, + }; + static components = { FileUploader } + + setup() { + this.action = useState(useService("action")); + + if (this.props.tooltipTemplate) { + useTooltip("tooltip", { + info: { record: this.props.record }, + template: this.props.tooltipTemplate, + }); + } + } + + switchToFormView(resId) { + this.action.switchView("form", { resId }); + } + + get imageUrl() { + return url("/web/image", { + model: this.props.model.resModel, + id: this.props.record.id, + field: this.props.model.imageField, + unique: this.props.record.write_date, + }); + } + + async _onFileUploaded({ data }) { + await this.props.onImageUpload(this.props.record.id, data); + } + +} diff --git a/awesome_gallery/static/src/gallery_image.xml b/awesome_gallery/static/src/gallery_image.xml new file mode 100644 index 00000000000..881c61e39ca --- /dev/null +++ b/awesome_gallery/static/src/gallery_image.xml @@ -0,0 +1,26 @@ + + + + + +
+ + + +
+ + + + Upload image + + + +
+ +
+ + + +
+ +
diff --git a/awesome_gallery/static/src/gallery_model.js b/awesome_gallery/static/src/gallery_model.js new file mode 100644 index 00000000000..3d4085ba2b8 --- /dev/null +++ b/awesome_gallery/static/src/gallery_model.js @@ -0,0 +1,55 @@ +/** @odoo-module */ + +import { KeepLast } from "@web/core/utils/concurrency"; +import { url } from "@web/core/utils/urls"; + +export class GalleryModel { + constructor(orm, resModel, archInfo, fields) { + this.orm = orm; + this.resModel = resModel; + const { imageField, limit, fieldsForTooltip } = archInfo; + this.imageField = imageField; + this.fieldsForTooltip = fieldsForTooltip; + this.limit = limit; + this.keepLast = new KeepLast(); + this.fields = fields; + this.pager = { offset: 0, limit: limit }; + } + + async loadImages(domain) { + const specification = { + [this.imageField]: {}, + write_date: {}, + } + for (const field of this.fieldsForTooltip) { + specification[field] = {}; + } + const { length, records } = await this.keepLast.add( + this.orm.webSearchRead(this.resModel, domain, { + limit: this.pager.limit, + offset: this.pager.offset, + specification, + context: { + bin_size: true, + } + }) + ); + this.records = records; + this.recordsLength = length; + + } + + async uploadImage(record_id, image_binary, domain) { + await this.orm.webSave( + this.resModel, + [record_id], + { + [this.imageField]: image_binary, + }, + { + specification: {}, + } + ) + await this.loadImages(domain); + } +} diff --git a/awesome_gallery/static/src/gallery_renderer.js b/awesome_gallery/static/src/gallery_renderer.js new file mode 100644 index 00000000000..90081851ffc --- /dev/null +++ b/awesome_gallery/static/src/gallery_renderer.js @@ -0,0 +1,37 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; +import { GalleryModel } from "./gallery_model"; +import { GalleryImage } from "./gallery_image"; +import { createElement } from "@web/core/utils/xml"; +import { xml } from "@odoo/owl"; + +export class GalleryRenderer extends Component { + static template = "awesome_gallery.GalleryRenderer"; + + static components = { GalleryImage }; + + static props = { + model: GalleryModel, + onImageUpload: Function, + tooltipTemplate: { + optional: true, + type: Element, + }, + } + + setup() { + if (this.props.tooltipTemplate) { + const fieldsToReplace = this.props.tooltipTemplate.querySelectorAll("field"); + for (const field of fieldsToReplace) { + const fieldName = field.getAttribute("name") + const t = document.createElement("t") + t.setAttribute("t-esc", `record.${fieldName}`) + field.replaceWith(t); + } + const tooltipHTML = createElement("t", [this.props.tooltipTemplate]).outerHTML + this.owlTooltipTemplate = xml`${tooltipHTML}` + } + } + +} diff --git a/awesome_gallery/static/src/gallery_renderer.xml b/awesome_gallery/static/src/gallery_renderer.xml new file mode 100644 index 00000000000..2b81d7b77f2 --- /dev/null +++ b/awesome_gallery/static/src/gallery_renderer.xml @@ -0,0 +1,17 @@ + + + + + + +
+ +
+ +
+
+
+ +
+ +
diff --git a/awesome_gallery/static/src/gallery_view.js b/awesome_gallery/static/src/gallery_view.js index db904d1f478..466c1764566 100644 --- a/awesome_gallery/static/src/gallery_view.js +++ b/awesome_gallery/static/src/gallery_view.js @@ -1,3 +1,35 @@ /** @odoo-module */ -// TODO: Begin here! +import { registry } from "@web/core/registry"; +import { GalleryController } from "./gallery_controller"; +import { galleryArchParser } from "./gallery_arch_parser"; +import {GalleryModel} from "./gallery_model"; +import {GalleryRenderer} from "./gallery_renderer"; + +export const galleryView = { + type: "gallery", + display_name: "Gallery", + icon: "fa fa-picture-o", + multiRecord: true, + Controller: GalleryController, + ArchParser: galleryArchParser, + Model: GalleryModel, + Renderer: GalleryRenderer, + + props(genericProps, view) { + const { arch } = genericProps; + const parser = new view.ArchParser(); + const archInfo = parser.parse(arch); + const gallerymodel = view.Model; + const galleryrenderer = view.Renderer; + + return { + ...genericProps, + archInfo, + Model: gallerymodel, + Renderer: galleryrenderer + }; + }, +}; + +registry.category("views").add("gallery", galleryView); diff --git a/awesome_gallery/validation.py b/awesome_gallery/validation.py new file mode 100644 index 00000000000..603f78d6cc0 --- /dev/null +++ b/awesome_gallery/validation.py @@ -0,0 +1,25 @@ +import logging +import os + +from lxml import etree + +from odoo.loglevels import ustr +from odoo.tools import misc, view_validation + +_logger = logging.getLogger(__name__) + + +@view_validation.validate('gallery') +def schema_viewname(arch, **kwargs): + if not hasattr(schema_viewname, "_gallery_validator"): + with misc.file_open(os.path.join('awesome_gallery', 'rng', 'gallery_view.rng')) as f: + schema_viewname._gallery_validator = etree.RelaxNG(etree.parse(f)) + + validator = schema_viewname._gallery_validator + + if validator.validate(arch): + return True + + for error in validator.error_log: + _logger.error(ustr(error)) + return False diff --git a/awesome_gallery/views/views.xml b/awesome_gallery/views/views.xml index 56327365875..a51365f62a7 100644 --- a/awesome_gallery/views/views.xml +++ b/awesome_gallery/views/views.xml @@ -1,10 +1,27 @@ - + + + + awesome_gallery.orders.gallery + res.partner + + + + + +

I can put anything here

+

name:

+

e-mail:

+
+
+
+
+ Contacts res.partner - kanban,tree,form,activity + kanban,list,form,activity,gallery {'default_is_company': True} @@ -14,6 +31,7 @@ Odoo helps you track all activities related to your contacts.

-
+ +
diff --git a/awesome_kanban/static/src/kanban_controller.js b/awesome_kanban/static/src/kanban_controller.js new file mode 100644 index 00000000000..e69de29bb2d From beb9d10f9754d12ed6704a1730c8c94413ec59ab Mon Sep 17 00:00:00 2001 From: sawer Date: Fri, 31 Oct 2025 14:51:02 +0100 Subject: [PATCH 5/5] [ADD] Chapter 3: Customize a kanban view [IMP] little change in the way in change tho pager offset --- .../static/src/awesome_kanban_view.js | 11 +++- .../static/src/customer_list/customer_list.js | 66 +++++++++++++++++++ .../src/customer_list/customer_list.xml | 31 +++++++++ .../static/src/kanban_controller.js | 34 ++++++++++ .../static/src/kanban_controller.scss | 10 +++ .../static/src/kanban_controller.xml | 14 ++++ 6 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 awesome_kanban/static/src/customer_list/customer_list.js create mode 100644 awesome_kanban/static/src/customer_list/customer_list.xml create mode 100644 awesome_kanban/static/src/kanban_controller.scss create mode 100644 awesome_kanban/static/src/kanban_controller.xml diff --git a/awesome_kanban/static/src/awesome_kanban_view.js b/awesome_kanban/static/src/awesome_kanban_view.js index 9f33fc1300b..bbad3597cd2 100644 --- a/awesome_kanban/static/src/awesome_kanban_view.js +++ b/awesome_kanban/static/src/awesome_kanban_view.js @@ -1,3 +1,12 @@ /** @odoo-module */ -// TODO: Define here your AwesomeKanban view +import { registry } from "@web/core/registry"; +import { kanbanView } from "@web/views/kanban/kanban_view"; +import { AwesomeKanbanController } from "./kanban_controller"; + +export const awesomeKanbanView = { + ...kanbanView, + Controller: AwesomeKanbanController, +}; + +registry.category("views").add("awesome_kanban", awesomeKanbanView); diff --git a/awesome_kanban/static/src/customer_list/customer_list.js b/awesome_kanban/static/src/customer_list/customer_list.js new file mode 100644 index 00000000000..3f2d8d95e41 --- /dev/null +++ b/awesome_kanban/static/src/customer_list/customer_list.js @@ -0,0 +1,66 @@ +/** @odoo-module */ + +import { Component, onWillStart, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { KeepLast } from "@web/core/utils/concurrency"; +import { fuzzyLookup } from "@web/core/utils/search"; +import { Pager } from "@web/core/pager/pager"; + +export class CustomerList extends Component { + static template = "awesome_kanban.CustomerList"; + + static components = { Pager }; + + static props = { + selectCustomer: Function + } + + setup() { + this.orm = useService("orm"); + this.customers = useState({ data: [] }); + this.keepLast = new KeepLast(); + this.state = useState({ displayActiveCustomers: false, searchString: ''}) + onWillStart(async () => { + this.customers.data = await this.loadCustomers([]); + this.pager.total = this.customers.data.length; + }) + this.pager = useState({ offset: 0, limit: 20 }); + } + + loadCustomers() { + const domain = this.state.displayActiveCustomers ? [["opportunity_ids", "!=", false]] : []; + return this.orm.searchRead("res.partner", domain, ["display_name"]); + } + + get displayedCustomers() { + const displayCustomers = this.filterCustomers(this.state.searchString); + this.pager.total = displayCustomers.length; + return displayCustomers.slice(this.pager.offset, this.pager.offset + this.pager.limit); + } + + async checkboxChange(ev) { + this.pager.offset = 0; + this.state.displayActiveCustomers = ev.target.checked; + this.customers.data = await this.keepLast.add(this.loadCustomers()); + this.pager.total = this.customers.data.length; + } + + filterCustomers(name) { + if (name) { + const filteredCustomers = fuzzyLookup( + name, + this.customers.data, + (customer) => customer.display_name + ); + if(this.pager.offset > filteredCustomers.length) this.pager.offset = 0; + return filteredCustomers; + } else { + return this.customers.data; + } + } + + onUpdatePager(data) { + Object.assign(this.pager, data); + } + +} diff --git a/awesome_kanban/static/src/customer_list/customer_list.xml b/awesome_kanban/static/src/customer_list/customer_list.xml new file mode 100644 index 00000000000..a214522080e --- /dev/null +++ b/awesome_kanban/static/src/customer_list/customer_list.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + +
+ Customers +
+ + +
+ +
+
+ +
+ +
diff --git a/awesome_kanban/static/src/kanban_controller.js b/awesome_kanban/static/src/kanban_controller.js index e69de29bb2d..d3e0b8874bd 100644 --- a/awesome_kanban/static/src/kanban_controller.js +++ b/awesome_kanban/static/src/kanban_controller.js @@ -0,0 +1,34 @@ +/** @odoo-module */ + +import { KanbanController } from "@web/views/kanban/kanban_controller"; +import { CustomerList } from "./customer_list/customer_list" + + +export class AwesomeKanbanController extends KanbanController { + static template = "awesome_kanban.AwesomeKanbanController"; + + static components = { ...KanbanController.components, CustomerList }; + + setup() { + super.setup(); + } + + emptyFunction(customerId, customerName) { + const customerFilters = this.env.searchModel.getSearchItems((searchItem) => + searchItem.isFromAwesomeKanban + ); + + for (const customerFilter of customerFilters) { + if (customerFilter.isActive) { + this.env.searchModel.toggleSearchItem(customerFilter.id); + } + } + + console.log(customerId); + this.env.searchModel.createNewFilters([{ + description: customerName, + domain: [["partner_id", "=", customerId]], + isFromAwesomeKanban: true, // this is a custom key to retrieve our filters later + }]) + }; +} diff --git a/awesome_kanban/static/src/kanban_controller.scss b/awesome_kanban/static/src/kanban_controller.scss new file mode 100644 index 00000000000..ca25c4a3ada --- /dev/null +++ b/awesome_kanban/static/src/kanban_controller.scss @@ -0,0 +1,10 @@ +.o_awesome_kanban_sidebar { + width: 300px; + float: left; +} + +.o_awesome_kanban_customer_hover { + &:hover { + background-color: $gray-200; + } +} diff --git a/awesome_kanban/static/src/kanban_controller.xml b/awesome_kanban/static/src/kanban_controller.xml new file mode 100644 index 00000000000..f5070698d08 --- /dev/null +++ b/awesome_kanban/static/src/kanban_controller.xml @@ -0,0 +1,14 @@ + + + + + + +
+ +
+
+ +
+ +