From cf178a2f068acd4b4c4fc2cda00b6ef8f6dc79c0 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 4 Jan 2018 16:17:14 +0100 Subject: [PATCH 01/10] [IMP] web,*: config: add XXS size to config.device In Less, the size_class XXS is defined and used in media queries. This rev. adds it in JS for sake of consistency. Code in addons comparing the size_class needed to be adapted due to the new XXS size_class. Moreover, we now use the helper 'config.device.isMobile' everywhere we should, instead of manually comparing the size_class. Finally, we changed the way the config.device has to be specified in the test environment. From now on, only the size_class should be set, and the isMobile flag is computed automatically. --- .../im_livechat/static/src/js/im_livechat.js | 3 +-- addons/mail/static/src/js/chat_window.js | 2 +- addons/mail/static/src/js/window_manager.js | 2 +- .../web/static/src/js/fields/basic_fields.js | 2 +- addons/web/static/src/js/services/config.js | 13 +++++----- .../static/src/js/views/basic/basic_view.js | 2 +- .../static/src/js/views/form/form_renderer.js | 2 +- .../src/js/views/graph/graph_renderer.js | 2 +- .../static/tests/fields/basic_fields_tests.js | 26 ++++++++----------- addons/web/static/tests/helpers/test_utils.js | 1 + addons/web_tour/static/src/js/tour_service.js | 2 +- addons/website/static/src/js/content/menu.js | 2 +- .../static/src/js/website_sale.js | 2 +- 13 files changed, 29 insertions(+), 32 deletions(-) diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 10cc5610214..049e88cb23c 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -78,11 +78,10 @@ var LivechatButton = Widget.extend({ start: function () { this.$el.text(this.options.button_text); - var small_screen = config.device.size_class === config.device.SIZES.XS; if (this.history) { _.each(this.history.reverse(), this.add_message.bind(this)); this.open_chat(); - } else if (!small_screen && this.rule.action === 'auto_popup') { + } else if (!config.device.isMobile && this.rule.action === 'auto_popup') { var auto_popup_cookie = utils.get_cookie('im_livechat_auto_popup'); if (!auto_popup_cookie || JSON.parse(auto_popup_cookie)) { this.auto_popup_timeout = setTimeout(this.open_chat.bind(this), this.rule.auto_popup_timer*1000); diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js index 0c3a2270045..c8cfecd193f 100644 --- a/addons/mail/static/src/js/chat_window.js +++ b/addons/mail/static/src/js/chat_window.js @@ -150,7 +150,7 @@ return Widget.extend({ this.trigger("close_chat_session"); }, on_click_fold: function () { - if (config.device.size_class !== config.device.SIZES.XS) { + if (!config.device.isMobile) { this.toggle_fold(); this.trigger("fold_channel", this.channel_id, this.folded); } diff --git a/addons/mail/static/src/js/window_manager.js b/addons/mail/static/src/js/window_manager.js index 28bf4ad71e9..147a3762068 100644 --- a/addons/mail/static/src/js/window_manager.js +++ b/addons/mail/static/src/js/window_manager.js @@ -193,7 +193,7 @@ function toggle_fold_chat (channel) { } function compute_available_slots (nb_windows) { - if (config.device.size_class === config.device.SIZES.XS) { + if (config.device.isMobile) { display_state.nb_slots = 1; // one chat window full screen in mobile return; } diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 94facd80ceb..c9375c2d4b9 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -1037,7 +1037,7 @@ var FieldPhone = FieldEmail.extend({ * @private */ _canCall: function () { - return config.device.size_class <= config.device.SIZES.XS; + return config.device.isMobile; } }); diff --git a/addons/web/static/src/js/services/config.js b/addons/web/static/src/js/services/config.js index 8a42e755322..cb32ed583d4 100644 --- a/addons/web/static/src/js/services/config.js +++ b/addons/web/static/src/js/services/config.js @@ -1,4 +1,4 @@ -odoo.define('web.config', function (require) { +odoo.define('web.config', function () { "use strict"; /** @@ -31,7 +31,7 @@ var config = { */ touch: 'ontouchstart' in window || 'onmsgesturechange' in window, /** - * size_class is an integer: 0, 1, 2 or 3, depending on the (current) + * size_class is an integer: 0, 1, 2, 3 or 4, depending on the (current) * size of the device. This is a dynamic property, updated whenever the * browser is resized * @@ -41,21 +41,22 @@ var config = { /** * A frequent use case is to have a different render in 'mobile' mode, * meaning when the screen is small. This flag (boolean) is true when - * the size is not 3. It is also updated dynamically. + * the size is XXS or XS. It is also updated dynamically. * * @type Boolean */ isMobile: null, /** - * Mapping between the numbers 0,1,2,3 and some descriptions + * Mapping between the numbers 0,1,2,3,4 and some descriptions */ - SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 }, + SIZES: { XXS: 0, XS: 1, SM: 2, MD: 3, LG: 4 }, }, }; var medias = [ - window.matchMedia('(max-width: 767px)'), + window.matchMedia('(max-width: 474px)'), + window.matchMedia('(min-width: 475px) and (max-width: 767px)'), window.matchMedia('(min-width: 768px) and (max-width: 991px)'), window.matchMedia('(min-width: 992px) and (max-width: 1199px)'), window.matchMedia('(min-width: 1200px)') diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index f428a83cd41..df9f9807bfd 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -246,7 +246,7 @@ var BasicView = AbstractView.extend({ } } if (mode.indexOf(',') !== -1) { - mode = config.device.size_class !== config.device.SIZES.XS ? 'tree' : 'kanban'; + mode = config.device.isMobile ? 'kanban' : 'tree'; } if (mode === 'tree') { mode = 'list'; diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js index 779121de3e9..7d9c3cd2d78 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -279,7 +279,7 @@ var FormRenderer = BasicRenderer.extend({ var visible_buttons = buttons_partition[1]; // Get the unfolded buttons according to window size - var nb_buttons = [2, 4, 6, 7][config.device.size_class]; + var nb_buttons = [2, 2, 4, 6, 7][config.device.size_class]; var unfolded_buttons = visible_buttons.slice(0, nb_buttons).concat(invisible_buttons); // Get the folded buttons diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js index 4d51f8786d0..07155e327e3 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -20,7 +20,7 @@ var qweb = core.qweb; var CHART_TYPES = ['pie', 'bar', 'line']; // hide top legend when too many items for device size -var MAX_LEGEND_LENGTH = 25 * (1 + config.device.size_class); +var MAX_LEGEND_LENGTH = 25 * (Math.max(1, config.device.size_class)); return AbstractRenderer.extend({ className: "o_graph_svg_container", diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 4fd9d6ee84e..44f00d1d7f9 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -3,6 +3,7 @@ odoo.define('web.basic_fields_tests', function (require) { var basicFields = require('web.basic_fields'); var concurrency = require('web.concurrency'); +var config = require('web.config'); var core = require('web.core'); var FormView = require('web.FormView'); var KanbanView = require('web.KanbanView'); @@ -2987,9 +2988,8 @@ QUnit.module('basic_fields', { res_id: 1, config: { device: { - size_class: 0, // Screen XS - SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 }, - } + size_class: config.device.SIZES.XS, + }, }, }); @@ -3032,9 +3032,8 @@ QUnit.module('basic_fields', { arch: '', config: { device: { - size_class: 0, // Screen XS - SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 }, - } + size_class: config.device.SIZES.XS, + }, }, }); @@ -3097,9 +3096,8 @@ QUnit.module('basic_fields', { res_id: 1, config: { device: { - size_class: 1, // Screen SM - SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 }, - } + size_class: config.device.SIZES.SM, + }, }, }); @@ -3145,9 +3143,8 @@ QUnit.module('basic_fields', { arch: '', config: { device: { - size_class: 1, // Screen SM - SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 }, - } + size_class: config.device.SIZES.SM, + }, }, }); @@ -3199,9 +3196,8 @@ QUnit.module('basic_fields', { }, config: { device: { - size_class: 0, - SIZES: { XS: 0, SM: 1, MD: 2, LG: 3 }, - } + size_class: config.device.SIZES.XS, + }, }, }); diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index 260436ff152..468b25e5733 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -346,6 +346,7 @@ function addMockEnvironment(widget, params) { initialConfig.device = _.clone(config.device); if ('device' in params.config) { _.extend(config.device, params.config.device); + config.device.isMobile = config.device.size_class <= config.device.SIZES.XS; } if ('debug' in params.config) { config.debug = params.config.debug; diff --git a/addons/web_tour/static/src/js/tour_service.js b/addons/web_tour/static/src/js/tour_service.js index 09b4e6896b9..37e6f1a5c0f 100644 --- a/addons/web_tour/static/src/js/tour_service.js +++ b/addons/web_tour/static/src/js/tour_service.js @@ -12,7 +12,7 @@ var TourManager = require('web_tour.TourManager'); var QWeb = core.qweb; -if (config.device.size_class <= config.device.SIZES.XS) { +if (config.device.isMobile) { return $.Deferred().reject(); } diff --git a/addons/website/static/src/js/content/menu.js b/addons/website/static/src/js/content/menu.js index 52e4e2c5319..5f8c337802f 100644 --- a/addons/website/static/src/js/content/menu.js +++ b/addons/website/static/src/js/content/menu.js @@ -113,7 +113,7 @@ sAnimation.registry.autohideMenu = sAnimation.Class.extend({ */ _adapt: function () { this._restore(); - if (config.device.size_class < config.device.SIZES.SM) { + if (config.device.isMobile) { return; } diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index d54978e290c..874b9f698b5 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -480,7 +480,7 @@ odoo.define('website_sale.website_sale', function (require) { }); // Deactivate image zoom for mobile devices, since it might prevent users to scroll - if (config.device.size_class > config.device.SIZES.XS) { + if (!config.device.isMobile) { $('.ecom-zoomable img[data-zoom]').zoomOdoo({ attach: '#o-carousel-product'}); } }); From af2686fee55b20d74ce1799aea84bf1910b20051 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 4 Jan 2018 10:02:48 +0100 Subject: [PATCH 02/10] [IMP] web: Kanban: cancel column quick create on ESC Record quick create can be cancelled by pressing ESC, so this rev. ensures that the same behavior applies for the column quick create, for the sake of consistency. --- .../js/views/kanban/kanban_quick_create.js | 13 +++++-- addons/web/static/tests/views/kanban_tests.js | 39 ++++++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/addons/web/static/src/js/views/kanban/kanban_quick_create.js b/addons/web/static/src/js/views/kanban/kanban_quick_create.js index 216cf1bbe88..9c317552329 100644 --- a/addons/web/static/src/js/views/kanban/kanban_quick_create.js +++ b/addons/web/static/src/js/views/kanban/kanban_quick_create.js @@ -266,6 +266,15 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({ // Private //-------------------------------------------------------------------------- + /** + * @override + * @private + */ + _cancel: function () { + this.folded = true; + this.$input.val(''); + this._update(); + }, /** * Triggers up an event to quick create a column with the given value * @@ -298,9 +307,7 @@ var ColumnQuickCreate = AbstractQuickCreate.extend({ if (hasFocus) { return; } - this.folded = true; - this.$input.val(''); - this._update(); + this._cancel(); }, /** * Stops the propagation of the event to prevent the quick create from diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index f003bba199c..d4054e9f7af 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -1,7 +1,6 @@ odoo.define('web.kanban_tests', function (require) { "use strict"; -var core = require('web.core'); var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar'); var KanbanRenderer = require('web.KanbanRenderer'); var KanbanView = require('web.KanbanView'); @@ -9,7 +8,6 @@ var testUtils = require('web.test_utils'); var Widget = require('web.Widget'); var widgetRegistry = require('web.widget_registry'); -var createAsyncView = testUtils.createAsyncView; var createView = testUtils.createView; QUnit.module('Views', { @@ -1425,6 +1423,43 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('cancel column quick create by pressing ESCAPE', function (assert) { + assert.expect(4); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
' + + '
', + groupBy: ['product_id'], + }); + + assert.strictEqual(kanban.$('.o_column_quick_create').length, 1, "should have a quick create column"); + assert.notOk(kanban.$('.o_column_quick_create input').is(':visible'), + "the input should not be visible"); + + kanban.$('.o_column_quick_create').click(); + + assert.ok(kanban.$('.o_column_quick_create input').is(':visible'), + "the input should be visible"); + + // discard the column creation by pressing ESCAPE + kanban.$('.o_column_quick_create input').trigger($.Event('keydown', { + keyCode: $.ui.keyCode.ESCAPE, + which: $.ui.keyCode.ESCAPE, + })); + + assert.notOk(kanban.$('.o_column_quick_create input').is(':visible'), + "the input should not be visible anymore"); + + kanban.destroy(); + }); + QUnit.test('if view was grouped at start, it stays grouped', function (assert) { assert.expect(1); From d18d08f01a589053e40c7af1fd4dd59c13aaa625 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 2 Jan 2018 16:25:28 +0100 Subject: [PATCH 03/10] [IMP] web: Kanban: quick create record with more fields This rev. improves the QuickCreate widget for grouped Kanban views. It allows specifying in the Kanban arch a form view xmlid, e.g.: ... When this attribute is set, the referenced form view is loaded and rendered inside the QuickCreate widget. A real FormView widget is instantiated, meaning that all form view mechanisms are available in the QuickCreate (modifiers, default_get, onchanges, widgets...). Task #27475. --- .../src/js/views/basic/basic_controller.js | 11 +- .../static/src/js/views/form/form_renderer.js | 11 +- .../src/js/views/kanban/kanban_column.js | 38 +- .../src/js/views/kanban/kanban_controller.js | 59 +- .../src/js/views/kanban/kanban_model.js | 40 +- .../js/views/kanban/kanban_quick_create.js | 539 ++++++++------- .../src/js/views/kanban/kanban_renderer.js | 43 +- .../js/views/kanban/kanban_renderer_mobile.js | 4 +- .../static/src/js/views/kanban/kanban_view.js | 1 + .../js/views/kanban/quick_create_form_view.js | 110 +++ addons/web/static/src/less/form_view.less | 51 ++ .../web/static/src/less/form_view_extra.less | 7 + addons/web/static/src/less/kanban_view.less | 11 +- addons/web/static/src/xml/kanban.xml | 5 +- .../web/static/tests/helpers/mock_server.js | 6 + addons/web/static/tests/views/kanban_tests.js | 626 ++++++++++++++++-- addons/web/views/webclient_templates.xml | 1 + 17 files changed, 1219 insertions(+), 344 deletions(-) create mode 100644 addons/web/static/src/js/views/kanban/quick_create_form_view.js diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 2c7c1c40210..c4dff8fad2e 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -68,7 +68,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { * rejected otherwise */ canBeDiscarded: function (recordID) { - if (!this.model.isDirty(recordID || this.handle)) { + if (!this.isDirty(recordID)) { return $.when(false); } @@ -120,6 +120,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { getSelectedIds: function () { return []; }, + /** + * Returns true iff the given recordID (or the main recordID) is dirty. + * + * @param {string} [recordID] - default to main recordID + * @returns {boolean} + */ + isDirty: function (recordID) { + return this.model.isDirty(recordID || this.handle); + }, /** * @override */ diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js index 7d9c3cd2d78..b289663fb19 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -18,10 +18,19 @@ var FormRenderer = BasicRenderer.extend({ /** * @override */ - init: function (parent, state, params) { + init: function () { this._super.apply(this, arguments); this.idsForLabels = {}; }, + /** + * @override + */ + start: function () { + if (config.device.size_class <= config.device.SIZES.XXS) { + this.$el.addClass('o_xxs_form_view'); + } + return this._super.apply(this, arguments); + }, //-------------------------------------------------------------------------- // Public diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js index bde447870bb..563df7a4630 100644 --- a/addons/web/static/src/js/views/kanban/kanban_column.js +++ b/addons/web/static/src/js/views/kanban/kanban_column.js @@ -47,6 +47,8 @@ var KanbanColumn = Widget.extend({ this.modelName = data.model; this.quick_create = options.quick_create; + this.quickCreateView = options.quickCreateView; + this.groupedBy = options.groupedBy; this.grouped_by_m2o = options.grouped_by_m2o; this.editable = options.editable; this.deletable = options.deletable; @@ -156,6 +158,18 @@ var KanbanColumn = Widget.extend({ return $.when.apply($, defs); }, + /** + * Called when a record has been quick created, as a new column is rendered + * and appended into a fragment, before replacing the old column in the DOM. + * When this happens, the quick create widget is inserted into the new + * column directly, and it should be focused. However, as it is rendered + * into a fragment, the focus has to be set manually once in the DOM. + */ + on_attach_callback: function () { + if (this.quickCreateWidget) { + this.quickCreateWidget.on_attach_callback(); + } + }, //-------------------------------------------------------------------------- // Public @@ -163,6 +177,8 @@ var KanbanColumn = Widget.extend({ /** * Adds the quick create record to the top of the column. + * + * @returns {Deferred} */ addQuickCreate: function () { if (this.folded) { @@ -173,11 +189,25 @@ var KanbanColumn = Widget.extend({ return; } if (this.quickCreateWidget) { - return; + return $.Deferred().reject(); + } + this.trigger_up('close_quick_create'); // close other quick create widgets + var context = this.data.getContext(); + context['default_' + this.groupedBy] = this.id; + this.quickCreateWidget = new RecordQuickCreate(this, { + context: context, + formViewRef: this.quickCreateView, + model: this.modelName, + }); + return this.quickCreateWidget.insertAfter(this.$header); + }, + /** + * Closes the quick create widget if it isn't dirty. + */ + cancelQuickCreate: function () { + if (this.quickCreateWidget) { + this.quickCreateWidget.cancel(); } - var width = this.records.length ? this.records[0].$el.innerWidth() : this.$el.width() - 8; - this.quickCreateWidget = new RecordQuickCreate(this, width); - this.quickCreateWidget.insertAfter(this.$header); }, /** * @returns {Boolean} true iff the column is empty diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js index 2ddd5577330..ca2999b05b0 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -297,52 +297,51 @@ var KanbanController = BasicController.extend({ /** * @private * @param {OdooEvent} event + * @param {KanbanColumn} event.target the column in which the record should + * be added + * @param {Object} event.data.values the field values of the record to + * create; if values only contains the value of the 'display_name', a + * 'name_create' is performed instead of 'create' */ _onQuickCreateRecord: function (event) { var self = this; + var values = event.data.values; var column = event.target; - var name = event.data.value; - var state = this.model.get(this.handle, {raw: true}); - var columnState = this.model.get(column.db_id, {raw: true}); - var context = columnState.getContext(); - context['default_' + state.groupedBy[0]] = columnState.res_id; - this._rpc({ - model: state.model, - method: 'name_create', - args: [name], - context: context, - }) - .then(add_record) + // function that updates the kanban view once the record has been added + // it receives the local id of the created record in arguments + var update = function (db_id) { + self._updateEnv(); + + var columnState = self.model.getColumn(db_id); + return self.renderer + .updateColumn(columnState.id, columnState, {openQuickCreate: true}) + .then(function () { + if (event.data.openRecord) { + self.trigger_up('open_record', {id: db_id, mode: 'edit'}); + } + }); + }; + + this.model.createRecordInGroup(column.db_id, values) + .then(update) .fail(function (error, event) { event.preventDefault(); + var columnState = self.model.get(column.db_id, {raw: true}); + var context = columnState.getContext(); + var state = self.model.get(self.handle, {raw: true}); + context['default_' + state.groupedBy[0]] = columnState.res_id; new view_dialogs.FormViewDialog(self, { res_model: state.model, context: _.extend({default_name: name}, context), title: _t("Create"), disable_multiple_selection: true, on_saved: function (record) { - add_record([record.res_id]); + self.model.addRecordToGroup(column.db_id, record.res_id) + .then(update); }, }).open(); }); - - function add_record(records) { - return self.model - .addRecordToGroup(columnState.id, records[0]) - .then(function (db_id) { - self._updateEnv(); - - var columnState = self.model.getColumn(db_id); - return self.renderer - .updateColumn(columnState.id, columnState, {openQuickCreate: true}) - .then(function () { - if (event.data.openRecord) { - self.trigger_up('open_record', {id: db_id, mode: 'edit'}); - } - }); - }); - } }, /** * @private diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index df6172f0d8b..ba6b2ce3dbd 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -33,7 +33,7 @@ var KanbanModel = BasicModel.extend({ parentID: groupID, }); - return this._fetchRecord(new_record).then(function (result) { + var def = this._fetchRecord(new_record).then(function (result) { group.data.unshift(new_record.id); group.res_ids.unshift(resId); group.count++; @@ -44,6 +44,7 @@ var KanbanModel = BasicModel.extend({ return result.id; }); + return this._reloadProgressBarGroupFromRecord(new_record.id, def); }, /** * Creates a new group from a name (performs a name_create). @@ -93,6 +94,43 @@ var KanbanModel = BasicModel.extend({ return newGroup.id; }); }, + /** + * Creates a new record from the given value, and add it to the given group. + * + * @param {string} groupID + * @param {Object} values + * @returns {Deferred} resolved with the local id of the created record + */ + createRecordInGroup: function (groupID, values) { + var self = this; + var group = this.localData[groupID]; + var context = this._getContext(group); + var parent = this.localData[group.parentID]; + context['default_' + parent.groupedBy[0]] = group.res_id; + var def; + if (Object.keys(values).length === 1 && 'display_name' in values) { + // only 'display_name is given, perform a 'name_create' + def = this._rpc({ + model: parent.model, + method: 'name_create', + args: [values.display_name], + context: context, + }).then(function (records) { + return records[0]; + }); + } else { + // other fields are specified, perform a classical 'create' + def = this._rpc({ + model: parent.model, + method: 'create', + args: [values], + context: context, + }); + } + return def.then(function (resID) { + return self.addRecordToGroup(group.id, resID); + }); + }, /** * Add the key `tooltipData` (kanban specific) when performing a `geĊ§`. * diff --git a/addons/web/static/src/js/views/kanban/kanban_quick_create.js b/addons/web/static/src/js/views/kanban/kanban_quick_create.js index 9c317552329..66c6a2676f1 100644 --- a/addons/web/static/src/js/views/kanban/kanban_quick_create.js +++ b/addons/web/static/src/js/views/kanban/kanban_quick_create.js @@ -6,17 +6,121 @@ odoo.define('web.kanban_quick_create', function (require) { * kanban records, and one to quick create kanban columns. */ +var core = require('web.core'); +var QuickCreateFormView = require('web.QuickCreateFormView'); var Widget = require('web.Widget'); -var AbstractQuickCreate = Widget.extend({ +var qweb = core.qweb; + +var RecordQuickCreate = Widget.extend({ + className: 'o_kanban_quick_create', + custom_events: { + add: '_onAdd', + cancel: '_onCancel', + }, events: { 'click .o_kanban_add': '_onAddClicked', + 'click .o_kanban_edit': '_onEditClicked', 'click .o_kanban_cancel': '_onCancelClicked', - 'keydown': '_onKeydown', - 'keypress input': '_onKeypress', - 'mousedown .o_kanban_add': '_onMousedown', - 'mousedown .o_kanban_cancel': '_onMousedown', - 'focusout': '_onFocusOut', + }, + + /** + * @override + * @param {Widget} parent + * @param {Object} options + * @param {Object} options.context + * @param {string|null} options.formViewRef + * @param {string} options.model + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.context = options.context; + this.formViewRef = options.formViewRef; + this.model = options.model; + }, + /** + * Loads the form fieldsView (if not provided), instantiates the form view + * and starts the form controller. + * + * @override + */ + willStart: function () { + var self = this; + var def1 = this._super.apply(this, arguments); + var def2; + if (this.formViewRef) { + var views = [[false, 'form']]; + var context = _.extend({}, this.context, { + form_view_ref: this.formViewRef, + }); + def2 = this.loadViews(this.model, context, views); + } else { + var fieldsView = {}; + fieldsView.arch = '
' + + '' + + ''; + var fields = { + display_name: {string: 'Display name', type: 'char'}, + }; + fieldsView.fields = fields; + fieldsView.viewFields = fields; + def2 = $.when({form: fieldsView}); + } + def2 = def2.then(function (fieldsViews) { + var formView = new QuickCreateFormView(fieldsViews.form, { + context: self.context, + modelName: self.model, + userContext: self.getSession().user_context, + }); + return formView.getController(self).then(function (controller) { + self.controller = controller; + return self.controller.appendTo(document.createDocumentFragment()); + }); + }); + return $.when(def1, def2); + }, + /** + * @override + */ + start: function () { + this.$el.append(this.controller.$el); + this.$el.append(qweb.render('KanbanView.RecordQuickCreate.buttons')); + + // focus the first field + this.controller.autofocus(); + + // destroy the quick create when the user clicks outside + core.bus.on('click', this, this._onWindowClicked); + + return this._super.apply(this, arguments); + }, + /** + * Called when the quick create is appended into the DOM. + */ + on_attach_callback: function () { + if (this.controller) { + this.controller.autofocus(); + } + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Cancels the quick creation if the record isn't dirty, i.e. if no changes + * have been made yet + * + * @private + * @returns {Deferred} + */ + cancel: function () { + var self = this; + return this.controller.commitChanges().then(function () { + if (!self.controller.isDirty()) { + self._cancel(); + } + }); }, //-------------------------------------------------------------------------- @@ -24,37 +128,210 @@ var AbstractQuickCreate = Widget.extend({ //-------------------------------------------------------------------------- /** - * Clears the input value and notify the environment that a quick create - * has been done - * + * @override * @private - * @param {Object} [options] dict of options to pass to call to '_notifyAdd' + * @param {Object} [options] + * @param {boolean} [options.openRecord] set to true to directly open the + * newly created record in a form view (in edit mode) + * @returns {Deferred} */ _add: function (options) { - var value = this.$input.val(); - this.$input.val(''); - if (/^\s*$/.test(value)) { + var self = this; + return this.controller.commitChanges().then(function () { + var canBeSaved = self.controller.canBeSaved(); + if (canBeSaved) { + self.trigger_up('quick_create_add_record', { + openRecord: options && options.openRecord || false, + values: self.controller.getChanges(), + }); + } + }); + }, + /** + * Notifies the environment that the quick creation must be cancelled + * + * @private + * @returns {Deferred} + */ + _cancel: function () { + this.trigger_up('cancel_quick_create'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} ev + */ + _onAdd: function (ev) { + ev.stopPropagation(); + this._add(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onAddClicked: function (ev) { + ev.stopPropagation(); + this._add(); + }, + /** + * @private + * @param {OdooEvent} ev + */ + _onCancel: function (ev) { + ev.stopPropagation(); + this._cancel(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onCancelClicked: function (ev) { + ev.stopPropagation(); + this._cancel(); + }, + /** + * Validates the quick creation and directly opens the record in a form + * view in edit mode. + * + * @private + * @param {MouseEvent} ev + */ + _onEditClicked: function (ev) { + ev.stopPropagation(); + this._add({openRecord: true}); + }, + /** + * When a click happens outside the quick create, we want to close the quick + * create. + * + * This is quite tricky, because in some cases a click is performed outside + * the quick create, but is still related to it (e.g. click in a dialog + * opened from the quick create). + * + * @param {MouseEvent} ev + */ + _onWindowClicked: function (ev) { + // ignore clicks if the quick create is not in the dom + if (!document.contains(this.el)) { return; } - this._notifyAdd(value, options); + + // ignore clicks on elements that open the quick create widget, to + // prevent from closing quick create widget that has just been opened + if ($(ev.target).closest('.o-kanban-button-new, .o_kanban_quick_add').length) { + return; + } + + // ignore clicks in autocomplete dropdowns + if ($(ev.target).parents('.ui-autocomplete').length) { + return; + } + + // ignore clicks in modals + if ($(ev.target).closest('.modal').length) { + return; + } + + // ignore clicks if target is no longer in dom (e.g., a click on the + // 'delete' trash icon of a m2m tag) + if (!document.contains(ev.target)) { + return; + } + + // ignore clicks if target is inside the quick create + if (this.el.contains(ev.target) && this.el !== ev.target) { + return; + } + + this.cancel(); + }, +}); + +var ColumnQuickCreate = Widget.extend({ + template: 'KanbanView.ColumnQuickCreate', + events: { + 'click': 'toggleFold', + 'click input': '_onInputClicked', + 'click .o_kanban_add': '_onAddClicked', + 'focusout': '_onFocusout', + 'keydown': '_onKeydown', + 'keypress input': '_onKeypress', + 'mousedown .o_kanban_add': '_onMousedown', + }, + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.folded = true; + }, + /** + * @override + */ + start: function () { + this.$header = this.$('.o_column_header'); + this.$quick_create = this.$('.o_kanban_quick_create'); + this.$input = this.$('input'); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Folds/unfolds the Column quick create widget + */ + toggleFold: function () { + this.folded = !this.folded; + this._update(); + if (!this.folded) { + this.$input.focus(); + this.trigger_up('scrollTo', {selector: '.o_column_quick_create'}); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Clears the input value and notify the environment to create a column + * + * @private + */ + _add: function () { + var value = this.$input.val().trim(); + if (!value.length) { + return; + } + this.$input.val(''); + this.trigger_up('quick_create_add_column', {value: value}); this.$input.focus(); }, /** - * Should implement what to do when the quick creation has been cancelled + * Cancels the quick creation * - * @abstract * @private */ _cancel: function () { + this.folded = true; + this.$input.val(''); + this._update(); }, /** - * Should implement what to do to notify the environment that a quick - * create has been done + * Updates the rendering according to the current state (folded/unfolded) * - * @abstract * @private */ - _notifyAdd: function (name, options) { + _update: function () { + this.$header.toggle(this.folded); + this.$quick_create.toggle(!this.folded); }, //-------------------------------------------------------------------------- @@ -70,21 +347,26 @@ var AbstractQuickCreate = Widget.extend({ this._add(); }, /** + * Cancels the quick creation when the input loses the focus + * * @private */ - _onCancelClicked: function () { + _onFocusout: function () { + var hasFocus = this.$(':focus').length > 0; + if (hasFocus) { + return; + } this._cancel(); }, /** - * Cancels quick creation on focusout input event + * Stops the propagation of the event to prevent the quick create from + * toggling when the user clicks in the input * * @private - * @param {KeyEvent} ev + * @param {MouseEvent} event */ - _onFocusOut: function (ev) { - if (!this.$input.val()) { - this._cancel(); - } + _onInputClicked: function (event) { + event.stopPropagation(); }, /** * Cancels quick creation on escape keydown event @@ -111,11 +393,10 @@ var AbstractQuickCreate = Widget.extend({ /** * In all browsers the 'focus/blur' event is triggered before a button's * 'click' (it's actually triggered by mousedown). - * The quick create is destroyed on blur of its input but still needs - * to create the data when the "Add" button is clicked. + * The quick create is hidden on blur of its input but still needs to create + * the data when the "Add" button is clicked. * This problem is adressed by suppressing the 'focus/blur' event from the - * relevant buttons (Add and Cancel) entirely by preventing the browser's - * default mousedown handler. + * Add button entirely by preventing the browser's default mousedown handler. * * @private * @param {MouseEvent} event @@ -125,202 +406,6 @@ var AbstractQuickCreate = Widget.extend({ }, }); -var RecordQuickCreate = AbstractQuickCreate.extend({ - template: "KanbanView.QuickCreate", - events: _.extend({}, AbstractQuickCreate.prototype.events, { - 'click .o_kanban_edit': '_onEditClicked', - 'mousedown .o_kanban_edit': '_onMousedown', - }), - /** - * @override - * @param {Widget} parent - * @param {Object} options - * @param {string|number} options.width defines the element's width - * @param {string} [options.defaultName] the record's default name - */ - init: function (parent, options) { - this._super.apply(this, arguments); - this.width = options.width; - this.defaultName = options.defaultName; - }, - /** - * @override - */ - start: function () { - this.$el.css({width: this.width}); - this.$input = this.$('input'); - this._addDefaultName(); - this.$input.focus(); - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Set the default value for the record name. - * - * @private - */ - _addDefaultName: function () { - if (this.defaultName) { - this.$input.val(this.defaultName); - } - }, - /** - * Triggers up an event to cancel the quick creation - * - * @override - * @private - */ - _cancel: function () { - this.trigger_up('cancel_quick_create'); - }, - /** - * Triggers up an event to quick create a record with the given value - * - * @override - * @private - * @param {string} value - * @param {Object} [options] - * @param {boolean} [options.openRecord] set to true to directly open the - * newly created record in a form view (in edit mode) - */ - _notifyAdd: function (value, options) { - this.trigger_up('quick_create_add_record', { - value: value, - openRecord: options && options.openRecord || false, - }); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Add the default value for the record name again after creating - * the previous record. - * - * @override - * @private - */ - _onAddClicked: function () { - this._super.apply(this, arguments); - this._addDefaultName(); - }, - /** - * Validates the quick creation and directly opens the record in a form - * view in edit mode. - * - * @private - * @param {MouseEvent} event - */ - _onEditClicked: function (event) { - event.stopPropagation(); - this._add({openRecord: true}); - }, -}); - -var ColumnQuickCreate = AbstractQuickCreate.extend({ - template: 'KanbanView.ColumnQuickCreate', - events: _.extend({}, AbstractQuickCreate.prototype.events, { - 'click': 'toggleFold', - 'click input': '_onInputClicked', - 'focusout': '_onFocusout', - }), - /** - * @override - */ - init: function () { - this._super.apply(this, arguments); - this.folded = true; - }, - /** - * @override - */ - start: function () { - this.$header = this.$('.o_column_header'); - this.$quick_create = this.$('.o_kanban_quick_create'); - this.$input = this.$('input'); - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Toggle fold/unfold the Column quick create widget - */ - toggleFold: function () { - this.folded = !this.folded; - this._update(); - if (!this.folded) { - this.$input.focus(); - this.trigger_up('scrollTo', {selector: '.o_column_quick_create'}); - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - * @private - */ - _cancel: function () { - this.folded = true; - this.$input.val(''); - this._update(); - }, - /** - * Triggers up an event to quick create a column with the given value - * - * @override - * @private - * @param {string} value - */ - _notifyAdd: function (value) { - this.trigger_up('quick_create_add_column', {value: value}); - }, - /** - * Updates the rendering according to the current state - * - * @private - */ - _update: function () { - this.$header.toggle(this.folded); - this.$quick_create.toggle(!this.folded); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onFocusout: function () { - var hasFocus = this.$(':focus').length > 0; - if (hasFocus) { - return; - } - this._cancel(); - }, - /** - * Stops the propagation of the event to prevent the quick create from - * toggling when the user clicks in the input - * - * @private - * @param {MouseEvent} event - */ - _onInputClicked: function (event) { - event.stopPropagation(); - }, -}); - return { RecordQuickCreate: RecordQuickCreate, ColumnQuickCreate: ColumnQuickCreate, diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js index 1dad8e9c35d..52f57287296 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js @@ -84,7 +84,8 @@ function transformQwebTemplate(node, fields) { var KanbanRenderer = BasicRenderer.extend({ className: 'o_kanban_view', custom_events: _.extend({}, BasicRenderer.prototype.custom_events || {}, { - 'set_progress_bar_state': '_onSetProgressBarState', + close_quick_create: '_onCloseQuickCreate', + set_progress_bar_state: '_onSetProgressBarState', }), /** @@ -117,9 +118,11 @@ var KanbanRenderer = BasicRenderer.extend({ /** * Displays the quick create record in the first column. + * + * @returns {Deferred} */ addQuickCreate: function () { - this.widgets[0].addQuickCreate(); + return this.widgets[0].addQuickCreate(); }, /** * Toggle fold/unfold the Column quick create widget @@ -152,11 +155,21 @@ var KanbanRenderer = BasicRenderer.extend({ var index = _.findIndex(this.widgets, {db_id: localID}); var column = this.widgets[index]; this.widgets[index] = newColumn; - return newColumn.insertAfter(column.$el).then(function () { + return newColumn.appendTo(document.createDocumentFragment()).then(function () { + var def; if (options && options.openQuickCreate) { - newColumn.addQuickCreate(); + def = newColumn.addQuickCreate(); } - column.destroy(); + return $.when(def).then(function () { + newColumn.$el.insertAfter(column.$el); + // When a record has been quick created, the new column directly + // renders the quick create widget (to allow quick creating several + // records in a row). However, as we render this column in a + // fragment, the quick create widget can't be correctly focused. So + // we manually call on_attach_callback to focus it once in the DOM. + newColumn.on_attach_callback(); + column.destroy(); + }); }); }, /** @@ -334,8 +347,9 @@ var KanbanRenderer = BasicRenderer.extend({ _setState: function (state) { this.state = state; - var groupByFieldAttrs = state.fields[state.groupedBy[0]]; - var groupByFieldInfo = state.fieldsInfo.kanban[state.groupedBy[0]]; + var groupByField = state.groupedBy[0]; + var groupByFieldAttrs = state.fields[groupByField]; + var groupByFieldInfo = state.fieldsInfo.kanban[groupByField]; // Deactivate the drag'n'drop if the groupedBy field: // - is a date or datetime since we group by month or // - is readonly (on the field attrs or in the view) @@ -353,13 +367,14 @@ var KanbanRenderer = BasicRenderer.extend({ } } this.groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one'); - var grouped_by_field = this.groupedByM2O && groupByFieldAttrs.relation; + var relation = this.groupedByM2O && groupByFieldAttrs.relation; var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip; this.columnOptions = _.extend(this.columnOptions, { draggable: draggable, group_by_tooltip: groupByTooltip, + groupedBy: groupByField, grouped_by_m2o: this.groupedByM2O, - relation: grouped_by_field, + relation: relation, }); this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable; }, @@ -368,6 +383,16 @@ var KanbanRenderer = BasicRenderer.extend({ // Handlers //-------------------------------------------------------------------------- + /** + * Closes the opened quick create widgets in columns + * + * @private + */ + _onCloseQuickCreate: function () { + if (this.state.groupedBy.length) { + _.invoke(this.widgets, 'cancelQuickCreate'); + } + }, /** * Updates progressbar internal states (necessary for animations) with * received data. diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js index 7614d6bca95..c609e45fc60 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js @@ -38,9 +38,11 @@ KanbanRenderer.include({ /** * Displays the quick create record in the active column + * + * @returns {Deferred} */ addQuickCreate: function () { - this.widgets[this.activeColumnIndex].addQuickCreate(); + return this.widgets[this.activeColumnIndex].addQuickCreate(); }, /** * Overrides to restore the left property and the scrollTop on the updated diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js index 87b4a366502..c5ebbf5243f 100644 --- a/addons/web/static/src/js/views/kanban/kanban_view.js +++ b/addons/web/static/src/js/views/kanban/kanban_view.js @@ -62,6 +62,7 @@ var KanbanView = BasicView.extend({ deletable: activeActions.group_delete, group_creatable: activeActions.group_create && !config.device.isMobile, quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(), + quickCreateView: this.arch.attrs.quick_create_view || null, hasProgressBar: !!progressBar, }; this.rendererParams.record_options = { diff --git a/addons/web/static/src/js/views/kanban/quick_create_form_view.js b/addons/web/static/src/js/views/kanban/quick_create_form_view.js new file mode 100644 index 00000000000..fe0684a9ada --- /dev/null +++ b/addons/web/static/src/js/views/kanban/quick_create_form_view.js @@ -0,0 +1,110 @@ +odoo.define('web.QuickCreateFormView', function (require) { +"use strict"; + +/** + * This file defines the QuickCreateFormView, an extension of the FormView that + * is used by the RecordQuickCreate in Kanban views. + */ + +var FormController = require('web.FormController'); +var BasicModel = require('web.BasicModel'); +var FormRenderer = require('web.FormRenderer'); +var FormView = require('web.FormView'); + +var QuickCreateFormRenderer = FormRenderer.extend({ + /** + * @override + */ + start: function () { + this.$el.addClass('o_xxs_form_view'); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + * @private + * @param {OdooEvent} ev + */ + _onNavigationMove: function (ev) { + var direction = ev.data.direction; + if (direction === 'cancel' || direction === 'next_line') { + ev.stopPropagation(); + this.trigger_up(direction === 'cancel' ? 'cancel' : 'add'); + } else { + this._super.apply(this, arguments); + } + }, +}); + +var QuickCreateFormModel = BasicModel.extend({ + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {Object} the changes of the given resource (server commands for + * x2manys) + */ + getChanges: function (localID) { + var record = this.localData[localID]; + return this._generateChanges(record, {changesOnly: false}); + }, +}); + +var QuickCreateFormController = FormController.extend({ + custom_events: _.extend({}, FormController.prototype.custom_events, { + env_updated: '_onEnvUpdated', + }), + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Asks all field widgets to notify the environment with their current value + * (useful for instance for input fields that still have the focus and that + * could have not notified the environment of their changes yet). + * + * @return {Deferred} + */ + commitChanges: function () { + return this.mutex + .exec(this.renderer.commitChanges.bind(this.renderer, this.handle)); + }, + /** + * @returns {Object} the changes done on the current record + */ + getChanges: function () { + return this.model.getChanges(this.handle); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Stops the propagation of the 'env_updated' event to prevent interferences + * with the kanban controller. + * + * @private + */ + _onEnvUpdated: function (ev) { + ev.stopPropagation(); + }, +}); + +var QuickCreateFormView = FormView.extend({ + config: _.extend({}, FormView.prototype.config, { + Model: QuickCreateFormModel, + Renderer: QuickCreateFormRenderer, + Controller: QuickCreateFormController, + }), +}); + +return QuickCreateFormView; + +}); diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less index 8a053a8cca3..505f62ac42f 100644 --- a/addons/web/static/src/less/form_view.less +++ b/addons/web/static/src/less/form_view.less @@ -649,6 +649,57 @@ label.o_light_label, .o_light_label label { font-weight: normal; } +// XXS form view specific rules +.form-break-table() { + display: block; + margin-bottom: @odoo-form-spacing-unit * 4; + + > tbody { + display: block; + + > tr { + .o-flex-display(); + .o-flex-flow(row, wrap); + + > td { + .o-flex(1, 0, auto); + display: block; + max-width: 100%; + padding: 0; + + width: auto!important; // !important is required to override the width computed in JS + &.o_td_label { + width: 94%!important; // The label must be on its own line except if the form field is small enough (checkbox) + line-height: @odoo-label-font-size-factor; + } + + .o_field_widget { + margin-bottom: @odoo-form-spacing-unit * 2; + &.o_field_boolean { + margin-right: 0; + } + } + + .o_input_dropdown { + width: auto; + max-width: 100%; + } + } + } + } +} +.o_form_view.o_xxs_form_view { + .oe_title { + word-break: break-all; + } + .o_group { + &.o_inner_group { + .form-break-table(); + } + } +} + +// Settings form views .o_settings_container { .o-flex-display(); .o-flex(0, 1, auto); diff --git a/addons/web/static/src/less/form_view_extra.less b/addons/web/static/src/less/form_view_extra.less index 2349ee5c0b0..95da6a2b35b 100644 --- a/addons/web/static/src/less/form_view_extra.less +++ b/addons/web/static/src/less/form_view_extra.less @@ -181,3 +181,10 @@ } } } + +// XXS form view specific rules +.o_form_view.o_xxs_form_view { + .o_group .o_td_label { + border: none; + } +} diff --git a/addons/web/static/src/less/kanban_view.less b/addons/web/static/src/less/kanban_view.less index 9158aa22df6..2b5482e36ff 100644 --- a/addons/web/static/src/less/kanban_view.less +++ b/addons/web/static/src/less/kanban_view.less @@ -161,12 +161,11 @@ box-shadow: 0 0 20px -10px; margin: 0 @o-kanban-record-margin -1px @o-kanban-record-margin; - input { - width: 100%; - margin: 2px 0 8px; - padding: 3px 6px; - color: @headings-color; - font-weight: 500; + .o_form_view { + padding: 0; + .o_group.o_inner_group { + margin: 0; + } } } diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml index dd8427a7643..97ac827df2a 100644 --- a/addons/web/static/src/xml/kanban.xml +++ b/addons/web/static/src/xml/kanban.xml @@ -54,9 +54,8 @@ - -
- + +
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 8a44e1eb1e7..a80aa2ff05b 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -485,6 +485,12 @@ var MockServer = Class.extend({ _.each(kwargs.views, function (view_descr) { var viewID = view_descr[0] || false; var viewType = view_descr[1]; + if (!viewID) { + var contextKey = viewType + '_view_ref'; + if (contextKey in kwargs.context) { + viewID = kwargs.context[contextKey]; + } + } var key = [model, viewID, viewType].join(','); var arch = self.archs[key]; if (!arch) { diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index d4054e9f7af..b899fee7a64 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -329,7 +329,367 @@ QUnit.module('Views', { kanban.destroy(); }); - QUnit.test('quick create and change state in grouped mode', function (assert) { + QUnit.test('quick create record without quick_create_view', function (assert) { + assert.expect(16); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
', + groupBy: ['bar'], + mockRPC: function (route, args) { + assert.step(args.method || route); + if (args.method === 'name_create') { + assert.strictEqual(args.args[0], 'new partner', + "should send the correct value"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "first column should contain one record"); + + // click on 'Create' -> should open the quick create in the first column + kanban.$buttons.find('.o-kanban-button-new').click(); + var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); + + assert.strictEqual($quickCreate.length, 1, + "should have a quick create element in the first column"); + assert.strictEqual($quickCreate.find('.o_form_view.o_xxs_form_view').length, 1, + "should have rendered an XXS form view"); + assert.strictEqual($quickCreate.find('input').length, 1, + "should have only one input"); + assert.ok($quickCreate.find('input').hasClass('o_required_modifier'), + "the field should be required"); + assert.strictEqual($quickCreate.find('input[placeholder=Title]').length, 1, + "input placeholder should be 'Title'"); + + // fill the quick create and validate + $quickCreate.find('input').val('new partner').trigger('input'); + $quickCreate.find('button.o_kanban_add').click(); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2, + "first column should contain two records"); + + assert.verifySteps([ + 'read_group', // initial read_group + '/web/dataset/search_read', // initial search_read (first column) + '/web/dataset/search_read', // initial search_read (second column) + 'default_get', // quick create + 'name_create', // should perform a name_create to create the record + 'read', // read the created record + 'default_get', // reopen the quick create automatically + ]); + + kanban.destroy(); + }); + + QUnit.test('quick create record with quick_create_view', function (assert) { + assert.expect(17); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
', + archs: { + 'partner,some_view_ref,form': '
' + + '' + + '' + + '' + + '', + }, + groupBy: ['bar'], + mockRPC: function (route, args) { + assert.step(args.method || route); + if (args.method === 'create') { + assert.deepEqual(args.args[0], { + foo: 'new partner', + int_field: 4, + state: 'def', + }, "should send the correct values"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "first column should contain one record"); + + // click on 'Create' -> should open the quick create in the first column + kanban.$buttons.find('.o-kanban-button-new').click(); + var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); + + assert.strictEqual($quickCreate.length, 1, + "should have a quick create element in the first column"); + assert.strictEqual($quickCreate.find('.o_form_view.o_xxs_form_view').length, 1, + "should have rendered an XXS form view"); + assert.strictEqual($quickCreate.find('input').length, 2, + "should have two inputs"); + assert.strictEqual($quickCreate.find('.o_field_widget').length, 3, + "should have rendered three widgets"); + + // fill the quick create and validate + $quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input'); + $quickCreate.find('.o_field_widget[name=int_field]').val('4').trigger('input'); + $quickCreate.find('.o_field_widget[name=state] .o_priority_star:first').click(); + $quickCreate.find('button.o_kanban_add').click(); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2, + "first column should contain two records"); + + assert.verifySteps([ + 'read_group', // initial read_group + '/web/dataset/search_read', // initial search_read (first column) + '/web/dataset/search_read', // initial search_read (second column) + 'load_views', // form view in quick create + 'default_get', // quick create + 'create', // should perform a create to create the record + 'read', // read the created record + 'load_views', // form view in quick create (is actually in cache) + 'default_get', // reopen the quick create automatically + ]); + + kanban.destroy(); + }); + + QUnit.test('quick create record in grouped on m2o (no quick_create_view)', function (assert) { + assert.expect(12); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
' + + '
', + groupBy: ['product_id'], + mockRPC: function (route, args) { + assert.step(args.method || route); + if (args.method === 'name_create') { + assert.strictEqual(args.args[0], 'new partner', + "should send the correct value"); + assert.deepEqual(args.kwargs.context, { + default_product_id: 3, + default_qux: 2.5, + }, "should send the correct context"); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + context: {default_qux: 2.5}, + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2, + "first column should contain two records"); + + // click on 'Create', fill the quick create and validate + kanban.$buttons.find('.o-kanban-button-new').click(); + var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); + $quickCreate.find('input').val('new partner').trigger('input'); + $quickCreate.find('button.o_kanban_add').click(); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3, + "first column should contain three records"); + + assert.verifySteps([ + 'read_group', // initial read_group + '/web/dataset/search_read', // initial search_read (first column) + '/web/dataset/search_read', // initial search_read (second column) + 'default_get', // quick create + 'name_create', // should perform a name_create to create the record + 'read', // read the created record + 'default_get', // reopen the quick create automatically + ]); + + kanban.destroy(); + }); + + QUnit.test('quick create record in grouped on m2o (with quick_create_view)', function (assert) { + assert.expect(14); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
', + archs: { + 'partner,some_view_ref,form': '
' + + '' + + '' + + '' + + '', + }, + groupBy: ['product_id'], + mockRPC: function (route, args) { + assert.step(args.method || route); + if (args.method === 'create') { + assert.deepEqual(args.args[0], { + foo: 'new partner', + int_field: 4, + state: 'def', + }, "should send the correct values"); + assert.deepEqual(args.kwargs.context, { + default_product_id: 3, + default_qux: 2.5, + }, "should send the correct context"); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + context: {default_qux: 2.5}, + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2, + "first column should contain two records"); + + // click on 'Create', fill the quick create and validate + kanban.$buttons.find('.o-kanban-button-new').click(); + var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); + $quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input'); + $quickCreate.find('.o_field_widget[name=int_field]').val('4').trigger('input'); + $quickCreate.find('.o_field_widget[name=state] .o_priority_star:first').click(); + $quickCreate.find('button.o_kanban_add').click(); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3, + "first column should contain three records"); + + assert.verifySteps([ + 'read_group', // initial read_group + '/web/dataset/search_read', // initial search_read (first column) + '/web/dataset/search_read', // initial search_read (second column) + 'load_views', // form view in quick create + 'default_get', // quick create + 'create', // should perform a create to create the record + 'read', // read the created record + 'load_views', // form view in quick create (is actually in cache) + 'default_get', // reopen the quick create automatically + ]); + + kanban.destroy(); + }); + + QUnit.test('quick create record with default values and onchanges', function (assert) { + assert.expect(11); + + this.data.partner.fields.int_field.default = 4; + this.data.partner.onchanges = { + foo: function (obj) { + if (obj.foo) { + obj.int_field = 8; + } + }, + }; + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
', + archs: { + 'partner,some_view_ref,form': '
' + + '' + + '' + + '', + }, + groupBy: ['bar'], + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + }, + }); + + // click on 'Create' -> should open the quick create in the first column + kanban.$buttons.find('.o-kanban-button-new').click(); + var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); + + assert.strictEqual($quickCreate.length, 1, + "should have a quick create element in the first column"); + assert.strictEqual($quickCreate.find('.o_field_widget[name=int_field]').val(), '4', + "default value should be set"); + + // fill the 'foo' field -> should trigger the onchange + $quickCreate.find('.o_field_widget[name=foo]').val('new partner').trigger('input'); + + assert.strictEqual($quickCreate.find('.o_field_widget[name=int_field]').val(), '8', + "onchange should have been triggered"); + + assert.verifySteps([ + 'read_group', // initial read_group + '/web/dataset/search_read', // initial search_read (first column) + '/web/dataset/search_read', // initial search_read (second column) + 'load_views', // form view in quick create + 'default_get', // quick create + 'onchange', // default_get's onchange + 'onchange', // onchange due to 'foo' field change + ]); + + kanban.destroy(); + }); + + QUnit.test('quick create record with quick_create_view: modifiers', function (assert) { + assert.expect(3); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
', + archs: { + 'partner,some_view_ref,form': '
' + + '' + + '' + + arch: '' + '' + '' + '
' + @@ -389,34 +749,41 @@ QUnit.module('Views', { } }); - // click to add an element and cancel the quick creation + // click to add an element and cancel the quick creation by pressing ESC kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); var $quickCreate = kanban.$('.o_kanban_quick_create'); assert.strictEqual($quickCreate.length, 1, "should have a quick create element"); - $quickCreate.find('input').trigger($.Event('keydown', {keyCode: $.ui.keyCode.ESCAPE})); + $quickCreate.find('input').trigger($.Event('keydown', { + keyCode: $.ui.keyCode.ESCAPE, + which: $.ui.keyCode.ESCAPE, + })); assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0, "should have destroyed the quick create element"); - //click to add and element and focus out the blank input, should cancel the quick creation + // click to add and element and click outside, should cancel the quick creation kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); - $quickCreate = kanban.$('.o_kanban_quick_create'); - $quickCreate.find('input').blur(); + kanban.$('.o_kanban_group .o_kanban_record:first').click(); assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0, - "Blur should have destroyed the quick create element"); + "the quick create should be destroyed when the user clicks outside"); // click to really add an element kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); $quickCreate = kanban.$('.o_kanban_quick_create'); - $quickCreate.find('input').val('new partner'); - $quickCreate.find('input').blur(); + $quickCreate.find('input').val('new partner').trigger('input'); - // When focus out the input containing value, should not delete the Quick Creation + // clicking outside should no longer destroy the quick create as it is dirty + kanban.$('.o_kanban_group .o_kanban_record:first').click(); assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1, - "Blur should not have destroyed the quick create element"); + "the quick create should not have been destroyed"); + + // confirm by pressing ENTER nbRecords = 5; - $quickCreate.find('button.o_kanban_add').click(); + $quickCreate.find('input').trigger($.Event('keydown', { + keyCode: $.ui.keyCode.ENTER, + which: $.ui.keyCode.ENTER, + })); assert.strictEqual(this.data.partner.records.length, 5, "should have created a partner"); @@ -470,7 +837,128 @@ QUnit.module('Views', { kanban.destroy(); }); - QUnit.test('quick create and edit in grouped mode', function (assert) { + QUnit.test('quick create record: cancel when not dirty', function (assert) { + assert.expect(9); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
', + groupBy: ['bar'], + }); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "first column should contain one record"); + + // click to add an element + kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1, + "should have open the quick create widget"); + + // click again to add an element -> should have kept the quick create open + kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1, + "should have kept the quick create open"); + + // click outside: should remove the quick create + kanban.$('.o_kanban_group .o_kanban_record:first').click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0, + "the quick create should not have been destroyed"); + + // click to reopen the quick create + kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1, + "should have open the quick create widget"); + + // press ESC: should remove the quick create + kanban.$('.o_kanban_quick_create input').trigger($.Event('keydown', { + keyCode: $.ui.keyCode.ESCAPE, + which: $.ui.keyCode.ESCAPE, + })); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0, + "quick create widget should have been removed"); + + // click to reopen the quick create + kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1, + "should have open the quick create widget"); + + // click on 'Discard': should remove the quick create + kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); + kanban.$('.o_kanban_group .o_kanban_record:first').click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0, + "the quick create should be destroyed when the user clicks outside"); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "first column should still contain one record"); + + kanban.destroy(); + }); + + QUnit.test('quick create record: cancel when dirty', function (assert) { + assert.expect(7); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '
' + + '
', + groupBy: ['bar'], + }); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "first column should contain one record"); + + // click to add an element and edit it + kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1, + "should have open the quick create widget"); + + var $quickCreate = kanban.$('.o_kanban_quick_create'); + $quickCreate.find('input').val('some value').trigger('input'); + + // click outside: should not remove the quick create + kanban.$('.o_kanban_group .o_kanban_record:first').click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1, + "the quick create should not have been destroyed"); + + // press ESC: should remove the quick create + $quickCreate.find('input').trigger($.Event('keydown', { + keyCode: $.ui.keyCode.ESCAPE, + which: $.ui.keyCode.ESCAPE, + })); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0, + "quick create widget should have been removed"); + + // click to reopen quick create and edit it + kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1, + "should have open the quick create widget"); + + $quickCreate = kanban.$('.o_kanban_quick_create'); + $quickCreate.find('input').val('some value').trigger('input'); + + // click on 'Discard': should remove the quick create + kanban.$('.o_kanban_quick_create .o_kanban_cancel').click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0, + "the quick create should be destroyed when the user clicks outside"); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "first column should still contain one record"); + + kanban.destroy(); + }); + + QUnit.test('quick create record and edit in grouped mode', function (assert) { assert.expect(6); var newRecordID; @@ -510,7 +998,7 @@ QUnit.module('Views', { var $quickCreate = kanban.$('.o_kanban_quick_create'); kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click(); $quickCreate = kanban.$('.o_kanban_quick_create'); - $quickCreate.find('input').val('new partner'); + $quickCreate.find('input').val('new partner').trigger('input'); $quickCreate.find('button.o_kanban_edit').click(); assert.strictEqual(this.data.partner.records.length, 5, @@ -549,7 +1037,8 @@ QUnit.module('Views', { kanban.$('.o_kanban_quick_create input') .val('new partner 1') - .trigger($.Event('keypress', { + .trigger('input') + .trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); @@ -562,7 +1051,8 @@ QUnit.module('Views', { // create a second element in a row kanban.$('.o_kanban_quick_create input') .val('new partner 2') - .trigger($.Event('keypress', { + .trigger('input') + .trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); @@ -621,7 +1111,8 @@ QUnit.module('Views', { def = $.Deferred(); kanban.$('.o_kanban_quick_create input') .val('new partner 1') - .trigger($.Event('keypress', { + .trigger('input') + .trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); @@ -629,7 +1120,8 @@ QUnit.module('Views', { def = null; kanban.$('.o_kanban_quick_create input') .val('new partner 2') - .trigger($.Event('keypress', { + .trigger('input') + .trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); @@ -649,7 +1141,7 @@ QUnit.module('Views', { kanban.destroy(); }); - QUnit.test('quick create fail in grouped', function (assert) { + QUnit.test('quick create record fail in grouped', function (assert) { assert.expect(7); var kanban = createView({ @@ -690,7 +1182,11 @@ QUnit.module('Views', { kanban.$('.o_kanban_quick_create input') .val('test') - .trigger($.Event('keypress', {keyCode: $.ui.keyCode.ENTER})); + .trigger('input') + .trigger($.Event('keydown', { + keyCode: $.ui.keyCode.ENTER, + which: $.ui.keyCode.ENTER, + })); assert.strictEqual($('.modal .o_form_view.o_form_editable').length, 1, "a form view dialog should have been opened (in edit)"); @@ -1181,37 +1677,6 @@ QUnit.module('Views', { kanban.destroy(); }); - QUnit.test('quick create record & column in grouped on m2o', function (assert) { - assert.expect(2); - - var kanban = createView({ - View: KanbanView, - model: 'partner', - data: this.data, - arch: '' + - '' + - '' + - '
' + - '
' + - '
', - groupBy: ['product_id'], - }); - kanban.$('.o_kanban_group:first .o_kanban_quick_add').click(); - var $quickCreate = kanban.$('.o_kanban_quick_create'); - $quickCreate.find('input').val('new partner'); - $quickCreate.find('button.o_kanban_add').click(); - assert.strictEqual(this.data.partner.records.length, 5, - "should have created a partner"); - - kanban.$('.o_column_quick_create').click(); - kanban.$('.o_column_quick_create input').val('new column'); - kanban.$('.o_column_quick_create button.o_kanban_add').click(); - - assert.strictEqual(kanban.$('.o_kanban_group:last span:contains(new column)').length, 1, - "the last column should be the newly created one"); - kanban.destroy(); - }); - QUnit.test('delete a column in grouped on m2o', function (assert) { assert.expect(28); @@ -2172,7 +2637,7 @@ QUnit.module('Views', { kanban.$('.o_kanban_group:eq(1) .o_kanban_quick_add i').click(); var $quickCreate = kanban.$('.o_kanban_group:eq(1) .o_kanban_quick_create'); - $quickCreate.find('input').val('new partner'); + $quickCreate.find('input').val('new partner').trigger('input'); $quickCreate.find('button.o_kanban_add').click(); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 0, @@ -2232,12 +2697,12 @@ QUnit.module('Views', { kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_add i').click(); var $quickCreate = kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_create'); - $quickCreate.find('input').val('record1'); + $quickCreate.find('input').val('record1').trigger('input'); $quickCreate.find('button.o_kanban_add').click(); kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_add i').click(); $quickCreate = kanban.$('.o_kanban_group:eq(0) .o_kanban_quick_create'); - $quickCreate.find('input').val('record2'); + $quickCreate.find('input').val('record2').trigger('input'); $quickCreate.find('button.o_kanban_add').click(); assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 2, @@ -2289,7 +2754,7 @@ QUnit.module('Views', { // add a new column kanban.$('.o_kanban_group:first .o_kanban_quick_add').click(); var $quickCreate = kanban.$('.o_kanban_quick_create'); - $quickCreate.find('input').val('new partner'); + $quickCreate.find('input').val('new partner').trigger('input'); $quickCreate.find('button.o_kanban_add').click(); assert.strictEqual(this.data.partner.records.length, 5, "should have created a 'new partner' column"); @@ -2414,11 +2879,11 @@ QUnit.module('Views', { groupBy: ['bar'], }); - var initialCount = parseInt(kanban.$('.o_kanban_counter_side').eq(1).text()); - kanban.$('.o_kanban_quick_add').eq(1).click(); - kanban.$('.o_input').val('Test'); + var initialCount = parseInt(kanban.$('.o_kanban_counter_side:first').text()); + kanban.$('.o_kanban_quick_add:first').click(); + kanban.$('.o_kanban_quick_create input').val('Test').trigger('input'); kanban.$('.o_kanban_add').click(); - var lastCount = parseInt(kanban.$('.o_kanban_counter_side').eq(1).text()); + var lastCount = parseInt(kanban.$('.o_kanban_counter_side:first').text()); assert.strictEqual(lastCount, initialCount + 1, "kanban counters should have updated on quick create"); @@ -2455,6 +2920,45 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('column progressbars on quick create with quick_create_view are updated', function (assert) { + assert.expect(1); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '' + + '' + + '' + + '' + + '
' + + '' + + '
' + + '
' + + '
', + archs: { + 'partner,some_view_ref,form': '' + + '' + + '', + }, + groupBy: ['bar'], + }); + + var initialCount = parseInt(kanban.$('.o_kanban_counter_side:first').text()); + + // click on 'Create', fill the quick create and validate + kanban.$buttons.find('.o-kanban-button-new').click(); + var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create'); + $quickCreate.find('.o_field_widget[name=int_field]').val('44').trigger('input'); + $quickCreate.find('button.o_kanban_add').click(); + + var lastCount = parseInt(kanban.$('.o_kanban_counter_side:first').text()); + assert.strictEqual(lastCount, initialCount + 44, + "kanban counters should have been updated on quick create"); + + kanban.destroy(); + }); + QUnit.test('keep adding quickcreate in first column after a record from this column was moved', function (assert) { assert.expect(2); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index f6e337a8731..44836526157 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -221,6 +221,7 @@ + From 973205f21d6c5e18ba3d0f160fedb4abf67ada73 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 4 Jan 2018 17:17:33 +0100 Subject: [PATCH 04/10] [IMP] crm,hr_recruitement,project: kanban quick create This rev. defines form views to use as quick create views in the kanban view of models 'crm.lead', 'hr.applicant' and 'project.task'. It uses a newly created mechanism allowing to specify a form view xmlid in the Kanban arch, such that the referenced view is rendered inside the QuickCreate widget (see parent commit). Task #27475. --- addons/crm/views/crm_lead_views.xml | 19 +++++++++++++++++-- .../views/hr_recruitment_views.xml | 18 +++++++++++++++++- addons/project/views/project_views.xml | 16 +++++++++++++++- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index e2971d27ea7..d896bc10c97 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -281,12 +281,28 @@ new + + crm.lead.form.quick_create + crm.lead + 1000 + +
+ + + + + + +
+
+
+ crm.lead.kanban.lead crm.lead - + @@ -305,7 +321,6 @@ -