diff --git a/addons/web/static/src/core/commands/command_service.js b/addons/web/static/src/core/commands/command_service.js index c79d52968a1..78a78d3f4cf 100644 --- a/addons/web/static/src/core/commands/command_service.js +++ b/addons/web/static/src/core/commands/command_service.js @@ -176,6 +176,7 @@ export const commandService = { registration.removeHotkey = hotkeyService.add(registration.hotkey, action, { activeElement: registration.activeElement, global: registration.global, + validate: registration.isAvailable, }); } diff --git a/addons/web/static/src/views/fields/statusbar/statusbar_field.js b/addons/web/static/src/views/fields/statusbar/statusbar_field.js index f2f90d9f02a..83fb685ec0b 100644 --- a/addons/web/static/src/views/fields/statusbar/statusbar_field.js +++ b/addons/web/static/src/views/fields/statusbar/statusbar_field.js @@ -36,7 +36,11 @@ export class StatusBarField extends Component { ], }; }, - { category: "smart_action", hotkey: "alt+shift+x" } + { + category: "smart_action", + hotkey: "alt+shift+x", + isAvailable: () => !this.props.readonly && !this.props.isDisabled, + } ); useCommand( sprintf(this.env._t(`Move to next %s`), this.props.displayName), @@ -60,8 +64,10 @@ export class StatusBarField extends Component { isAvailable: () => { const options = this.computeItems(false); return ( + !this.props.readonly && + !this.props.isDisabled && options[options.length - 1].id !== - (this.type === "many2one" ? this.props.value[0] : this.props.value) + (this.type === "many2one" ? this.props.value[0] : this.props.value) ); }, } diff --git a/addons/web/static/tests/core/commands/command_service_tests.js b/addons/web/static/tests/core/commands/command_service_tests.js index f39e5f875c9..2a8acd7c958 100644 --- a/addons/web/static/tests/core/commands/command_service_tests.js +++ b/addons/web/static/tests/core/commands/command_service_tests.js @@ -245,6 +245,27 @@ QUnit.test("global command with hotkey", async (assert) => { assert.verifySteps([globalHotkey]); }); +QUnit.test("command with hotkey and isAvailable", async (assert) => { + assert.expect(3); + + const hotkey = "a"; + let isAvailable = false; + env.services.command.add("test", () => assert.step(hotkey), { + hotkey, + isAvailable: () => isAvailable, + }); + await nextTick(); + + triggerHotkey("a"); + await nextTick(); + assert.verifySteps([]); + + isAvailable = true; + triggerHotkey("a"); + await nextTick(); + assert.verifySteps([hotkey]); +}); + QUnit.test("open command palette with command config", async (assert) => { const hotkey = "alt+a"; const action = () => {}; diff --git a/addons/web/static/tests/views/fields/statusbar_field_tests.js b/addons/web/static/tests/views/fields/statusbar_field_tests.js index 0f59051d406..a8085bafc59 100644 --- a/addons/web/static/tests/views/fields/statusbar_field_tests.js +++ b/addons/web/static/tests/views/fields/statusbar_field_tests.js @@ -550,4 +550,51 @@ QUnit.module("Fields", (hooks) => { ); await click(target, "#o_command_2"); }); + + QUnit.test("smart actions are unavailable if readonly", async function (assert) { + await makeView({ + serverData, + type: "form", + resModel: "partner", + arch: ` +
`, + resId: 1, + }); + + assert.containsOnce(target, ".o_field_widget"); + + triggerHotkey("control+k"); + await nextTick(); + const moveStages = [...target.querySelectorAll(".o_command")].map((el) => el.textContent); + assert.notOk(moveStages.includes("Move to Trululu...ALT + SHIFT + X")); + assert.notOk(moveStages.includes("Move to next...ALT + X")); + }); + + QUnit.test("hotkeys are unavailable if readonly", async function (assert) { + await makeView({ + serverData, + type: "form", + resModel: "partner", + arch: ` + `, + resId: 1, + }); + + assert.containsOnce(target, ".o_field_widget"); + triggerHotkey("alt+shift+x"); // Move to stage... + await nextTick(); + assert.containsNone(target, ".modal", "command palette should not open"); + + triggerHotkey("alt+x"); // Move to next + await nextTick(); + assert.containsNone(target, ".modal", "command palette should not open"); + }); });