summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/src/js/core/dialog.js5
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js11
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js4
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js47
-rw-r--r--addons/web/static/src/scss/modal.scss26
-rw-r--r--addons/web/static/src/xml/dialog.xml2
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js157
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();