[IMP] web: move properties field's "add a property" btn in the cog menu

Purpose:
--------
When the property field is added into a form view, the "add a property"
button is shown by default.
However, most of the time the users won't add properties when opening a
form view, making this button take up space for nothing.

To alleviate this issue, commit [1] added an option that allows to hide
this button, and added actions to allow adding properties from the cog
menu instead.

Now that properties are being added into several views, one would like
this "add properties" button to be hidden by default, and show an entry
in the cogmenu allowing add properties by default, without needing to
create an action for every view using a property field.
Therefore, a new static action "Add Properties" has been added in the form
controller that is available if a properties field has been encountered
during the parsing of the form view's arch.

The "hideAddButton" has been changed into "showAddButton" and now allows
to show the "Add a Property" by default.

Task-3458627

[1]: https://github.com/odoo/odoo/commit/1f80810e39f811122a65e11cd8cac99aa39a7960

Part-of: odoo/odoo#137589
This commit is contained in:
Adrien Schoffeniels
2023-10-11 17:02:02 +00:00
committed by Thibault Delavallée
parent 5801ec664d
commit 2d7a49dab1
13 changed files with 107 additions and 74 deletions
-1
View File
@@ -32,7 +32,6 @@
'data/crm_team_data.xml',
'data/digest_data.xml',
'data/ir_action_data.xml',
'data/ir_actions_client_data.xml',
'data/ir_cron_data.xml',
'data/mail_message_subtype_data.xml',
'data/crm_recurring_plan_data.xml',
@@ -1,10 +0,0 @@
<?xml version="1.0"?>
<odoo>
<record id="crm_lead_action_configure_properties_field" model="ir.actions.client">
<field name="name">Add Properties</field>
<field name="res_model">crm.lead</field>
<field name="tag">action_configure_properties_field</field>
<field name="binding_model_id" ref="crm.model_crm_lead"/>
<field name="binding_view_types">form</field>
</record>
</odoo>
+1 -2
View File
@@ -252,8 +252,7 @@
</group>
</group>
<div class="d-flex">
<field name="lead_properties" nolabel="1" columns="2"
hideKanbanOption="1" hideAddButton="1"/>
<field name="lead_properties" nolabel="1" columns="2" hideKanbanOption="1"/>
</div>
<notebook>
<page string="Internal Notes" name="internal_notes">
-8
View File
@@ -13,14 +13,6 @@
<field eval="False" name="doall" />
</record>
<record id="fleet_vehicle_action_configure_properties_field" model="ir.actions.client">
<field name="name">Add Properties</field>
<field name="res_model">fleet.vehicle</field>
<field name="tag">action_configure_properties_field</field>
<field name="binding_model_id" ref="fleet.model_fleet_vehicle"/>
<field name="binding_view_types">form</field>
</record>
<record id="fleet_vehicle_state_new_request" model="fleet.vehicle.state">
<field name="name">New Request</field>
<field name="sequence">4</field>
+1 -2
View File
@@ -112,8 +112,7 @@
</group>
</group>
<div class="d-flex">
<field name="vehicle_properties" nolabel="1" columns="2"
hideKanbanOption="1" hideAddButton="1"/>
<field name="vehicle_properties" nolabel="1" columns="2" hideKanbanOption="1"/>
</div>
<notebook>
<page string="Tax Info">
-1
View File
@@ -24,7 +24,6 @@
'security/ir.model.access.csv',
'security/ir.model.access.xml',
'data/digest_data.xml',
'data/ir_actions_client_data.xml',
'report/project_task_burndown_chart_report_views.xml',
'views/account_analytic_account_views.xml',
'views/digest_digest_views.xml',
@@ -1,10 +0,0 @@
<?xml version="1.0"?>
<odoo>
<record id="project_task_action_configure_properties_field" model="ir.actions.client">
<field name="name">Add Properties</field>
<field name="res_model">project.task</field>
<field name="tag">action_configure_properties_field</field>
<field name="binding_model_id" ref="project.model_project_task"/>
<field name="binding_view_types">form</field>
</record>
</odoo>
+1 -2
View File
@@ -372,8 +372,7 @@
</group>
</group>
<div class="d-flex">
<field name="task_properties" nolabel="1" columns="2"
hideKanbanOption="1" hideAddButton="1"/>
<field name="task_properties" nolabel="1" columns="2" hideKanbanOption="1"/>
</div>
<notebook>
<page name="description_page" string="Description">
@@ -8,7 +8,7 @@ import { PropertyDefinition } from "./property_definition";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { PropertyValue } from "./property_value";
import { useService } from "@web/core/utils/hooks";
import { useBus, useService } from "@web/core/utils/hooks";
import { usePopover } from "@web/core/popover/popover_hook";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { reposition } from "@web/core/position_hook";
@@ -30,7 +30,7 @@ export class PropertiesField extends Component {
...standardFieldProps,
context: { type: Object, optional: true },
columns: { type: Number, optional: true },
hideAddButton: { type: Boolean, optional: true },
showAddButton: { type: Boolean, optional: true },
hideKanbanOption: { type: Boolean, optional: true },
};
@@ -56,10 +56,17 @@ export class PropertiesField extends Component {
this.state = useState({
canChangeDefinition: true,
movedPropertyName: null,
hideAddButton: this.props.hideAddButton,
showAddButton: this.props.showAddButton,
unfoldedSeparators: this._getUnfoldedSeparators(),
});
// Properties can be added from the cogmenu of the form controller
if (this.env.config?.viewType === "form") {
useBus(this.env.model.bus, "PROPERTY_FIELD:ADD_PROPERTY_VALUE", () => {
this.onPropertyCreate();
});
}
onWillStart(async () => {
await this._checkDefinitionAccess();
});
@@ -609,7 +616,7 @@ export class PropertiesField extends Component {
definition_changed: true,
});
this.openPropertyDefinition = newName;
this.state.hideAddButton = false;
this.state.showAddButton = true;
this.props.record.update({ [this.props.name]: propertiesDefinitions });
}
@@ -939,29 +946,10 @@ export const propertiesField = {
return {
context: dynamicInfo.context,
columns: parseInt(attrs.columns || "1"),
hideAddButton: archParseBoolean(attrs.hideAddButton),
showAddButton: archParseBoolean(attrs.showAddButton),
hideKanbanOption: archParseBoolean(attrs.hideKanbanOption),
};
},
};
registry.category("fields").add("properties", propertiesField);
/**
* This action is meant to be called from a form client action
* and will let the user create properties, typically used along
* with the "hideAddButton" option of the properties field.
*
* @param {object} env
*/
async function actionAddProperty(env) {
const addProperty = document.querySelector(".o_field_property_add button");
if (addProperty) {
addProperty.click();
} else {
const message = _t("You can not create a new property.");
env.services.notification.add(message, { type: "danger" });
}
}
registry.category("actions").add("action_configure_properties_field", actionAddProperty);
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="web.PropertiesField">
<div t-ref="properties" class="row"
t-att-class="{'d-none': propertiesList.length === 0 and state.hideAddButton}"
t-att-class="{'d-none': propertiesList.length === 0 and !state.showAddButton}"
t-att-columns="renderedColumnsCount">
<t t-set="unfoldedSeparators" t-value="state.unfoldedSeparators"/>
<t t-set="_groupedPropertiesList" t-value="groupedPropertiesList"/>
@@ -76,9 +76,9 @@
</div>
</div>
<div
t-if="propertiesListGroup_index === _groupedPropertiesList.length - 1"
t-if="propertiesListGroup_index === _groupedPropertiesList.length - 1 and state.showAddButton"
class="o_field_property_add"
t-att-class="{'g-col-2': props.columns !== 1, 'd-none': state.hideAddButton}">
t-att-class="{'g-col-2': props.columns !== 1}">
<button
t-if="!props.readonly and state.canChangeDefinition and definitionRecordId"
class="btn btn-light text-muted text-break m-0"
@@ -27,6 +27,9 @@ export class FormArchParser {
if (archParseBoolean(node.getAttribute("default_focus") || "")) {
autofocusFieldId = fieldId;
}
if (fieldInfo.type === "properties") {
activeActions.addPropertyFieldValue = true;
}
return false;
} else if (node.tagName === "div" && node.classList.contains("oe_chatter")) {
// remove this when chatter fields are declared as attributes on the root node
@@ -403,6 +403,13 @@ export class FormController extends Component {
callback: () => this.deleteRecord(),
skipSave: true,
},
addPropertyFieldValue: {
isAvailable: () => activeActions.addPropertyFieldValue,
sequence: 50,
icon: "fa fa-cogs",
description: _t("Add Properties"),
callback: () => this.model.bus.trigger("PROPERTY_FIELD:ADD_PROPERTY_VALUE"),
},
};
}
@@ -12,6 +12,7 @@ import {
patchWithCleanup,
triggerEvent,
} from "@web/../tests/helpers/utils";
import { toggleActionMenu } from "@web/../tests/search/helpers";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
@@ -111,6 +112,7 @@ async function makePropertiesGroupView(properties) {
</sheet>
</form>`,
mockRPC,
actionMenus: {},
});
}
@@ -352,13 +354,18 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC,
actionMenus: {},
});
const field = target.querySelector(".o_field_properties");
assert.ok(field, "The field must be in the view");
const addButton = target.querySelector(".o_field_property_add button");
assert.notOk(addButton, "The add button must not be in the view");
await toggleActionMenu(target);
assert.containsOnce(
target,
".o_cp_action_menus span:contains(Add Properties)",
"Show Add Properties btn in cog menu",
);
const editButton = field.querySelector(".o_field_property_open_popover");
assert.notOk(editButton, "The edit definition button must not be in the view");
@@ -393,13 +400,18 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC,
actionMenus: {},
});
const field = target.querySelector(".o_field_properties");
assert.ok(field, "The field must be in the view");
const addButton = target.querySelector(".o_field_property_add button");
assert.ok(addButton, "The add button must be in the view");
await toggleActionMenu(target);
assert.containsOnce(
target,
".o_cp_action_menus span:contains(Add Properties)",
"The add button must be in the cog menu",
);
const editButton = field.querySelectorAll(".o_field_property_open_popover");
assert.ok(editButton, "The edit definition button must be in the view");
@@ -491,16 +503,21 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC,
actionMenus: {},
});
const field = target.querySelector(".o_field_properties");
assert.ok(field, "The field must be in the view");
const addButton = target.querySelector(".o_field_property_add button");
assert.ok(addButton, "The add button must be in the view");
await toggleActionMenu(target);
assert.containsOnce(
target,
".o_cp_action_menus span:contains(Add Properties)",
"The add button must be in the cog menu",
);
// Create a new property
await click(target, ".o_field_property_add button");
await click(target, ".o_cp_action_menus span .fa-cogs");
await nextTick();
@@ -1805,6 +1822,7 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC,
actionMenus: {},
});
const field = target.querySelector(".o_field_properties");
@@ -1813,7 +1831,8 @@ QUnit.module("Fields", (hooks) => {
// create a new property
// edit the default value and close the popover definition
// because we just created the property, the default value should be propagated
await click(target, ".o_field_property_add button");
await toggleActionMenu(target);
await click(target, ".o_cp_action_menus span .fa-cogs");
await nextTick();
await editInput(target, ".o_field_property_definition_value input", "First Default Value");
await closePopover(target);
@@ -1969,11 +1988,13 @@ QUnit.module("Fields", (hooks) => {
</sheet>
</form>`,
mockRPC,
actionMenus: {},
});
assert.notOk(target.querySelector(".o_test_properties_not_empty"));
// create the first property
await click(target, ".o_field_property_add button");
await toggleActionMenu(target);
await click(target, ".o_cp_action_menus span .fa-cogs");
assert.ok(target.querySelector(".o_test_properties_not_empty"));
}
);
@@ -2041,7 +2062,8 @@ QUnit.module("Fields", (hooks) => {
);
// create 3 new properties
await click(target, ".o_field_property_add button");
await toggleActionMenu(target);
await click(target, ".o_cp_action_menus span .fa-cogs");
await click(target, ".o_field_property_add button");
await click(target, ".o_field_property_add button");
await nextTick();
@@ -2283,7 +2305,8 @@ QUnit.module("Fields", (hooks) => {
assertFolded([false, false, false, true]);
// now, create a new property, it must unfold the last group
await click(target, ".o_field_property_add button");
await toggleActionMenu(target);
await click(target, ".o_cp_action_menus span .fa-cogs");
assert.deepEqual(getGroups(), [
[
["SEPARATOR 2", "property_2"],
@@ -2454,4 +2477,49 @@ QUnit.module("Fields", (hooks) => {
],
]);
});
QUnit.test("properties: showAddButton option", async function (assert) {
async function mockRPC(route, { method, model, kwargs }) {
if (["check_access_rights", "check_access_rule"].includes(method)) {
return true;
}
}
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<sheet>
<group>
<field name="company_id"/>
<field name="properties" showAddButton="True"/>
</group>
</sheet>
</form>`,
mockRPC,
});
assert.containsOnce(
target,
".o_field_property_add button",
"The add button must be in the view",
);
});
QUnit.test(
"properties: no add properties action in cogmenu if no properties field",
async function (assert) {
await makeView({
type: "form",
resModel: "res.users",
resId: 1,
serverData,
arch: '<form><field name="name"/></form>',
actionMenus: {},
});
await toggleActionMenu(target);
assert.containsNone(target, ".o_cp_action_menus span:contains(Add Properties)");
},
);
});