diff options
Diffstat (limited to 'addons/web/static')
| -rw-r--r-- | addons/web/static/src/js/core/dialog.js | 5 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/relational_fields.js | 11 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/form/form_controller.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/view_dialogs.js | 47 | ||||
| -rw-r--r-- | addons/web/static/src/scss/modal.scss | 26 | ||||
| -rw-r--r-- | addons/web/static/src/xml/dialog.xml | 2 | ||||
| -rw-r--r-- | addons/web/static/tests/fields/relational_fields_tests.js | 157 |
7 files changed, 220 insertions, 32 deletions
diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js index 9cca9c1127a..811fd9fd0de 100644 --- a/addons/web/static/src/js/core/dialog.js +++ b/addons/web/static/src/js/core/dialog.js @@ -31,6 +31,8 @@ var Dialog = Widget.extend({ * @param {string} [options.title=Odoo] * @param {string} [options.subtitle] * @param {string} [options.size=large] - 'large', 'medium' or 'small' + * @param {boolean} [options.fullscreen=false] - whether or not the dialog + * should be open in fullscreen mode (the main usecase is mobile) * @param {string} [options.dialogClass] - class to add to the modal-body * @param {jQuery} [options.$content] * Element which will be the $el, replace the .modal-body and get the @@ -56,6 +58,7 @@ var Dialog = Widget.extend({ options = _.defaults(options || {}, { title: _t('Odoo'), subtitle: '', size: 'large', + fullscreen: true, dialogClass: '', $content: false, buttons: [{text: _t("Ok"), close: true}], @@ -65,6 +68,7 @@ var Dialog = Widget.extend({ this.$content = options.$content; this.title = options.title; this.subtitle = options.subtitle; + this.fullscreen = options.fullscreen; this.dialogClass = options.dialogClass; this.size = options.size; this.buttons = options.buttons; @@ -81,6 +85,7 @@ var Dialog = Widget.extend({ return this._super.apply(this, arguments).then(function () { // Render modal once xml dependencies are loaded self.$modal = $(QWeb.render('Dialog', { + fullscreen: self.fullscreen, title: self.title, subtitle: self.subtitle, technical: self.technical, diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 65ec9f33ddf..5aecf7b592f 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -1315,6 +1315,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, })); }, @@ -1375,9 +1376,9 @@ var FieldOne2Many = FieldX2Many.extend({ */ _onOpenRecord: function (ev) { // we don't want interference with the components upstream. + var self = this; ev.stopPropagation(); - var self = this; var id = ev.data.id; var onSaved = function (record) { if (_.some(self.value.data, {id: record.id})) { @@ -1398,6 +1399,10 @@ var FieldOne2Many = FieldX2Many.extend({ this._openFormDialog({ id: id, on_saved: onSaved, + on_remove: function () { + self._setValue({operation: 'DELETE', ids: [id]}); + }, + deletable: this.activeActions.delete, readonly: this.mode === 'readonly', }); }, @@ -1464,7 +1469,11 @@ var FieldMany2Many = FieldX2Many.extend({ self._setValue({operation: 'TRIGGER_ONCHANGE'}, {forceChange: true}); self.trigger_up('reload', {db_id: ev.data.id}); }, + on_remove: function () { + self._setValue({operation: 'FORGET', ids: [ev.data.id]}); + }, readonly: this.mode === 'readonly', + deletable: this.activeActions.delete, string: this.string, }); }, diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index d0ca5c3d09f..5e669e0aa57 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -597,8 +597,10 @@ var FormController = BasicController.extend({ fields_view: data.fields_view, model: this.model, on_saved: data.on_saved, + on_remove: data.on_remove, parentID: data.parentID, readonly: data.readonly, + deletable: data.deletable, recordID: record && record.id, res_id: record && record.res_id, res_model: data.field.relation, @@ -620,7 +622,9 @@ var FormController = BasicController.extend({ context: event.data.context, fields_view: event.data.fields_view, on_saved: event.data.on_saved, + on_remove: event.data.on_remove, readonly: event.data.readonly, + deletable: event.data.deletable, res_id: record.res_id, res_model: record.model, title: _t("Open: ") + event.data.string, diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index e99f81ccb08..b8eadf4fe69 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -1,6 +1,7 @@ odoo.define('web.view_dialogs', function (require) { "use strict"; +var config = require('web.config'); var core = require('web.core'); var data = require('web.data'); var Dialog = require('web.Dialog'); @@ -35,6 +36,7 @@ var ViewDialog = Dialog.extend({ */ init: function (parent, options) { options = options || {}; + options.fullscreen = config.device.isMobile; options.dialogClass = options.dialogClass || '' + ' o_act_window'; this._super(parent, $.extend(true, {}, options)); @@ -81,9 +83,15 @@ var FormViewDialog = ViewDialog.extend({ * @param {Object} [options.fields_view] optional form fields_view * @param {boolean} [options.readonly=false] only applicable when not in * creation mode + * @param {boolean} [options.deletable=false] whether or not the record can + * be deleted + * @param {boolean} [options.disable_multiple_selection=false] set to true + * to remove the possibility to create several records in a row * @param {function} [options.on_saved] callback executed after saving a * record. It will be called with the record data, and a boolean which * indicates if something was changed + * @param {function} [options.on_remove] callback executed when the user + * clicks on the 'Remove' button * @param {BasicModel} [options.model] if given, it will be used instead of * a new form view model * @param {string} [options.recordID] if given, the model has to be given as @@ -93,20 +101,24 @@ var FormViewDialog = ViewDialog.extend({ */ init: function (parent, options) { var self = this; + options = options || {}; this.res_id = options.res_id || null; this.on_saved = options.on_saved || (function () {}); + this.on_remove = options.on_remove || (function () {}); this.context = options.context; this.model = options.model; this.parentID = options.parentID; this.recordID = options.recordID; this.shouldSaveLocally = options.shouldSaveLocally; + this.readonly = options.readonly; + this.deletable = options.deletable; + this.disable_multiple_selection = options.disable_multiple_selection; var multi_select = !_.isNumber(options.res_id) && !options.disable_multiple_selection; var readonly = _.isNumber(options.res_id) && options.readonly; - if (!options || !options.buttons) { - options = options || {}; + if (!options.buttons) { options.buttons = [{ text: (readonly ? _t("Close") : _t("Discard")), classes: "btn-secondary o_form_button_cancel", @@ -138,6 +150,15 @@ var FormViewDialog = ViewDialog.extend({ }, }); } + + var multi = options.disable_multiple_selection; + if (!multi && this.deletable) { + options.buttons.push({ + text: _t("Remove"), + classes: 'btn-secondary o_btn_remove', + click: this._remove.bind(this), + }); + } } } this._super(parent, options); @@ -209,6 +230,7 @@ var FormViewDialog = ViewDialog.extend({ //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- + /** * @override */ @@ -216,13 +238,26 @@ var FormViewDialog = ViewDialog.extend({ this.trigger_up('form_dialog_discarded'); return true; }, + + /** + * @private + */ + _remove: function () { + this.on_remove(); + this.close(); + }, + + /** + * @private + * @returns {Deferred} + */ _save: function () { var self = this; return this.form_view.saveRecord(this.form_view.handle, { - stayInEdit: true, - reload: false, - savePoint: this.shouldSaveLocally, - viewType: 'form', + stayInEdit: true, + reload: false, + savePoint: this.shouldSaveLocally, + viewType: 'form', }).then(function (changedFields) { // record might have been changed by the save (e.g. if this was a new record, it has an // id now), so don't re-use the copy obtained before the save diff --git a/addons/web/static/src/scss/modal.scss b/addons/web/static/src/scss/modal.scss index a71f8aa0ff2..c525050e1ec 100644 --- a/addons/web/static/src/scss/modal.scss +++ b/addons/web/static/src/scss/modal.scss @@ -83,6 +83,32 @@ } } } + + @include media-breakpoint-down(xs) { + &.o_modal_full { + .modal-dialog { + margin: 0px; + height: 100%; + + .modal-content { + height: 100%; + border: none; + + .modal-header { + background: $o-brand-odoo; + .modal-title, .o_subtitle, button.close { + color: white; + } + } + + .modal-body { + height: 100%; + overflow-y: auto; + } + } + } + } + } } // Temporary fix for modals which are not instantiated thanks to the Dialog diff --git a/addons/web/static/src/xml/dialog.xml b/addons/web/static/src/xml/dialog.xml index 955deb0e7c8..d5f6025d968 100644 --- a/addons/web/static/src/xml/dialog.xml +++ b/addons/web/static/src/xml/dialog.xml @@ -3,7 +3,7 @@ <!-- These templates are accessible in backend and frontend --> -<div role="dialog" t-name="Dialog" t-attf-class="modal#{technical ? ' o_technical_modal' : ''}" tabindex="-1" data-backdrop="static" t-att-id="_.uniqueId('modal_')" aria-hidden="true"> +<div role="dialog" t-name="Dialog" t-attf-class="modal#{technical ? ' o_technical_modal' : ''}#{fullscreen ? ' o_modal_full': ''}" tabindex="-1" data-backdrop="static" t-att-id="_.uniqueId('modal_')" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <header class="modal-header"> diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index bff0781653f..f0e71ca5316 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -4,6 +4,7 @@ odoo.define('web.relational_fields_tests', function (require) { var AbstractField = require('web.AbstractField'); var BasicModel = require('web.BasicModel'); var concurrency = require('web.concurrency'); +var config = require('web.config'); var FormView = require('web.FormView'); var ListRenderer = require('web.ListRenderer'); var ListView = require('web.ListView'); @@ -4656,9 +4657,9 @@ QUnit.module('relational_fields', { form.destroy(); }); - QUnit.test('one2many list: unlink one record', function (assert) { - assert.expect(6); - this.data.partner.records[0].p = [2, 4]; + QUnit.test('one2many list: unlink two records', function (assert) { + assert.expect(9); + this.data.partner.records[0].p = [1, 2, 4]; var form = createView({ View: FormView, model: 'partner', @@ -4674,37 +4675,51 @@ QUnit.module('relational_fields', { mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { var commands = args.args[1].p; - assert.strictEqual(commands.length, 2, - 'should have generated two commands'); + assert.strictEqual(commands.length, 3, + 'should have generated three commands'); assert.ok(commands[0][0] === 4 && commands[0][1] === 4, 'should have generated the command 4 (LINK_TO) with id 4'); - assert.ok(commands[1][0] === 3 && commands[1][1] === 2, + assert.ok(commands[1][0] === 3 && commands[1][1] === 1, + 'should have generated the command 3 (UNLINK) with id 1'); + assert.ok(commands[2][0] === 3 && commands[2][1] === 2, 'should have generated the command 3 (UNLINK) with id 2'); } return this._super.apply(this, arguments); }, + archs: { + 'partner,false,form': + '<form string="Partner"><field name="display_name"/></form>', + }, }); form.$buttons.find('.o_form_button_edit').click(); - assert.strictEqual(form.$('td.o_list_record_remove button').length, 2, - "should have 2 remove buttons"); + assert.strictEqual(form.$('td.o_list_record_remove button').length, 3, + "should have 3 remove buttons"); assert.ok(form.$('td.o_list_record_remove button').first().hasClass('fa fa-times'), "should have X icons to remove (unlink) records"); form.$('td.o_list_record_remove button').first().click(); + assert.strictEqual(form.$('td.o_list_record_remove button').length, 2, + "should have 2 remove buttons (a record is supposed to have been unlinked)"); + + form.$('tr.o_data_row').first().click(); + assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1, + 'there should be a modal having Remove Button'); + $('.modal .modal-footer .o_btn_remove').click(); + assert.strictEqual(form.$('td.o_list_record_remove button').length, 1, - "should have 1 remove button (a record is supposed to have been unlinked)"); + "should have 1 delete button (another record is supposed to have been unlinked)"); // save and check that the correct command has been generated form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); - QUnit.test('one2many list: deleting one record', function (assert) { - assert.expect(6); - this.data.partner.records[0].p = [2, 4]; + QUnit.test('one2many list: deleting two records', function (assert) { + assert.expect(9); + this.data.partner.records[0].p = [1, 2, 4]; var form = createView({ View: FormView, model: 'partner', @@ -4720,28 +4735,42 @@ QUnit.module('relational_fields', { mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { var commands = args.args[1].p; - assert.strictEqual(commands.length, 2, - 'should have generated two commands'); + assert.strictEqual(commands.length, 3, + 'should have generated three commands'); assert.ok(commands[0][0] === 4 && commands[0][1] === 4, 'should have generated the command 4 (LINK_TO) with id 4'); - assert.ok(commands[1][0] === 2 && commands[1][1] === 2, + assert.ok(commands[1][0] === 2 && commands[1][1] === 1, + 'should have generated the command 2 (DELETE) with id 1'); + assert.ok(commands[2][0] === 2 && commands[2][1] === 2, 'should have generated the command 2 (DELETE) with id 2'); } return this._super.apply(this, arguments); }, + archs: { + 'partner,false,form': + '<form string="Partner"><field name="display_name"/></form>', + }, }); form.$buttons.find('.o_form_button_edit').click(); - assert.strictEqual(form.$('td.o_list_record_remove button').length, 2, - "should have 2 remove buttons"); + assert.strictEqual(form.$('td.o_list_record_remove button').length, 3, + "should have 3 remove buttons"); assert.ok(form.$('td.o_list_record_remove button').first().hasClass('fa fa-trash-o'), "should have trash bin icons to remove (delete) records"); form.$('td.o_list_record_remove button').first().click(); + assert.strictEqual(form.$('td.o_list_record_remove button').length, 2, + "should have 2 remove buttons (a record is supposed to have been deleted)"); + + form.$('tr.o_data_row').first().click(); + assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1, + 'there should be a modal having Remove Button'); + $('.modal .modal-footer .o_btn_remove').click(); + assert.strictEqual(form.$('td.o_list_record_remove button').length, 1, - "should have 1 remove button (a record is supposed to have been deleted)"); + "should have 1 remove button (another record is supposed to have been deleted)"); // save and check that the correct command has been generated form.$buttons.find('.o_form_button_save').click(); @@ -4753,7 +4782,7 @@ QUnit.module('relational_fields', { }); QUnit.test('one2many kanban: edition', function (assert) { - assert.expect(16); + assert.expect(17); this.data.partner.records[0].p = [2]; var form = createView({ @@ -4835,7 +4864,10 @@ QUnit.module('relational_fields', { 'should contain 4 records'); // delete subrecords - form.$('.o_kanban_view .delete_icon:first()').click(); + form.$('.oe_kanban_global_click').first().click(); + assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1, + 'There should be a modal having Remove Button'); + $('.modal .modal-footer .o_btn_remove').click(); // Click Remove Button of Formview Dialog assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 3, 'should contain 3 records'); form.$('.o_kanban_view .delete_icon:first()').click(); @@ -4847,7 +4879,7 @@ QUnit.module('relational_fields', { form.destroy(); }); - QUnit.test('one2many kabnan(editable): properly handle create_text node option', function (assert) { + QUnit.test('one2many kanban (editable): properly handle create_text node option', function (assert) { assert.expect(1); var form = createView({ @@ -4877,6 +4909,74 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('one2many kanban: deletion in mobile', function (assert) { + assert.expect(9); + + this.data.partner.records[0].p = [1, 2]; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="p">' + + '<kanban>' + + '<templates>' + + '<t t-name="kanban-box">' + + '<div class="oe_kanban_global_click">' + + '<field name="display_name"/>' + + '</div>' + + '</t>' + + '</templates>' + + '</kanban>' + + '<form string="Partners">' + + '<field name="display_name"/>' + + '</form>' + + '</field>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + var commands = args.args[1].p; + assert.strictEqual(commands.length, 2, + 'should have generated two commands'); + assert.ok(commands[0][0] === 4 && commands[0][1] === 2, + 'should have generated the command 2 (DELETE) with id 1'); + assert.ok(commands[1][0] === 2 && commands[1][1] === 1, + 'should have generated the command 2 (DELETE) with id 2'); + } + return this._super.apply(this, arguments); + }, + config: { + device: { + size_class: config.device.SIZES.XS, + }, + }, + }); + + assert.strictEqual(form.$('.o_field_one2many .o-kanban-button-new').length, 0, + '"Create" button should not be visible in readonly'); + + form.$buttons.find('.o_form_button_edit').click(); + + assert.strictEqual(form.$('.o_field_one2many .o-kanban-button-new').length, 1, + '"Create" button should be visible in edit'); + assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2, + "should have 2 records"); + + // open and delete record + form.$('.oe_kanban_global_click').first().click(); + assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1, + 'there should be a Remove button in the modal footer'); + $('.modal .modal-footer .o_btn_remove').click(); + assert.strictEqual($('.o_modal').length, 0, "there shoul be no more modal"); + assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1, + 'should contain 1 records'); + + // save and check that the correct command has been generated + form.$buttons.find('.o_form_button_save').click(); + form.destroy(); + }); + QUnit.test('one2many kanban: create action disabled', function (assert) { assert.expect(3); @@ -10139,7 +10239,7 @@ QUnit.module('relational_fields', { QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { - assert.expect(29); + assert.expect(32); this.data.partner.records[0].timmy = [12, 14]; this.data.partner_type.records.push({id: 15, display_name: "red", color: 6}); @@ -10191,7 +10291,7 @@ QUnit.module('relational_fields', { var createdType = _.findWhere(this.data.partner_type.records, { display_name: "A new type" }); - var ids = _.sortBy([12, 15, 18, 21].concat(createdType.id), _.identity.bind(_)); + var ids = _.sortBy([12, 15, 18].concat(createdType.id), _.identity.bind(_)); assert.ok(_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), ids), "new value should be " + ids); } @@ -10269,12 +10369,21 @@ QUnit.module('relational_fields', { 'the newly created type should be in the kanban'); // delete subrecords - form.$('.o_kanban_record:contains(silver) .delete_icon').click(); + form.$('.o_kanban_record:contains(silver)').click(); + assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1, + 'There should be a modal having Remove Button'); + $('.modal .modal-footer .o_btn_remove').click(); // Click Remove Button of Formview Dialog assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 5, 'should contain 5 records'); assert.ok(!form.$('.o_kanban_record:contains(silver)').length, 'the removed record should not be in kanban anymore'); + form.$('.o_kanban_record:contains(blue) .delete_icon').click(); + assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 4, + 'should contain 4 records'); + assert.ok(!form.$('.o_kanban_record:contains(blue)').length, + 'the removed record should not be in kanban anymore'); + // save the record form.$buttons.find('.o_form_button_save').click(); form.destroy(); |
