From cd367a3967cd56fb4d1147885f1318d77f63c9f3 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 5 Mar 2021 12:55:33 +0000 Subject: [PATCH] [FIX] web: FieldMany2Many: link/unlink options Commit [1] altered the way the FieldMany2Many behaves with respect to 'create' and 'delete' options. Indeed, for many2many fields, adding or removing records doesn't mean "creating" or "deleting" records, as it is only about adding/removing records to/from a relation. This is completely fine and correct. Unfortunately, a feature has been lost in the process: it is no longer possible to state that a many2many field should be editable but should not allow to add (or remove) record to the relation. This commit fixes the issue by adding two new options: 'link' and 'unlink' for that purpose. [1] https://github.com/odoo/odoo/commit/c98579d25af01c14df4baf57fb4652f3e7469096 opw~2466213 closes odoo/odoo#67495 X-original-commit: df44e65bbbba55a7ee2224ad5b4f13248a39423a Signed-off-by: Lucas Perais (lpe) Signed-off-by: Aaron Bohy (aab) --- .../static/src/js/fields/relational_fields.js | 36 ++- .../field_many2many_tests.js | 206 ++++++++++++++++++ doc/reference/javascript_reference.rst | 2 + 3 files changed, 239 insertions(+), 5 deletions(-) diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index fac5d69d2c1..6f83108725d 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -1212,8 +1212,14 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, { // re-evaluate available actions const oldCanCreate = this.canCreate; const oldCanDelete = this.canDelete; + const oldCanLink = this.canLink; + const oldCanUnlink = this.canUnlink; this._computeAvailableActions(record); - const actionsChanged = this.canCreate !== oldCanCreate || this.canDelete !== oldCanDelete; + const actionsChanged = + this.canCreate !== oldCanCreate || + this.canDelete !== oldCanDelete || + this.canLink !== oldCanLink || + this.canUnlink !== oldCanUnlink; // If 'fieldChanged' is false, it means that the reset was triggered by // the 'resetOnAnyFieldChange' mechanism. If it is the case, if neither @@ -1290,6 +1296,12 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, { this.canDelete = 'delete' in this.nodeOptions ? new Domain(this.nodeOptions.delete, evalContext).compute(evalContext) : true; + this.canLink = 'link' in this.nodeOptions ? + new Domain(this.nodeOptions.link, evalContext).compute(evalContext) : + true; + this.canUnlink = 'unlink' in this.nodeOptions ? + new Domain(this.nodeOptions.unlink, evalContext).compute(evalContext) : + true; }, /** * @private @@ -1378,8 +1390,8 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, { */ _hasCreateLine: function () { return !this.hasReadonlyModifier && ( - (this.activeActions.create && this.canCreate) || - (this.isMany2Many) + (!this.isMany2Many && this.activeActions.create && this.canCreate) || + (this.isMany2Many && this.canLink) ); }, /** @@ -1388,8 +1400,8 @@ var FieldX2Many = AbstractField.extend(WidgetAdapterMixin, { */ _hasTrashIcon: function () { return !this.isReadonly && ( - (this.activeActions.delete && this.canDelete) || - (this.isMany2Many) + (!this.isMany2Many && this.activeActions.delete && this.canDelete) || + (this.isMany2Many && this.canUnlink) ); }, /** @@ -2167,6 +2179,20 @@ var FieldMany2Many = FieldX2Many.extend({ }).open(); }, + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _getButtonsRenderingContext() { + const renderingContext = this._super(...arguments); + renderingContext.noCreate = !this.canLink; + return renderingContext; + }, + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- diff --git a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js index 4bacfee8862..b6329a22aad 100644 --- a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js @@ -828,6 +828,212 @@ QUnit.module('fields', {}, function () { form.destroy(); }); + QUnit.test('many2many field with link/unlink options (list)', async function (assert) { + assert.expect(5); + + this.data.partner.records[0].timmy = [12, 14]; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + + `, + archs: { + 'partner_type,false,list': '', + 'partner_type,false,search': '', + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + // color is red -> link and unlink actions are available + assert.containsOnce(form, '.o_field_x2many_list_row_add', + "should have the 'Add an item' link"); + assert.containsN(form, '.o_list_record_remove', 2, + "should have two remove icons"); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + + assert.containsN(document.body, '.modal .modal-footer button', 3, + 'there should be 3 buttons available in the modal (Create action is available)'); + + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + // set color to black -> link and unlink actions are no longer available + await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"'); + + assert.containsNone(form, '.o_field_x2many_list_row_add', + '"Add a line" should no longer be available after color field changed'); + assert.containsNone(form, '.o_list_record_remove', + "should no longer have remove icon after color field changed"); + + form.destroy(); + }); + + QUnit.test('many2many field with link/unlink options (list, create="0")', async function (assert) { + assert.expect(5); + + this.data.partner.records[0].timmy = [12, 14]; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + + `, + archs: { + 'partner_type,false,list': '', + 'partner_type,false,search': '', + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + // color is red -> link and unlink actions are available + assert.containsOnce(form, '.o_field_x2many_list_row_add', + "should have the 'Add an item' link"); + assert.containsN(form, '.o_list_record_remove', 2, + "should have two remove icons"); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + + assert.containsN(document.body, '.modal .modal-footer button', 2, + 'there should be 2 buttons available in the modal (Create action is not available)'); + + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + // set color to black -> link and unlink actions are no longer available + await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"'); + + assert.containsNone(form, '.o_field_x2many_list_row_add', + '"Add a line" should no longer be available after color field changed'); + assert.containsNone(form, '.o_list_record_remove', + "should no longer have remove icon after color field changed"); + + form.destroy(); + }); + + QUnit.test('many2many field with link option (kanban)', async function (assert) { + assert.expect(3); + + this.data.partner.records[0].timmy = [12, 14]; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + +
+
+
+
+
+ `, + archs: { + 'partner_type,false,list': '', + 'partner_type,false,search': '', + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + // color is red -> link and unlink actions are available + assert.containsOnce(form, '.o-kanban-button-new', "should have the 'Add' button"); + + await testUtils.dom.click(form.$('.o-kanban-button-new')); + + assert.containsN(document.body, '.modal .modal-footer button', 3, + 'there should be 3 buttons available in the modal (Create action is available'); + + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + // set color to black -> link and unlink actions are no longer available + await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"'); + + assert.containsNone(form, '.o-kanban-button-new', + '"Add" should no longer be available after color field changed'); + + form.destroy(); + }); + + QUnit.test('many2many field with link option (kanban, create="0")', async function (assert) { + assert.expect(3); + + this.data.partner.records[0].timmy = [12, 14]; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + +
+
+
+
+
+ `, + archs: { + 'partner_type,false,list': '', + 'partner_type,false,search': '', + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + // color is red -> link and unlink actions are available + assert.containsOnce(form, '.o-kanban-button-new', "should have the 'Add' button"); + + await testUtils.dom.click(form.$('.o-kanban-button-new')); + + assert.containsN(document.body, '.modal .modal-footer button', 2, + 'there should be 2 buttons available in the modal (Create action is not available'); + + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + // set color to black -> link and unlink actions are no longer available + await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"'); + + assert.containsNone(form, '.o-kanban-button-new', + '"Add" should no longer be available after color field changed'); + + form.destroy(); + }); + QUnit.test('many2many list: list of id as default value', async function (assert) { assert.expect(1); diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index 7f33d6c8d97..eaa8c927dcf 100644 --- a/doc/reference/javascript_reference.rst +++ b/doc/reference/javascript_reference.rst @@ -2417,6 +2417,8 @@ many2many (FieldMany2Many) - create_text: allow the customization of the text displayed when adding a new record + - link: domain determining whether or not records can be added to the relation (default: True). + - unlink: domain determining whether or not records can be removed from the relation (default: True). many2many_binary (FieldMany2ManyBinaryMultiFiles) This widget helps the user to upload or delete one or more files at the same