[REF] web, *: Unique id for field nodes

Before this commit, the field node id (`field_id`) uses the field name
for the first occurrence on the arch, and add an underscore and a number
for the rest of the occurrences. This can create inconsistencies when
sombody assumes that the field_id is equal to the name, and don't take
into account the possibility of multiple occurrences.

Now, a unique id is created since the first occurrence, this remove all
ambiguity between the id and the name.

Part-of task-id 3179751

closes odoo/odoo#117799

Related: odoo/enterprise#39511
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Jorge Pinna Puissant
2023-04-11 15:17:20 +02:00
parent c6d0c12c44
commit baebb6a5b0
24 changed files with 153 additions and 90 deletions
@@ -27,12 +27,12 @@
run: 'click',
},
{
trigger: '#name',
trigger: '#name_0',
content: 'Give a name to the new event',
run: 'text TEST EVENT',
},
{
trigger: '#start',
trigger: '#start_0',
content: 'Give a date to the new event',
run: `text ${todayDate()}`,
},
@@ -15,7 +15,7 @@ registry.category("web_tour.tours").add('create_crm_team_tour', {
}, {
trigger: 'button.o_list_button_add',
}, {
trigger: 'input[id="name"]',
trigger: 'input[id="name_0"]',
run: 'text My CRM Team',
}, {
trigger: 'button.o-kanban-button-new',
@@ -18,14 +18,14 @@ const commonSteps = [
trigger: ".o_list_button_add",
},
{
trigger: 'input[id="title"]',
trigger: 'input[id="title_0"]',
run: "text Test Chatbot Sequence",
},
{
trigger: 'div[name="script_step_ids"] .o_field_x2many_list_row_add a',
},
{
trigger: "textarea#message",
trigger: "textarea#message_0",
run: "text Step 1",
},
{
@@ -37,7 +37,7 @@ const commonSteps = [
run: () => {},
},
{
trigger: "textarea#message",
trigger: "textarea#message_0",
run: "text Step 2",
},
{
@@ -49,7 +49,7 @@ const commonSteps = [
run: () => {},
},
{
trigger: "textarea#message",
trigger: "textarea#message_0",
run: "text Step 3",
},
];
@@ -90,7 +90,7 @@ registry.category("web_tour.tours").add("im_livechat_chatbot_steps_sequence_with
run: () => {},
},
{
trigger: "textarea#message",
trigger: "textarea#message_0",
run: "text Step 4",
},
{
@@ -102,7 +102,7 @@ registry.category("web_tour.tours").add("im_livechat_chatbot_steps_sequence_with
run: () => {},
},
{
trigger: "textarea#message",
trigger: "textarea#message_0",
run: "text Step 5",
},
{
@@ -136,14 +136,16 @@ registry.category("web_tour.tours").add("im_livechat_chatbot_steps_sequence_with
clientY: fromPosition.y,
})
);
from.dispatchEvent(new MouseEvent("mousemove", {
bubbles: true,
which: 1,
button: 0,
// dragging is only enabled when the mouse have moved from at least 10 pixels from the original position
clientX: fromPosition.x + 20,
clientY: fromPosition.y + 20,
}));
from.dispatchEvent(
new MouseEvent("mousemove", {
bubbles: true,
which: 1,
button: 0,
// dragging is only enabled when the mouse have moved from at least 10 pixels from the original position
clientX: fromPosition.x + 20,
clientY: fromPosition.y + 20,
})
);
to.dispatchEvent(new Event("mouseenter", { bubbles: true }));
from.dispatchEvent(new Event("mouseup", { bubbles: true }));
},
@@ -152,7 +154,7 @@ registry.category("web_tour.tours").add("im_livechat_chatbot_steps_sequence_with
trigger: 'div[name="script_step_ids"] .o_field_x2many_list_row_add a',
},
{
trigger: "textarea#message",
trigger: "textarea#message_0",
run: "text Step 6",
},
{
@@ -12,6 +12,7 @@ export class ActivityArchParser extends XMLParser {
const fieldNodes = {};
const templateDocs = {};
const fieldNextIds = {};
const activeFields = {};
this.visitXML(xmlDoc, (node) => {
@@ -28,9 +29,12 @@ export class ActivityArchParser extends XMLParser {
"activity",
jsClass
);
const name = fieldInfo.name;
fieldNodes[name] = fieldInfo;
node.setAttribute("field_id", name);
if (!(fieldInfo.name in fieldNextIds)) {
fieldNextIds[fieldInfo.name] = 0;
}
const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;
fieldNodes[fieldId] = fieldInfo;
node.setAttribute("field_id", fieldId);
addFieldDependencies(
activeFields,
@@ -47,8 +51,20 @@ export class ActivityArchParser extends XMLParser {
}
}
for (const [key, field] of Object.entries(fieldNodes)) {
activeFields[key] = field;
// TODO: generate activeFields for the model based on fieldNodes (merge duplicated fields)
for (const fieldNode of Object.values(fieldNodes)) {
const fieldName = fieldNode.name;
if (activeFields[fieldName]) {
const { alwaysInvisible } = fieldNode;
activeFields[fieldName] = {
...fieldNode,
// a field can only be considered to be always invisible
// if all its nodes are always invisible
alwaysInvisible: activeFields[fieldName].alwaysInvisible && alwaysInvisible,
};
} else {
activeFields[fieldName] = fieldNode;
}
}
});
return {
@@ -62,15 +62,15 @@ QUnit.test("fieldmany2many tags email (edition)", async (assert) => {
"there should be one modal opened to edit the empty email"
);
assert.strictEqual(
$(".modal-content .o_form_view .o_input#name").val(),
$(".modal-content .o_form_view .o_input#name_0").val(),
"silver",
"the opened modal in edit mode should be a form view dialog with the res.partner 14"
);
assert.containsOnce($, ".modal-content .o_form_view .o_input#email");
assert.containsOnce($, ".modal-content .o_form_view .o_input#email_0");
// set the email and save the modal (will rerender the form view)
await testUtils.fields.editInput(
$(".modal-content .o_form_view .o_input#email"),
$(".modal-content .o_form_view .o_input#email_0"),
"coucou@petite.perruche"
);
await testUtils.dom.click($(".modal-content .o_form_button_save"));
@@ -58,7 +58,7 @@ QUnit.test(
}
},
});
const textarea = $('textarea[id="description"]')[0];
const textarea = $('textarea[id="description_0"]')[0];
await dom.click(textarea);
for (const key of "testing the keydown event") {
// trigger each key separately to simulate a user typing
@@ -46,7 +46,7 @@
trigger: '[name="body_arch"] iframe .o_editable h1',
run: () => {},
}, {
trigger: 'input#subject',
trigger: 'input#subject_0',
run: 'text Test',
}, {
trigger: '.o_form_view', // blur previous input
@@ -23,7 +23,7 @@ registry.category("web_tour.tours").add('test_mrp_manual_consumption', {
trigger: ".o_pager_value:first()",
},
{
trigger: "input[id='qty_producing']",
trigger: "input[id='qty_producing_0']",
run: 'text 8.0',
},
{
@@ -48,7 +48,7 @@ registry.category("web_tour.tours").add('test_mrp_manual_consumption', {
run: 'click',
},
{
trigger: "input[id='qty_producing']",
trigger: "input[id='qty_producing_0']",
run: 'text 2.0',
},
{
@@ -14,7 +14,7 @@ const projectSharingSteps = [...stepUtils.goToAppSteps("project.menu_main_pm", '
content: 'Select "Edit" as Access mode in the "Share Project" wizard.',
}, {
trigger: '.o_field_many2many_tags_email[name=partner_ids] input',
extra_trigger: 'label[for=partner_ids]:contains("Invite People")',
extra_trigger: 'label[for=partner_ids_0]:contains("Invite People")',
content: 'Select the user portal as collaborator to the "Project Sharing" project.',
run: 'text Georges',
}, {
+1 -1
View File
@@ -28,7 +28,7 @@ registry.category("web_tour.tours").add("sale_tour", {
position: "bottom",
skip_trigger: 'a[data-method=action_open_base_onboarding_company].o_onboarding_step_action__done',
}, {
trigger: 'input[id=street]',
trigger: 'input[id=street_0]',
content: _t("Complete your company's data"),
position: "bottom",
skip_trigger: 'a[data-method=action_open_base_onboarding_company].o_onboarding_step_action__done',
@@ -24,12 +24,12 @@ stepUtils.showAppsMenuItem(),
trigger: 'ul.ui-autocomplete > li > a:contains(Azure)',
}, {
content: "search the pricelist",
trigger: 'input[id="pricelist_id"]',
trigger: 'input[id="pricelist_id_0"]',
// Wait for onchange to come back
extra_trigger: "[name=partner_id]:contains(Fremont)",
}, {
content: "search the pricelist",
trigger: 'input[id="pricelist_id"]',
trigger: 'input[id="pricelist_id_0"]',
run: 'text Custo'
}, {
content: "select the pricelist",
@@ -20,12 +20,10 @@ export class FormArchParser extends XMLParser {
this.visitXML(xmlDoc, (node) => {
if (node.tagName === "field") {
const fieldInfo = Field.parseFieldNode(node, models, modelName, "form", jsClass);
let fieldId = fieldInfo.name;
if (fieldInfo.name in fieldNextIds) {
fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;
} else {
fieldNextIds[fieldInfo.name] = 1;
if (!(fieldInfo.name in fieldNextIds)) {
fieldNextIds[fieldInfo.name] = 0;
}
const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;
fieldNodes[fieldId] = fieldInfo;
node.setAttribute("field_id", fieldId);
if (archParseBoolean(node.getAttribute("default_focus") || "")) {
@@ -189,7 +189,7 @@ export class FormCompiler extends ViewCompiler {
const fieldName = el.getAttribute("name");
const fieldString = el.getAttribute("string");
const fieldId = el.getAttribute("field_id") || fieldName;
const fieldId = el.getAttribute("field_id");
const labelsForAttr = el.getAttribute("id") || fieldName;
const labels = this.getLabels(labelsForAttr);
const dynamicLabel = (label) => {
@@ -609,7 +609,7 @@ export class FormCompiler extends ViewCompiler {
setting.setAttribute("fieldName", toStringExpression(fieldName));
setting.setAttribute(
"fieldId",
toStringExpression(child.getAttribute("field_id") || fieldName)
toStringExpression(child.getAttribute("field_id"))
);
}
append(setting, fieldSlot);
@@ -53,6 +53,7 @@ export class KanbanArchParser extends XMLParser {
const tooltipInfo = {};
let handleField = null;
const fieldNodes = {};
const fieldNextIds = {};
const widgetNodes = {};
let widgetNextId = 0;
const jsClass = xmlDoc.getAttribute("js_class");
@@ -111,8 +112,12 @@ export class KanbanArchParser extends XMLParser {
fieldInfo.forceSave = true;
}
const name = fieldInfo.name;
fieldNodes[name] = fieldInfo;
node.setAttribute("field_id", name);
if (!(fieldInfo.name in fieldNextIds)) {
fieldNextIds[fieldInfo.name] = 0;
}
const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;
fieldNodes[fieldId] = fieldInfo;
node.setAttribute("field_id", fieldId);
if (fieldInfo.options.group_by_tooltip) {
tooltipInfo[name] = fieldInfo.options.group_by_tooltip;
}
@@ -140,8 +145,12 @@ export class KanbanArchParser extends XMLParser {
// Keep track of last update so images can be reloaded when they may have changed.
if (node.tagName === "img") {
const attSrc = node.getAttribute("t-att-src");
if (attSrc && /\bkanban_image\b/.test(attSrc) && !fieldNodes.write_date) {
fieldNodes.write_date = { type: "datetime" };
if (
attSrc &&
/\bkanban_image\b/.test(attSrc) &&
!Object.values(fieldNodes).some((f) => f.name === "write_date")
) {
fieldNodes.write_date_0 = { name: "write_date", type: "datetime" };
}
}
});
@@ -170,8 +179,19 @@ export class KanbanArchParser extends XMLParser {
defaultOrder = stringToOrderBy(handleField);
}
for (const [key, field] of Object.entries(fieldNodes)) {
activeFields[key] = field; // TODO process
for (const fieldNode of Object.values(fieldNodes)) {
const fieldName = fieldNode.name;
if (activeFields[fieldName]) {
const { alwaysInvisible } = fieldNode;
activeFields[fieldName] = {
...fieldNode,
// a field can only be considered to be always invisible
// if all its nodes are always invisible
alwaysInvisible: activeFields[fieldName].alwaysInvisible && alwaysInvisible,
};
} else {
activeFields[fieldName] = fieldNode;
}
}
return {
@@ -119,7 +119,7 @@ export class KanbanCompiler extends ViewCompiler {
compiled = createElement("span", { "t-out": `record["${fieldName}"].value` });
} else {
compiled = super.compileField(el, params);
const fieldId = el.getAttribute("field_id") || el.getAttribute("name");
const fieldId = el.getAttribute("field_id");
compiled.setAttribute("id", `'${fieldId}_' + __comp__.props.record.id`);
}
@@ -312,12 +312,14 @@ export class KanbanRecord extends Component {
}
case "set_cover": {
const { autoOpen, fieldName } = params;
const { widget } = archInfo.fieldNodes[fieldName];
const widgets = Object.values(archInfo.fieldNodes)
.filter((x) => x.name === fieldName)
.map((x) => x.widget);
const field = record.fields[fieldName];
if (
field.type === "many2one" &&
field.relation === "ir.attachment" &&
widget === "attachment_image"
widgets.includes("attachment_image")
) {
this.dialog.add(KanbanCoverImageDialog, { autoOpen, fieldName, record });
} else {
@@ -16,6 +16,7 @@ import { encodeObjectForTemplate } from "@web/views/view_compiler";
export class GroupListArchParser extends XMLParser {
parse(arch, models, modelName, jsClass) {
const fieldNodes = {};
const fieldNextIds = {};
const buttons = [];
let buttonId = 0;
this.visitXML(arch, (node) => {
@@ -27,12 +28,20 @@ export class GroupListArchParser extends XMLParser {
return false;
} else if (node.tagName === "field") {
const fieldInfo = Field.parseFieldNode(node, models, modelName, "list", jsClass);
fieldNodes[fieldInfo.name] = fieldInfo;
node.setAttribute("field_id", fieldInfo.name);
if (!(fieldInfo.name in fieldNextIds)) {
fieldNextIds[fieldInfo.name] = 0;
}
const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;
fieldNodes[fieldId] = fieldInfo;
node.setAttribute("field_id", fieldId);
return false;
}
});
return { fieldNodes, buttons };
const activeFields = {};
for (const fieldNode of Object.values(fieldNodes)) {
activeFields[fieldNode.name] = fieldNode;
}
return { fieldNodes, activeFields, buttons };
}
}
@@ -74,6 +83,7 @@ export class ListArchParser extends XMLParser {
const treeAttr = {};
let nextId = 0;
const activeFields = {};
const fieldNextIds = {};
this.visitXML(arch, (node) => {
if (node.tagName !== "button") {
buttonGroup = undefined;
@@ -101,8 +111,12 @@ export class ListArchParser extends XMLParser {
}
} else if (node.tagName === "field") {
const fieldInfo = this.parseFieldNode(node, models, modelName);
fieldNodes[fieldInfo.name] = fieldInfo;
node.setAttribute("field_id", fieldInfo.name);
if (!(fieldInfo.name in fieldNextIds)) {
fieldNextIds[fieldInfo.name] = 0;
}
const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;
fieldNodes[fieldId] = fieldInfo;
node.setAttribute("field_id", fieldId);
if (fieldInfo.widget === "handle") {
handleField = fieldInfo.name;
}
@@ -158,7 +172,7 @@ export class ListArchParser extends XMLParser {
const groupByArchInfo = groupListArchParser.parse(groupByArch, models, coModelName);
groupBy.buttons[fieldName] = groupByArchInfo.buttons;
groupBy.fields[fieldName] = {
activeFields: groupByArchInfo.fieldNodes,
activeFields: groupByArchInfo.activeFields,
fieldNodes: groupByArchInfo.fieldNodes,
fields: models[coModelName],
};
@@ -233,8 +247,19 @@ export class ListArchParser extends XMLParser {
treeAttr.defaultOrder = stringToOrderBy(handleField);
}
for (const [key, field] of Object.entries(fieldNodes)) {
activeFields[key] = field; // TODO process
for (const fieldNode of Object.values(fieldNodes)) {
const fieldName = fieldNode.name;
if (activeFields[fieldName]) {
const { alwaysInvisible } = fieldNode;
activeFields[fieldName] = {
...fieldNode,
// a field can only be considered to be always invisible
// if all its nodes are always invisible
alwaysInvisible: activeFields[fieldName].alwaysInvisible && alwaysInvisible,
};
} else {
activeFields[fieldName] = fieldNode;
}
}
return {
+1 -1
View File
@@ -361,7 +361,7 @@ export class ViewCompiler {
*/
compileField(el, params) {
const fieldName = el.getAttribute("name");
const fieldId = el.getAttribute("field_id") || fieldName;
const fieldId = el.getAttribute("field_id");
const field = createElement("Field");
field.setAttribute("id", `'${fieldId}'`);
@@ -44,7 +44,7 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_tags" placeholder="foo"/>
</form>`,
});
assert.strictEqual(target.querySelector("#timmy").placeholder, "foo");
assert.strictEqual(target.querySelector("#timmy_0").placeholder, "foo");
});
QUnit.test("Many2ManyTagsField placeholder should be empty", async function (assert) {
@@ -57,6 +57,6 @@ QUnit.module("Fields", (hooks) => {
<field name="timmy" widget="many2many_tags"/>
</form>`,
});
assert.strictEqual(target.querySelector("#timmy").placeholder, "");
assert.strictEqual(target.querySelector("#timmy_0").placeholder, "");
});
});
@@ -49,7 +49,7 @@ QUnit.module("Form Compiler", (hooks) => {
QUnit.test(
"label with empty string compiles to FormLabel with empty string",
async (assert) => {
const arch = /*xml*/ `<form><field name="test"/><label for="test" string=""/></form>`;
const arch = /*xml*/ `<form><field field_id="test" name="test"/><label for="test" string=""/></form>`;
const expected = /*xml*/ `
<t>
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
@@ -62,7 +62,7 @@ QUnit.module("Form Compiler", (hooks) => {
);
QUnit.test("properly compile simple div with field", async (assert) => {
const arch = /*xml*/ `<form><div class="someClass">lol<field name="display_name"/></div></form>`;
const arch = /*xml*/ `<form><div class="someClass">lol<field field_id="display_name" name="display_name"/></div></form>`;
const expected = /*xml*/ `
<t>
<div t-att-class="__comp__.props.class" t-attf-class="{{__comp__.props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ __comp__.props.record.isDirty ? 'o_form_dirty' : !__comp__.props.record.isNew ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
@@ -80,8 +80,8 @@ QUnit.module("Form Compiler", (hooks) => {
const arch = /*xml*/ `
<form>
<group>
<group><field name="display_name"/></group>
<group><field name="charfield"/></group>
<group><field field_id="display_name" name="display_name"/></group>
<group><field field_id="charfield" name="charfield"/></group>
</group>
</form>`;
const expected = /*xml*/ `
@@ -112,7 +112,7 @@ QUnit.module("Form Compiler", (hooks) => {
<group>
<form>
<div>
<field name="test"/>
<field field_id="test" name="test"/>
</div>
</form>
</group>
@@ -143,8 +143,8 @@ QUnit.module("Form Compiler", (hooks) => {
const arch = /*xml*/ `
<form>
<notebook>
<page name="p1" string="Page1"><field name="charfield"/></page>
<page name="p2" string="Page2"><field name="display_name"/></page>
<page name="p1" string="Page1"><field field_id="charfield" name="charfield"/></page>
<page name="p2" string="Page2"><field field_id="display_name" name="display_name"/></page>
</notebook>
</form>`;
@@ -164,7 +164,7 @@ QUnit.module("Form Compiler", (hooks) => {
QUnit.test("properly compile field without placeholder", async (assert) => {
const arch = /*xml*/ `
<form>
<field name="display_name" placeholder="e.g. Contact's Name or //someinfo..."/>
<field field_id="display_name" name="display_name" placeholder="e.g. Contact's Name or //someinfo..."/>
</form>`;
const expected = /*xml*/ `
@@ -232,7 +232,7 @@ QUnit.module("Form Compiler", (hooks) => {
// </form>````
const arch = /*xml*/ `
<form>
<field name="display_name" modifiers="{&quot;invisible&quot;: true}" />
<field field_id="display_name" name="display_name" modifiers="{&quot;invisible&quot;: true}" />
<div class="visible3" modifiers="{&quot;invisible&quot;: false}"/>
<div modifiers="{&quot;invisible&quot;: [[&quot;display_name&quot;, &quot;=&quot;, &quot;take&quot;]]}"/>
</form>`;
@@ -299,7 +299,7 @@ QUnit.module("Form Compiler", (hooks) => {
help="this is bar"
documentation="/applications/technical/web/settings/this_is_a_test.html"
company_dependent="1">
<field name="bar"/>
<field field_id="bar" name="bar"/>
<label>label with content</label>
</setting>
</form>`;
@@ -863,9 +863,9 @@ QUnit.module("Views", (hooks) => {
assert.hasClass(target.querySelector(".o_field_widget[name=foo]"), "o_field_char");
assert.hasClass(target.querySelector(".o_field_widget[name=foo]"), "col-lg-6");
assert.hasClass(target.querySelector(".o_form_label[for=foo]"), "plop");
assert.hasClass(target.querySelector(".o_form_label[for=foo]"), "plop2");
assert.hasClass(target.querySelector(".o_form_label[for=foo]"), "col-lg-6");
assert.hasClass(target.querySelector(".o_form_label[for=foo_0]"), "plop");
assert.hasClass(target.querySelector(".o_form_label[for=foo_0]"), "plop2");
assert.hasClass(target.querySelector(".o_form_label[for=foo_0]"), "col-lg-6");
});
QUnit.test("group containing both a field and a group", async function (assert) {
@@ -2350,14 +2350,14 @@ QUnit.module("Views", (hooks) => {
</form>`,
});
await mouseEnter(target.querySelector(".o_form_label[for=foo] sup"));
await mouseEnter(target.querySelector(".o_form_label[for=foo_0] sup"));
await nextTick();
assert.strictEqual(
target.querySelector(".o-tooltip .o-tooltip--help").textContent,
"foo tooltip"
);
await mouseEnter(target.querySelector(".o_form_label[for=bar] sup"));
await mouseEnter(target.querySelector(".o_form_label[for=bar_0] sup"));
await nextTick();
assert.strictEqual(
target.querySelector(".o-tooltip .o-tooltip--help").textContent,
@@ -4911,7 +4911,7 @@ QUnit.module("Views", (hooks) => {
".o_statusbar_status button",
"Must have only one statusbar button"
);
await editInput(target, "input#name.o_input", "xpad");
await editInput(target, "input#name_0.o_input", "xpad");
assert.containsN(
target,
".o_statusbar_status button",
@@ -8750,7 +8750,7 @@ QUnit.module("Views", (hooks) => {
assert.containsOnce(rows[0], "> .o_cell", "Should only contain one cell");
assert.containsOnce(rows[0], ".o_horizontal_separator");
assert.containsN(rows[1], "> .o_cell", 2, "Should contain 2 cells");
assert.containsOnce(rows[1], "label[for=display_name]");
assert.containsOnce(rows[1], "label[for=display_name_0]");
assert.containsOnce(rows[1], "div[name=display_name]");
}
);
@@ -11220,14 +11220,14 @@ QUnit.module("Views", (hooks) => {
</form>`,
});
await mouseEnter(target.querySelector(".o_form_label[for=product_id] sup"));
await mouseEnter(target.querySelector(".o_form_label[for=product_id_0] sup"));
await nextTick();
assert.strictEqual(
target.querySelector(".o-tooltip .o-tooltip--help").textContent,
"this is a tooltip\n\nValues set here are company-specific."
);
await mouseEnter(target.querySelector(".o_form_label[for=foo] sup"));
await mouseEnter(target.querySelector(".o_form_label[for=foo_0] sup"));
await nextTick();
assert.strictEqual(
target.querySelector(".o-tooltip .o-tooltip--help").textContent,
@@ -867,7 +867,7 @@ QUnit.module("Views", (hooks) => {
str: "some string",
bool: "true",
num: "4.5",
field_id: "int_field",
field_id: "int_field_0",
});
}
}
@@ -658,7 +658,7 @@ QUnit.module("SettingsFormView", (hooks) => {
"checkbox should not be checked"
);
assert.containsNone(target, ".o_dirty_warning", "warning message should not be shown");
await click(target.querySelector(".o_field_boolean input[id=bar]"));
await click(target.querySelector(".o_field_boolean input[id=bar_0]"));
assert.containsOnce(target, ".o_field_boolean input:checked", "checkbox should be checked");
window.dispatchEvent(new Event("beforeunload"));
@@ -1139,7 +1139,7 @@ QUnit.module("SettingsFormView", (hooks) => {
"checkbox should not be checked"
);
assert.containsNone(target, ".o_dirty_warning", "warning message should not be shown");
await click(target.querySelector(".o_field_boolean input[id=bar]"));
await click(target.querySelector(".o_field_boolean input[id=bar_0]"));
assert.containsOnce(target, ".o_field_boolean input:checked", "checkbox should be checked");
assert.containsOnce(target, ".o_dirty_warning", "warning message should be shown");
});
@@ -1170,7 +1170,7 @@ QUnit.module("SettingsFormView", (hooks) => {
</form>`,
});
await click(target.querySelector("input[id=bar]"));
await click(target.querySelector("input[id=bar_0]"));
assert.containsOnce(target, ".o_dirty_warning", "warning message should be shown");
await click(target.querySelector(".o_form_button_save"));
assert.containsOnce(target, ".o_dirty_warning", "warning message should be shown");
@@ -1517,8 +1517,8 @@ QUnit.module("SettingsFormView", (hooks) => {
<SettingsPage slots="{NoContentHelper:__comp__.props.slots.NoContentHelper}" initialTab="__comp__.props.initialApp" t-slot-scope="settings" modules="[{&quot;key&quot;:&quot;crm&quot;,&quot;string&quot;:&quot;CRM&quot;,&quot;imgurl&quot;:&quot;/crm/static/description/icon.png&quot;}]">
<SettingsApp key="\`crm\`" string="\`CRM\`" imgurl="\`/crm/static/description/icon.png\`" selectedTab="settings.selectedTab">
<SearchableSetting title="\`\`" help="\`\`" companyDependent="false" documentation="\`\`" record="__comp__.props.record" string="\`\`" addLabel="true" labels="[&quot;\`My\\&quot; little ' Label\`&quot;]">
<FormLabel id="'display_name'" fieldName="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']" className="&quot;highhopes&quot;" string="\`My&quot; little ' Label\`"/>
<Field id="'display_name'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name']"/>
<FormLabel id="'display_name_0'" fieldName="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name_0']" className="&quot;highhopes&quot;" string="\`My&quot; little ' Label\`"/>
<Field id="'display_name_0'" name="'display_name'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['display_name_0']"/>
</SearchableSetting>
</SettingsApp>
</SettingsPage>`;
@@ -1559,7 +1559,7 @@ QUnit.module("SettingsFormView", (hooks) => {
const expectedCompiled = `
<HighlightText originalText="\`this is Baz value: \`"/>
<Field id="'baz'" name="'baz'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['baz']"/>
<Field id="'baz_0'" name="'baz'" record="__comp__.props.record" fieldInfo="__comp__.props.archInfo.fieldNodes['baz_0']"/>
<HighlightText originalText="\` and this is the after text\`"/>`;
assert.areEquivalent(
compiled.querySelector("SearchableSetting div.text-muted").innerHTML,
@@ -26,9 +26,9 @@
trigger: '.modal-dialog div[name=date_begin]',
content: _t("Open date range picker. Pick a Start date for your event"),
run: function () {
$('input[id="date_begin"]').val('09/30/2020 08:00:00').change();
$('input[id="date_end"]').val('10/02/2020 23:00:00').change();
$('input[id="date_begin"]').click();
$('input[id="date_begin_0"]').val('09/30/2020 08:00:00').change();
$('input[id="date_end_0"]').val('10/02/2020 23:00:00').change();
$('input[id="date_begin_0"]').click();
}
}, {
content: _t("Apply change."),