[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:
committed by
Barad Mahendra
parent
2c911fe176
commit
ff2ffb9e0d
@@ -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>
|
||||
|
||||
@@ -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}));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user