From be9f264011363565333f450d7601e2bafea3c409 Mon Sep 17 00:00:00 2001 From: Florian Charlier Date: Fri, 29 Apr 2022 14:33:09 +0000 Subject: [PATCH] [IMP] web_editor: add filter-in to m2o/m2m snippet widget Adds basic support to filter possible m2x values based on another record. This is necessary for the website_appointment snippet introduced within this bundle (see related ENT PR). An important need implemented here is to not store a list of valid IDs in the DOM but fetch them based on data stored by a different widget. Use case covered: Model A has a Many2Many relationship with Model B via a `model_b_ids` field. A first widget (Wa) on a snippet's options allows to select a record of Model A. A second widget (Wb) allows to select records of Model B. ```xml ``` Before this commit, the second widget would only be able to show all records of Model B linked to any Model A record and matching a static domain provided as attribute, which is still supported. This commit allows, after having selected `record_a` in the first widget, to only populate the second widget with the records of Model B that are in `record_a.model_b_ids`. Implementing this is done by * Adding `data-filter-in` to Wb's xml attributes (as above) * Calling `Wb.setFilterInDomainIds()` when another record is selected in Wa. Task-2574175 odoo/odoo#90748 See odoo/enterprise#23750 Signed-off-by: Thibault Delavallee (tde) --- .../static/src/js/editor/snippets.options.js | 78 ++++++++++++++----- addons/website/views/snippets/snippets.xml | 2 +- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index fdc5dded37b..1905f2e2231 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -1982,21 +1982,9 @@ const ListUserValueWidget = UserValueWidget.extend({ const selectedIds = currentValues.map(({ id }) => id) .filter(id => typeof id === 'number'); // Note: it's important to simplify the domain at its maximum as the - // rpc using it are cached. Similar domains should be written the - // same way for the cache to work. - const selectedIdsDomain = selectedIds.length ? ['id', 'not in', selectedIds] : null; - const selectedIdsDomainIndex = this.createWidget.options.domain.findIndex(domain => domain[0] === 'id' && domain[1] === 'not in'); - if (selectedIdsDomainIndex > -1) { - if (selectedIdsDomain) { - this.createWidget.options.domain[selectedIdsDomainIndex] = selectedIdsDomain; - } else { - this.createWidget.options.domain.splice(selectedIdsDomainIndex, 1); - } - } else { - if (selectedIdsDomain) { - this.createWidget.options.domain = [...this.createWidget.options.domain, selectedIdsDomain]; - } - } + // rpc using it are cached. Similar domain components should be + // written the same way for the cache to work. + this.createWidget.options.domainComponents.selected = selectedIds.length ? ['id', 'not in', selectedIds] : null; this.createWidget.setValue(''); this.createWidget.inputEl.value = ''; $(this.createWidget.inputEl).trigger('input'); @@ -2493,7 +2481,11 @@ const Many2oneUserValueWidget = SelectUserValueWidget.extend({ }), // Data-attributes that will be read into `this.options` on init and not // transfered to inner buttons. - configAttributes: ['model', 'fields', 'limit', 'domain', 'callWith', 'createMethod'], + // `domain` is the static part of the domain used in searches, not + // depending on already selected ids and other filters. + configAttributes: [ + 'model', 'fields', 'limit', 'domain', 'callWith', 'createMethod', 'filterInModel', 'filterInField' + ], /** * @override @@ -2521,6 +2513,7 @@ const Many2oneUserValueWidget = SelectUserValueWidget.extend({ options.fields.push('display_name'); } options.domain = JSON.parse(options.domain); + options.domainComponents = {}; return this._super(...arguments); }, /** @@ -2615,6 +2608,34 @@ const Many2oneUserValueWidget = SelectUserValueWidget.extend({ } return this._super(...arguments); }, + /** + * Updates the domain with defined inclusive filter to make sure that only + * records that are linked to specific records are retrieved. + * Filtering-in is configured with + * * a `filterInModel` attribute, the linked model + * * a `filterInField` attribute, field of the linked model holding + * allowed values for this widget + * + * @param {integer[]} linkedRecordsIds + * @returns {Promise} + */ + async setFilterInDomainIds(linkedRecordsIds) { + const allowedIds = new Set(); + if (linkedRecordsIds) { + const parentRecordsData = await this._rpc({ + model: this.options.filterInModel, + method: 'search_read', + fields: [this.options.filterInField], + domain: [['id', 'in', linkedRecordsIds]], + }); + parentRecordsData.forEach(record => { + record[this.options.filterInField].forEach(item => allowedIds.add(item)); + }); + } + if (allowedIds.size) { + this.options.domainComponents.filterInModel = ['id', 'in', [...allowedIds]]; + } + }, //-------------------------------------------------------------------------- // Private @@ -2643,7 +2664,9 @@ const Many2oneUserValueWidget = SelectUserValueWidget.extend({ method: 'name_search', kwargs: { name: needle, - args: this.options.domain, + args: this.options.domain.concat( + Object.values(this.options.domainComponents).filter(item => item !== null) + ), operator: "ilike", limit: this.options.limit + 1, }, @@ -2819,7 +2842,7 @@ const Many2oneUserValueWidget = SelectUserValueWidget.extend({ }); const Many2manyUserValueWidget = UserValueWidget.extend({ - configAttributes: ['model', 'recordId', 'm2oField', 'createMethod', 'fakem2m'], + configAttributes: ['model', 'recordId', 'm2oField', 'createMethod', 'fakem2m', 'filterIn'], /** * @override @@ -2832,6 +2855,12 @@ const Many2manyUserValueWidget = UserValueWidget.extend({ delete dataAttributes[attr]; } }); + this.filterIn = options.filterIn !== undefined; + if (this.filterIn) { + // Transfer filter-in values to child m2o. + dataAttributes.filterInModel = options.model; + dataAttributes.filterInField = options.m2oField; + } return this._super(...arguments); }, /** @@ -2901,6 +2930,19 @@ const Many2manyUserValueWidget = UserValueWidget.extend({ this.listWidget.el.querySelector('we-select').style.position = 'static'; this.el.style.position = 'relative'; }, + /** + * Only allow to fetch/select records which are linked (via `m2oField`) to the + * specified records. + * + * @param {integer[]} linkedRecordsIds + * @returns {Promise} + * @see Many2oneUserValueWidget.setFilterInDomainIds + */ + async setFilterInDomainIds(linkedRecordsIds) { + if (this.filterIn) { + return this.listWidget.createWidget.setFilterInDomainIds(linkedRecordsIds); + } + }, /** * @override */ diff --git a/addons/website/views/snippets/snippets.xml b/addons/website/views/snippets/snippets.xml index c884156d5dc..522da45278d 100644 --- a/addons/website/views/snippets/snippets.xml +++ b/addons/website/views/snippets/snippets.xml @@ -637,7 +637,7 @@ - blockquote, .s_card:not(.s_timeline_card), .s_alert, .o_facebook_page, .s_share, .s_social_media, .s_rating, .s_hr, .s_google_map, .s_map, .s_countdown, .s_chart, .s_text_highlight, .s_progress_bar, .s_badge, .s_embed_code, .s_donation, .s_add_to_cart + blockquote, .s_card:not(.s_timeline_card), .s_alert, .o_facebook_page, .s_share, .s_social_media, .s_rating, .s_hr, .s_google_map, .s_map, .s_countdown, .s_chart, .s_text_highlight, .s_progress_bar, .s_badge, .s_embed_code, .s_donation, .s_add_to_cart, .s_online_appointment