From a105e5f2082eac0535d57005febbc1b1bb61e0a4 Mon Sep 17 00:00:00 2001 From: Dharmraj Jhala Date: Tue, 18 Apr 2017 14:33:57 +0530 Subject: [PATCH] [IMP] web,various: Add 'Remove' button to view dialogs opened from x2m fields Currently, to remove link of x2m field from any record, sometimes there's no way. For example, if we are on mobile and open any Quotation, it's linked quotation lines are automatically converted in kanban, and there is no way to delete a line from that quotation. However, in some of the manually designed kanban, there is special 'x' for removing / unlinking x2m record, for example, Contacts of Partner. This commit makes the behavior more consistent. During edition (mobile and desktop), if a view dialog is opened on click of x2m field, it will have one 'Remove' button (based on user rights) on right side of dialog footer, allowing users to delete / unlink the record. Moreover, in mobile, the modal is opened fullscreen, with sticky header and footer. Related Task# : 31641 Closes PR #15857 Co-authored-by: Mohammed Shekha --- .../fleet/views/fleet_vehicle_model_views.xml | 5 +- .../views/im_livechat_channel_views.xml | 3 +- addons/sales_team/views/crm_team_views.xml | 3 - addons/web/static/src/js/core/dialog.js | 5 + .../static/src/js/fields/relational_fields.js | 11 +- .../src/js/views/form/form_controller.js | 4 + .../web/static/src/js/views/view_dialogs.js | 47 +++++- addons/web/static/src/scss/modal.scss | 26 +++ addons/web/static/src/xml/dialog.xml | 2 +- .../tests/fields/relational_fields_tests.js | 157 +++++++++++++++--- addons/website_sale/views/product_views.xml | 1 - odoo/addons/base/views/res_partner_views.xml | 1 - 12 files changed, 223 insertions(+), 42 deletions(-) diff --git a/addons/fleet/views/fleet_vehicle_model_views.xml b/addons/fleet/views/fleet_vehicle_model_views.xml index f85a81649bd..184f753f7b6 100644 --- a/addons/fleet/views/fleet_vehicle_model_views.xml +++ b/addons/fleet/views/fleet_vehicle_model_views.xml @@ -19,13 +19,12 @@ - + -
- +
diff --git a/addons/im_livechat/views/im_livechat_channel_views.xml b/addons/im_livechat/views/im_livechat_channel_views.xml index 4281ef830c1..f4341bea2ef 100644 --- a/addons/im_livechat/views/im_livechat_channel_views.xml +++ b/addons/im_livechat/views/im_livechat_channel_views.xml @@ -105,8 +105,7 @@ - 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 @@ -