diff --git a/addons/mail/static/src/js/m2x_avatar_user.js b/addons/mail/static/src/js/m2x_avatar_user.js index d30d5a95302..0b9e4548818 100644 --- a/addons/mail/static/src/js/m2x_avatar_user.js +++ b/addons/mail/static/src/js/m2x_avatar_user.js @@ -1,8 +1,9 @@ /** @odoo-module **/ +import core from 'web.core'; import fieldRegistry from 'web.field_registry'; -import { Many2OneAvatar } from 'web.relational_fields'; -import { FieldMany2ManyTagsAvatar, KanbanMany2ManyTagsAvatar, ListMany2ManyTagsAvatar } from 'web.relational_fields'; +import { FieldMany2ManyTagsAvatar, KanbanMany2ManyTagsAvatar, ListMany2ManyTagsAvatar, Many2OneAvatar } from 'web.relational_fields'; +import session from 'web.session'; const { Component } = owl; @@ -58,6 +59,124 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, { 'click .o_m2o_avatar > img': '_onAvatarClicked', }), + on_attach_callback() { + console.log("on_attach_callback ", this.mode) + + this._registerCommandAssignTo() + }, + on_detach_callback(){ + this._unregisterCommandAssignTo() + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _registerCommandAssignTo() { + const self = this; + if (self.viewType === "form") { + let provide = async (env, options) => { + if (self.isDestroyed()) { + return + } + const records = await self._searchAssignTo(options.searchValue, 10) + return records.map((record) => ({ + name: record[1], + action: () => { + if (self.isDestroyed()) { + return + } + self.reinitialize({ id: record[0], display_name: record[1] }); + } + })) + } + let getCommandDefinition = (env) => ({ + name: env._t("Assign to ..."), + options: { + activeElement: env.services.ui.getActiveElementOf(self.el), + category: "smart_action", + hotkey: "alt+i", + }, + async action() { + return env.services.command.openPalette({ + emptyMessageByNamespace: { "default": env._t("No users found") }, + placeholder: env._t("Select a user..."), + providers: [{ provide }], + }) + }, + }); + core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignTo", getCommandDefinition, self.el); + + + getCommandDefinition = (env) => ({ + name: env._t("Assign/unassign to me"), + options: { + activeElement: env.services.ui.getActiveElementOf(self.el), + category: "smart_action", + hotkey: "alt+shift+i", + }, + action() { + if (self.isDestroyed()) { + return + } + if (self.value.res_id === session.user_id[0]) { + self._setValue({ + operation: 'DELETE', + ids: [session.user_id[0]], + }); + } else { + self.reinitialize({ id: session.user_id[0], display_name: session.name }); + } + }, + }); + core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignToMe", getCommandDefinition); + } + }, + + /** + * @override + * @private + */ + _unregisterCommandAssignTo() { + core.bus.trigger("remove_legacy_command", "web.Many2OneAvatar.assignTo"); + core.bus.trigger("remove_legacy_command", "web.Many2OneAvatar.assignToMe"); + }, + + /** + * @override + * @private + */ + _searchAssignTo(searchValue, limit) { + const value = searchValue.trim(); + const domain = this.record.getDomain(this.recordParams); + const context = Object.assign( + this.record.getContext(this.recordParams), + this.additionalContext, + ); + + // Exclude black-listed ids from the domain + const blackListedIds = this._getSearchBlacklist(); + if (blackListedIds.length) { + domain.push(['id', 'not in', blackListedIds]); + } + + const nameSearch = this._rpc({ + model: this.field.relation, + method: "name_search", + kwargs: { + name: value, + args: domain, + operator: "ilike", + limit: limit + 1, + context, + } + }); + return this.orderer.add(nameSearch); + }, //---------------------------------------------------------------------- // Handlers //---------------------------------------------------------------------- @@ -99,7 +218,108 @@ const M2MAvatarMixin = Object.assign(M2XAvatarMixin, { }, }); -export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixin, {}); +export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixin, { + + on_attach_callback() { + this._registerCommandAssignTo() + }, + on_detach_callback(){ + this._unregisterCommandAssignTo() + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _registerCommandAssignTo() { + const self = this; + if (self.viewType === "form") { + const provide = async (env, options) => { + if (self.isDestroyed()) { + return + } + const many2one = new Many2OneAvatarUser(self, self.name, self.record, { + mode: 'edit', + noOpen: true, + noCreate: !self.canCreate, + viewType: self.viewType, + attrs: self.attrs, + }); + many2one._getSearchBlacklist = function () { + return self.value.res_ids; + }; + const records = await many2one._searchAssignTo(options.searchValue, 10) + return records.map((record) => ({ + name: record[1], + action: () => { + if (self.isDestroyed()) { + return + } + self._setValue({ + operation: 'ADD_M2M', + ids: { id: record[0], display_name: record[1] } + }); + }, + })) + }; + let getCommandDefinition = (env) => ({ + name: env._t("Assign to ..."), + options: { + activeElement: env.services.ui.getActiveElementOf(self.el), + category: "smart_action", + hotkey: "alt+i", + }, + action() { + return env.services.command.openPalette({ + emptyMessageByNamespace: { "default": env._t("No users found") }, + placeholder: env._t("Select a user..."), + providers: [{ provide }], + }) + }, + }) + core.bus.trigger("set_legacy_command", "web.FieldMany2ManyTagsAvatar.assignTo", getCommandDefinition) + + getCommandDefinition = (env) => ({ + name: env._t("Assign/unassign to me"), + options: { + activeElement: env.services.ui.getActiveElementOf(self.el), + category: "smart_action", + hotkey: "alt+shift+i", + }, + action() { + if (self.isDestroyed()) { + return + } + if (self.value.res_ids.includes(session.user_id[0])) { + self._setValue({ + operation: 'DELETE', + ids: [session.user_id[0]], + }); + } else { + self._setValue({ + operation: 'ADD_M2M', + ids: { id: session.user_id[0], display_name: session.name } + }); + } + }, + }); + core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignToMe", getCommandDefinition); + } + }, + + /** + * @override + * @private + */ + _unregisterCommandAssignTo() { + core.bus.trigger("remove_legacy_command", "web.FieldMany2ManyTagsAvatar.assignTo"); + core.bus.trigger("remove_legacy_command", "web.Many2OneAvatar.assignToMe"); + } +}); export const KanbanMany2ManyAvatarUser = KanbanMany2ManyTagsAvatar.extend(M2MAvatarMixin, {}); diff --git a/addons/mail/static/tests/m2x_avatar_user_tests.js b/addons/mail/static/tests/m2x_avatar_user_tests.js index 98e3efa1df3..6c045b7107e 100644 --- a/addons/mail/static/tests/m2x_avatar_user_tests.js +++ b/addons/mail/static/tests/m2x_avatar_user_tests.js @@ -1,11 +1,17 @@ /** @odoo-module **/ +import { Many2OneAvatarUser } from '@mail/js/m2x_avatar_user'; import { afterEach, beforeEach, start } from '@mail/utils/test_utils'; - +import { click, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils"; +import { createWebClient, doAction } from '@web/../tests/webclient/helpers'; +import { registry } from "@web/core/registry"; +import { makeLegacyCommandService } from "@web/legacy/utils"; +import core from 'web.core'; import FormView from 'web.FormView'; import KanbanView from 'web.KanbanView'; import ListView from 'web.ListView'; -import { Many2OneAvatarUser } from '@mail/js/m2x_avatar_user'; +import session from 'web.session'; +import makeTestEnvironment from "web.test_env"; import { dom, mock, nextTick } from 'web.test_utils'; @@ -21,7 +27,7 @@ QUnit.module('mail', {}, function () { 'foo': { fields: { user_id: { string: "User", type: 'many2one', relation: 'res.users' }, - user_ids: { string: "Users", type: "many2many", relation: 'res.users' }, + user_ids: { string: "Users", type: "many2many", relation: 'res.users', default:[] }, }, records: [ { id: 1, user_id: 11, user_ids: [11, 23], }, @@ -408,4 +414,195 @@ QUnit.module('mail', {}, function () { form.destroy(); }); + QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { + assert.expect(4); + + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + + const views = { + 'foo,false,form': '
', + 'foo,false,search': '', + }; + const models = { + 'foo': this.data.foo, + 'res.partner': this.data['res.partner'], + 'res.users': this.data['res.users'], + } + const serverData = { models, views} + const webClient = await createWebClient({serverData}); + await doAction(webClient, { + res_id: 1, + type: 'ir.actions.act_window', + target: 'current', + res_model: 'foo', + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + assert.strictEqual(webClient.el.querySelector(".o_m2o_avatar > span").textContent, "Mario") + + triggerHotkey("control+k") + await nextTick(); + const idx = [...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I") + assert.ok(idx >= 0); + + await click([...webClient.el.querySelectorAll(".o_command")][idx]) + await nextTick(); + assert.deepEqual([...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent), [ + "Your Company, Mitchell Admin", + "Public user", + "Mario", + "Luigi", + "Yoshi", + ]) + await click(webClient.el, "#o_command_3") + await legacyExtraNextTick(); + assert.strictEqual(webClient.el.querySelector(".o_m2o_avatar > span").textContent, "Luigi") + }); + + QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { + assert.expect(4); + + patchWithCleanup(session, { user_id: [7] }) + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + + const views = { + 'foo,false,form': '
', + 'foo,false,search': '', + }; + const models = { + 'foo': this.data.foo, + 'res.partner': this.data['res.partner'], + 'res.users': this.data['res.users'], + } + const serverData = { models, views} + const webClient = await createWebClient({serverData}); + await doAction(webClient, { + res_id: 1, + type: 'ir.actions.act_window', + target: 'current', + res_model: 'foo', + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + assert.strictEqual(webClient.el.querySelector(".o_m2o_avatar > span").textContent, "Mario") + triggerHotkey("control+k") + await nextTick(); + const idx = [...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I") + assert.ok(idx >= 0); + + // Assign me (Luigi) + triggerHotkey("alt+shift+i") + await legacyExtraNextTick(); + assert.strictEqual(webClient.el.querySelector(".o_m2o_avatar > span").textContent, "Luigi") + + // Unassign me + triggerHotkey("control+k"); + await nextTick(); + await click([...webClient.el.querySelectorAll(".o_command")][idx]) + await legacyExtraNextTick(); + assert.strictEqual(webClient.el.querySelector(".o_m2o_avatar > span").textContent, "") + }); + + QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { + assert.expect(4); + + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + + const views = { + 'foo,false,form': '
', + 'foo,false,search': '', + }; + const models = { + 'foo': this.data.foo, + 'res.partner': this.data['res.partner'], + 'res.users': this.data['res.users'], + } + const serverData = { models, views} + const webClient = await createWebClient({serverData}); + await doAction(webClient, { + res_id: 1, + type: 'ir.actions.act_window', + target: 'current', + res_model: 'foo', + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + let userNames = [...webClient.el.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + + triggerHotkey("control+k") + await nextTick(); + const idx = [...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I") + assert.ok(idx >= 0); + + await click([...webClient.el.querySelectorAll(".o_command")][idx]) + await nextTick(); + assert.deepEqual([...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent), [ + "Your Company, Mitchell Admin", + "Public user", + "Luigi" + ]); + + await click(webClient.el, "#o_command_2"); + await legacyExtraNextTick(); + userNames = [...webClient.el.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent); + assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); + }); + + QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { + assert.expect(4); + + patchWithCleanup(session, { user_id: [7] }) + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + + const views = { + 'foo,false,form': '
', + 'foo,false,search': '', + }; + const models = { + 'foo': this.data.foo, + 'res.partner': this.data['res.partner'], + 'res.users': this.data['res.users'], + } + const serverData = { models, views} + const webClient = await createWebClient({serverData}); + await doAction(webClient, { + res_id: 1, + type: 'ir.actions.act_window', + target: 'current', + res_model: 'foo', + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + let userNames = [...webClient.el.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + + triggerHotkey("control+k"); + await nextTick(); + const idx = [...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I"); + assert.ok(idx >= 0); + + // Assign me (Luigi) + triggerHotkey("alt+shift+i"); + await legacyExtraNextTick(); + userNames = [...webClient.el.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); + assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); + + // Unassign me + triggerHotkey("control+k"); + await nextTick(); + await click([...webClient.el.querySelectorAll(".o_command")][idx]); + await legacyExtraNextTick(); + userNames = [...webClient.el.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + }); + }); diff --git a/addons/web/static/src/core/hotkeys/hotkey_service.js b/addons/web/static/src/core/hotkeys/hotkey_service.js index e4658be6c5a..7ffcadee786 100644 --- a/addons/web/static/src/core/hotkeys/hotkey_service.js +++ b/addons/web/static/src/core/hotkeys/hotkey_service.js @@ -243,6 +243,7 @@ export const hotkeyService = { * @param {string} hotkey * @param {()=>void} callback * @param {Object} options additional options + * @param {HTMLElement} [options.activeElement=undefined] * @param {boolean} [options.allowRepeat=false] * allow registration to perform multiple times when hotkey is held down * @param {boolean} [options.global=false] @@ -288,17 +289,19 @@ export const hotkeyService = { const registration = { hotkey: hotkey.toLowerCase(), callback, - activeElement: null, + activeElement: options.activeElement, allowRepeat: options && options.allowRepeat, global: options && options.global, }; registrations.set(token, registration); - // Due to the way elements are mounted in the DOM by Owl (bottom-to-top), - // we need to wait the next micro task tick to set the context owner of the registration. - Promise.resolve().then(() => { - registration.activeElement = ui.activeElement; - }); + if (!registrations.activeElement) { + // Due to the way elements are mounted in the DOM by Owl (bottom-to-top), + // we need to wait the next micro task tick to set the context owner of the registration. + Promise.resolve().then(() => { + registration.activeElement = ui.activeElement; + }); + } return token; } @@ -318,6 +321,7 @@ export const hotkeyService = { * @param {() => void} callback * @param {Object} options * @param {boolean} [options.allowRepeat=false] + * @param {HTMLElement} [options.activeElement=undefined] * @param {boolean} [options.global=false] * @returns {() => void} */ diff --git a/addons/web/static/src/core/ui/ui_service.js b/addons/web/static/src/core/ui/ui_service.js index 0c14a8077cf..6dfdb450385 100644 --- a/addons/web/static/src/core/ui/ui_service.js +++ b/addons/web/static/src/core/ui/ui_service.js @@ -79,10 +79,18 @@ export const uiService = { activeElems = activeElems.filter((x) => x !== el); bus.trigger("active-element-changed", ui.activeElement); } + function getActiveElementOf(el) { + for (const activeElement of [...activeElems].reverse()) { + if (activeElement.contains(el)) { + return activeElement; + } + } + } Object.assign(ui, { activateElement, deactivateElement, + getActiveElementOf, }); Object.defineProperty(ui, "activeElement", { get() { diff --git a/addons/web/static/src/legacy/js/fields/basic_fields.js b/addons/web/static/src/legacy/js/fields/basic_fields.js index 97c124fd73c..7be1fde08a0 100644 --- a/addons/web/static/src/legacy/js/fields/basic_fields.js +++ b/addons/web/static/src/legacy/js/fields/basic_fields.js @@ -2504,6 +2504,44 @@ var PriorityWidget = AbstractField.extend({ var checked = this.$("[aria-checked='true']"); return checked.length ? checked : this.$("[data-index='1']"); }, + init: function () { + this._super.apply(this, arguments); + console.log("init ", this.mode) + }, + + on_attach_callback() { + console.log("on_attach_callback ") + const self = this; + if (self.viewType === "form") { + let provide = () => { + return self.field.selection.map((value) => ({ + name: value[1], + action: () => { + this._setValue(value[0]); + } + })) + } + let getCommandDefinition = (env) => ({ + name: env._t("Set priority..."), + options: { + activeElement: env.services.ui.getActiveElementOf(self.el), + category: "smart_action", + hotkey: "alt+r", + }, + action() { + return env.services.command.openPalette({ + placeholder: env._t("Set a priority..."), + providers: [{ provide }], + }) + }, + }); + core.bus.trigger("set_legacy_command", "web.PriorityWidget.setPriority", getCommandDefinition); + } + }, + + on_detach_callback() { + core.bus.trigger("remove_legacy_command", "web.PriorityWidget.setPriority"); + }, //-------------------------------------------------------------------------- // Private @@ -2679,6 +2717,38 @@ var StateSelectionWidget = AbstractField.extend({ return this.$("a[data-toggle='dropdown']"); }, + on_attach_callback() { + const self = this; + if (self.viewType === "form") { + let provide = () => { + return self.field.selection.map((value) => ({ + name: value[1], + action: () => { + this._setValue(value[0]); + } + })) + } + let getCommandDefinition = (env) => ({ + name: env._t("Set kanban state..."), + options: { + activeElement: env.services.ui.getActiveElementOf(self.el), + category: "smart_action", + hotkey: "alt+shift+r", + }, + action() { + return env.services.command.openPalette({ + placeholder: env._t("Set a kanban state..."), + providers: [{ provide }], + }) + }, + }); + core.bus.trigger("set_legacy_command", "web.StateSelectionWidget.setKanbanState", getCommandDefinition); + } + }, + + on_detach_callback() { + core.bus.trigger("remove_legacy_command", "web.StateSelectionWidget.setKanbanState"); + }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/legacy/legacy_setup.js b/addons/web/static/src/legacy/legacy_setup.js index e0f3c7de58f..b29673ba33f 100644 --- a/addons/web/static/src/legacy/legacy_setup.js +++ b/addons/web/static/src/legacy/legacy_setup.js @@ -8,6 +8,7 @@ import { makeLegacySessionService, makeLegacyDialogMappingService, makeLegacyCrashManagerService, + makeLegacyCommandService, } from "./utils"; import * as AbstractService from "web.AbstractService"; import * as legacyEnv from "web.env"; @@ -43,6 +44,8 @@ export const legacySetupProm = new Promise((resolve) => { serviceRegistry.add("legacy_crash_manager", makeLegacyCrashManagerService(legacyEnv)); const legacyDialogMappingService = makeLegacyDialogMappingService(legacyEnv); serviceRegistry.add("legacy_dialog_mapping", legacyDialogMappingService); + const legacyCommandService = makeLegacyCommandService(legacyEnv); + serviceRegistry.add("legacy_command", legacyCommandService); await Promise.all([whenReady(), session.is_bound]); legacyEnv.qweb.addTemplates(session.owlTemplates); legacySetupResolver(legacyEnv); diff --git a/addons/web/static/src/legacy/utils.js b/addons/web/static/src/legacy/utils.js index 4cb9d7d3811..20d1a7e65e7 100644 --- a/addons/web/static/src/legacy/utils.js +++ b/addons/web/static/src/legacy/utils.js @@ -120,6 +120,41 @@ export function makeLegacyDialogMappingService(legacyEnv) { }; } +/** + * Deploys a service allowing legacy to add/remove commands. + * + * @param {object} legacyEnv + * @returns a wowl deployable service + */ +export function makeLegacyCommandService(legacyEnv) { + return { + dependencies: ["command"], + start(env) { + const { command } = env.services; + + const commandRemoveMap = new Map(); + + function setLegacyCommand(uniqueId, getCommandDefinition) { + console.log("setLegacyCommand ", uniqueId); + const { name, action, options } = getCommandDefinition(env); + removeLegacyCommand(uniqueId); + commandRemoveMap.set(uniqueId, command.add(name, action, options)); + } + + function removeLegacyCommand(uniqueId) { + if (commandRemoveMap.has(uniqueId)) { + const removeCommand = commandRemoveMap.get(uniqueId); + removeCommand(); + commandRemoveMap.delete(uniqueId); + } + } + + legacyEnv.bus.on("set_legacy_command", null, setLegacyCommand); + legacyEnv.bus.on("remove_legacy_command", null, removeLegacyCommand); + }, + }; +} + export function makeLegacySessionService(legacyEnv, session) { return { dependencies: ["user"], diff --git a/addons/web/static/src/webclient/commands/command_category.js b/addons/web/static/src/webclient/commands/command_category.js index 697411f51eb..0481e1535db 100644 --- a/addons/web/static/src/webclient/commands/command_category.js +++ b/addons/web/static/src/webclient/commands/command_category.js @@ -5,6 +5,7 @@ import { registry } from "@web/core/registry"; const commandCategoryRegistry = registry.category("command_categories"); commandCategoryRegistry .add("app", {}, { sequence: 10 }) + .add("smart_action", {}, { sequence: 15 }) .add("actions", {}, { sequence: 30 }) .add("navbar", {}, { sequence: 40 }) .add("default", {}, { sequence: 100 }) diff --git a/addons/web/static/src/webclient/commands/command_service.js b/addons/web/static/src/webclient/commands/command_service.js index 8737df373db..21bb2a757c9 100644 --- a/addons/web/static/src/webclient/commands/command_service.js +++ b/addons/web/static/src/webclient/commands/command_service.js @@ -19,6 +19,7 @@ const { xml } = owl.tags; /** * @typedef {{ + * activeElement?: HTMLElement; * category?: string; * global?: boolean; * hotkey?: string; @@ -27,7 +28,6 @@ const { xml } = owl.tags; /** * @typedef {Command & CommandOptions & { - * activeElement?: HTMLElement; * removeHotkey?: ()=>void; * }} CommandRegistration */ @@ -114,13 +114,14 @@ export const commandService = { throw new Error("A Command must have a name and an action function."); } - const registration = Object.assign({}, command, options, { activeElement: null }); + const registration = Object.assign({}, command, options); if (registration.hotkey) { registration.removeHotkey = hotkeyService.add( registration.hotkey, registration.action, { + activeElement: registration.activeElement, global: registration.global, } ); @@ -128,13 +129,14 @@ export const commandService = { const token = nextToken++; registeredCommands.set(token, registration); - - // Due to the way elements are mounted in the DOM by Owl (bottom-to-top), - // we need to wait the next micro task tick to set the context activate - // element of the subscription. - Promise.resolve().then(() => { - registration.activeElement = ui.activeElement; - }); + if (!options.activeElement) { + // Due to the way elements are mounted in the DOM by Owl (bottom-to-top), + // we need to wait the next micro task tick to set the context activate + // element of the subscription. + Promise.resolve().then(() => { + registration.activeElement = ui.activeElement; + }); + } return token; } diff --git a/addons/web/static/tests/legacy/fields/basic_fields_tests.js b/addons/web/static/tests/legacy/fields/basic_fields_tests.js index 8b7bb5005fd..2994ff99309 100644 --- a/addons/web/static/tests/legacy/fields/basic_fields_tests.js +++ b/addons/web/static/tests/legacy/fields/basic_fields_tests.js @@ -14,6 +14,11 @@ var session = require('web.session'); var testUtils = require('web.test_utils'); var testUtilsDom = require('web.test_utils_dom'); var field_registry = require('web.field_registry'); +const makeTestEnvironment = require("web.test_env"); +const { makeLegacyCommandService } = require("@web/legacy/utils"); +const { registry } = require("@web/core/registry"); +const { legacyExtraNextTick, patchWithCleanup, triggerHotkey, nextTick, click } = require("@web/../tests/helpers/utils"); +const { createWebClient, doAction } = require('@web/../tests/webclient/helpers'); var createView = testUtils.createView; var patchDate = testUtils.mock.patchDate; @@ -6564,6 +6569,48 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test('priority widget edited by the smart action "Set priority..."', async function (assert) { + assert.expect(4); + + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + + const views = { + 'partner,false,form': '
' + + '' + + '', + 'partner,false,search': '', + }; + const serverData = { models: this.data, views} + const webClient = await createWebClient({serverData}); + await doAction(webClient, { + res_id: 1, + type: 'ir.actions.act_window', + target: 'current', + res_model: 'partner', + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + assert.containsOnce(webClient, ".fa-star") + + triggerHotkey("control+k") + await nextTick(); + const idx = [...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Set priority...ALT + R") + assert.ok(idx >= 0); + + await click([...webClient.el.querySelectorAll(".o_command")][idx]) + await nextTick(); + assert.deepEqual([...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent), [ + "Normal", + "Blocked", + "Done" + ]) + await click(webClient.el, "#o_command_2") + await legacyExtraNextTick(); + assert.containsN(webClient, ".fa-star", 2) + }); + QUnit.module('StateSelection Widget'); QUnit.test('state_selection widget in form view', async function (assert) { @@ -6821,6 +6868,48 @@ QUnit.module('basic_fields', { list.destroy(); }); + QUnit.test('state_selection edited by the smart action "Set kanban state..."', async function (assert) { + assert.expect(4); + + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + + const views = { + 'partner,false,form': '
' + + '' + + '', + 'partner,false,search': '', + }; + const serverData = { models: this.data, views} + const webClient = await createWebClient({serverData}); + await doAction(webClient, { + res_id: 1, + type: 'ir.actions.act_window', + target: 'current', + res_model: 'partner', + 'view_mode': 'form', + 'views': [[false, 'form']], + }); + assert.containsOnce(webClient, ".o_status_red") + + triggerHotkey("control+k") + await nextTick(); + const idx = [...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Set kanban state...ALT + SHIFT + R") + assert.ok(idx >= 0); + + await click([...webClient.el.querySelectorAll(".o_command")][idx]) + await nextTick(); + assert.deepEqual([...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent), [ + "Normal", + "Blocked", + "Done" + ]) + await click(webClient.el, "#o_command_2") + await legacyExtraNextTick(); + assert.containsOnce(webClient, ".o_status_green") + }); + QUnit.module('FavoriteWidget');