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