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');