[FIX] web: fix default_export_compatible fields in Export dialog
This commit fixes the exported fields list used by the export dialog. Since the list was deducted from the column titles, it was not possible to get the complete attributes required to filter them. Now we use the real fields fetched from the RPC in the dialog. It fetches the correct list of exportable fields, and the defaultExportList props only serves to list fields associated with visible columns. It is now possible to filter fields correctly, and improve the reliability of the export feature, to use the data from the RPC instead of using the data deducted from the visible columns only. A test has been added to verify the presence of a field that has the defaut_exportable attribute in the export list by default. task-3203958 closes odoo/odoo#116003 X-original-commit: 9ea5cf81a2b5ddcb0f19827d4a699193046fafb6 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -27,15 +27,21 @@ class ExportDataItem extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
async onClick(ev) {
|
||||
async onClick() {
|
||||
if (this.props.isFieldExpandable(this.props.field)) {
|
||||
this.subFields = await this.props.onClick(ev);
|
||||
this.state.isExpanded = this.props.isFieldExpanded(this.props.field.name);
|
||||
this.subFields = await this.props.onClick(this.props.field.id);
|
||||
this.state.isExpanded = this.props.isFieldExpanded(this.props.field.id);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClick(fieldId) {
|
||||
if (!this.props.isFieldExpandable(this.props.field) && !this.isFieldSelected(fieldId)) {
|
||||
this.props.onAdd(fieldId);
|
||||
}
|
||||
}
|
||||
|
||||
isFieldSelected(current) {
|
||||
return this.props.exportList.find(({ name }) => name === current);
|
||||
return this.props.exportList.find(({ id }) => id === current);
|
||||
}
|
||||
}
|
||||
ExportDataItem.template = "web.ExportDataItem";
|
||||
@@ -94,7 +100,7 @@ export class ExportDataDialog extends Component {
|
||||
(e) =>
|
||||
e &&
|
||||
Object.values(this.state.exportList).findIndex(
|
||||
({ name }) => name === e.dataset.field_id
|
||||
({ id }) => id === e.dataset.field_id
|
||||
)
|
||||
);
|
||||
let target;
|
||||
@@ -103,7 +109,7 @@ export class ExportDataDialog extends Component {
|
||||
} else {
|
||||
target = next ? indexes[2] : this.state.exportList.length - 1;
|
||||
}
|
||||
this.onDraggingEnd([indexes[0], target]);
|
||||
this.onDraggingEnd(indexes[0], target);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -154,7 +160,7 @@ export class ExportDataDialog extends Component {
|
||||
this.state.search = [];
|
||||
this.knownFields = {};
|
||||
await this.loadFields();
|
||||
this.setDefaultExportList();
|
||||
await this.setDefaultExportList();
|
||||
if (this.searchRef.el) {
|
||||
this.searchRef.el.value = "";
|
||||
}
|
||||
@@ -177,8 +183,8 @@ export class ExportDataDialog extends Component {
|
||||
return this.expandedFields[id] && !this.expandedFields[id].hidden;
|
||||
}
|
||||
|
||||
isFieldExpandable({ name }) {
|
||||
return this.knownFields[name].children && name.split("/").length < 3;
|
||||
isFieldExpandable({ id }) {
|
||||
return this.knownFields[id].children && id.split("/").length < 3;
|
||||
}
|
||||
|
||||
async loadExportList(value) {
|
||||
@@ -219,7 +225,6 @@ export class ExportDataDialog extends Component {
|
||||
parentParams
|
||||
);
|
||||
for (const field of fields) {
|
||||
field.name = field.id;
|
||||
field.label = field.string;
|
||||
field.parent = parentField;
|
||||
if (!this.knownFields[field.id]) {
|
||||
@@ -232,22 +237,19 @@ export class ExportDataDialog extends Component {
|
||||
return fields;
|
||||
}
|
||||
|
||||
onDraggingEnd([item, target]) {
|
||||
onDraggingEnd(item, target) {
|
||||
this.state.exportList.splice(target, 0, this.state.exportList.splice(item, 1)[0]);
|
||||
}
|
||||
|
||||
onAddItemExportList(ev) {
|
||||
const field = ev.target.closest(".o_export_tree_item").dataset.field_id;
|
||||
this.state.exportList.push(this.knownFields[field]);
|
||||
onAddItemExportList(fieldId) {
|
||||
this.state.exportList.push(this.knownFields[fieldId]);
|
||||
this.state.search = [];
|
||||
this.searchRef.el.value = "";
|
||||
this.enterTemplateEdition();
|
||||
}
|
||||
|
||||
onRemoveItemExportList(ev) {
|
||||
const item = this.state.exportList.findIndex(
|
||||
({ name }) => name === ev.target.parentElement.dataset.field_id
|
||||
);
|
||||
onRemoveItemExportList(fieldId) {
|
||||
const item = this.state.exportList.findIndex(({ id }) => id === fieldId);
|
||||
this.state.exportList.splice(item, 1);
|
||||
this.enterTemplateEdition();
|
||||
}
|
||||
@@ -272,7 +274,7 @@ export class ExportDataDialog extends Component {
|
||||
0,
|
||||
0,
|
||||
{
|
||||
name: field.name,
|
||||
name: field.id,
|
||||
},
|
||||
]),
|
||||
resource: this.props.root.resModel,
|
||||
@@ -334,25 +336,19 @@ export class ExportDataDialog extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
async onToggleCompatibleExport(value) {
|
||||
onToggleCompatibleExport(value) {
|
||||
this.state.isCompatible = value;
|
||||
await this.fetchFields();
|
||||
this.fetchFields();
|
||||
}
|
||||
|
||||
async onToggleExpandField(ev) {
|
||||
const id = ev.target.closest(".o_export_tree_item").dataset.field_id;
|
||||
const fields = await this.loadFields(id);
|
||||
return fields;
|
||||
onToggleExpandField(id) {
|
||||
return this.loadFields(id);
|
||||
}
|
||||
|
||||
setDefaultExportList() {
|
||||
if (this.state.isCompatible) {
|
||||
this.state.exportList = this.state.exportList.filter(
|
||||
({ name }) => this.knownFields[name]
|
||||
);
|
||||
} else {
|
||||
this.state.exportList = this.props.defaultExportList;
|
||||
}
|
||||
async setDefaultExportList() {
|
||||
this.state.exportList = Object.values(this.knownFields).filter(
|
||||
(e) => e.default_export || this.props.defaultExportList.find((i) => i.name === e.id)
|
||||
);
|
||||
}
|
||||
|
||||
setFormat(ev) {
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
</t>
|
||||
|
||||
<t t-name="web.ExportDataItem" owl="1">
|
||||
<div t-att-data-field_id="props.field.name" t-attf-class="o_export_tree_item cursor-pointer position-relative ps-4 {{ state.isExpanded ? 'o_expanded mb-2' : '' }} {{ props.field.parent ? '' : 'pe-3'}}" role="treeitem" t-on-click.stop="onClick" t-on-dblclick="(ev) => !props.isFieldExpandable(props.field) and !this.isFieldSelected(props.field.name) and this.props.onAdd(ev)">
|
||||
<div t-att-data-field_id="props.field.id" t-attf-class="o_export_tree_item cursor-pointer position-relative ps-4 {{ state.isExpanded ? 'o_expanded mb-2' : '' }} {{ props.field.parent ? '' : 'pe-3'}}" role="treeitem" t-on-click.stop="onClick" t-on-dblclick="() => this.onDoubleClick(props.field.id)">
|
||||
<div t-attf-class="o_tree_column d-flex align-items-center {{ props.field.required ? 'fw-bolder' : ''}}">
|
||||
<span t-if="props.isFieldExpandable(props.field)" t-attf-class="ms-n3 float-start o_expand_parent small fa {{ state.isExpanded ? 'fa-chevron-down' : 'fa-chevron-right' }}" role="img" aria-label="Show sub-fields" title="Show sub-fields" />
|
||||
<span t-if="props.isDebug and props.field.id" class="overflow-hidden w-100" t-esc="`${props.field.string} (${props.field.id})`" />
|
||||
<span t-else="" class="overflow-hidden w-100" t-esc="props.field.string" />
|
||||
<span title="Select field" t-attf-class="fa fa-plus float-end m-1 o_add_field {{ isFieldSelected(props.field.name) ? 'o_inactive opacity-25' : '' }}" t-on-click.stop="(ev) => !this.isFieldSelected(props.field.name) and this.props.onAdd(ev)" />
|
||||
<span title="Select field" t-attf-class="fa fa-plus float-end m-1 o_add_field {{ isFieldSelected(props.field.id) ? 'o_inactive opacity-25' : '' }}" t-on-click.stop="(ev) => !this.isFieldSelected(this.props.field.id) and this.props.onAdd(this.props.field.id)" />
|
||||
</div>
|
||||
<t t-if="state.isExpanded">
|
||||
<t t-foreach="subFields" t-as="field" t-key="field.name">
|
||||
<t t-foreach="subFields" t-as="field" t-key="field.id">
|
||||
<ExportDataItem
|
||||
t-props="props"
|
||||
field="field"
|
||||
@@ -44,7 +44,7 @@
|
||||
<div class="o_left_field_panel h-100 overflow-auto border">
|
||||
<div class="o_field_tree_structure">
|
||||
<t t-if="fieldsAvailable">
|
||||
<t t-foreach="rootFields" t-as="field" t-key="field.name">
|
||||
<t t-foreach="rootFields" t-as="field" t-key="field.id">
|
||||
<ExportDataItem
|
||||
field="field"
|
||||
expandedContent.bind="expandedContent"
|
||||
@@ -107,11 +107,11 @@
|
||||
</div>
|
||||
<div class="o_right_field_panel h-100 px-2 overflow-auto border">
|
||||
<ul class="o_fields_list list-unstyled" t-ref="draggable">
|
||||
<t t-foreach="state.exportList" t-as="field" t-key="field.name">
|
||||
<li t-attf-class="o_export_field {{ state.isSmall ? '' : 'o_export_field_sortable' }}" t-att-data-field_id="field.name">
|
||||
<t t-foreach="state.exportList" t-as="field" t-key="field.id">
|
||||
<li t-attf-class="o_export_field {{ state.isSmall ? '' : 'o_export_field_sortable' }}" t-att-data-field_id="field.id">
|
||||
<span t-if="!state.isSmall" class="fa fa-sort o_sort_field mx-1" t-attf-style="opacity:{{ state.exportList.length === 1 ? 0 : 1 }}" />
|
||||
<span t-esc="isDebug and field.id ? `${field.string or field.label} (${field.id})` : field.string or field.label" />
|
||||
<span class="fa fa-trash m-1 pe-2 float-end o_remove_field cursor-pointer" t-att-title="removeFieldText" t-on-click.stop="onRemoveItemExportList" />
|
||||
<span class="fa fa-trash m-1 pe-2 float-end o_remove_field cursor-pointer" t-att-title="removeFieldText" t-on-click.stop="() => this.onRemoveItemExportList(field.id)" />
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
|
||||
@@ -98,7 +98,7 @@ QUnit.module("ViewDialogs", (hooks) => {
|
||||
},
|
||||
{
|
||||
children: false,
|
||||
field_type: "char",
|
||||
field_type: "boolean",
|
||||
id: "bar",
|
||||
relation_field: null,
|
||||
required: false,
|
||||
@@ -636,7 +636,19 @@ QUnit.module("ViewDialogs", (hooks) => {
|
||||
}
|
||||
if (route === "/web/export/get_fields") {
|
||||
if (!args.parent_field) {
|
||||
return Promise.resolve(fetchedFields.root);
|
||||
return Promise.resolve([
|
||||
...fetchedFields.root,
|
||||
{
|
||||
id: "not_exportable",
|
||||
string: "Not exportable",
|
||||
type: "char",
|
||||
exportable: false,
|
||||
},
|
||||
{
|
||||
id: "exportable",
|
||||
string: "Exportable",
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (args.prefix === "partner_ids") {
|
||||
assert.step("fetch fields for 'partner_ids'");
|
||||
@@ -973,4 +985,46 @@ QUnit.module("ViewDialogs", (hooks) => {
|
||||
"the field to export corresponds to the field displayed in the list view"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"Export dialog: export list contains field with 'default_export: true'",
|
||||
async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
resModel: "partner",
|
||||
arch: `<tree export_xlsx="1">
|
||||
<field name="foo"/>
|
||||
</tree>`,
|
||||
actionMenus: {},
|
||||
mockRPC(route, args) {
|
||||
if (route === "/web/export/formats") {
|
||||
return Promise.resolve([{ tag: "csv", label: "CSV" }]);
|
||||
}
|
||||
if (route === "/web/export/get_fields") {
|
||||
if (!args.parent_field) {
|
||||
return Promise.resolve([
|
||||
...fetchedFields.root,
|
||||
{
|
||||
id: "default_exportable",
|
||||
string: "Default exportable",
|
||||
type: "char",
|
||||
default_export: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return Promise.resolve(fetchedFields[args.prefix]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await openExportDataDialog();
|
||||
assert.containsN(target, ".o_export_field", 2, "two fields are selected in the list");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_fields_list").textContent,
|
||||
"FooDefault exportable",
|
||||
"values are the correct ones"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user