diff --git a/addons/calendar/static/tests/tours/calendar_tour.js b/addons/calendar/static/tests/tours/calendar_tour.js
index 0fded337c8c..08f0f1855f3 100644
--- a/addons/calendar/static/tests/tours/calendar_tour.js
+++ b/addons/calendar/static/tests/tours/calendar_tour.js
@@ -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()}`,
},
diff --git a/addons/crm/static/tests/tours/create_crm_team_tour.js b/addons/crm/static/tests/tours/create_crm_team_tour.js
index d4531ef6b62..dd9f4826ed9 100644
--- a/addons/crm/static/tests/tours/create_crm_team_tour.js
+++ b/addons/crm/static/tests/tours/create_crm_team_tour.js
@@ -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',
diff --git a/addons/im_livechat/static/tests/tours/im_livechat_chatbot_steps_sequence_tour.js b/addons/im_livechat/static/tests/tours/im_livechat_chatbot_steps_sequence_tour.js
index 3c22c651bcf..0d3131d4340 100644
--- a/addons/im_livechat/static/tests/tours/im_livechat_chatbot_steps_sequence_tour.js
+++ b/addons/im_livechat/static/tests/tours/im_livechat_chatbot_steps_sequence_tour.js
@@ -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",
},
{
diff --git a/addons/mail/static/src/views/activity/activity_arch_parser.js b/addons/mail/static/src/views/activity/activity_arch_parser.js
index f5d0b77ed7a..e9a3e0f9205 100644
--- a/addons/mail/static/src/views/activity/activity_arch_parser.js
+++ b/addons/mail/static/src/views/activity/activity_arch_parser.js
@@ -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 {
diff --git a/addons/mail/static/tests/web/fields/many2many_tags_email_tests.js b/addons/mail/static/tests/web/fields/many2many_tags_email_tests.js
index 277212c0108..d7df7fa553b 100644
--- a/addons/mail/static/tests/web/fields/many2many_tags_email_tests.js
+++ b/addons/mail/static/tests/web/fields/many2many_tags_email_tests.js
@@ -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"));
diff --git a/addons/mail/static/tests/web/fields/onchange_on_keydown_tests.js b/addons/mail/static/tests/web/fields/onchange_on_keydown_tests.js
index 1b42e7db605..f25dbdda7dd 100644
--- a/addons/mail/static/tests/web/fields/onchange_on_keydown_tests.js
+++ b/addons/mail/static/tests/web/fields/onchange_on_keydown_tests.js
@@ -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
diff --git a/addons/mass_mailing/static/src/js/tours/mass_mailing_editor_tour.js b/addons/mass_mailing/static/src/js/tours/mass_mailing_editor_tour.js
index 8438309458d..cae88ec8f13 100644
--- a/addons/mass_mailing/static/src/js/tours/mass_mailing_editor_tour.js
+++ b/addons/mass_mailing/static/src/js/tours/mass_mailing_editor_tour.js
@@ -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
diff --git a/addons/mrp/static/tests/tours/mrp_manual_consumption.js b/addons/mrp/static/tests/tours/mrp_manual_consumption.js
index 155af8bb1e5..17e32d56b43 100644
--- a/addons/mrp/static/tests/tours/mrp_manual_consumption.js
+++ b/addons/mrp/static/tests/tours/mrp_manual_consumption.js
@@ -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',
},
{
diff --git a/addons/project/static/tests/tours/project_sharing_tour.js b/addons/project/static/tests/tours/project_sharing_tour.js
index 0ce0b83cb11..fbe50508cf4 100644
--- a/addons/project/static/tests/tours/project_sharing_tour.js
+++ b/addons/project/static/tests/tours/project_sharing_tour.js
@@ -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',
}, {
diff --git a/addons/sale/static/src/js/tours/sale.js b/addons/sale/static/src/js/tours/sale.js
index e375b3b6abd..b3401d500b1 100644
--- a/addons/sale/static/src/js/tours/sale.js
+++ b/addons/sale/static/src/js/tours/sale.js
@@ -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',
diff --git a/addons/test_sale_product_configurators/static/tests/tours/product_configurator_pricelist_ui.js b/addons/test_sale_product_configurators/static/tests/tours/product_configurator_pricelist_ui.js
index 1e002a289c6..9f03a113988 100644
--- a/addons/test_sale_product_configurators/static/tests/tours/product_configurator_pricelist_ui.js
+++ b/addons/test_sale_product_configurators/static/tests/tours/product_configurator_pricelist_ui.js
@@ -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",
diff --git a/addons/web/static/src/views/form/form_arch_parser.js b/addons/web/static/src/views/form/form_arch_parser.js
index c2130146420..d50975f9316 100644
--- a/addons/web/static/src/views/form/form_arch_parser.js
+++ b/addons/web/static/src/views/form/form_arch_parser.js
@@ -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") || "")) {
diff --git a/addons/web/static/src/views/form/form_compiler.js b/addons/web/static/src/views/form/form_compiler.js
index 3e87df25729..9b3232360bb 100644
--- a/addons/web/static/src/views/form/form_compiler.js
+++ b/addons/web/static/src/views/form/form_compiler.js
@@ -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);
diff --git a/addons/web/static/src/views/kanban/kanban_arch_parser.js b/addons/web/static/src/views/kanban/kanban_arch_parser.js
index e6dbae7d0f3..c0c2dab5886 100644
--- a/addons/web/static/src/views/kanban/kanban_arch_parser.js
+++ b/addons/web/static/src/views/kanban/kanban_arch_parser.js
@@ -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 {
diff --git a/addons/web/static/src/views/kanban/kanban_compiler.js b/addons/web/static/src/views/kanban/kanban_compiler.js
index 634e0892d20..b202caf2f99 100644
--- a/addons/web/static/src/views/kanban/kanban_compiler.js
+++ b/addons/web/static/src/views/kanban/kanban_compiler.js
@@ -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`);
}
diff --git a/addons/web/static/src/views/kanban/kanban_record.js b/addons/web/static/src/views/kanban/kanban_record.js
index 384074d2b60..5584e411acb 100644
--- a/addons/web/static/src/views/kanban/kanban_record.js
+++ b/addons/web/static/src/views/kanban/kanban_record.js
@@ -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 {
diff --git a/addons/web/static/src/views/list/list_arch_parser.js b/addons/web/static/src/views/list/list_arch_parser.js
index b5180f3ae32..1f9b7157225 100644
--- a/addons/web/static/src/views/list/list_arch_parser.js
+++ b/addons/web/static/src/views/list/list_arch_parser.js
@@ -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 {
diff --git a/addons/web/static/src/views/view_compiler.js b/addons/web/static/src/views/view_compiler.js
index 78878e20511..956891dc524 100644
--- a/addons/web/static/src/views/view_compiler.js
+++ b/addons/web/static/src/views/view_compiler.js
@@ -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}'`);
diff --git a/addons/web/static/tests/mobile/views/fields/many2many_tags_field_tests.js b/addons/web/static/tests/mobile/views/fields/many2many_tags_field_tests.js
index b708762fd3b..4a553525600 100644
--- a/addons/web/static/tests/mobile/views/fields/many2many_tags_field_tests.js
+++ b/addons/web/static/tests/mobile/views/fields/many2many_tags_field_tests.js
@@ -44,7 +44,7 @@ QUnit.module("Fields", (hooks) => {