diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index deff2fbebcf..500ebf1f609 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -22,6 +22,7 @@ var data = require('web.data'); var Dialog = require('web.Dialog'); var dialogs = require('web.view_dialogs'); var dom = require('web.dom'); +const Domain = require('web.Domain'); var KanbanRecord = require('web.KanbanRecord'); var KanbanRenderer = require('web.KanbanRenderer'); var ListRenderer = require('web.ListRenderer'); @@ -117,21 +118,24 @@ var FieldMany2One = AbstractField.extend({ * @override * @param {boolean} [options.noOpen=false] if true, there is no external * button to open the related record in a dialog + * @param {boolean} [options.noCreate=false] if true, the many2one does not + * allow to create records */ init: function (parent, name, record, options) { + options = options || {}; this._super.apply(this, arguments); this.limit = 7; this.orderer = new concurrency.DropMisordered(); - // should normally also be set, except in standalone M20 - this.can_create = ('can_create' in this.attrs ? JSON.parse(this.attrs.can_create) : true) && - !this.nodeOptions.no_create; + // should normally be set, except in standalone M20 + const canCreate = 'can_create' in this.attrs ? JSON.parse(this.attrs.can_create) : true; + this.can_create = canCreate && !this.nodeOptions.no_create && !options.noCreate; this.can_write = 'can_write' in this.attrs ? JSON.parse(this.attrs.can_write) : true; this.nodeOptions = _.defaults(this.nodeOptions, { quick_create: true, }); - this.noOpen = 'noOpen' in (options || {}) ? options.noOpen : this.nodeOptions.no_open; + this.noOpen = 'noOpen' in options ? options.noOpen : this.nodeOptions.no_open; this.m2o_value = this._formatValue(this.value); // 'recordParams' is a dict of params used when calling functions // 'getDomain' and 'getContext' on this.record @@ -591,10 +595,9 @@ var FieldMany2One = AbstractField.extend({ if (values.length > self.limit) { values = self._manageSearchMore(values, search_val, domain, context); } - var create_enabled = self.can_create && !self.nodeOptions.no_create; // quick create var raw_result = _.map(result, function (x) { return x[1]; }); - if (create_enabled && !self.nodeOptions.no_quick_create && + if (self.can_create && !self.nodeOptions.no_quick_create && search_val.length > 0 && !_.contains(raw_result, search_val)) { values.push({ label: _.str.sprintf(_t('Create "%s"'), @@ -604,7 +607,7 @@ var FieldMany2One = AbstractField.extend({ }); } // create and edit ... - if (create_enabled && !self.nodeOptions.no_create_edit) { + if (self.can_create && !self.nodeOptions.no_create_edit) { var createAndEditAction = function () { // Clear the value in case the user clicks on discard self.$('input').val(''); @@ -1012,6 +1015,7 @@ var FieldX2Many = AbstractField.extend({ true; this.editable = arch.attrs.editable; } + this._computeAvailableActions(record); if (this.attrs.columnInvisibleFields) { this._processColumnInvisibleFields(); } @@ -1079,15 +1083,21 @@ var FieldX2Many = AbstractField.extend({ * @returns {Promise} */ reset: function (record, ev, fieldChanged) { + // re-evaluate available actions + const oldCanCreate = this.canCreate; + const oldCanDelete = this.canDelete; + this._computeAvailableActions(record); + const actionsChanged = this.canCreate !== oldCanCreate || this.canDelete !== oldCanDelete; + // If 'fieldChanged' is false, it means that the reset was triggered by - // the 'resetOnAnyFieldChange' mechanism. If it is the case the - // modifiers are evaluated and if there is no change in the modifiers - // values, the reset is skipped. - if (!fieldChanged) { - var newEval = this._evalColumnInvisibleFields(); - if (_.isEqual(this.currentColInvisibleFields, newEval)) { - return Promise.resolve(); - } + // the 'resetOnAnyFieldChange' mechanism. If it is the case, if neither + // the modifiers (so the visible columns) nor the available actions + // changed, the reset is skipped. + if (!fieldChanged && !actionsChanged) { + var newEval = this._evalColumnInvisibleFields(); + if (_.isEqual(this.currentColInvisibleFields, newEval)) { + return Promise.resolve(); + } } else if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') { var command = ev.data.changes[this.name]; // Here, we only consider 'UPDATE' commands with data, which occur @@ -1104,7 +1114,9 @@ var FieldX2Many = AbstractField.extend({ return this.renderer.confirmUpdate(state, command.id, fieldNames, ev.initialEvent); } } - return this._super.apply(this, arguments); + return this._super.apply(this, arguments).then(() => { + this._updateControlPanel(); + }); }, /** @@ -1141,6 +1153,19 @@ var FieldX2Many = AbstractField.extend({ // Private //-------------------------------------------------------------------------- + /** + * @private + * @param {Object} record + */ + _computeAvailableActions: function (record) { + const evalContext = record.evalContext; + this.canCreate = 'create' in this.nodeOptions ? + new Domain(this.nodeOptions.create, evalContext).compute(evalContext) : + true; + this.canDelete = 'delete' in this.nodeOptions ? + new Domain(this.nodeOptions.delete, evalContext).compute(evalContext) : + true; + }, /** * Evaluates the 'column_invisible' modifier for the parent record. * @@ -1155,6 +1180,18 @@ var FieldX2Many = AbstractField.extend({ }).column_invisible; }); }, + /** + * Returns qweb context to render buttons. + * + * @private + * @returns {Object} + */ + _getButtonsRenderingContext() { + return { + btnClass: 'btn-secondary', + create_text: this.nodeOptions.create_text, + }; + }, /** * Computes the default renderer to use depending on the view type. * We create this as a method so we can override it if we want to use @@ -1171,6 +1208,20 @@ var FieldX2Many = AbstractField.extend({ return KanbanRenderer; } }, + /** + * @private + * @returns {boolean} true iff the list should contain a 'create' line. + */ + _hasCreateLine: function () { + return !this.isReadonly && this.activeActions.create && (this.isMany2Many || this.canCreate); + }, + /** + * @private + * @returns {boolean} true iff the list should add a trash icon on each row. + */ + _hasTrashIcon: function () { + return !this.isReadonly && this.activeActions.delete && (this.isMany2Many || this.canDelete); + }, /** * Instanciates or updates the adequate renderer. * @@ -1183,9 +1234,12 @@ var FieldX2Many = AbstractField.extend({ if (!this.view) { return this._super(); } + if (this.renderer) { this.currentColInvisibleFields = this._evalColumnInvisibleFields(); return this.renderer.updateState(this.value, { + addCreateLine: this._hasCreateLine(), + addTrashIcon: this._hasTrashIcon(), columnInvisibleFields: this.currentColInvisibleFields, keepWidths: true, }).then(function () { @@ -1203,8 +1257,8 @@ var FieldX2Many = AbstractField.extend({ this.currentColInvisibleFields = this._evalColumnInvisibleFields(); _.extend(rendererParams, { editable: this.mode === 'edit' && arch.attrs.editable, - addCreateLine: !this.isReadonly && this.activeActions.create, - addTrashIcon: !this.isReadonly && this.activeActions.delete, + addCreateLine: this._hasCreateLine(), + addTrashIcon: this._hasTrashIcon(), isMany2Many: this.isMany2Many, columnInvisibleFields: this.currentColInvisibleFields, }); @@ -1301,10 +1355,8 @@ var FieldX2Many = AbstractField.extend({ */ _renderButtons: function () { if (!this.isReadonly && this.view.arch.tag === 'kanban') { - this.$buttons = $(qweb.render('KanbanView.buttons', { - btnClass: 'btn-secondary', - create_text: this.nodeOptions.create_text, - })); + const renderingContext = this._getButtonsRenderingContext(); + this.$buttons = $(qweb.render('KanbanView.buttons', renderingContext)); this.$buttons.on('click', 'button.o-kanban-button-new', this._onAddRecord.bind(this)); } }, @@ -1347,6 +1399,23 @@ var FieldX2Many = AbstractField.extend({ } }); }, + /** + * Re-renders buttons and updates the control panel. This method is called + * when the widget is reset, as the available buttons might have changed. + * + * @private + */ + _updateControlPanel: function () { + if (this._controlPanel) { + this._renderButtons(); + const params = { + cp_content: { + $buttons: this.$buttons, + } + }; + this._controlPanel.updateContents(params, { clear: false }); + } + }, /** * Parses the 'columnInvisibleFields' attribute to search for the domains * containing the key 'parent'. If there are such domains, the string @@ -1729,6 +1798,15 @@ var FieldOne2Many = FieldX2Many.extend({ // Private //-------------------------------------------------------------------------- + /** + * @override + * @private + */ + _getButtonsRenderingContext() { + const renderingContext = this._super(...arguments); + renderingContext.noCreate = !this.canCreate; + return renderingContext; + }, /** * @override * @private @@ -1769,7 +1847,7 @@ var FieldOne2Many = FieldX2Many.extend({ fields_view: this.attrs.views && this.attrs.views.form, parentID: this.value.id, viewInfo: this.view, - deletable: this.activeActions.delete && params.deletable, + deletable: this.activeActions.delete && params.deletable && this.canDelete, })); }, @@ -1874,7 +1952,7 @@ var FieldOne2Many = FieldX2Many.extend({ on_remove: function () { self._setValue({operation: 'DELETE', ids: [id]}); }, - deletable: this.activeActions.delete && this.view.arch.tag !== 'tree', + deletable: this.activeActions.delete && this.view.arch.tag !== 'tree' && this.canDelete, readonly: this.mode === 'readonly', }); }, @@ -1900,7 +1978,7 @@ var FieldMany2Many = FieldX2Many.extend({ domain: domain.concat(["!", ["id", "in", this.value.res_ids]]), context: this.record.getContext(this.recordParams), title: _t("Add: ") + this.string, - no_create: this.nodeOptions.no_create || !this.activeActions.create, + no_create: this.nodeOptions.no_create || !this.activeActions.create || !this.canCreate, fields_view: this.attrs.views.form, kanban_view_ref: this.attrs.kanban_view_ref, on_selected: function (records) { @@ -1960,7 +2038,7 @@ var FieldMany2Many = FieldX2Many.extend({ self._setValue({operation: 'FORGET', ids: [ev.data.id]}); }, readonly: this.mode === 'readonly', - deletable: this.activeActions.delete && this.view.arch.tag !== 'tree', + deletable: this.activeActions.delete && this.view.arch.tag !== 'tree' && this.canDelete, string: this.string, }); }, @@ -2195,6 +2273,10 @@ var FieldMany2ManyTags = AbstractField.extend({ this.colorField = this.nodeOptions.color_field; this.hasDropdown = false; + + this._computeAvailableActions(this.record); + // have listen to react to other fields changes to re-evaluate 'create' option + this.resetOnAnyFieldChange = this.resetOnAnyFieldChange || 'create' in this.nodeOptions; }, //-------------------------------------------------------------------------- @@ -2228,7 +2310,8 @@ var FieldMany2ManyTags = AbstractField.extend({ */ reset: function (record, event) { var self = this; - return this._super.apply(this, arguments).then(function(){ + this._computeAvailableActions(record); + return this._super.apply(this, arguments).then(function () { if (event && event.target === self) { self.activate(); } @@ -2251,6 +2334,16 @@ var FieldMany2ManyTags = AbstractField.extend({ }); } }, + /** + * @private + * @param {Object} record + */ + _computeAvailableActions: function (record) { + const evalContext = record.evalContext; + this.canCreate = 'create' in this.nodeOptions ? + new Domain(this.nodeOptions.create, evalContext).compute(evalContext) : + true; + }, /** * Get the QWeb rendering context used by the tag template; this computation * is placed in a separate function for other tags to override it. @@ -2290,6 +2383,7 @@ var FieldMany2ManyTags = AbstractField.extend({ this.many2one = new FieldMany2One(this, this.name, this.record, { mode: 'edit', noOpen: true, + noCreate: !this.canCreate, viewType: this.viewType, attrs: this.attrs, }); diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js index 62f715d3967..53c63d3fbb9 100644 --- a/addons/web/static/src/js/views/list/list_editable_renderer.js +++ b/addons/web/static/src/js/views/list/list_editable_renderer.js @@ -42,7 +42,6 @@ ListRenderer.include({ * @param {boolean} params.isMultiEditable */ init: function (parent, state, params) { - var self = this; this._super.apply(this, arguments); this.editable = params.editable; @@ -62,33 +61,27 @@ ListRenderer.include({ // The following code will browse the arch to find // all the that are inside - - if (this.addCreateLine) { - this.creates = []; - - _.each(this.arch.children, function (child) { - if (child.tag !== 'control') { + this.creates = []; + this.arch.children.forEach(child => { + if (child.tag !== 'control') { + return; + } + child.children.forEach(child => { + if (child.tag !== 'create' || child.attrs.invisible) { return; } - - _.each(child.children, function (child) { - if (child.tag !== 'create' || child.attrs.invisible) { - return; - } - - self.creates.push({ - context: child.attrs.context, - string: child.attrs.string, - }); + this.creates.push({ + context: child.attrs.context, + string: child.attrs.string, }); }); + }); - // Add the default button if we didn't find any custom button. - if (this.creates.length === 0) { - this.creates.push({ - string: _t("Add a line"), - }); - } + // Add the default button if we didn't find any custom button. + if (this.creates.length === 0) { + this.creates.push({ + string: _t("Add a line"), + }); } // if addTrashIcon is true, there will be a small trash icon at the end @@ -532,6 +525,15 @@ ListRenderer.include({ // them to be recomputed at next (sub-)rendering this.allModifiersData = []; } + if ('addTrashIcon' in params) { + if (this.addTrashIcon !== params.addTrashIcon) { + this.columnWidths = false; // columns changed, so forget stored widths + } + this.addTrashIcon = params.addTrashIcon; + } + if ('addCreateLine' in params) { + this.addCreateLine = params.addCreateLine; + } return this._super.apply(this, arguments); }, @@ -1200,15 +1202,17 @@ ListRenderer.include({ $tr.append($td); $rows.push($tr); - _.each(this.creates, function (create, index) { - var $a = $('') - .attr('data-context', create.context) - .text(create.string); - if (index > 0) { - $a.addClass('ml16'); - } - $td.append($a); - }); + if (this.addCreateLine) { + _.each(this.creates, function (create, index) { + var $a = $('') + .attr('data-context', create.context) + .text(create.string); + if (index > 0) { + $a.addClass('ml16'); + } + $td.append($a); + }); + } } return $rows; }, diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml index 9d998677dc4..1e7f36f8ef3 100644 --- a/addons/web/static/src/xml/kanban.xml +++ b/addons/web/static/src/xml/kanban.xml @@ -2,7 +2,7 @@
-
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 487cd8647b8..7af0216e20e 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 @@ -350,6 +350,74 @@ QUnit.module('fields', {}, function () { form.destroy(); }); + QUnit.test('many2many kanban: conditional create/delete actions', async function (assert) { + assert.expect(6); + + this.data.partner.records[0].timmy = [12, 14]; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + +
+ +
+
+
+
+
+ `, + archs: { + 'partner_type,false,form': '
', + 'partner_type,false,list': '', + 'partner_type,false,search': '', + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + // color is red + assert.containsOnce(form, '.o-kanban-button-new', '"Add" button should be available'); + + await testUtils.dom.click(form.$('.o_kanban_record:contains(silver)')); + assert.containsOnce(document.body, '.modal .modal-footer .o_btn_remove', + 'remove button should be visible in modal'); + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + 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'); + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + // set color to black + await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"'); + assert.containsOnce(form, '.o-kanban-button-new', + '"Add" button should still be available even after color field changed'); + + await testUtils.dom.click(form.$('.o-kanban-button-new')); + // only select and cancel button should be available, create + // button should be removed based on color field condition + assert.containsN(document.body, '.modal .modal-footer button', 2, + '"Create" button should not be available in the modal after color field changed'); + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + await testUtils.dom.click(form.$('.o_kanban_record:contains(silver)')); + assert.containsNone(document.body, '.modal .modal-footer .o_btn_remove', + 'remove button should be visible in modal'); + + form.destroy(); + }); + QUnit.test('many2many list (non editable): edition', async function (assert) { assert.expect(29); @@ -642,6 +710,64 @@ QUnit.module('fields', {}, function () { form.destroy(); }); + QUnit.test('many2many list: conditional create/delete actions', async function (assert) { + assert.expect(6); + + 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 -> create and delete 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'); + + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + // set color to black -> create and delete actions are no longer available + await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"'); + + // add a line and remove icon should still be there as they don't create/delete records, + // but rather add/remove links + assert.containsOnce(form, '.o_field_x2many_list_row_add', + '"Add a line" button should still be available even after color field changed'); + assert.containsN(form, '.o_list_record_remove', 2, + "should still have remove icon even after color field changed"); + + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + assert.containsN(document.body, '.modal .modal-footer button', 2, + '"Create" button should not be available in the modal after color field changed'); + + form.destroy(); + }); + QUnit.test('many2many list: list of id as default value', async function (assert) { assert.expect(1); @@ -1232,6 +1358,95 @@ QUnit.module('fields', {}, function () { form.destroy(); }); + + QUnit.test('many2many_tags widget: conditional create/delete actions', async function (assert) { + assert.expect(10); + + this.data.turtle.records[0].partner_ids = [2]; + for (var i = 1; i <= 10; i++) { + this.data.partner.records.push({ id: 100 + i, display_name: "Partner" + i }); + } + + const form = await createView({ + View: FormView, + model: 'turtle', + data: this.data, + arch: ` +
+ + + + `, + archs: { + 'partner,false,list': '', + 'partner,false,search': '', + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + // turtle_bar is true -> create and delete actions are available + assert.containsOnce(form, '.o_field_many2manytags.o_field_widget .badge .o_delete', + 'X icon on badges should not be available'); + + await testUtils.fields.many2one.clickOpenDropdown('partner_ids'); + + const $dropdown1 = form.$('.o_field_many2one input').autocomplete('widget'); + assert.containsOnce($dropdown1, 'li.o_m2o_dropdown_option:contains(Create and Edit...)', + 'autocomplete should contain Create and Edit...'); + + await testUtils.fields.many2one.clickItem('partner_ids', 'Search More'); + + assert.containsN(document.body, '.modal .modal-footer button', 3, + 'there should be 3 buttons (Select, Create and Cancel) available in the modal footer'); + + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + // type something that doesn't exist + await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'), + 'Something that does not exist', 'keydown'); + // await testUtils.nextTick(); + assert.containsN($dropdown1, 'li.o_m2o_dropdown_option', 2, + 'autocomplete should contain Create and Create and Edit... options'); + + // set turtle_bar false -> create and delete actions are no longer available + await testUtils.dom.click(form.$('.o_field_widget[name="turtle_bar"] input').first()); + + // remove icon should still be there as it doesn't delete records but rather remove links + assert.containsOnce(form, '.o_field_many2manytags.o_field_widget .badge .o_delete', + 'X icon on badge should still be there even after turtle_bar is not checked'); + + await testUtils.fields.many2one.clickOpenDropdown('partner_ids'); + const $dropdown2 = form.$('.o_field_many2one input').autocomplete('widget'); + + // only Search More option should be available + assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option', + 'autocomplete should contain only one option'); + assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option:contains(Search More)', + 'autocomplete option should be Search More'); + + await testUtils.fields.many2one.clickItem('partner_ids', 'Search More'); + + assert.containsN(document.body, '.modal .modal-footer button', 2, + 'there should be 2 buttons (Select and Cancel) available in the modal footer'); + + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + // type something that doesn't exist + await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'), + 'Something that does not exist', 'keyup'); + // await testUtils.nextTick(); + + // only Search More option should be available + assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option', + 'autocomplete should contain only one option'); + assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option:contains(Search More)', + 'autocomplete option should be Search More'); + + form.destroy(); + }); }); }); }); diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js index 8aacf099de4..ad68ede00d2 100644 --- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js @@ -2142,6 +2142,46 @@ QUnit.module('fields', {}, function () { form.destroy(); }); + QUnit.test('one2many list: conditional create/delete actions', async function (assert) { + assert.expect(4); + + this.data.partner.records[0].p = [2, 4]; + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + + `, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + // bar is true -> create and delete action are available + assert.containsOnce(form, '.o_field_x2many_list_row_add', + '"Add an item" link should be available'); + assert.hasClass(form.$('td.o_list_record_remove button').first(), 'fa fa-trash-o', + "should have trash bin icons"); + + // set bar to false -> create and delete action are no longer available + await testUtils.dom.click(form.$('.o_field_widget[name="bar"] input').first()); + + assert.containsNone(form, '.o_field_x2many_list_row_add', + '"Add an item" link should not be available if bar field is False'); + assert.containsNone(form, 'td.o_list_record_remove button', + "should not have trash bin icons if bar field is False"); + + form.destroy(); + }); + QUnit.test('one2many list: unlink two records', async function (assert) { assert.expect(8); this.data.partner.records[0].p = [1, 2, 4]; @@ -2441,6 +2481,65 @@ QUnit.module('fields', {}, function () { form.destroy(); }); + QUnit.test('one2many kanban: conditional create/delete actions', async function (assert) { + assert.expect(4); + + this.data.partner.records[0].p = [2, 4]; + + const form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` +
+ + + + + + +
+ +
+
+
+
+ + + + + + `, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + // bar is initially true -> create and delete actions are available + assert.containsOnce(form, '.o-kanban-button-new', '"Add" button should be available'); + + await testUtils.dom.click(form.$('.oe_kanban_global_click').first()); + + assert.containsOnce(document.body, '.modal .modal-footer .o_btn_remove', + 'There should be a Remove Button inside modal'); + + await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel')); + + // set bar false -> create and delete actions are no longer available + await testUtils.dom.click(form.$('.o_field_widget[name="bar"] input').first()); + + assert.containsNone(form, '.o-kanban-button-new', + '"Add" button should not be available as bar is False'); + + await testUtils.dom.click(form.$('.oe_kanban_global_click').first()); + + assert.containsNone(document.body, '.modal .modal-footer .o_btn_remove', + 'There should not be a Remove Button as bar field is False'); + + form.destroy(); + }); + QUnit.test('editable one2many list, pager is updated', async function (assert) { assert.expect(1); diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index 912c4db25da..f0db5cd40b2 100644 --- a/doc/reference/javascript_reference.rst +++ b/doc/reference/javascript_reference.rst @@ -2137,6 +2137,12 @@ Relational fields Options: + - create: domain determining whether or not new tags can be created (default: True). + + .. code-block:: xml + + + - color_field: the name of a numeric field, which should be present in the view. A color will be chosen depending on its value. @@ -2176,6 +2182,14 @@ Relational fields Options: + - create: domain determining whether or not related records can be created (default: True). + + - delete: domain determining whether or not related records can be deleted (default: True). + + .. code-block:: xml + + + - create_text: a string that is used to customize the 'Add' label/text. .. code-block:: xml