[FIX] web: allow expanding fields in export dialog after search
This commit fixes the display of subfields in the export dialog. Currently, after a search, the results are filtered to only show the matching results. But when unfolding an expandable field, the results are not shown below. It is only set visible after the search has been modified to rerender the list completely. Now, with this commit, the list is updated as the field is unfolded. A test has been added to verify that the element is loaded and displayed as expected. opw-3316900, opw-3302785 closes odoo/odoo#123707 X-original-commit: bc18cd55f97ec6119717455217372bac161c9b07 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
@@ -23,18 +23,31 @@ DeleteExportListDialog.template = "web.DeleteExportListDialog";
|
||||
class ExportDataItem extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
isExpanded: this.subFields.length > 0 && this.props.isExpanded,
|
||||
subfields: [],
|
||||
});
|
||||
onWillStart(() => {
|
||||
if (this.props.isExpanded) {
|
||||
// automatically expand the item when subfields are already loaded
|
||||
// and display subfields that match the search string
|
||||
return this.toggleItem(this.props.field.id, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get subFields() {
|
||||
return this.props.getSubFields(this.props.field.id);
|
||||
}
|
||||
|
||||
async loadExpandedContent(id) {
|
||||
async toggleItem(id, isUserToggle) {
|
||||
if (this.props.isFieldExpandable(id)) {
|
||||
await this.props.onToggleExpandField(id);
|
||||
this.state.isExpanded = !this.state.isExpanded;
|
||||
if (this.state.subfields.length) {
|
||||
this.state.subfields = [];
|
||||
} else {
|
||||
const subfields = await this.props.loadFields(id, !isUserToggle);
|
||||
if (subfields) {
|
||||
this.state.subfields = isUserToggle
|
||||
? subfields
|
||||
: this.props.filterSubfields(subfields);
|
||||
} else {
|
||||
this.state.subfields = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +66,12 @@ ExportDataItem.components = { ExportDataItem };
|
||||
ExportDataItem.props = {
|
||||
exportList: { type: Object, optional: true },
|
||||
field: { type: Object, optional: true },
|
||||
getSubFields: Function,
|
||||
filterSubfields: Function,
|
||||
isDebug: Boolean,
|
||||
isExpanded: Boolean,
|
||||
isFieldExpandable: Function,
|
||||
onAdd: Function,
|
||||
onToggleExpandField: Function,
|
||||
search: Array,
|
||||
loadFields: Function,
|
||||
};
|
||||
|
||||
export class ExportDataDialog extends Component {
|
||||
@@ -164,17 +176,20 @@ export class ExportDataDialog extends Component {
|
||||
return this.fieldsAvailable.filter(({ parent }) => !parent);
|
||||
}
|
||||
|
||||
getSubFields(id) {
|
||||
filterSubfields(subfields) {
|
||||
let subfieldsFromSearchResults = [];
|
||||
const fieldsAvailable = this.fieldsAvailable;
|
||||
const expandedFields = (this.expandedFields[id] && this.expandedFields[id].fields) || [];
|
||||
let searchResults;
|
||||
if (this.searchRef.el && this.searchRef.el.value) {
|
||||
searchResults = this.lookup(this.searchRef.el.value);
|
||||
}
|
||||
const fieldsAvailable = Object.values(searchResults || this.knownFields);
|
||||
if (this.searchRef.el && this.searchRef.el.value) {
|
||||
subfieldsFromSearchResults = fieldsAvailable
|
||||
.filter((f) => f.parent && this.knownFields[f.parent.id].parent)
|
||||
.map((f) => f.parent);
|
||||
}
|
||||
const availableSubFields = unique([...fieldsAvailable, ...subfieldsFromSearchResults]);
|
||||
return expandedFields.filter((a) => availableSubFields.some((b) => a.id === b.id));
|
||||
return subfields.filter((a) => availableSubFields.some((b) => a.id === b.id));
|
||||
}
|
||||
|
||||
updateSize() {
|
||||
@@ -225,7 +240,7 @@ export class ExportDataDialog extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
async loadFields(id) {
|
||||
async loadFields(id, preventLoad = false) {
|
||||
let model = this.props.root.resModel;
|
||||
let parentField, parentParams;
|
||||
if (id) {
|
||||
@@ -243,6 +258,9 @@ export class ExportDataDialog extends Component {
|
||||
exclude: [parentField.relation_field],
|
||||
};
|
||||
}
|
||||
if (preventLoad) {
|
||||
return;
|
||||
}
|
||||
const fields = await this.props.getExportedFields(
|
||||
model,
|
||||
this.state.isCompatible,
|
||||
@@ -349,22 +367,27 @@ export class ExportDataDialog extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
async onSearch(ev) {
|
||||
this.state.search = fuzzyLookup(
|
||||
ev.target.value,
|
||||
onSearch(ev) {
|
||||
this.state.search = this.lookup(ev.target.value);
|
||||
}
|
||||
|
||||
lookup(value) {
|
||||
let lookupResult = fuzzyLookup(
|
||||
value,
|
||||
Object.values(this.knownFields),
|
||||
// because fuzzyLookup gives an higher score if the string starts with the pattern,
|
||||
// reversing the string makes the search more reliable in this context
|
||||
(field) => field.id.split("/").reverse().join("/")
|
||||
(field) => field.string.split("/").reverse().join("/")
|
||||
);
|
||||
if (this.isDebug) {
|
||||
this.state.search = unique([
|
||||
...this.state.search,
|
||||
lookupResult = unique([
|
||||
...lookupResult,
|
||||
...Object.values(this.knownFields).filter((f) => {
|
||||
return f.id.includes(ev.target.value);
|
||||
return f.id.includes(value);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
return lookupResult;
|
||||
}
|
||||
|
||||
onToggleCompatibleExport(value) {
|
||||
@@ -372,10 +395,6 @@ export class ExportDataDialog extends Component {
|
||||
this.fetchFields();
|
||||
}
|
||||
|
||||
onToggleExpandField(id) {
|
||||
return this.loadFields(id);
|
||||
}
|
||||
|
||||
async setDefaultExportList() {
|
||||
this.state.exportList = Object.values(this.knownFields).filter(
|
||||
(e) => e.default_export || this.props.defaultExportList.find((i) => i.name === e.id)
|
||||
|
||||
@@ -12,21 +12,19 @@
|
||||
</t>
|
||||
|
||||
<t t-name="web.ExportDataItem" owl="1">
|
||||
<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="() => this.loadExpandedContent(props.field.id)" t-on-dblclick="() => this.onDoubleClick(props.field.id)">
|
||||
<div t-att-data-field_id="props.field.id" t-attf-class="o_export_tree_item cursor-pointer position-relative ps-4 {{ state.subfields.length ? 'o_expanded mb-2' : '' }} {{ props.field.parent ? '' : 'pe-3'}}" role="treeitem" t-on-click.stop="() => this.toggleItem(props.field.id, true)" 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.id)" t-attf-class="ms-n3 float-start o_expand_parent small oi {{ state.isExpanded ? 'oi-chevron-down' : 'oi-chevron-right' }}" role="img" aria-label="Show sub-fields" title="Show sub-fields" />
|
||||
<span t-if="props.isFieldExpandable(props.field.id)" t-attf-class="ms-n3 float-start o_expand_parent small oi {{ state.subfields.length ? 'oi-chevron-down' : 'oi-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.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.id">
|
||||
<t t-foreach="state.subfields" t-as="field" t-key="field.id">
|
||||
<ExportDataItem
|
||||
t-props="props"
|
||||
field="field"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -48,13 +46,12 @@
|
||||
<ExportDataItem
|
||||
exportList="state.exportList"
|
||||
field="field"
|
||||
getSubFields.bind="getSubFields"
|
||||
filterSubfields.bind="filterSubfields"
|
||||
isFieldExpandable.bind="isFieldExpandable"
|
||||
isDebug="isDebug"
|
||||
isExpanded="state.search.length > 0"
|
||||
onToggleExpandField.bind="onToggleExpandField"
|
||||
loadFields.bind="loadFields"
|
||||
onAdd.bind="onAddItemExportList"
|
||||
search="state.search"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -1135,9 +1135,7 @@ QUnit.module("ViewDialogs", (hooks) => {
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Export dialog: search in debug", async function (assert) {
|
||||
patchWithCleanup(odoo, { debug: "1" });
|
||||
|
||||
QUnit.test("Export dialog: expand subfields after search", async function (assert) {
|
||||
await makeView({
|
||||
serverData,
|
||||
type: "list",
|
||||
@@ -1163,8 +1161,57 @@ QUnit.module("ViewDialogs", (hooks) => {
|
||||
const firstField = target.querySelector(
|
||||
".o_left_field_panel .o_export_tree_item:first-child"
|
||||
);
|
||||
// show then hide content for the 'activity_ids' field.
|
||||
// this will load subfields and make them available to search
|
||||
await click(firstField);
|
||||
await click(firstField.querySelector(".o_export_tree_item"));
|
||||
await click(firstField);
|
||||
await editInput(target, ".o_export_search_input", "Attendants");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_export_tree_item[data-field_id='activity_ids/partner_ids']",
|
||||
"subfield that was known has been found and is displayed"
|
||||
);
|
||||
|
||||
await click(
|
||||
target.querySelector(".o_export_tree_item[data-field_id='activity_ids/partner_ids']")
|
||||
);
|
||||
await nextTick();
|
||||
// 'Company' should be shown even if the company_ids string doesn't match the search string
|
||||
// since the toggle was done by the user to show subfields
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_export_tree_item[data-field_id='activity_ids/partner_ids/company_ids']",
|
||||
"subfield has been loaded and is displayed"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Export dialog: search in debug", async function (assert) {
|
||||
patchWithCleanup(odoo, { debug: "1" });
|
||||
|
||||
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);
|
||||
}
|
||||
return Promise.resolve(fetchedFields[args.prefix]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await openExportDataDialog();
|
||||
|
||||
await click(target.querySelector(".o_left_field_panel .o_export_tree_item:first-child"));
|
||||
await click(target.querySelector(".o_export_tree_item:first-child .o_export_tree_item"));
|
||||
await editInput(target, ".o_export_search_input", "company_ids");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
|
||||
Reference in New Issue
Block a user