[IMP] web,sale_(project): improve keyboard navigation

The purpose of this commit is, to add keyboard shortcut
to multiple actions.

So in this commit, added keyboard shortcut to several actions
in project, sale_project, web_calendar and added smart action
in command palette on statusbar.

task-2655790

closes odoo/odoo#79805

Related: odoo/enterprise#22267
Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
This commit is contained in:
Krina Oza
2022-01-11 12:14:43 +00:00
committed by Barad Mahendra
parent 2c911fe176
commit ff2ffb9e0d
10 changed files with 112 additions and 15 deletions
@@ -68,7 +68,7 @@
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
<button icon="fa-lg fa-save" type="object" groups="mail.group_mail_template_editor"
name="action_save_as_template" string="Save as new template"
class="float-right btn-secondary" help="Save as a new template"/>
class="float-right btn-secondary" help="Save as a new template" data-hotkey="w"/>
</footer>
</form>
</field>
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="project.ProjectControlPanelContentBadge" owl="1">
<t t-tag="isProjectUser ? 'button' : 'span'" class="badge border d-flex p-2 ml-2 bg-white">
<t t-tag="isProjectUser ? 'button' : 'span'" class="badge border d-flex p-2 ml-2 bg-white" data-hotkey="y">
<span t-attf-class="o_status_bubble o_color_bubble_{{data.color}}"/>
<span t-att-class="'font-weight-normal ml-1' + (data.color === 0 ? ' text-muted' : '')" t-esc="data.status"/>
</t>
+2 -2
View File
@@ -289,9 +289,9 @@
<form string="Project" class="o_form_project_project" delete="0">
<header>
<button name="%(project.project_share_wizard_action)d" string="Share Readonly" type="action" class="oe_highlight" groups="project.group_project_manager"
attrs="{'invisible': [('privacy_visibility', '!=', 'portal')]}" context="{'default_access_mode': 'read'}"/>
attrs="{'invisible': [('privacy_visibility', '!=', 'portal')]}" context="{'default_access_mode': 'read'}" data-hotkey="r"/>
<button name="%(project.project_share_wizard_action)d" string="Share Editable" type="action" class="oe_highlight" groups="project.group_project_manager"
attrs="{'invisible': [('privacy_visibility', '!=', 'portal')]}" context="{'default_access_mode': 'edit'}"/>
attrs="{'invisible': [('privacy_visibility', '!=', 'portal')]}" context="{'default_access_mode': 'edit'}" data-hotkey="e"/>
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" groups="project.group_project_stages"/>
</header>
<sheet string="Project">
@@ -31,8 +31,8 @@
<xpath expr="//header" position="inside">
<field name="has_any_so_to_invoice" invisible="1"/>
<field name="has_any_so_with_nothing_to_invoice" invisible="1"/>
<button name="action_create_invoice" string="Create Invoice" type="object" class="btn-primary" groups="sales_team.group_sale_salesman_all_leads" attrs="{'invisible': [('has_any_so_to_invoice', '=', False)]}"/>
<button name="action_create_invoice" string="Create Invoice" type="object" class="btn-secondary" groups="sales_team.group_sale_salesman_all_leads" attrs="{'invisible': ['|', ('has_any_so_with_nothing_to_invoice', '=', False), ('has_any_so_to_invoice', '=', True)]}"/>
<button name="action_create_invoice" string="Create Invoice" type="object" class="btn-primary" groups="sales_team.group_sale_salesman_all_leads" attrs="{'invisible': [('has_any_so_to_invoice', '=', False)]}" data-hotkey="w"/>
<button name="action_create_invoice" string="Create Invoice" type="object" class="btn-secondary" groups="sales_team.group_sale_salesman_all_leads" attrs="{'invisible': ['|', ('has_any_so_with_nothing_to_invoice', '=', False), ('has_any_so_to_invoice', '=', True)]}" data-hotkey="w"/>
</xpath>
<xpath expr="//button[@name='%(project.project_update_all_action)d']" position="after">
<button class="d-none d-md-inline oe_stat_button"
@@ -319,6 +319,7 @@ var Dialog = Widget.extend({
attrs: {
class: buttonData.classes || (buttons.length > 1 ? 'btn-secondary' : 'btn-primary'),
disabled: buttonData.disabled,
'data-hotkey': buttonData.hotkey,
},
icon: buttonData.icon,
text: buttonData.text,
@@ -3163,6 +3163,47 @@ var FieldStatus = AbstractField.extend({
isEmpty: function () {
return false;
},
/**
* Set the legacy command for statusbar to CommandPalette.
*
* @override
*/
on_attach_callback() {
if (this.isClickable && this.viewType === 'form') {
const provide = () => {
return this.status_information.map((value) => ({
name: value['display_name'],
action: () => {
this._setValue(value['id']);
}
}));
};
const statusLabel = sprintf(_t(`Move to %s...`), escape(this.string));
const getCommandDefinition = (env) => ({
name: statusLabel,
options: {
activeElement: env.services.ui.getActiveElementOf(this.el),
category: "smart_action",
hotkey: "alt+shift+x",
},
action() {
return env.services.command.openPalette({
placeholder: statusLabel,
providers: [{ provide }],
});
},
});
core.bus.trigger("set_legacy_command", "web.FieldStatus.moveToStage", getCommandDefinition);
}
},
/**
* Remove the legacy command from CommandPalette.
*
* @override
*/
on_detach_callback() {
core.bus.trigger("remove_legacy_command", "web.FieldStatus.moveToStage");
},
//--------------------------------------------------------------------------
// Private
@@ -119,6 +119,7 @@ var FormViewDialog = ViewDialog.extend({
text: options.close_text || (readonly ? _t("Close") : _t("Discard")),
classes: "btn-secondary o_form_button_cancel",
close: true,
hotkey: 'j',
click: function () {
if (!readonly) {
self.form_view.model.discardChanges(self.form_view.handle, {
@@ -132,6 +133,7 @@ var FormViewDialog = ViewDialog.extend({
options.buttons.unshift({
text: options.save_text || (multi_select ? _t("Save & Close") : _t("Save")),
classes: "btn-primary",
hotkey: 'c',
click: function () {
self._save().then(self.close.bind(self));
}
@@ -141,6 +143,7 @@ var FormViewDialog = ViewDialog.extend({
options.buttons.splice(1, 0, {
text: _t("Save & New"),
classes: "btn-primary",
hotkey: 'n',
click: function () {
self._save()
.then(function () {
@@ -298,6 +301,7 @@ var FormViewDialog = ViewDialog.extend({
options.buttons.push({
text: options.removeButtonText || _t("Remove"),
classes: 'btn-secondary ' + btnClasses,
hotkey: 'x',
click: function() {
self._remove().then(self.close.bind(self));
}
@@ -452,12 +456,14 @@ var SelectCreateDialog = ViewDialog.extend({
text: _t("Cancel"),
classes: 'btn-secondary o_form_button_cancel',
close: true,
hotkey: 'z'
}];
if (!this.options.no_create) {
this.__buttons.unshift({
text: _t("Create"),
classes: 'btn-primary',
click: this.create_edit_record.bind(this)
click: this.create_edit_record.bind(this),
hotkey: 'c'
});
}
if (!this.options.disable_multiple_selection) {
@@ -466,6 +472,7 @@ var SelectCreateDialog = ViewDialog.extend({
classes: 'btn-primary o_select_button',
disabled: true,
close: true,
hotkey: 'v',
click: async () => {
const resIds = await this.viewController.getSelectedIdsWithDomain();
const values = resIds.map(e => ({id: e}));
+3 -3
View File
@@ -70,9 +70,9 @@
<t t-name="KanbanView.RecordQuickCreate.buttons">
<div class="d-flex">
<button class="btn btn-primary o_kanban_add mr-1 text-truncate">Add</button>
<button class="btn btn-primary o_kanban_edit mr-1 text-truncate">Edit</button>
<button class="btn btn-secondary o_kanban_cancel ml-auto">
<button class="btn btn-primary o_kanban_add mr-1 text-truncate" data-hotkey="r">Add</button>
<button class="btn btn-primary o_kanban_edit mr-1 text-truncate" data-hotkey="a">Edit</button>
<button class="btn btn-secondary o_kanban_cancel ml-auto" data-hotkey="d">
<i class="fa fa-trash"/>
</button>
</div>
@@ -121,16 +121,16 @@
<t t-name="CalendarView.navigation_buttons">
<button class="o_calendar_button_prev btn btn-primary" aria-label="Previous" title="Previous"><span class="fa fa-arrow-left"/></button>
<button class="o_calendar_button_today btn btn-primary">Today</button>
<button class="o_calendar_button_today btn btn-primary" data-hotkey="t">Today</button>
<button class="o_calendar_button_next btn btn-primary" aria-label="Next" title="Next"><span class="fa fa-arrow-right"/></button>
</t>
<t t-name="CalendarView.scale_buttons">
<div class="btn-group">
<button type="button" t-if="scales.includes('day')" class="o_calendar_button_day btn btn-secondary">Day</button>
<button type="button" t-if="scales.includes('week')" class="o_calendar_button_week btn btn-secondary">Week</button>
<button type="button" t-if="scales.includes('month')" class="o_calendar_button_month btn btn-secondary">Month</button>
<button type="button" t-if="scales.includes('year')" class="o_calendar_button_year btn btn-secondary">Year</button>
<button type="button" t-if="scales.includes('day')" class="o_calendar_button_day btn btn-secondary" data-hotkey="d">Day</button>
<button type="button" t-if="scales.includes('week')" class="o_calendar_button_week btn btn-secondary" data-hotkey="w">Week</button>
<button type="button" t-if="scales.includes('month')" class="o_calendar_button_month btn btn-secondary" data-hotkey="m">Month</button>
<button type="button" t-if="scales.includes('year')" class="o_calendar_button_year btn btn-secondary" data-hotkey="y">Year</button>
</div>
</t>
@@ -9,6 +9,12 @@ var ListView = require('web.ListView');
var RamStorage = require('web.RamStorage');
var relationalFields = require('web.relational_fields');
var testUtils = require('web.test_utils');
const core = require('web.core');
const makeTestEnvironment = require("web.test_env");
const { makeLegacyCommandService } = require("@web/legacy/utils");
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
const { triggerHotkey, nextTick, click } = require("@web/../tests/helpers/utils");
const { registry } = require("@web/core/registry");
const { makeLegacyDialogMappingTestEnv } = require('@web/../tests/helpers/legacy_env_utils');
@@ -1019,6 +1025,48 @@ QUnit.module('relational_fields', {
form.destroy();
});
// TODO: Once the code base is converted with wowl, replace webclient by formview.
QUnit.test('statusbar edited by the smart action "Move to stage..."', async function (assert) {
assert.expect(3);
const legacyEnv = makeTestEnvironment({ bus: core.bus });
const serviceRegistry = registry.category("services");
serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv));
const views = {
'partner,false,form': '<form>' +
'<header><field name="trululu" widget="statusbar" options=\'{"clickable": "1"}\'/></header>' +
'</form>',
'partner,false,search': '<search></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_field_widget")
triggerHotkey("control+k")
await nextTick();
const movestage = webClient.el.querySelectorAll(".o_command");
const idx = [...movestage].map(el => el.textContent).indexOf("Move to Trululu...ALT + SHIFT + X")
assert.ok(idx >= 0);
await click(movestage[idx])
await nextTick();
assert.deepEqual([...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent), [
"first record",
"second record",
"aaa",
])
await click(webClient.el, "#o_command_2")
});
QUnit.module('FieldSelection');
QUnit.test('widget selection in a list view', async function (assert) {