[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
<!--Widget A-->
<we-many2many data-model="model.a" data-m2o-field="name"
 data-fakem2m="true".../>

<!--Widget B-->
<we-many2many data-model="model.a" data-m2o-field="model_b_ids"
 data-filter-in="true" .../>
```

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) <tde@openerp.com>
This commit is contained in:
Florian Charlier
2023-02-10 10:13:00 +01:00
parent b189d1ba59
commit be9f264011
2 changed files with 61 additions and 19 deletions
@@ -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
*/
+1 -1
View File
@@ -637,7 +637,7 @@
<!-- /!\ drop-in here is partly duplicated for s_popup (see dedicated options) -->
<!-- TODO should be improved -->
<t t-set="so_content_addition_selector" t-translation="off">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</t>
<t t-set="so_content_addition_selector" t-translation="off">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</t>
<div id="so_content_addition"
t-att-data-selector="so_content_addition_selector"
t-attf-data-drop-near="p, h1, h2, h3, ul, ol, .row > div:not(.o_grid_item_image) > img, #{so_content_addition_selector}"