diff --git a/addons/web_client/__manifest__.py b/addons/web_client/__manifest__.py index 0df940719f36e..f70c8991a0f65 100644 --- a/addons/web_client/__manifest__.py +++ b/addons/web_client/__manifest__.py @@ -17,6 +17,7 @@ 'web_client.assets': [ ('include', 'web._assets_helpers'), ('include', 'web._assets_backend_helpers'), + ('include', 'web.icons_fonts'), 'web/static/src/scss/pre_variables.scss', 'web/static/lib/bootstrap/scss/_variables.scss', 'web/static/lib/bootstrap/scss/_variables-dark.scss', diff --git a/addons/web_client/static/src/action/action_plugin.js b/addons/web_client/static/src/action/action_plugin.js new file mode 100644 index 0000000000000..0a5d5dc0aab7d --- /dev/null +++ b/addons/web_client/static/src/action/action_plugin.js @@ -0,0 +1,120 @@ +import { plugin, Plugin } from "@odoo/owl"; +import { DisplayedActionPlugin } from "@web_client/action/displayed_action_plugin"; +import { notify } from "@web_core/notification/notification_plugin"; +import { RPC } from "@web_core/rpc"; +import { serviceRegistry } from "@web_core/services"; +import { session } from "@web_core/session"; +import { actionRegistry } from "./action_registry"; +import { ViewAction } from "@web_client/action/view_action"; + +export class ActionPlugin extends Plugin { + static id = this.name; + static { + serviceRegistry.addById(this); + } + + display = plugin(DisplayedActionPlugin); + rpc = plugin(RPC); + + /** + * @param {string | number | Record} request + */ + async doAction(request) { + const description = await this._loadActionDescription(request); + switch (description.type) { + case "ir.actions.act_url": + return this._executeUrlAction(description); + case "ir.actions.act_window": + return this._executeWindowAction(description); + case "ir.actions.act_window_close": + return this._executeWindowCloseAction(description); + case "ir.actions.client": + return this._executeClientAction(description); + case "ir.actions.server": + return this._executeServerAction(description); + case "ir.actions.report": + return this._executeReportAction(description); + default: + notify(`Unknown action type "${description.type}"`, { type: "danger" }); + } + } + + /** + * @private + * @param {string | number | Record} request + */ + async _loadActionDescription(request) { + if (typeof request === "string" && actionRegistry.has(request)) { + // request is a tag of a client action. + return { + tag: request, + target: "current", + type: "ir.actions.client", + }; + } + + if (["string", "number"].includes(typeof request)) { + // request is an action id or a xmlid. + return this.rpc.call("/web/action/load", { + action_id: request, + context: session.user_context, + }); + } + + // request is an object describing the action. + return request; + } + + /** + * @private + * @param {Record} description + */ + _executeClientAction(description) { + const actionId = description.tag; + if (actionRegistry.has(actionId)) { + this.display.setDisplay(actionRegistry.get(actionId), description); + } else { + notify("Nope, action does not exist"); + } + } + + /** + * @private + * @param {Record} description + */ + _executeReportAction(description) { + notify(`Not implemented action type "${description.type}"`, { type: "danger" }); + } + + /** + * @private + * @param {Record} description + */ + _executeServerAction(description) { + notify(`Not implemented action type "${description.type}"`, { type: "danger" }); + } + + /** + * @private + * @param {Record} description + */ + _executeUrlAction(description) { + notify(`Not implemented action type "${description.type}"`, { type: "danger" }); + } + + /** + * @private + * @param {Record} description + */ + _executeWindowAction(description) { + this.display.setDisplay(ViewAction, description); + } + + /** + * @private + * @param {Record} description + */ + _executeWindowCloseAction(description) { + notify(`Not implemented action type "${description.type}"`, { type: "danger" }); + } +} diff --git a/addons/web_client/static/src/action_registry.js b/addons/web_client/static/src/action/action_registry.js similarity index 91% rename from addons/web_client/static/src/action_registry.js rename to addons/web_client/static/src/action/action_registry.js index 29ccfff2788e9..08aa0e9d3842c 100644 --- a/addons/web_client/static/src/action_registry.js +++ b/addons/web_client/static/src/action/action_registry.js @@ -1,3 +1,3 @@ import { Component, Registry } from "@odoo/owl"; -export const actionRegistry = new Registry("action", Component.prototype); \ No newline at end of file +export const actionRegistry = new Registry("action", Component.prototype); diff --git a/addons/web_client/static/src/action/displayed_action_plugin.js b/addons/web_client/static/src/action/displayed_action_plugin.js new file mode 100644 index 0000000000000..7f03af75ec873 --- /dev/null +++ b/addons/web_client/static/src/action/displayed_action_plugin.js @@ -0,0 +1,38 @@ +import { Component, Plugin, signal, xml } from "@odoo/owl"; +import { serviceRegistry } from "@web_core/services"; + +class EmptyAction extends Component { + static template = xml``; +} + +export class DisplayedActionPlugin extends Plugin { + static id = this.name; + static { + serviceRegistry.addById(this); + } + + /** + * @private + * @type {import("@odoo/owl").Signal<{ component: import("@odoo/owl").ComponentConstructor; description: Record }>} + */ + _displayInfo = signal({ + component: EmptyAction, + description: {}, + }); + + get component() { + return this._displayInfo().component; + } + + get description() { + return this._displayInfo().description; + } + + /** + * @param {import("@odoo/owl").ComponentConstructor} component + * @param {Record} description + */ + setDisplay(component, description) { + this._displayInfo.set({ component, description }); + } +} diff --git a/addons/web_client/static/src/action/view_action.js b/addons/web_client/static/src/action/view_action.js new file mode 100644 index 0000000000000..ea159822a96e9 --- /dev/null +++ b/addons/web_client/static/src/action/view_action.js @@ -0,0 +1,65 @@ +import { Component, computed, plugin, signal, usePlugins, useResource, xml } from "@odoo/owl"; +import { DisplayedActionPlugin } from "@web_client/action/displayed_action_plugin"; +import { DebugPlugin } from "@web_client/debug_menu/debug_plugin"; +import { ControlPanel } from "@web_client/views/control_panel"; +import { ViewLoaderPlugin } from "@web_client/views/view_loader_plugin"; +import { ViewPlugin } from "@web_client/views/view_plugin"; +import { viewRegistry } from "@web_client/views/view_registry"; + +class UnknownViewMode extends Component { + static template = xml` + +
+ Unknown view mode: +
+ `; + static components = { ControlPanel }; + + view = plugin(ViewPlugin); +} + +class LoadingView extends Component { + static template = xml` + +
+ Loading... +
+ `; + static components = { ControlPanel }; + + view = plugin(ViewPlugin); +} + +export class ViewAction extends Component { + static template = xml``; + + actionDisplay = plugin(DisplayedActionPlugin); + debug = plugin(DebugPlugin); + viewLoader = plugin(ViewLoaderPlugin); + + setup() { + useResource(this.debug.items, [ + { + label: `Model: ${this.actionDisplay.description.res_model}`, + action: () => console.log("Open Model Info"), + }, + { label: `Action`, action: () => console.log("Open Action Info") }, + { label: `View`, action: () => console.log("Open View Info") }, + ]); + + usePlugins([ViewPlugin]); + const view = plugin(ViewPlugin); + const loaded = signal(false); + this.component = computed(() => { + if (loaded()) { + return viewRegistry.get(view.mode(), UnknownViewMode); + } else { + return LoadingView; + } + }); + const action = this.actionDisplay.description; + this.viewLoader.loadView(action.res_model, action.id, action.views).then(() => { + loaded.set(true); + }); + } +} diff --git a/addons/web_client/static/src/action_plugin.js b/addons/web_client/static/src/action_plugin.js deleted file mode 100644 index 68242479c1a22..0000000000000 --- a/addons/web_client/static/src/action_plugin.js +++ /dev/null @@ -1,94 +0,0 @@ -import { Component, computed, plugin, Plugin, signal, usePlugins, xml } from "@odoo/owl"; -import { ViewPlugin } from "@web_client/view_plugin"; -import { viewRegistry } from "@web_client/views/view_registry"; -import { notify } from "@web_core/notification/notification_plugin"; -import { rpc } from "@web_core/rpc"; -import { serviceRegistry } from "@web_core/services"; -import { session } from "@web_core/session"; -import { actionRegistry } from "./action_registry"; - -class ActionContainer extends Component { - static template = xml``; - - setup() { - usePlugins([ViewPlugin]); - this.view = plugin(ViewPlugin); - this.component = computed(() => viewRegistry.get(this.view.viewType())); - } -} - -export class ActionPlugin extends Plugin { - static id = this.name; - static { - serviceRegistry.addById(this); - } - - /** @type {import("@odoo/owl").Signal} */ - action = signal(null); - - // action = computed(() => { - // const id = this.actionId(); - // const C = actionRegistry.get(id); - // return C; - // }); - - /** - * @param {string | number} actionId - */ - async doAction(actionId) { - if (typeof actionId === "number") { - const result = await rpc("/web/action/load", { - action_id: actionId, - context: session.user_context, - }); - return this._doAction(result); - } - } - - /** - * @param {import("./menu_plugin").AppMenu} app - */ - switchApp(app) { - console.log(app); - this.doAction(app.actionId); - } - - /** - * @private - */ - async _doAction(actionDescr) { - switch (actionDescr.type) { - case "ir.actions.client": - return this._doClientAction(actionDescr); - case "ir.actions.act_window": - return this._doActWindowAction(actionDescr); - } - } - - /** - * @private - */ - _doClientAction(actionDescr) { - console.log("client action"); - const actionId = actionDescr.tag; - const obj = {}; - if (actionRegistry.get(actionId, obj) === obj) { - notify("Nope, action does not exist"); - } else { - this.action.set({ - Component: actionRegistry.get(actionId), - }); - } - } - - /** - * @private - */ - _doActWindowAction(actionDescr) { - console.log("act window", actionDescr); - this.action.set({ - Component: ActionContainer, - description: actionDescr, - }); - } -} diff --git a/addons/web_client/static/src/debug_menu/debug_menu.js b/addons/web_client/static/src/debug_menu/debug_menu.js new file mode 100644 index 0000000000000..d6141fa91ce81 --- /dev/null +++ b/addons/web_client/static/src/debug_menu/debug_menu.js @@ -0,0 +1,24 @@ +import { Component, plugin, useResource } from "@odoo/owl"; +import { DebugPlugin } from "@web_client/debug_menu/debug_plugin"; +import { systrayRegistry } from "@web_client/systray_menu/systray_menu"; +import { Dropdown } from "@web_core/dropdown/dropdown"; +import { MenuItem } from "@web_core/menu/menu"; + +export class DebugMenu extends Component { + static { + systrayRegistry.add("DebugMenu", this); + } + + static template = "web_client.DebugMenu"; + static components = { Dropdown, MenuItem }; + + debug = plugin(DebugPlugin); + + setup() { + useResource(this.debug.items, [ + { label: "Leave Debug Mode", action: () => console.log("Leave Debug Mode") }, + { label: "Run Unit Tests", action: () => console.log("Run Unit Tests") }, + { label: "Open View", action: () => console.log("Open View") }, + ]); + } +} diff --git a/addons/web_client/static/src/debug_menu/debug_menu.xml b/addons/web_client/static/src/debug_menu/debug_menu.xml new file mode 100644 index 0000000000000..059509654a52c --- /dev/null +++ b/addons/web_client/static/src/debug_menu/debug_menu.xml @@ -0,0 +1,21 @@ + + + + + +
+ + + +
+ + + + + + + +
+
+ +
diff --git a/addons/web_client/static/src/debug_menu/debug_plugin.js b/addons/web_client/static/src/debug_menu/debug_plugin.js new file mode 100644 index 0000000000000..3b9e3b8756bd9 --- /dev/null +++ b/addons/web_client/static/src/debug_menu/debug_plugin.js @@ -0,0 +1,11 @@ +import { Plugin, Resource } from "@odoo/owl"; +import { serviceRegistry } from "@web_core/services"; + +export class DebugPlugin extends Plugin { + static id = this.name; + static { + serviceRegistry.addById(this); + } + + items = new Resource(); +} diff --git a/addons/web_client/static/src/demo/demo_client_action.js b/addons/web_client/static/src/demo/demo_client_action.js index 84a3571287f22..2ecd411929bc4 100644 --- a/addons/web_client/static/src/demo/demo_client_action.js +++ b/addons/web_client/static/src/demo/demo_client_action.js @@ -1,6 +1,5 @@ -import { Component, plugin } from "@odoo/owl"; -import { ActionPlugin } from "@web_client/action_plugin"; -import { actionRegistry } from "@web_client/action_registry"; +import { Component } from "@odoo/owl"; +import { actionRegistry } from "@web_client/action/action_registry"; import { notify } from "@web_core/notification/notification_plugin"; export class DemoClientAction extends Component { @@ -8,8 +7,6 @@ export class DemoClientAction extends Component { static { actionRegistry.add("demo", this); } - - action = plugin(ActionPlugin); async notify() { const rnd = Math.random().toString(36).substring(2, 10); @@ -20,8 +17,4 @@ export class DemoClientAction extends Component { type: types[Math.floor(Math.random() * types.length)], }); } - - switchAction() { - this.action.doAction("other"); - } } diff --git a/addons/web_client/static/src/demo/demo_client_action.xml b/addons/web_client/static/src/demo/demo_client_action.xml index 0ac67b769324f..a50d4a99d2b29 100644 --- a/addons/web_client/static/src/demo/demo_client_action.xml +++ b/addons/web_client/static/src/demo/demo_client_action.xml @@ -5,7 +5,6 @@

Demo Client Action!!!

-

This is the content of the page. Scroll down to see more.

...more content...

...more content...

diff --git a/addons/web_client/static/src/demo/discuss_action.js b/addons/web_client/static/src/demo/discuss_action.js index 0b29347c6f3f1..6612226ddf67a 100644 --- a/addons/web_client/static/src/demo/discuss_action.js +++ b/addons/web_client/static/src/demo/discuss_action.js @@ -1,11 +1,9 @@ - import { Component } from "@odoo/owl"; -import { actionRegistry } from "@web_client/action_registry"; +import { actionRegistry } from "@web_client/action/action_registry"; export class DiscussAction extends Component { static template = "web_client.DiscussAction"; static { actionRegistry.add("mail.action_discuss", this); } - } diff --git a/addons/web_client/static/src/demo/discuss_action.xml b/addons/web_client/static/src/demo/discuss_action.xml index 05b20b08fa6ef..12ef88eb53a76 100644 --- a/addons/web_client/static/src/demo/discuss_action.xml +++ b/addons/web_client/static/src/demo/discuss_action.xml @@ -4,7 +4,6 @@

Discuss!!!

-
diff --git a/addons/web_client/static/src/navbar/navbar.js b/addons/web_client/static/src/navbar/navbar.js index 6b655ccd66ada..c28f41baa6d05 100644 --- a/addons/web_client/static/src/navbar/navbar.js +++ b/addons/web_client/static/src/navbar/navbar.js @@ -1,54 +1,62 @@ -import { Component, computed, plugin, signal } from "@odoo/owl"; -import { ActionPlugin } from "@web_client/action_plugin"; +import { Component, computed, plugin, signal, useListener } from "@odoo/owl"; +import { ActionPlugin } from "@web_client/action/action_plugin"; import { MenuPlugin } from "@web_client/menu_plugin"; import { SystrayMenu } from "@web_client/systray_menu/systray_menu"; +import { Dropdown } from "@web_core/dropdown/dropdown"; import { Menu, MenuItem } from "@web_core/menu/menu"; export class Navbar extends Component { static template = "web_client.Navbar"; - static components = { Menu, MenuItem, SystrayMenu }; + static components = { Dropdown, Menu, MenuItem, SystrayMenu }; menu = plugin(MenuPlugin); apps = computed(() => Object.values(this.menu.apps())); - /** @type {import("@odoo/owl").Signal} */ - appMenuToggler = signal(null); - isAppMenuOpen = signal(false); - /** @type {import("@odoo/owl").Signal} */ navMenuToggler = signal(null); isNavMenuOpen = signal(false); - /** @type {import("@odoo/owl").Signal} */ + /** @type {import("@odoo/owl").Signal} */ menuItem = signal(null); action = plugin(ActionPlugin); + setup() { + useListener( + document.body, + "click", + (ev) => { + if (!this.navMenuToggler()?.contains(ev.target)) { + this.isNavMenuOpen.set(false); + } + }, + { capture: true } + ); + } + /** - * @param {any} app + * @param {import("@web_client/menu_plugin").AppMenu} app */ selectApp(app) { this.menu.currentAppId.set(app.id); - this.isAppMenuOpen.set(false); - this.action.switchApp(app); + this.action.doAction(app.actionId); } /** - * @param {import("../menu_plugin").MenuItem} menuItem + * @param {import("@web_client/menu_plugin").MenuItem} menuItem */ selectMenuItem(menuItem) { - this.isAppMenuOpen.set(false); if (menuItem.menuItems.length) { this.isNavMenuOpen.update((open) => !open); } else { this.isNavMenuOpen.set(false); if (menuItem.actionId) { - this.action.doAction(menuItem.actionId) + this.action.doAction(menuItem.actionId); } } } /** * @param {HTMLElement} target - * @param {any} menuItem + * @param {import("@web_client/menu_plugin").MenuItem} menuItem */ setCurrentNavMenu(target, menuItem) { if (menuItem.menuItems.length) { @@ -59,6 +67,5 @@ export class Navbar extends Component { toggleAppMenu() { this.isNavMenuOpen.set(false); - this.isAppMenuOpen.update((open) => !open); } } diff --git a/addons/web_client/static/src/navbar/navbar.xml b/addons/web_client/static/src/navbar/navbar.xml index d4cc9c7d9cabf..7eb5a67af1717 100644 --- a/addons/web_client/static/src/navbar/navbar.xml +++ b/addons/web_client/static/src/navbar/navbar.xml @@ -3,9 +3,18 @@
-
- Odoo -
+ +
+ Odoo +
+ + + + + + + +
@@ -14,14 +23,7 @@
- - - - - - - -
+
diff --git a/addons/web_client/static/src/view_plugin.js b/addons/web_client/static/src/view_plugin.js deleted file mode 100644 index 98eb428f79eb6..0000000000000 --- a/addons/web_client/static/src/view_plugin.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Plugin, signal } from "@odoo/owl"; - -export class ViewPlugin extends Plugin { - static id = this.name; - - viewType = signal("list"); -} diff --git a/addons/web_client/static/src/views/control_panel.js b/addons/web_client/static/src/views/control_panel.js index c37546731f89b..24394e649b5f7 100644 --- a/addons/web_client/static/src/views/control_panel.js +++ b/addons/web_client/static/src/views/control_panel.js @@ -1,10 +1,10 @@ import { Component, plugin } from "@odoo/owl"; -import { ActionPlugin } from "@web_client/action_plugin"; -import { ViewPlugin } from "@web_client/view_plugin"; +import { ViewPlugin } from "@web_client/views/view_plugin"; +import { ViewSwitcher } from "@web_client/views/view_switcher"; export class ControlPanel extends Component { static template = "web_client.ControlPanel"; + static components = { ViewSwitcher }; view = plugin(ViewPlugin); - action = plugin(ActionPlugin); } diff --git a/addons/web_client/static/src/views/control_panel.xml b/addons/web_client/static/src/views/control_panel.xml index 94aaee0d33928..27096f239b70b 100644 --- a/addons/web_client/static/src/views/control_panel.xml +++ b/addons/web_client/static/src/views/control_panel.xml @@ -5,15 +5,12 @@
- +
-
- - -
+
diff --git a/addons/web_client/static/src/views/form_view.js b/addons/web_client/static/src/views/form_view.js new file mode 100644 index 0000000000000..1ed7f077b6840 --- /dev/null +++ b/addons/web_client/static/src/views/form_view.js @@ -0,0 +1,42 @@ +import { Component, onWillStart, plugin, useResource } from "@odoo/owl"; +import { DebugPlugin } from "@web_client/debug_menu/debug_plugin"; +import { ControlPanel } from "@web_client/views/control_panel"; +import { ViewPlugin } from "@web_client/views/view_plugin"; +import { viewRegistry } from "@web_client/views/view_registry"; +import { ORM } from "@web_core/orm"; + +export class FormView extends Component { + static { + viewRegistry.add("form", this); + } + + static template = "web_client.FormView"; + static components = { ControlPanel }; + + /** @type {Record} */ + record = {}; + + debug = plugin(DebugPlugin); + orm = plugin(ORM); + view = plugin(ViewPlugin); + + setup() { + useResource(this.debug.items, [ + { label: "Record Metadata", action: () => console.log("Open Record Metadata") }, + { label: "Record Data", action: () => console.log("Open Record Data") }, + ]); + onWillStart(async () => { + const resModel = this.view.resModel(); + const recordId = this.view.recordId(); + if (recordId) { + [this.record] = await this.orm.call(resModel, "read", { + args: [[recordId], ["id", "display_name"]], + }); + } else { + this.record = await this.orm.call(resModel, "onchange", { + args: [[], [], ["id", "display_name"], {}], + }); + } + }); + } +} diff --git a/addons/web_client/static/src/views/form_view.xml b/addons/web_client/static/src/views/form_view.xml new file mode 100644 index 0000000000000..cf6813a0acb1d --- /dev/null +++ b/addons/web_client/static/src/views/form_view.xml @@ -0,0 +1,15 @@ + + + + +
+
+ Form view +
+ + + diff --git a/addons/web_client/static/src/views/kanban_view.js b/addons/web_client/static/src/views/kanban_view.js index 276bc5d014e7e..18931db09b0d6 100644 --- a/addons/web_client/static/src/views/kanban_view.js +++ b/addons/web_client/static/src/views/kanban_view.js @@ -1,6 +1,8 @@ -import { Component } from "@odoo/owl"; +import { Component, onWillStart, plugin } from "@odoo/owl"; import { ControlPanel } from "@web_client/views/control_panel"; +import { ViewPlugin } from "@web_client/views/view_plugin"; import { viewRegistry } from "@web_client/views/view_registry"; +import { ORM } from "@web_core/orm"; export class KanbanView extends Component { static { @@ -10,11 +12,21 @@ export class KanbanView extends Component { static template = "web_client.KanbanView"; static components = { ControlPanel }; - records = [ - { id: 1, name: "Mitchell Admin" }, - { id: 2, name: "Marc Demo" }, - { id: 5, name: "Joel Willis" }, - { id: 15, name: "Azure Interior" }, - { id: 16, name: "Azure Interior, Brandon Freeman" }, - ]; + /** @type {Record[]} */ + records = []; + + orm = plugin(ORM); + view = plugin(ViewPlugin); + + setup() { + onWillStart(async () => { + const resModel = this.view.resModel(); + this.records = await this.orm.call(resModel, "search_read", { + args: [[], ["id", "display_name"]], + kwargs: { + limit: 20, + }, + }); + }); + } } diff --git a/addons/web_client/static/src/views/kanban_view.xml b/addons/web_client/static/src/views/kanban_view.xml index f7a3610ef38e0..f9e2293436bfa 100644 --- a/addons/web_client/static/src/views/kanban_view.xml +++ b/addons/web_client/static/src/views/kanban_view.xml @@ -6,9 +6,9 @@
-
+
- +
diff --git a/addons/web_client/static/src/views/list_view.js b/addons/web_client/static/src/views/list_view.js index 49d37a792c817..572850ee4757e 100644 --- a/addons/web_client/static/src/views/list_view.js +++ b/addons/web_client/static/src/views/list_view.js @@ -1,6 +1,9 @@ -import { Component } from "@odoo/owl"; +import { Component, onWillStart, plugin } from "@odoo/owl"; import { ControlPanel } from "@web_client/views/control_panel"; +import { ViewPlugin } from "@web_client/views/view_plugin"; import { viewRegistry } from "@web_client/views/view_registry"; +import { parseXml } from "@web_client/xml_parser"; +import { ORM } from "@web_core/orm"; export class ListView extends Component { static { @@ -10,15 +13,41 @@ export class ListView extends Component { static template = "web_client.ListView"; static components = { ControlPanel }; - columns = [ - { id: "field_1", title: "Id", fieldName: "id" }, - { id: "field_2", title: "Name", fieldName: "name" }, - ]; - records = [ - { id: 1, name: "Mitchell Admin" }, - { id: 2, name: "Marc Demo" }, - { id: 5, name: "Joel Willis" }, - { id: 15, name: "Azure Interior" }, - { id: 16, name: "Azure Interior, Brandon Freeman" }, - ]; + /** @type {{ id: string; title: string; fieldName: string }[]} */ + columns = []; + /** @type {Record[]} */ + records = []; + + orm = plugin(ORM); + view = plugin(ViewPlugin); + + setup() { + const resModel = this.view.resModel(); + const fields = this.view.models()[resModel].fields; + + const arch = parseXml(this.view.archs()["list"]); + console.log(arch); + let id = 0; + for (const child of arch.children) { + if (child.tagName !== "field") { + continue; + } + const name = String(child.getAttribute("name")); + const title = child.getAttribute("string") || fields[name].string; + this.columns.push({ + id: `field_${id++}`, + title, + fieldName: name, + }); + } + + onWillStart(async () => { + this.records = await this.orm.call(resModel, "search_read", { + args: [[], ["id", ...this.columns.map((c) => c.fieldName)]], + kwargs: { + limit: 20, + }, + }); + }); + } } diff --git a/addons/web_client/static/src/views/list_view.xml b/addons/web_client/static/src/views/list_view.xml index b9af60838f0dc..5e53bd07afa19 100644 --- a/addons/web_client/static/src/views/list_view.xml +++ b/addons/web_client/static/src/views/list_view.xml @@ -4,8 +4,8 @@
-
- +
+
@@ -15,9 +15,11 @@ - + - + diff --git a/addons/web_client/static/src/views/view_loader_plugin.js b/addons/web_client/static/src/views/view_loader_plugin.js new file mode 100644 index 0000000000000..51caf35e1f329 --- /dev/null +++ b/addons/web_client/static/src/views/view_loader_plugin.js @@ -0,0 +1,44 @@ +import { plugin, Plugin, signal } from "@odoo/owl"; +import { ORM } from "@web_core/orm"; +import { serviceRegistry } from "@web_core/services"; + +export class ViewLoaderPlugin extends Plugin { + static id = this.name; + static { + serviceRegistry.addById(this); + } + + /** @private */ + orm = plugin(ORM); + + /** @type {import("@odoo/owl").Signal>} */ + models = signal({}); + /** @type {import("@odoo/owl").Signal>} */ + archs = signal({}); + + /** + * @param {string} resModel + * @param {number} actionId + * @param {[number, string][]} views + */ + async loadView(resModel, actionId, views) { + const data = await this.orm.call(resModel, "get_views", { + // It needs more info than that + kwargs: { + views, + options: { + action_id: actionId, + }, + }, + }); + + // This plugin may / should be stateless + this.models.set(data.models); + /** @type {Record} */ + const archs = {}; + for (const [mode, { arch }] of Object.entries(data.views)) { + archs[mode] = arch; + } + this.archs.set(archs); + } +} diff --git a/addons/web_client/static/src/views/view_plugin.js b/addons/web_client/static/src/views/view_plugin.js new file mode 100644 index 0000000000000..2b7eedecb4ac1 --- /dev/null +++ b/addons/web_client/static/src/views/view_plugin.js @@ -0,0 +1,55 @@ +import { computed, plugin, Plugin, signal } from "@odoo/owl"; +import { DisplayedActionPlugin } from "@web_client/action/displayed_action_plugin"; +import { ViewLoaderPlugin } from "@web_client/views/view_loader_plugin"; + +export class ViewPlugin extends Plugin { + static id = this.name; + + /** @private */ + displayedAction = plugin(DisplayedActionPlugin); + /** @private */ + viewLoader = plugin(ViewLoaderPlugin); + + displayName = computed(() => this.displayedAction.description.display_name); + fields = computed(() => this.viewLoader.models()[this.resModel()]); + /** @type {import("@odoo/owl").ReactiveValue} */ + modes = computed(() => this.displayedAction.description.view_mode.split(",")); + resModel = computed(() => this.displayedAction.description.res_model); + archs = computed(() => this.viewLoader.archs()); + models = computed(() => this.viewLoader.models()); + + mode = signal(this.modes()[0]); + /** @type {import("@odoo/owl").Signal} */ + recordId = signal(false); + + /** @private @type {string | null} */ + previousMode = null; + + setup() { + console.log(this.displayedAction.description); + } + + closeRecord() { + if (this.previousMode) { + this.switchView(this.previousMode); + } + } + + /** + * @param {number | false} recordId + */ + openRecord(recordId) { + this.previousMode = this.mode(); + this.mode.set("form"); + this.recordId.set(recordId); + } + + /** + * @param {string} mode + */ + switchView(mode) { + this.previousMode = null; + this.mode.set(mode); + this.recordId.set(false); + } +} diff --git a/addons/web_client/static/src/views/view_switcher.js b/addons/web_client/static/src/views/view_switcher.js new file mode 100644 index 0000000000000..d132f045e58e2 --- /dev/null +++ b/addons/web_client/static/src/views/view_switcher.js @@ -0,0 +1,16 @@ +import { Component, plugin } from "@odoo/owl"; +import { ViewPlugin } from "@web_client/views/view_plugin"; +import { session } from "@web_core/session"; + +export class ViewSwitcher extends Component { + static template = "web_client.ViewSwitcher"; + + view = plugin(ViewPlugin); + + /** + * @param {string} mode + */ + getModeInfo(mode) { + return session.view_info[mode]; + } +} diff --git a/addons/web_client/static/src/views/view_switcher.xml b/addons/web_client/static/src/views/view_switcher.xml new file mode 100644 index 0000000000000..3895d39c9f018 --- /dev/null +++ b/addons/web_client/static/src/views/view_switcher.xml @@ -0,0 +1,17 @@ + + + + +
+ + + + + + +
+
+ +
diff --git a/addons/web_client/static/src/web_client/web_client.js b/addons/web_client/static/src/web_client/web_client.js index d4e31c8cd55bf..d0383b99f7e73 100644 --- a/addons/web_client/static/src/web_client/web_client.js +++ b/addons/web_client/static/src/web_client/web_client.js @@ -1,5 +1,6 @@ -import { Component, onWillStart, plugin } from "@odoo/owl"; -import { ActionPlugin } from "@web_client/action_plugin"; +import { Component, plugin } from "@odoo/owl"; +import { ActionPlugin } from "@web_client/action/action_plugin"; +import { DisplayedActionPlugin } from "@web_client/action/displayed_action_plugin"; import { DemoClientAction } from "@web_client/demo/demo_client_action"; import { MenuPlugin } from "@web_client/menu_plugin"; import { Navbar } from "@web_client/navbar/navbar"; @@ -11,17 +12,12 @@ export class WebClient extends Component { static template = "web_client.WebClient"; static components = { Navbar, OverlayContainer, DemoClientAction }; - orm = protectedPlugin(ORM); action = plugin(ActionPlugin); + displayedAction = plugin(DisplayedActionPlugin); menu = plugin(MenuPlugin); + orm = protectedPlugin(ORM); setup() { - // this.action.switchApp(this.menu.currentApp()); - - onWillStart(() => { - this.orm.call("res.partner", "read", { - args: [[7], ["id", "display_name"]], - }); - }); + this.action.doAction("demo"); } } diff --git a/addons/web_client/static/src/web_client/web_client.xml b/addons/web_client/static/src/web_client/web_client.xml index cbd021834a23c..d19c6ea1a1d95 100644 --- a/addons/web_client/static/src/web_client/web_client.xml +++ b/addons/web_client/static/src/web_client/web_client.xml @@ -4,9 +4,7 @@
- - - +
diff --git a/addons/web_client/static/src/xml_parser.js b/addons/web_client/static/src/xml_parser.js new file mode 100644 index 0000000000000..5d6fdf471b730 --- /dev/null +++ b/addons/web_client/static/src/xml_parser.js @@ -0,0 +1,10 @@ +const parser = new DOMParser(); + +/** + * @param {string} str + * @returns {Element} + */ +export function parseXml(str) { + const xml = parser.parseFromString(str, "text/xml"); + return xml.documentElement; +} diff --git a/addons/web_core/static/lib/owl/owl.js b/addons/web_core/static/lib/owl/owl.js index 9e1606edf956f..9ae7ed3ca896e 100644 --- a/addons/web_core/static/lib/owl/owl.js +++ b/addons/web_core/static/lib/owl/owl.js @@ -6174,6 +6174,9 @@ this._items.set(items); return this; } + has(item) { + return this._items().some(([s, value]) => value === item); + } } function useResource(r, elements) { for (let elem of elements) { @@ -6228,6 +6231,9 @@ delete this._map()[key]; this._map.update(); } + has(key) { + return key in this._map(); + } } function status() { @@ -6426,8 +6432,8 @@ Object.defineProperty(exports, '__esModule', { value: true }); - __info__.date = '2025-12-17T13:43:38.783Z'; - __info__.hash = '4dfa9f0'; + __info__.date = '2025-12-19T14:18:42.031Z'; + __info__.hash = 'da75e84'; __info__.url = 'https://github.com/odoo/owl'; diff --git a/addons/web_core/static/lib/owl/owl_types.d.ts b/addons/web_core/static/lib/owl/owl_types.d.ts index cd55b2606205b..c41fd40ca29cb 100644 --- a/addons/web_core/static/lib/owl/owl_types.d.ts +++ b/addons/web_core/static/lib/owl/owl_types.d.ts @@ -202,6 +202,7 @@ declare class Resource { items: ReactiveValue; add(item: T, sequence?: number): Resource; remove(item: T): Resource; + has(item: T): boolean; } declare function useResource(r: Resource, elements: T[]): void; @@ -220,6 +221,7 @@ declare class Registry { add(key: string, value: T, sequence?: number): Registry; get(key: string, defaultValue?: T): T; remove(key: string): void; + has(key: string): boolean; } declare const enum STATUS { diff --git a/addons/web_core/static/src/dropdown/dropdown.js b/addons/web_core/static/src/dropdown/dropdown.js new file mode 100644 index 0000000000000..1b2f5eaf0fc28 --- /dev/null +++ b/addons/web_core/static/src/dropdown/dropdown.js @@ -0,0 +1,35 @@ +import { Component, signal, useEffect, useListener } from "@odoo/owl"; +import { Menu } from "@web_core/menu/menu"; + +export class Dropdown extends Component { + static template = "web_core.Dropdown"; + static components = { Menu }; + + /** @type {import("@odoo/owl").Signal} */ + toggler = signal(null); + + isMenuOpen = signal(false); + + setup() { + useListener( + document.body, + "click", + (ev) => { + if (!this.toggler()?.contains(ev.target)) { + this.isMenuOpen.set(false); + } + }, + { capture: true } + ); + useEffect(() => { + const el = this.toggler(); + if (!el) { + return; + } + + el.addEventListener("click", () => { + this.isMenuOpen.update((b) => !b); + }); + }); + } +} diff --git a/addons/web_core/static/src/dropdown/dropdown.xml b/addons/web_core/static/src/dropdown/dropdown.xml new file mode 100644 index 0000000000000..1ef9ac6a326d8 --- /dev/null +++ b/addons/web_core/static/src/dropdown/dropdown.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/addons/web_core/static/src/menu/menu.js b/addons/web_core/static/src/menu/menu.js index 5def8fb476005..b54bcd6820785 100644 --- a/addons/web_core/static/src/menu/menu.js +++ b/addons/web_core/static/src/menu/menu.js @@ -9,13 +9,13 @@ export class Menu extends Component { static template = xml``; controls = props({ - target: Function, + anchor: Function, isOpen: Function, }); content = props(["slots"]); - popover = plugin(PopoverPlugin).createPopover(this.controls.target, { + popover = plugin(PopoverPlugin).createPopover(this.controls.anchor, { component: MenuPopover, props: { slots: this.content.slots, diff --git a/addons/web_core/static/src/session.js b/addons/web_core/static/src/session.js index 04472359f6d01..c5261fe9753bd 100644 --- a/addons/web_core/static/src/session.js +++ b/addons/web_core/static/src/session.js @@ -12,5 +12,6 @@ * ----------------------------------------------------------------------------- */ +/** @type {Record} */ export const session = odoo.__session_info__ || {}; delete odoo.__session_info__;
+
+