[IMP] web,project: add and modify commands in the palette
\- Add 'Move to next stage' Generic, we think it is a useful tool to have anywhere. \- Change the 'Set priority...' command to 'Set priority as Low/High' : Only 1 command will be available with the 'not current' priority. Specific, because it is used on selection fields, which means there could be more than 2 options. \- Transform the 'Set kanban state...' command into three commands Generic. 2 commands will be available. One for each 'not current' kanban state. task-2857447 closes odoo/odoo#91821 Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { PriorityField } from "@web/views/fields/priority/priority_field";
|
||||
import { useCommand } from "@web/core/commands/command_hook";
|
||||
import { useState } from "@odoo/owl";
|
||||
|
||||
export class PrioritySwitchField extends PriorityField {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
index: -1,
|
||||
});
|
||||
if (this.props.record.activeFields[this.props.name].viewType !== "form") {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [id, name] of this.options) {
|
||||
useCommand(
|
||||
sprintf(this.env._t("Set priority as %s"), name),
|
||||
() => this.props.update(id),
|
||||
{
|
||||
category: "smart_action",
|
||||
hotkey: "alt+r",
|
||||
isAvailable: () => this.props.value !== id,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("priority_switch", PrioritySwitchField);
|
||||
@@ -1090,7 +1090,7 @@
|
||||
<button name="action_assign_to_me" string="Assign to Me" type="object" attrs="{'invisible': "['|', ('user_ids', 'in', uid), ('user_ids', '=', [])]"}" data-hotkey="q"/>
|
||||
<button name="action_assign_to_me" string="Assign to Me" type="object" class="oe_highlight" attrs="{'invisible' : "['|', ('user_ids', 'in', uid), ('user_ids', '!=', [])]"}" data-hotkey="q"/>
|
||||
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)]}"/>
|
||||
<field name="personal_stage_type_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '!=', False)]}" domain="[('user_id', '=', uid)]"/>
|
||||
<field name="personal_stage_type_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '!=', False)]}" domain="[('user_id', '=', uid)]" string="Personal Stage"/>
|
||||
</header>
|
||||
<div class="alert alert-info oe_edit_only mb-0" role="status" attrs="{'invisible': ['|', ('recurring_task', '=', False), ('recurrence_id', '=', False)]}" groups="project.group_project_recurring_tasks">
|
||||
<p>Edit recurring task</p>
|
||||
@@ -1151,7 +1151,7 @@
|
||||
<div class="oe_title pe-0">
|
||||
<h1 class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex w-100">
|
||||
<field name="priority" widget="priority" class="me-3"/>
|
||||
<field name="priority" widget="priority_switch" class="me-3"/>
|
||||
<field name="name" class="o_task_name text-truncate" placeholder="Task Title..."/>
|
||||
</div>
|
||||
<field name="kanban_state" widget="state_selection" class=""/>
|
||||
|
||||
@@ -4,42 +4,34 @@ import { useCommand } from "@web/core/commands/command_hook";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { standardFieldProps } from "../standard_field_props";
|
||||
import { formatSelection } from "../formatters";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class StateSelectionField extends Component {
|
||||
setup() {
|
||||
if (this.props.record.activeFields[this.props.name].viewType === "form") {
|
||||
const commandName = this.env._t("Set kanban state...");
|
||||
useCommand(
|
||||
commandName,
|
||||
() => {
|
||||
return {
|
||||
placeholder: commandName,
|
||||
providers: [
|
||||
{
|
||||
provide: () =>
|
||||
this.options.map((value) => ({
|
||||
name: value[1],
|
||||
action: () => {
|
||||
this.props.update(value[0]);
|
||||
},
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
{ category: "smart_action", hotkey: "alt+shift+r" }
|
||||
);
|
||||
}
|
||||
this.colorPrefix = "o_status_";
|
||||
this.colors = {
|
||||
blocked: "red",
|
||||
done: "green",
|
||||
};
|
||||
if (this.props.record.activeFields[this.props.name].viewType !== "form") {
|
||||
return;
|
||||
}
|
||||
const hotkeys = ["D", "F", "G"];
|
||||
for (const [index, [value, label]] of this.options.entries()) {
|
||||
useCommand(
|
||||
sprintf(this.env._t("Set kanban state as %s"), label),
|
||||
() => this.props.update(value),
|
||||
{
|
||||
category: "smart_action",
|
||||
hotkey: "alt+" + hotkeys[index],
|
||||
isAvailable: () => this.props.value !== value,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
get options() {
|
||||
return this.props.record.fields[this.props.name].selection.map(([state, label]) => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { escape, sprintf } from "@web/core/utils/strings";
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { standardFieldProps } from "../standard_field_props";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class StatusBarField extends Component {
|
||||
@@ -27,7 +26,7 @@ export class StatusBarField extends Component {
|
||||
providers: [
|
||||
{
|
||||
provide: () =>
|
||||
this.computeItems().unfolded.map((value) => ({
|
||||
this.computeItems(false).map((value) => ({
|
||||
name: value.name,
|
||||
action: () => {
|
||||
this.selectItem(value);
|
||||
@@ -39,11 +38,39 @@ export class StatusBarField extends Component {
|
||||
},
|
||||
{ category: "smart_action", hotkey: "alt+shift+x" }
|
||||
);
|
||||
useCommand(
|
||||
sprintf(this.env._t(`Move to next %s`), this.props.displayName),
|
||||
() => {
|
||||
const options = this.computeItems(false);
|
||||
const nextOption =
|
||||
options[
|
||||
options.findIndex(
|
||||
(option) =>
|
||||
option.id ===
|
||||
(this.type === "many2one"
|
||||
? this.props.value[0]
|
||||
: this.props.value)
|
||||
) + 1
|
||||
];
|
||||
this.selectItem(nextOption);
|
||||
},
|
||||
{
|
||||
category: "smart_action",
|
||||
hotkey: "alt+x",
|
||||
isAvailable: () => {
|
||||
const options = this.computeItems(false);
|
||||
return (
|
||||
options[options.length - 1].id !==
|
||||
(this.type === "many2one" ? this.props.value[0] : this.props.value)
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get currentName() {
|
||||
switch (this.props.record.fields[this.props.name].type) {
|
||||
switch (this.type) {
|
||||
case "many2one": {
|
||||
const item = this.options.find((item) => this.props.value && item.id === this.props.value[0]);
|
||||
return item ? item.display_name : "";
|
||||
@@ -56,7 +83,7 @@ export class StatusBarField extends Component {
|
||||
throw new Error("Unsupported field type for StatusBarField");
|
||||
}
|
||||
get options() {
|
||||
switch (this.props.record.fields[this.props.name].type) {
|
||||
switch (this.type) {
|
||||
case "many2one":
|
||||
return this.props.record.preloadedData[this.props.name];
|
||||
case "selection":
|
||||
@@ -66,6 +93,10 @@ export class StatusBarField extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.props.record.fields[this.props.name].type;
|
||||
}
|
||||
|
||||
getDropdownItemClassNames(item) {
|
||||
const classNames = [
|
||||
"btn",
|
||||
@@ -110,13 +141,16 @@ export class StatusBarField extends Component {
|
||||
}));
|
||||
}
|
||||
|
||||
computeItems() {
|
||||
computeItems(grouped = true) {
|
||||
let items = null;
|
||||
if (this.props.type === "many2one") {
|
||||
items = this.getVisibleMany2Ones();
|
||||
} else {
|
||||
items = this.getVisibleSelection();
|
||||
}
|
||||
if (!grouped) {
|
||||
return items;
|
||||
}
|
||||
|
||||
if (this.env.isSmall) {
|
||||
return {
|
||||
|
||||
@@ -431,7 +431,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
'StateSelectionField edited by the smart action "Set kanban state..."',
|
||||
'StateSelectionField edited by the smart actions "Set kanban state as <state name>"',
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
@@ -448,20 +448,23 @@ QUnit.module("Fields", (hooks) => {
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
const idx = [...target.querySelectorAll(".o_command")]
|
||||
.map((el) => el.textContent)
|
||||
.indexOf("Set kanban state...ALT + SHIFT + R");
|
||||
var commandTexts = [...target.querySelectorAll(".o_command")].map(
|
||||
(el) => el.textContent
|
||||
);
|
||||
assert.ok(commandTexts.includes("Set kanban state as NormalALT + D"));
|
||||
const idx = commandTexts.indexOf("Set kanban state as DoneALT + G");
|
||||
assert.ok(idx >= 0);
|
||||
|
||||
await click([...target.querySelectorAll(".o_command")][idx]);
|
||||
await nextTick();
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_command")].map((el) => el.textContent),
|
||||
["Normal", "Blocked", "Done"]
|
||||
);
|
||||
await click(target, "#o_command_2");
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_status_green");
|
||||
|
||||
triggerHotkey("control+k");
|
||||
await nextTick();
|
||||
commandTexts = [...target.querySelectorAll(".o_command")].map((el) => el.textContent);
|
||||
assert.ok(commandTexts.includes("Set kanban state as NormalALT + D"));
|
||||
assert.ok(commandTexts.includes("Set kanban state as BlockedALT + F"));
|
||||
assert.notOk(commandTexts.includes("Set kanban state as DoneALT + G"));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user