diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js index 3cadf6cf6fafc102b417b5c000c39575a75a30ed..925b405051e358a28a0273a154e51e9f36ed82ee 100644 --- a/addons/web/static/src/js/fields/field_registry.js +++ b/addons/web/static/src/js/fields/field_registry.js @@ -84,6 +84,7 @@ registry .add('many2one_barcode', relational_fields.Many2oneBarcode) .add('list.many2one', relational_fields.ListFieldMany2One) .add('kanban.many2one', relational_fields.KanbanFieldMany2One) + .add('many2one_avatar', relational_fields.Many2OneAvatar) .add('many2many', relational_fields.FieldMany2Many) .add('many2many_binary', relational_fields.FieldMany2ManyBinaryMultiFiles) .add('many2many_tags', relational_fields.FieldMany2ManyTags) diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 72476d74c921551f3d4a65ded6f4370ba95b7931..b5b6d7252e5bce3b9b0378428d7d15a283381399 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -959,6 +959,44 @@ var KanbanFieldMany2One = AbstractField.extend({ }, }); +/** + * Widget Many2OneAvatar is only supported on many2one fields pointing to a + * model which inherits from 'image.mixin'. In readonly, it displays the + * record's image next to the display_name. In edit, it behaves exactly like a + * regular many2one widget. + */ +const Many2OneAvatar = FieldMany2One.extend({ + _template: 'web.Many2OneAvatar', + + init() { + this._super.apply(this, arguments); + if (this.mode === 'readonly') { + this.template = null; + this.tagName = 'div'; + this.className = 'o_field_many2one_avatar'; + // disable the redirection to the related record on click, in readonly + this.noOpen = true; + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _renderReadonly() { + this.$el.empty(); + if (this.value) { + this.$el.html(qweb.render(this._template, { + url: `/web/image/${this.field.relation}/${this.value.res_id}/image_128`, + value: this.m2o_value, + })); + } + }, +}); + //------------------------------------------------------------------------------ // X2Many widgets //------------------------------------------------------------------------------ @@ -3308,8 +3346,9 @@ return { Many2oneBarcode: Many2oneBarcode, KanbanFieldMany2One: KanbanFieldMany2One, ListFieldMany2One: ListFieldMany2One, + Many2OneAvatar: Many2OneAvatar, - FieldX2Many : FieldX2Many, + FieldX2Many: FieldX2Many, FieldOne2Many: FieldOne2Many, FieldMany2Many: FieldMany2Many, diff --git a/addons/web/static/src/scss/fields.scss b/addons/web/static/src/scss/fields.scss index f0d15adbd52de952ff7661c9034c098fe0f0e817..8152389c32ce803daa56f0c68f8e66ef8f5c8e1a 100644 --- a/addons/web/static/src/scss/fields.scss +++ b/addons/web/static/src/scss/fields.scss @@ -83,6 +83,17 @@ } } + // Many2OneAvatar + &.o_field_many2one_avatar { + > img.o_m2o_avatar { + border-radius: 50%; + width: 19px; + height: 19px; + object-fit: cover; + margin-right: 4px; + } + } + // Many2many tags &.o_field_many2manytags { flex-flow: row wrap; diff --git a/addons/web/static/src/scss/kanban_view.scss b/addons/web/static/src/scss/kanban_view.scss index 29cfade38574e192a7c1dc634643d4beee38b129..d24a928536ef4017c8e9e2f4aec2aff0ce8ff37b 100644 --- a/addons/web/static/src/scss/kanban_view.scss +++ b/addons/web/static/src/scss/kanban_view.scss @@ -280,6 +280,12 @@ } } + .o_field_many2one_avatar { + img.o_m2o_avatar { + margin-right: 0; + } + } + // Commonly used to place an image beside the text // (e.g. Fleet, Employees, ...) .o_kanban_image { diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index b06b84026200afd94ddb6e1e22a22222a2e5f810..3abde74ac5144b283a52518bcdf4ea3a3731c300 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1346,6 +1346,12 @@ <button type="button" t-if="!widget.noOpen" class="fa fa-external-link btn btn-secondary o_external_button" tabindex="-1" draggable="false" aria-label="External link" title="External link"/> </div> </t> + +<t t-name="web.Many2OneAvatar"> + <img t-att-src="url" t-att-alt="value" class="o_m2o_avatar"/> + <span t-esc="value"/> +</t> + <t t-name="FieldReference" t-extend="FieldMany2One"> <t t-jquery=".o_input_dropdown" t-operation="before"> <select t-att-class="'o_input o_field_widget' + (widget.nodeOptions.hide_model and ' d-none' or '')"> diff --git a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js index 04c934290a1eb50e6c15e0027a049a89bae3c529..c2c4942381a73e9054aa1f7c372d784ef0c1d056 100644 --- a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js @@ -3204,6 +3204,108 @@ QUnit.module('fields', {}, function () { "should be 1 column after the value change"); form.destroy(); }); + + QUnit.module('Many2OneAvatar'); + + QUnit.test('many2one_avatar widget in form view', async function (assert) { + assert.expect(10); + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form><field name="user_id" widget="many2one_avatar"/></form>', + res_id: 1, + }); + + assert.hasClass(form.$('.o_form_view'), 'o_form_readonly'); + assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), 'Aline'); + assert.containsOnce(form, 'img.o_m2o_avatar[data-src="/web/image/user/17/image_128"]'); + + await testUtils.form.clickEdit(form); + + assert.hasClass(form.$('.o_form_view'), 'o_form_editable'); + assert.containsOnce(form, '.o_input_dropdown'); + assert.strictEqual(form.$('.o_input_dropdown input').val(), 'Aline'); + assert.containsOnce(form, '.o_external_button'); + + await testUtils.fields.many2one.clickOpenDropdown("user_id"); + await testUtils.fields.many2one.clickItem("user_id", "Christine"); + await testUtils.form.clickSave(form); + + assert.hasClass(form.$('.o_form_view'), 'o_form_readonly'); + assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), 'Christine'); + assert.containsOnce(form, 'img.o_m2o_avatar[data-src="/web/image/user/19/image_128"]'); + + form.destroy(); + }); + + QUnit.test('many2one_avatar widget in form view, with onchange', async function (assert) { + assert.expect(7); + + this.data.partner.onchanges = { + int_field: function (obj) { + if (obj.int_field === 1) { + obj.user_id = [19, 'Christine']; + } else if (obj.int_field === 2) { + obj.user_id = false; + } else { + obj.user_id = [17, 'Aline']; // default value + } + }, + }; + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form> + <field name="int_field"/> + <field name="user_id" widget="many2one_avatar" readonly="1"/> + </form>`, + }); + + assert.hasClass(form.$('.o_form_view'), 'o_form_editable'); + assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), 'Aline'); + assert.containsOnce(form, 'img.o_m2o_avatar[data-src="/web/image/user/17/image_128"]'); + + await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 1); + + assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), 'Christine'); + assert.containsOnce(form, 'img.o_m2o_avatar[data-src="/web/image/user/19/image_128"]'); + + await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 2); + + assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), ''); + assert.containsNone(form, 'img.o_m2o_avatar'); + + form.destroy(); + }); + + QUnit.test('many2one_avatar widget in list view', async function (assert) { + assert.expect(5); + + this.data.partner.records = [ + { id: 1, user_id: 17, }, + { id: 2, user_id: 19, }, + { id: 3, user_id: 17, }, + { id: 3, user_id: false, }, + ]; + const list = await createView({ + View: ListView, + model: 'partner', + data: this.data, + arch: '<tree><field name="user_id" widget="many2one_avatar"/></tree>', + }); + + assert.strictEqual(list.$('.o_data_cell span').text(), 'AlineChristineAline'); + assert.containsOnce(list.$('.o_data_cell:nth(0)'), 'img.o_m2o_avatar[data-src="/web/image/user/17/image_128"]'); + assert.containsOnce(list.$('.o_data_cell:nth(1)'), 'img.o_m2o_avatar[data-src="/web/image/user/19/image_128"]'); + assert.containsOnce(list.$('.o_data_cell:nth(2)'), 'img.o_m2o_avatar[data-src="/web/image/user/17/image_128"]'); + assert.containsNone(list.$('.o_data_cell:nth(3)'), 'img.o_m2o_avatar'); + + list.destroy(); + }); }); }); }); diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index 92c6a292ff96a421520e35bbca9d47a995b4a293..433409acd5e42df6f3254366bf66d87aeb8048ff 100644 --- a/doc/reference/javascript_reference.rst +++ b/doc/reference/javascript_reference.rst @@ -2139,6 +2139,15 @@ Relational fields - Supported field types: *many2one* +- many2one_avatar (Many2OneAvatar) + This widget is only supported on many2one fields pointing to a model which + inherits from 'image.mixin'. In readonly, it displays the image of the + related record next to its display_name. Note that the display_name isn't a + clickable link in this case. In edit, it behaves exactly like the regular + many2one. + + - Supported field types: *many2one* + - kanban.many2one (KanbanFieldMany2One) Default widget for many2one fields (in kanban view). We need to disable all editing in kanban views.