From 65dd6e2f7861ed9df843daa15a6a5d811284e9cd Mon Sep 17 00:00:00 2001 From: Bruno Boi Date: Tue, 22 Nov 2022 11:18:35 +0000 Subject: [PATCH] [FIX] web: calendar filter autocomplete issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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) Co-authored-by: Martin Fockedey Co-authored-by: Hubert Van de Walle Co-authored-by: Michaƫl Mattiello --- .../filter_panel/calendar_filter_panel.js | 141 +++++----- .../filter_panel/calendar_filter_panel.xml | 6 +- .../views/calendar/calendar_view_tests.js | 251 ++++++++++++++++++ 3 files changed, 318 insertions(+), 80 deletions(-) diff --git a/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.js b/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.js index 5b0917d912e..ab7cc7fd958 100644 --- a/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.js +++ b/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.js @@ -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"; diff --git a/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.xml b/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.xml index 1748f32f54b..b73f3c3c3dc 100644 --- a/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.xml +++ b/addons/web/static/src/views/calendar/filter_panel/calendar_filter_panel.xml @@ -61,7 +61,7 @@ - + @@ -143,8 +143,4 @@ - - - - diff --git a/addons/web/static/tests/views/calendar/calendar_view_tests.js b/addons/web/static/tests/views/calendar/calendar_view_tests.js index 6cba24ebad3..e8ef821bc96 100644 --- a/addons/web/static/tests/views/calendar/calendar_view_tests.js +++ b/addons/web/static/tests/views/calendar/calendar_view_tests.js @@ -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: ` + + + + `, + }); + 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"] = ``; + serverData.views["partner,false,list"] = ` + + + + `; + 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: ` + + + + `, + 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) => {