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) => {