[FIX] web: calendar filter autocomplete issues
**Before this commit** Various issues are present in the calendar filter autocomplete: - typing a character in the input would close the autocomplete dropdown - search for a term yielding to no results would not display the "No records" entry - click on the "Search more..." entry yields to a validation error message: "a mandatory field is not set". [1] **After this commit** The FilterAutoComplete component has been removed as it was useless. It was a kind of higher order component trying to manage the AutoComplete component's input state, leading to unwanted complexity. We decided to remove this component despite being in a stable branch for the following reasons: - it was not exported and only used by the CalendarFilterPanel - it brought unnecessary complexity - it was not working properly - the branch is still a bit young - the diff in the CalendarFilterPanel template is minimal Now that the CalendarFilterPanel directly make use of the AutoComplete component, the first listed issue has been solved. The two other issues were simple fixes addressed in the mean time. [1] this issue comes from opw-3031045, more infos below: Steps to reproduce: - Go to calendar basic view - Click on "+ Add attendees" and then on "Search more ..." Current behavior: Validation error on partner_id in calendar_filter create Expected behavior: You can look for a partner with advanced research options Explanation: The code treated the "Search more ..." option like a partner option instead off calling the onSearchMore method that creates the advanced research dialogue. opw-3031045 closes odoo/odoo#106292 X-original-commit: 73af1d04d9d49eace16e3f88c32ce5d457b44b83 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com> Co-authored-by: Martin Fockedey <mafo@odoo.com> Co-authored-by: Hubert Van de Walle <huvw@odoo.com> Co-authored-by: Michaël Mattiello <mcm@odoo.com>
This commit is contained in:
co-authored by
Martin Fockedey
Hubert Van de Walle
Michaël Mattiello
parent
a963ea8a02
commit
65dd6e2f78
@@ -10,20 +10,54 @@ import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
|
||||
class FilterAutoComplete extends Component {
|
||||
class CalendarFilterTooltip extends Component {}
|
||||
CalendarFilterTooltip.template = "web.CalendarFilterPanel.tooltip";
|
||||
|
||||
let nextId = 1;
|
||||
|
||||
export class CalendarFilterPanel extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.addDialog = useOwnedDialogs();
|
||||
this.state = useState({
|
||||
value: "",
|
||||
collapsed: {},
|
||||
fieldRev: 1,
|
||||
});
|
||||
this.sources = [{ placeholder: _t("Loading..."), options: this.loadSource.bind(this) }];
|
||||
this.addDialog = useOwnedDialogs();
|
||||
this.orm = useService("orm");
|
||||
this.popover = usePopover();
|
||||
this.removePopover = null;
|
||||
}
|
||||
async loadSource(request) {
|
||||
const records = await this.orm.call(this.props.resModel, "name_search", [], {
|
||||
|
||||
getAutoCompleteProps(section) {
|
||||
return {
|
||||
autoSelect: true,
|
||||
resetOnSelect: true,
|
||||
placeholder: `+ ${_t("Add")} ${section.label}`,
|
||||
sources: [
|
||||
{
|
||||
placeholder: _t("Loading..."),
|
||||
options: (request) => this.loadSource(section, request),
|
||||
},
|
||||
],
|
||||
onSelect: (option, params = {}) => {
|
||||
if (option.action) {
|
||||
option.action(params);
|
||||
return;
|
||||
}
|
||||
this.props.model.createFilter(section.fieldName, option.value);
|
||||
},
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
|
||||
async loadSource(section, request) {
|
||||
const resModel = this.props.model.fields[section.fieldName].relation;
|
||||
const domain = [
|
||||
["id", "not in", section.filters.filter((f) => f.type !== "all").map((f) => f.value)],
|
||||
];
|
||||
const records = await this.orm.call(resModel, "name_search", [], {
|
||||
name: request,
|
||||
operator: "ilike",
|
||||
args: this.props.domain,
|
||||
args: domain,
|
||||
limit: 8,
|
||||
context: {},
|
||||
});
|
||||
@@ -36,91 +70,48 @@ class FilterAutoComplete extends Component {
|
||||
if (records.length > 7) {
|
||||
options.push({
|
||||
label: _t("Search More..."),
|
||||
action: this.onSearchMore.bind(this, request),
|
||||
action: () => this.onSearchMore(section, resModel, domain, request),
|
||||
});
|
||||
}
|
||||
|
||||
if (records.length === 0) {
|
||||
options.push({
|
||||
label: _t("No records"),
|
||||
classList: "o_m2o_no_result",
|
||||
unselectable: true,
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
async onSearchMore(request) {
|
||||
let dynamicFilters = [];
|
||||
|
||||
async onSearchMore(section, resModel, domain, request) {
|
||||
const dynamicFilters = [];
|
||||
if (request.length) {
|
||||
const nameGets = await this.orm.call(this.props.resModel, "name_search", [], {
|
||||
const nameGets = await this.orm.call(resModel, "name_search", [], {
|
||||
name: request,
|
||||
args: this.props.domain,
|
||||
args: domain,
|
||||
operator: "ilike",
|
||||
context: {},
|
||||
});
|
||||
|
||||
dynamicFilters = [
|
||||
{
|
||||
description: sprintf(_t("Quick search: %s"), request),
|
||||
domain: [["id", "in", nameGets.map((nameGet) => nameGet[0])]],
|
||||
},
|
||||
];
|
||||
dynamicFilters.push({
|
||||
description: sprintf(_t("Quick search: %s"), request),
|
||||
domain: [["id", "in", nameGets.map((nameGet) => nameGet[0])]],
|
||||
});
|
||||
}
|
||||
const title = sprintf(this.env._t("Search: %s"), this.props.searchMoreTitle);
|
||||
const title = sprintf(_t("Search: %s"), section.label);
|
||||
this.addDialog(SelectCreateDialog, {
|
||||
title: title || _t("Select records"),
|
||||
title,
|
||||
noCreate: true,
|
||||
multiSelect: false,
|
||||
resModel: this.props.resModel,
|
||||
resModel,
|
||||
context: {},
|
||||
domain: this.props.domain,
|
||||
onSelected: (...args) => {
|
||||
console.log(...args);
|
||||
},
|
||||
domain,
|
||||
onSelected: ([resId]) => this.props.model.createFilter(section.fieldName, resId),
|
||||
dynamicFilters,
|
||||
});
|
||||
}
|
||||
|
||||
onInput({ inputValue }) {
|
||||
this.state.value = inputValue;
|
||||
}
|
||||
onSelect(option, params = {}) {
|
||||
this.props.onSelect(option.value);
|
||||
this.state.value = "";
|
||||
}
|
||||
}
|
||||
FilterAutoComplete.template = "web.FilterAutoComplete";
|
||||
FilterAutoComplete.components = {
|
||||
AutoComplete,
|
||||
};
|
||||
|
||||
class CalendarFilterTooltip extends Component {}
|
||||
CalendarFilterTooltip.template = "web.CalendarFilterPanel.tooltip";
|
||||
|
||||
let nextId = 1;
|
||||
|
||||
export class CalendarFilterPanel extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
collapsed: {},
|
||||
fieldRev: 1,
|
||||
});
|
||||
|
||||
this.popover = usePopover();
|
||||
this.removePopover = null;
|
||||
}
|
||||
|
||||
getAutoCompleteProps(section) {
|
||||
return {
|
||||
resModel: this.props.model.fields[section.fieldName].relation,
|
||||
domain: [
|
||||
[
|
||||
"id",
|
||||
"not in",
|
||||
section.filters.filter((f) => f.type !== "all").map((f) => f.value),
|
||||
],
|
||||
],
|
||||
searchMoreTitle: section.label,
|
||||
placeholder: `+ ${_t("Add")} ${section.label}`,
|
||||
onSelect: (value) => {
|
||||
this.props.model.createFilter(section.fieldName, value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get nextFilterId() {
|
||||
nextId += 1;
|
||||
return nextId;
|
||||
@@ -220,7 +211,7 @@ export class CalendarFilterPanel extends Component {
|
||||
}
|
||||
|
||||
CalendarFilterPanel.components = {
|
||||
FilterAutoComplete,
|
||||
AutoComplete,
|
||||
Transition,
|
||||
};
|
||||
CalendarFilterPanel.template = "web.CalendarFilterPanel";
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
</Transition>
|
||||
<t t-if="section.canAddFilter">
|
||||
<FilterAutoComplete t-props="getAutoCompleteProps(section)" />
|
||||
<AutoComplete t-props="getAutoCompleteProps(section)" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
@@ -143,8 +143,4 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.FilterAutoComplete" owl="1">
|
||||
<AutoComplete value="state.value" sources="sources" autoSelect="true" placeholder="props.placeholder" onSelect.bind="onSelect" onInput.bind="onInput" />
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -477,6 +477,257 @@ QUnit.module("Views", ({ beforeEach }) => {
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("filter panel autocomplete: updates when typing", async (assert) => {
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: "event",
|
||||
serverData,
|
||||
arch: `
|
||||
<calendar date_start="start" date_stop="stop">
|
||||
<field name="partner_ids" write_model="filter_partner" write_field="partner_id" />
|
||||
</calendar>
|
||||
`,
|
||||
});
|
||||
const section = findFilterPanelSection(target, "partner_ids");
|
||||
assert.containsNone(section, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsNone(section, ".o-autocomplete--dropdown-item");
|
||||
await click(section, ".o-autocomplete--input");
|
||||
assert.containsOnce(section, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsN(section, ".o-autocomplete--dropdown-item", 2);
|
||||
assert.deepEqual(
|
||||
[...section.querySelectorAll(".o-autocomplete--dropdown-item")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
["partner 3", "partner 4"]
|
||||
);
|
||||
|
||||
const input = section.querySelector(".o-autocomplete--input");
|
||||
input.value = "partner 3";
|
||||
await triggerEvent(section, ".o-autocomplete--input", "input");
|
||||
assert.containsOnce(section, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsOnce(section, ".o-autocomplete--dropdown-item");
|
||||
assert.strictEqual(
|
||||
section.querySelector(".o-autocomplete--dropdown-item").textContent.trim(),
|
||||
"partner 3"
|
||||
);
|
||||
|
||||
input.value = "a string that would yield to no result as it is too very much convoluted";
|
||||
await triggerEvent(section, ".o-autocomplete--input", "input");
|
||||
assert.containsOnce(section, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsOnce(section, ".o-autocomplete--dropdown-item");
|
||||
assert.strictEqual(
|
||||
section.querySelector(".o-autocomplete--dropdown-item").textContent.trim(),
|
||||
"No records"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("add a filter with the search more dialog", async (assert) => {
|
||||
serverData.views["partner,false,search"] = `<search></search>`;
|
||||
serverData.views["partner,false,list"] = `
|
||||
<tree>
|
||||
<field name="display_name" />
|
||||
</tree>
|
||||
`;
|
||||
serverData.models.partner.records.push(
|
||||
{ id: 5, display_name: "foo partner 5" },
|
||||
{ id: 6, display_name: "foo partner 6" },
|
||||
{ id: 7, display_name: "foo partner 7" },
|
||||
{ id: 8, display_name: "foo partner 8" },
|
||||
{ id: 9, display_name: "foo partner 9" },
|
||||
{ id: 10, display_name: "foo partner 10" },
|
||||
{ id: 11, display_name: "foo partner 11" },
|
||||
{ id: 12, display_name: "foo partner 12" },
|
||||
{ id: 13, display_name: "foo partner 13" }
|
||||
);
|
||||
await makeView({
|
||||
type: "calendar",
|
||||
resModel: "event",
|
||||
serverData,
|
||||
arch: `
|
||||
<calendar date_start="start" date_stop="stop">
|
||||
<field name="partner_ids" write_model="filter_partner" write_field="partner_id" />
|
||||
</calendar>
|
||||
`,
|
||||
mockRPC(route) {
|
||||
if (route.endsWith("/has_group")) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
});
|
||||
const section = findFilterPanelSection(target, "partner_ids");
|
||||
assert.containsN(section, ".o_calendar_filter_item", 3);
|
||||
assert.deepEqual(
|
||||
[...section.querySelectorAll(".o_calendar_filter_item")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
["partner 2", "partner 1", "Everything"]
|
||||
);
|
||||
|
||||
// Open the autocomplete dropdown
|
||||
assert.containsNone(section, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsNone(section, ".o-autocomplete--dropdown-item");
|
||||
await click(section, ".o-autocomplete--input");
|
||||
assert.containsOnce(section, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsN(section, ".o-autocomplete--dropdown-item", 9);
|
||||
assert.deepEqual(
|
||||
[...section.querySelectorAll(".o-autocomplete--dropdown-item")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
[
|
||||
"partner 3",
|
||||
"partner 4",
|
||||
"foo partner 5",
|
||||
"foo partner 6",
|
||||
"foo partner 7",
|
||||
"foo partner 8",
|
||||
"foo partner 9",
|
||||
"foo partner 10",
|
||||
"Search More...",
|
||||
]
|
||||
);
|
||||
|
||||
// Change the search term
|
||||
const input = section.querySelector(".o-autocomplete--input");
|
||||
input.value = "foo";
|
||||
await triggerEvent(section, ".o-autocomplete--input", "input");
|
||||
assert.containsOnce(section, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsN(section, ".o-autocomplete--dropdown-item", 9);
|
||||
assert.deepEqual(
|
||||
[...section.querySelectorAll(".o-autocomplete--dropdown-item")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
[
|
||||
"foo partner 5",
|
||||
"foo partner 6",
|
||||
"foo partner 7",
|
||||
"foo partner 8",
|
||||
"foo partner 9",
|
||||
"foo partner 10",
|
||||
"foo partner 11",
|
||||
"foo partner 12",
|
||||
"Search More...",
|
||||
]
|
||||
);
|
||||
|
||||
// Open the search more dialog
|
||||
assert.containsNone(target, ".modal");
|
||||
await click(section, ".o-autocomplete--dropdown-item:last-child");
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsN(target, ".modal .o_data_row", 9);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".modal .o_data_row")].map((el) => el.textContent.trim()),
|
||||
[
|
||||
"foo partner 5",
|
||||
"foo partner 6",
|
||||
"foo partner 7",
|
||||
"foo partner 8",
|
||||
"foo partner 9",
|
||||
"foo partner 10",
|
||||
"foo partner 11",
|
||||
"foo partner 12",
|
||||
"foo partner 13",
|
||||
]
|
||||
);
|
||||
assert.containsOnce(target, ".modal .o_searchview_facet");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".modal .o_searchview_facet").textContent.trim(),
|
||||
"Quick search: foo"
|
||||
);
|
||||
|
||||
// Choose a record
|
||||
await click(target, ".modal .o_data_row:first-child > td:first-child");
|
||||
assert.containsNone(target, ".modal");
|
||||
assert.containsN(section, ".o_calendar_filter_item", 4);
|
||||
assert.deepEqual(
|
||||
[...section.querySelectorAll(".o_calendar_filter_item")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
["partner 2", "partner 1", "foo partner 5", "Everything"]
|
||||
);
|
||||
assert.strictEqual(input.value, "");
|
||||
|
||||
// Open the autocomplete dropdown
|
||||
assert.containsNone(section, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsNone(section, ".o-autocomplete--dropdown-item");
|
||||
await click(section, ".o-autocomplete--input");
|
||||
assert.containsOnce(section, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsN(section, ".o-autocomplete--dropdown-item", 9);
|
||||
assert.deepEqual(
|
||||
[...section.querySelectorAll(".o-autocomplete--dropdown-item")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
[
|
||||
"partner 3",
|
||||
"partner 4",
|
||||
"foo partner 6",
|
||||
"foo partner 7",
|
||||
"foo partner 8",
|
||||
"foo partner 9",
|
||||
"foo partner 10",
|
||||
"foo partner 11",
|
||||
"Search More...",
|
||||
]
|
||||
);
|
||||
|
||||
// Change the search term
|
||||
input.value = "foo";
|
||||
await triggerEvent(section, ".o-autocomplete--input", "input");
|
||||
assert.containsOnce(section, ".o-autocomplete--dropdown-menu");
|
||||
assert.containsN(section, ".o-autocomplete--dropdown-item", 9);
|
||||
assert.deepEqual(
|
||||
[...section.querySelectorAll(".o-autocomplete--dropdown-item")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
[
|
||||
"foo partner 6",
|
||||
"foo partner 7",
|
||||
"foo partner 8",
|
||||
"foo partner 9",
|
||||
"foo partner 10",
|
||||
"foo partner 11",
|
||||
"foo partner 12",
|
||||
"foo partner 13",
|
||||
"Search More...",
|
||||
]
|
||||
);
|
||||
|
||||
// Open the search more dialog
|
||||
assert.containsNone(target, ".modal");
|
||||
await click(section, ".o-autocomplete--dropdown-item:last-child");
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsN(target, ".modal .o_data_row", 8);
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".modal .o_data_row")].map((el) => el.textContent.trim()),
|
||||
[
|
||||
"foo partner 6",
|
||||
"foo partner 7",
|
||||
"foo partner 8",
|
||||
"foo partner 9",
|
||||
"foo partner 10",
|
||||
"foo partner 11",
|
||||
"foo partner 12",
|
||||
"foo partner 13",
|
||||
]
|
||||
);
|
||||
assert.containsOnce(target, ".modal .o_searchview_facet");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".modal .o_searchview_facet").textContent.trim(),
|
||||
"Quick search: foo"
|
||||
);
|
||||
|
||||
// Close the search more dialog without choosing a record
|
||||
await click(target, ".modal .o_form_button_cancel");
|
||||
assert.containsNone(target, ".modal");
|
||||
assert.containsN(section, ".o_calendar_filter_item", 4);
|
||||
assert.deepEqual(
|
||||
[...section.querySelectorAll(".o_calendar_filter_item")].map((el) =>
|
||||
el.textContent.trim()
|
||||
),
|
||||
["partner 2", "partner 1", "foo partner 5", "Everything"]
|
||||
);
|
||||
assert.strictEqual(input.value, "");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
`delete attribute on calendar doesn't show delete button in popover`,
|
||||
async (assert) => {
|
||||
|
||||
Reference in New Issue
Block a user