diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-10-22 15:39:05 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-10-22 16:59:51 +0200 |
| commit | b81c2bce846c94b5695d680ebbc30f1cfe51e614 (patch) | |
| tree | 385190eb54adfbcdf8ff437d2cc667ddd1013718 /addons/web/static | |
| parent | 933165ec3afa0676de4b2f3662c4c1bb2f97efc5 (diff) | |
| parent | 3c108977c1e7d9361894c38bcd253030e8097479 (diff) | |
[MERGE] forward port branch saas-11.4 up to 3c108977c1e
Diffstat (limited to 'addons/web/static')
23 files changed, 1019 insertions, 101 deletions
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 00b8874a6b1..b324d6f2efa 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -500,6 +500,10 @@ var FieldDate = InputField.extend({ this._super.apply(this, arguments); // use the session timezone when formatting dates this.formatOptions.timezone = true; + this.datepickerOptions = _.defaults( + this.nodeOptions.datepicker || {}, + {defaultDate: this.value} + ); }, /** * In edit mode, instantiates a DateWidget datepicker and listen to changes. @@ -583,13 +587,7 @@ var FieldDate = InputField.extend({ * @private */ _makeDatePicker: function () { - return new datepicker.DateWidget( - this, - _.defaults( - this.nodeOptions.datepicker || {}, - {defaultDate: this.value} - ) - ); + return new datepicker.DateWidget(this, this.datepickerOptions); }, /** @@ -607,6 +605,17 @@ var FieldDate = InputField.extend({ var FieldDateTime = FieldDate.extend({ supportedFieldTypes: ['datetime'], + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + if (this.value) { + var offset = this.getSession().getTZOffset(this.value); + var displayedValue = this.value.clone().add(offset, 'minutes'); + this.datepickerOptions.defaultDate = displayedValue; + } + }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -637,10 +646,8 @@ var FieldDateTime = FieldDate.extend({ * @private */ _makeDatePicker: function () { - var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes'); - return new datepicker.DateTimeWidget(this, {defaultDate: value}); + return new datepicker.DateTimeWidget(this, this.datepickerOptions); }, - /** * Set the datepicker to the right value rather than the default one. * @@ -1171,6 +1178,9 @@ var HandleWidget = AbstractField.extend({ var FieldEmail = InputField.extend({ className: 'o_field_email', + events: _.extend({}, InputField.prototype.events, { + 'click': '_onClick', + }), prefix: 'mailto', supportedFieldTypes: ['char'], @@ -1211,7 +1221,21 @@ var FieldEmail = InputField.extend({ this.$el.text(this.value) .addClass('o_form_uri o_text_overflow') .attr('href', this.prefix + ':' + this.value); - } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Prevent the URL click from opening the record (when used on a list). + * + * @private + * @param {MouseEvent} ev + */ + _onClick: function (ev) { + ev.stopPropagation(); + }, }); var FieldPhone = FieldEmail.extend({ @@ -1238,6 +1262,9 @@ var FieldPhone = FieldEmail.extend({ var UrlWidget = InputField.extend({ className: 'o_field_url', + events: _.extend({}, InputField.prototype.events, { + 'click': '_onClick', + }), supportedFieldTypes: ['char'], /** @@ -1279,7 +1306,21 @@ var UrlWidget = InputField.extend({ .addClass('o_form_uri o_text_overflow') .attr('target', '_blank') .attr('href', this.value); - } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Prevent the URL click from opening the record (when used on a list). + * + * @private + * @param {MouseEvent} ev + */ + _onClick: function (ev) { + ev.stopPropagation(); + }, }); var CopyClipboard = { @@ -1645,19 +1686,23 @@ var FieldPdfViewer = FieldBinaryFile.extend({ }, /** * @private + * @param {string} [fileURI] file URI if specified * @returns {string} the pdf viewer URI */ - _getURI: function () { - var queryObj = { - model: this.model, - field: this.name, - id: this.res_id, - }; + _getURI: function (fileURI) { var page = this.recordData[this.name + '_page'] || 1; - var queryString = $.param(queryObj); - var url = encodeURIComponent('/web/image?' + queryString); + if (!fileURI) { + var queryObj = { + model: this.model, + field: this.name, + id: this.res_id, + }; + var queryString = $.param(queryObj); + fileURI = '/web/image?' + queryString + } + fileURI = encodeURIComponent(fileURI); var viewerURL = '/web/static/lib/pdfjs/web/viewer.html?file='; - return viewerURL + url + '#page=' + page; + return viewerURL + fileURI + '#page=' + page; }, /** * @private @@ -1701,14 +1746,16 @@ var FieldPdfViewer = FieldBinaryFile.extend({ */ on_file_change: function (ev) { this._super.apply(this, arguments); + var files = ev.target.files; + if (!files || files.length === 0) { + return; + } + // TOCheck: is there requirement to fallback on FileReader if browser don't support URL + var fileURI = URL.createObjectURL(files[0]); if (this.PDFViewerApplication) { - var files = ev.target.files; - if (!files || files.length === 0) { - return; - } - var file = files[0]; - // TOCheck: is there requirement to fallback on FileReader if browser don't support URL - this.PDFViewerApplication.open(URL.createObjectURL(file), 0); + this.PDFViewerApplication.open(fileURI, 0); + } else { + this.$('.o_pdfview_iframe').attr('src', this._getURI(fileURI)); } }, /** diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index e27128a1e3b..bf46c538c23 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -2584,10 +2584,12 @@ var FieldSelectionBadge = FieldSelection.extend({ * a FieldMany2one for its value. * Its intern representation is similar to the many2one (a datapoint with a * `name_get` as data). + * Note that there is some logic to support char field because of one use in our + * codebase, but this use should be removed along with this note. */ var FieldReference = FieldMany2One.extend({ specialData: "_fetchSpecialReference", - supportedFieldTypes: ['char', 'reference'], + supportedFieldTypes: ['reference'], template: 'FieldReference', events: _.extend({}, FieldMany2One.prototype.events, { 'change select': '_onSelectionChange', diff --git a/addons/web/static/src/js/services/notification_service.js b/addons/web/static/src/js/services/notification_service.js index cc87305a6a2..a0d2720604b 100644 --- a/addons/web/static/src/js/services/notification_service.js +++ b/addons/web/static/src/js/services/notification_service.js @@ -58,6 +58,8 @@ var NotificationService = AbstractService.extend({ * Note that this method does not wait for the appendTo method to complete. * * @param {Object} params + * @param {function} [params.Notification] javascript class of a notification + * to instantiate by default use 'web.Notification' * @param {string} params.title notification title * @param {string} params.message notification main message * @param {string} params.type 'notification' or 'warning' @@ -78,7 +80,8 @@ var NotificationService = AbstractService.extend({ this.$el = $('<div class="o_notification_manager"/>'); this.$el.prependTo('body'); } - var notification = this.notifications[++id] = new Notification(this, params); + var NotificationWidget = params.Notification || Notification; + var notification = this.notifications[++id] = new NotificationWidget(this, params); notification.appendTo(this.$el); return id; }, diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index 7705ee5972e..f3277d394a3 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -308,3 +308,55 @@ var AbstractView = Class.extend({ return AbstractView; }); + +odoo.define('web.viewUtils', function () { +"use strict"; + +/** + * FIXME: move this module to its own file in master + */ + +var utils = { + /** + * Returns the value of a group dataPoint, i.e. the value of the groupBy + * field for the records in that group. + * + * @param {Object} group dataPoint of type list, corresponding to a group + * @param {string} groupByField the name of the groupBy field + * @returns {string | integer | false} + */ + getGroupValue: function (group, groupByField) { + var groupedByField = group.fields[groupByField]; + switch (groupedByField.type) { + case 'many2one': + return group.res_id || false; + case 'selection': + var descriptor = _.findWhere(groupedByField.selection, group.value); + return descriptor && descriptor[0]; + default: + return group.value; + } + }, + /** + * States whether or not the quick create feature is available for the given + * datapoint, depending on its groupBy field. + * + * @param {Object} list dataPoint of type list + * @returns {Boolean} true iff the kanban quick create feature is available + */ + isQuickCreateEnabled: function (list) { + var groupByField = list.groupedBy[0] && list.groupedBy[0].split(':')[0]; + if (!groupByField) { + return false; + } + var availableTypes = ['char', 'boolean', 'many2one', 'selection']; + if (!_.contains(availableTypes, list.fields[groupByField].type)) { + return false; + } + return true; + }, +}; + +return utils; + +}); diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 0afb65a9a88..de879018ad5 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1016,27 +1016,51 @@ var BasicModel = AbstractModel.extend({ route: '/web/dataset/resequence', params: params, }) - .then(function () { - var offset = options.offset ? options.offset : 0; - var old_data = data.data.slice(); - data.data = _.sortBy(data.data, function (d) { - if (_.contains(resIDs, self.localData[d].res_id)) { - return _.indexOf(resIDs, self.localData[d].res_id) + offset; - } else { - return _.indexOf(old_data, d); - } - }); - data.res_ids = []; - _.each(data.data, function (d) { - var dataPoint = self.localData[d]; - if (dataPoint.type === 'record') { - data.res_ids.push(dataPoint.res_id); - } else { - data.res_ids = data.res_ids.concat(dataPoint.res_ids); + .then(function (wasResequenced) { + if (!wasResequenced) { + // the field on which the resequence was triggered does not + // exist, so no resequence happened server-side + return $.when(); + } + var field = params.field ? params.field : 'sequence'; + + return self._rpc({ + model: modelName, + method: 'read', + args: [resIDs, [field]], + }).then(function (records) { + if (data.data.length) { + var dataType = self.localData[data.data[0]].type; + if (dataType === 'record') { + _.each(data.data, function (dataPoint) { + var recordData = self.localData[dataPoint].data; + var inRecords = _.findWhere(records, {id: recordData.id}); + if (inRecords) { + recordData[field] = inRecords[field]; + } + }); + data.data = _.sortBy(data.data, function (d) { + return self.localData[d].data[field]; + }); + } + if (dataType === 'list') { + data.data = _.sortBy(data.data, function (d) { + return _.indexOf(resIDs, self.localData[d].res_id) + }); + } } - }); - self._updateParentResIDs(data); - return parentID; + data.res_ids = []; + _.each(data.data, function (d) { + var dataPoint = self.localData[d]; + if (dataPoint.type === 'record') { + data.res_ids.push(dataPoint.res_id); + } else { + data.res_ids = data.res_ids.concat(dataPoint.res_ids); + } + }); + self._updateParentResIDs(data); + return parentID; + }) }); }, /** 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 7bc5e95bfc8..9e1cde2aada 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -225,7 +225,7 @@ var BasicView = AbstractView.extend({ attrs.options = attrs.options ? pyUtils.py_eval(attrs.options) : {}; } - if (attrs.on_change && !field.onChange) { + if (attrs.on_change && attrs.on_change !== "0" && !field.onChange) { field.onChange = "1"; } @@ -378,6 +378,10 @@ var BasicView = AbstractView.extend({ if (!(dependency_name in fields)) { fields[dependency_name] = dependency_dict; } + + if (fv.fields && !(dependency_name in fv.fields)) { + fv.fields[dependency_name] = dependency_dict; + } } } return false; diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js index e76a487b73b..c2c90ea7e62 100644 --- a/addons/web/static/src/js/views/calendar/calendar_renderer.js +++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js @@ -77,9 +77,9 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, { self.model.get(recordID), { mode: 'edit', - can_create: false, attrs: { - 'placeholder': _.str.sprintf(_t("Add %s"), self.title), + placeholder: _.str.sprintf(_t("Add %s"), self.title), + can_create: false }, }); }); 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 190fbe88ad2..c7a32b72ba9 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -995,10 +995,7 @@ var FormRenderer = BasicRenderer.extend({ * @param {OdooEvent} ev */ _onNavigationMove: function (ev) { - if (ev.data.direction !== "cancel") { - ev.stopPropagation(); - } - + ev.stopPropagation(); var index; if (ev.data.direction === "next") { index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target || ev.target); 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 ef75caca8c0..ffb63ad9083 100644 --- a/addons/web/static/src/js/views/kanban/kanban_column.js +++ b/addons/web/static/src/js/views/kanban/kanban_column.js @@ -7,6 +7,7 @@ var Dialog = require('web.Dialog'); var KanbanRecord = require('web.KanbanRecord'); var RecordQuickCreate = require('web.kanban_record_quick_create'); var view_dialogs = require('web.view_dialogs'); +var viewUtils = require('web.viewUtils'); var Widget = require('web.Widget'); var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar'); @@ -201,7 +202,7 @@ var KanbanColumn = Widget.extend({ this.trigger_up('close_quick_create'); // close other quick create widgets this.trigger_up('start_quick_create'); var context = this.data.getContext(); - context['default_' + this.groupedBy] = this.id; + context['default_' + this.groupedBy] = viewUtils.getGroupValue(this.data, this.groupedBy); this.quickCreateWidget = new RecordQuickCreate(this, { context: context, formViewRef: this.quickCreateView, 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 9599d29db26..e195b33068c 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -12,6 +12,7 @@ var Context = require('web.Context'); var core = require('web.core'); var Domain = require('web.Domain'); var view_dialogs = require('web.view_dialogs'); +var viewUtils = require('web.viewUtils'); var _t = core._t; var qweb = core.qweb; @@ -35,12 +36,15 @@ var KanbanController = BasicController.extend({ /** * @override * @param {Object} params + * @param {boolean} params.quickCreateEnabled set to false to disable the + * quick create feature */ init: function (parent, model, renderer, params) { this._super.apply(this, arguments); this.on_create = params.on_create; this.hasButtons = params.hasButtons; + this.quickCreateEnabled = params.quickCreateEnabled; }, //-------------------------------------------------------------------------- @@ -268,8 +272,8 @@ var KanbanController = BasicController.extend({ */ _onButtonNew: function () { var state = this.model.get(this.handle, {raw: true}); - var hasColumns = state.groupedBy.length > 0 && state.data.length > 0; - if (hasColumns && this.on_create === 'quick_create') { + var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state); + if (this.on_create === 'quick_create' && quickCreateEnabled) { // Activate the quick create in the first column this.renderer.addQuickCreate(); } else if (this.on_create && this.on_create !== 'quick_create') { @@ -396,10 +400,11 @@ var KanbanController = BasicController.extend({ 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; + var groupedBy = state.groupedBy[0]; + context['default_' + groupedBy] = viewUtils.getGroupValue(columnState, groupedBy); new view_dialogs.FormViewDialog(self, { res_model: state.model, - context: _.extend({default_name: name}, context), + context: _.extend({default_name: values.name || values.display_name}, context), title: _t("Create"), disable_multiple_selection: true, on_saved: function (record) { 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 905ea05ac1e..fd21ed4df86 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -7,6 +7,7 @@ odoo.define('web.KanbanModel', function (require) { */ var BasicModel = require('web.BasicModel'); +var viewUtils = require('web.viewUtils'); var KanbanModel = BasicModel.extend({ @@ -105,7 +106,8 @@ var KanbanModel = BasicModel.extend({ 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 groupedBy = parent.groupedBy; + context['default_' + groupedBy] = viewUtils.getGroupValue(group, groupedBy); var def; if (Object.keys(values).length === 1 && 'display_name' in values) { // only 'display_name is given, perform a 'name_create' @@ -132,7 +134,7 @@ var KanbanModel = BasicModel.extend({ }, /** * Add the following (kanban specific) keys when performing a `get`: - * + * * - tooltipData * - progressBarValues * - isGroupedByM2ONoColumn diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js index 7013464ea87..d9d7ac77fe2 100644 --- a/addons/web/static/src/js/views/kanban/kanban_record.js +++ b/addons/web/static/src/js/views/kanban/kanban_record.js @@ -509,10 +509,12 @@ var KanbanRecord = Widget.extend({ ischild = false; } var test_event = events && events.click && (events.click.length > 1 || events.click[0].namespace !== 'bs.tooltip'); + var testLinkWithHref = elem.nodeName.toLowerCase() === 'a' && elem.href; if (ischild) { children.push(elem); - if (test_event) { - // do not trigger global click if one child has a click event registered + if (test_event || testLinkWithHref) { + // Do not trigger global click if one child has a click + // event registered (or it is a link with href) trigger = false; } } 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 c51ac945de1..9356311d51c 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js @@ -9,6 +9,7 @@ var ColumnQuickCreate = require('web.kanban_column_quick_create'); var QWeb = require('web.QWeb'); var session = require('web.session'); var utils = require('web.utils'); +var viewUtils = require('web.viewUtils'); var qweb = core.qweb; @@ -98,6 +99,9 @@ var KanbanRenderer = BasicRenderer.extend({ /** * @override + * @param {Object} params + * @param {boolean} params.quickCreateEnabled set to false to disable the + * quick create feature */ init: function (parent, state, params) { this._super.apply(this, arguments); @@ -116,6 +120,7 @@ var KanbanRenderer = BasicRenderer.extend({ if (this.columnOptions.hasProgressBar) { this.columnOptions.progressBarStates = {}; } + this.quickCreateEnabled = params.quickCreateEnabled; this._setState(state); }, /** @@ -453,6 +458,7 @@ var KanbanRenderer = BasicRenderer.extend({ groupedBy: groupByField, grouped_by_m2o: this.groupedByM2O, relation: relation, + quick_create: this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state), }); this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable; }, 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 791e3664668..523f0ef1c57 100644 --- a/addons/web/static/src/js/views/kanban/kanban_view.js +++ b/addons/web/static/src/js/views/kanban/kanban_view.js @@ -63,7 +63,6 @@ var KanbanView = BasicView.extend({ deletable: activeActions.group_delete, archivable: this.arch.attrs.archivable ? JSON.parse(this.arch.attrs.archivable) : true, group_creatable: activeActions.group_create && !config.device.isMobile, - quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(), quickCreateView: this.arch.attrs.quick_create_view || null, hasProgressBar: !!progressBar, }; @@ -72,6 +71,7 @@ var KanbanView = BasicView.extend({ deletable: activeActions.delete, read_only_mode: params.readOnlyMode, }; + this.rendererParams.quickCreateEnabled = this._isQuickCreateEnabled(); var examples = this.arch.attrs.examples; if (examples) { this.rendererParams.examples = kanbanExamplesRegistry.get(examples); @@ -80,6 +80,7 @@ var KanbanView = BasicView.extend({ this.controllerParams.on_create = this.arch.attrs.on_create; this.controllerParams.readOnlyMode = false; this.controllerParams.hasButtons = true; + this.controllerParams.quickCreateEnabled = this.rendererParams.quickCreateEnabled; if (config.device.isMobile) { this.jsLibs.push('/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js'); @@ -93,13 +94,10 @@ var KanbanView = BasicView.extend({ /** * @private * @param {Object} viewInfo + * @returns {boolean} true iff the quick create feature is not explicitely + * disabled (with create="False" or quick_create="False" in the arch) */ _isQuickCreateEnabled: function () { - var groupBy = this.loadParams.groupBy[0]; - groupBy = groupBy !== undefined ? groupBy.split(':')[0] : undefined; - if (groupBy !== undefined && !_.contains(['char', 'boolean', 'many2one'], this.fields[groupBy].type)) { - return false; - } if (!this.controllerParams.activeActions.create) { return false; } diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 907a02c0237..1eef5047f46 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -810,7 +810,7 @@ <t t-name="FieldMany2One"> <t t-if="widget.mode === 'readonly'"> - <a t-if="!widget.nodeOptions.no_open" class="o_form_uri" href="#"/> + <a t-if="!widget.nodeOptions.no_open" t-att-tabindex="widget.attrs.tabindex" class="o_form_uri" href="#"/> <span t-if="widget.nodeOptions.no_open"/> </t> <div t-if="widget.mode === 'edit'" class="o_field_widget o_field_many2one" aria-atomic="true"> diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 7db578b5d4e..81c21cf6052 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -10,6 +10,7 @@ var KanbanView = require('web.KanbanView'); var ListView = require('web.ListView'); var session = require('web.session'); var testUtils = require('web.test_utils'); +var field_registry = require('web.field_registry'); var createView = testUtils.createView; var createAsyncView = testUtils.createAsyncView; @@ -1907,6 +1908,49 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test("pdf_viewer: upload rendering", function (assert) { + assert.expect(6); + + testUtils.patch(field_registry.map.pdf_viewer, { + on_file_change: function (ev) { + ev.target = {files: [new Blob()]}; + this._super.apply(this, arguments); + }, + _getURI: function (fileURI) { + var res = this._super.apply(this, arguments); + assert.step('_getURI'); + assert.ok(_.str.startsWith(fileURI, 'blob:')); + this.PDFViewerApplication = { + open: function (URI) { + assert.step('open'); + assert.ok(_.str.startsWith(URI, 'blob:')); + }, + }; + return 'about:blank'; + }, + }); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<field name="document" widget="pdf_viewer"/>' + + '</form>', + }); + + // first upload initialize iframe + form.$('input[type="file"]').trigger('change'); + assert.verifySteps(['_getURI']); + // second upload call pdfjs method inside iframe + form.$('input[type="file"]').trigger('change'); + assert.verifySteps(['_getURI', 'open']); + + testUtils.unpatch(field_registry.map.pdf_viewer); + form.destroy(); + }); + QUnit.test('text field rendering in list view', function (assert) { assert.expect(1); @@ -2106,6 +2150,40 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test('image fields in x2many list are loaded correctly', function (assert) { + assert.expect(2); + + this.data.partner_type.fields.image = {name: 'image', type: 'binary'}; + this.data.partner_type.records[0].image = 'product_image'; + this.data.partner.records[0].timmy = [12]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="timmy" widget="many2many">' + + '<tree>' + + '<field name="image" widget="image"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + mockRPC: function (route, args) { + if (route === 'data:image/png;base64,product_image') { + assert.ok(true, "The list's image should have been fetched"); + return $.when(); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(form.$('tr.o_data_row').length, 1, + 'There should be one record in the many2many'); + + form.destroy(); + }); + QUnit.test('image fields with required attribute', function (assert) { assert.expect(2); @@ -2779,8 +2857,7 @@ QUnit.module('basic_fields', { form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('.o_datepicker_input').val(), expectedDateString, 'the datetime should be correct in edit mode'); - // click on the input and select 22 February at 8:23:33 - form.$('.o_datepicker_input').click(); + // select 22 February at 8:23:33 assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open'); $('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection $('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection @@ -2900,8 +2977,7 @@ QUnit.module('basic_fields', { assert.strictEqual(list.$('input.o_datepicker_input').val(), expectedDateString, 'the date should be correct in edit mode'); - // click on the input and select 22 February at 8:23:33 - list.$('input.o_datepicker_input').click(); + // select 22 February at 8:23:33 assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open'); $('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection $('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection @@ -3061,6 +3137,32 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test('datepicker option: daysOfWeekDisabled', function (assert) { + assert.expect(2); + + this.data.partner.fields.datetime.default = "2017-08-02 12:00:05"; + this.data.partner.fields.datetime.required = true; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="datetime" ' + + 'options=\'{"datepicker": {"daysOfWeekDisabled": [0, 6]}}\'/>' + + '</form>', + res_id: 1, + }); + + form.$buttons.find('.o_form_button_create').click(); + assert.ok($('.day:last-child(),.day:nth-child(2)').hasClass('disabled'), + 'first and last days must be disabled'); + assert.notOk($('.day:not(:last-child()):not(:nth-child(2))').hasClass('disabled'), + 'other days must stay clickable'); + + form.destroy(); + }); + QUnit.module('FieldMonetary'); QUnit.test('monetary field in form view', function (assert) { diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 99f97fbda54..e1e91863448 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -5580,7 +5580,7 @@ QUnit.module('relational_fields', { form.destroy(); }); - QUnit.test('pressing enter in a o2m with a required empty m2o', function (assert) { + QUnit.test('pressing enter in a o2m with a required empty field', function (assert) { assert.expect(4); this.data.turtle.fields.turtle_foo.required = true; @@ -10259,6 +10259,81 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('click on URL should not open the record', function (assert) { + assert.expect(2); + + this.data.partner.records[0].turtles = [1]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="turtles">' + + '<tree>' + + '<field name="display_name" widget="email"/>' + + '<field name="turtle_foo" widget="url"/>' + + '</tree>' + + '<form></form>' + + '</field>' + + '</form>', + res_id: 1, + }); + + form.$('.o_email_cell a').click(); + assert.strictEqual($('.modal .o_form_view').length, 0, + 'click should not open the modal'); + + form.$('.o_url_cell a').click(); + assert.strictEqual($('.modal .o_form_view').length, 0, + 'click should not open the modal'); + form.destroy(); + }); + + QUnit.test('create and edit on m2o in o2m, and press ESCAPE', function (assert) { + assert.expect(4); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="turtles">' + + '<tree editable="top">' + + '<field name="turtle_trululu"/>' + + '</tree>' + + '</field>' + + '</form>', + archs: { + 'partner,false,form': '<form><field name="display_name"/></form>', + }, + }); + + form.$('.o_field_x2many_list_row_add a').click(); + + assert.strictEqual(form.$('.o_selected_row').length, 1, + "should have create a new row in edition"); + + var $input = form.$('.o_field_widget[name="turtle_trululu"] input'); + $input.click(); + $input.autocomplete('widget').find('.o_m2o_dropdown_option').focus().click(); + + assert.strictEqual($('.modal .o_form_view').length, 1, + "should have opened a form view in a dialog"); + + $('.modal .o_form_view .o_field_widget[name=display_name]').trigger($.Event('keydown', { + which: $.ui.keyCode.ESCAPE, + keyCode: $.ui.keyCode.ESCAPE, + })); + + assert.strictEqual($('.modal .o_form_view').length, 0, + "should have closed the dialog"); + assert.strictEqual(form.$('.o_selected_row').length, 1, + "new row should still be present"); + + form.destroy(); + }); + QUnit.test('one2many add a line should not crash if orderedResIDs is not set', function (assert) { // There is no assertion, the code will just crash before the bugfix. assert.expect(0); @@ -13679,6 +13754,59 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('when creating a new many2one on a x2many then discarding it immediately with ESCAPE, it should not crash', function (assert) { + var done = assert.async(); + assert.expect(1); + + this.data.partner.records[0].turtles = []; + + var form = createView({ + View: FormView, + model: 'partner', + viewOptions: { + mode: 'edit', + }, + data: this.data, + arch:'<form string="Partners">' + + '<sheet>' + + '<field name="turtles">' + + '<tree editable="top">' + + '<field name="turtle_foo"/>' + + '<field name="turtle_trululu"/>' + + '</tree>' + + '</field>' + + '</sheet>' + + '</form>', + res_id: 1, + archs: { + 'partner,false,form': '<form><group><field name="foo"/><field name="bar"/></group></form>' + }, + }); + + // add a new line + form.$el.find('.o_field_x2many_list_row_add>a').click(); + + // open the field turtle_trululu (one2many) + var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; + form.$el.find('.o_input_dropdown>input').click(); + + concurrency.delay(0).then(function () { + // click create and edit + $('.ui-autocomplete .ui-menu-item a:contains(Create and)').trigger('mouseenter').click(); + + // hit escape immediately + var escapeKey = $.ui.keyCode.ESCAPE; + $(document.activeElement).trigger( + $.Event('keydown', {which: escapeKey, keyCode: escapeKey})); + + assert.ok('did not crash'); + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; + form.destroy(); + done(); + }); + }); + QUnit.test('navigating through an editable list with custom controls [REQUIRE FOCUS]', function (assert) { assert.expect(5); diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 83bd275d9e0..75f8afeeff2 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -904,10 +904,14 @@ var MockServer = Class.extend({ var offset = args.offset ? Number(args.offset) : 0; var field = args.field ? args.field : 'sequence'; var records = this.data[args.model].records; + if (!(field in this.data[args.model].fields)) { + return false; + } for (var i in args.ids) { var record = _.findWhere(records, {id: args.ids[i]}); record[field] = Number(i) + offset; } + return true; }, /** * Simulate a 'search_count' operation diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index 6594b274be1..6b8ddfee91b 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -257,6 +257,12 @@ function createAsyncView(params) { controlPanel.appendTo($web_client); var $content = $('<div>').addClass('o_content').appendTo($web_client); + if (params.interceptsPropagate) { + _.each(params.interceptsPropagate, function (cb, name) { + intercept(widget, name, cb, true); + }); + } + return view.getController(widget).then(function (view) { // override the view's 'destroy' so that it calls 'destroy' on the widget // instead, as the widget is the parent of the view and the mockServer. diff --git a/addons/web/static/tests/services/notification_service_tests.js b/addons/web/static/tests/services/notification_service_tests.js index 195222881c5..df3c1657c92 100644 --- a/addons/web/static/tests/services/notification_service_tests.js +++ b/addons/web/static/tests/services/notification_service_tests.js @@ -246,4 +246,38 @@ QUnit.module('Services', { }); }); + QUnit.test('Display a custom notification', function (assert) { + var done = assert.async(); + assert.expect(3); + + var Custom = Notification.extend({ + init: function (parent, params) { + this._super.apply(this, arguments); + assert.ok(params.customParams, 'instantiate custom notification'); + }, + start: function () { + var self = this; + return this._super().then(function () { + self.$el.html('Custom'); + }); + }, + }); + + var view = createView(this.viewParams); + view.call('notification', 'notify', { + Notification: Custom, + customParams: true, + }); + assert.strictEqual( + $('body .o_notification_manager .o_notification:contains(Custom)').length, 1, + "should display the notification"); + view.destroy(); + setTimeout(function () { + assert.strictEqual( + $('body .o_notification_manager .o_notification').length, 0, + "should destroy the notification"); + done(); + }); + }); + });}); diff --git a/addons/web/static/tests/views/kanban_model_tests.js b/addons/web/static/tests/views/kanban_model_tests.js index 1a3391c720d..20eb2918d04 100644 --- a/addons/web/static/tests/views/kanban_model_tests.js +++ b/addons/web/static/tests/views/kanban_model_tests.js @@ -192,6 +192,8 @@ QUnit.module('Views', { var done = assert.async(); assert.expect(8); + this.data.product.fields.sequence = {string: "Sequence", type: "integer"}; + this.data.partner.fields.sequence = {string: "Sequence", type: "integer"}; this.data.partner.records.push({id: 3, foo: 'aaa', product_id: 37}); var nbReseq = 0; @@ -204,7 +206,7 @@ QUnit.module('Views', { if (nbReseq === 1) { // resequencing columns assert.deepEqual(args.ids, [41, 37], "ids should be correct"); - assert.strictEqual(args.model, 'product_id', + assert.strictEqual(args.model, 'product', "model should be correct"); } else if (nbReseq === 2) { // resequencing records assert.deepEqual(args.ids, [3, 1], @@ -212,7 +214,6 @@ QUnit.module('Views', { assert.strictEqual(args.model, 'partner', "model should be correct"); } - return $.when(); } return this._super.apply(this, arguments); }, @@ -229,7 +230,7 @@ QUnit.module('Views', { "first group should be res_id 37"); // resequence columns - return model.resequence('product_id', [41, 37], stateID); + return model.resequence('product', [41, 37], stateID); }) .then(function (stateID) { var state = model.get(stateID); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 66476aaad35..17c0c022e85 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -1559,7 +1559,7 @@ QUnit.module('Views', { kanban.destroy(); }); - QUnit.test('quick create record fail in grouped', function (assert) { + QUnit.test('quick create record fail in grouped by many2one', function (assert) { assert.expect(8); var kanban = createView({ @@ -1684,6 +1684,120 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('quick create record fails in grouped by char', function (assert) { + assert.expect(7); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test" on_create="quick_create">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="foo"/></div>' + + '</t></templates>' + + '</kanban>', + archs: { + 'partner,false,form': '<form>' + + '<field name="foo"/>' + + '</form>', + }, + mockRPC: function (route, args) { + if (args.method === 'name_create') { + return $.Deferred().reject({ + code: 200, + data: {}, + message: "Odoo server error", + }, $.Event()); + } + if (args.method === 'create') { + assert.deepEqual(args.args[0], {foo: 'yop'}, + "should write the correct value for foo"); + assert.deepEqual(args.kwargs.context, {default_foo: 'yop', default_name: 'test'}, + "should send the correct default value for foo"); + } + return this._super.apply(this, arguments); + }, + groupBy: ['foo'], + }); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "there should be 1 record in first column"); + + kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click(); + kanban.$('.o_kanban_quick_create input').val('test').trigger('input'); + kanban.$('.o_kanban_add').click(); + + assert.strictEqual($('.modal .o_form_view.o_form_editable').length, 1, + "a form view dialog should have been opened (in edit)"); + assert.strictEqual($('.modal .o_field_widget[name=foo]').val(), 'yop', + "the correct default value for foo should already be set"); + + $('.modal-footer .btn-primary').click(); + + assert.strictEqual($('.modal').length, 0, "the modal should be closed"); + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2, + "there should be 2 records in first column"); + + kanban.destroy(); + }); + + QUnit.test('quick create record fails in grouped by selection', function (assert) { + assert.expect(7); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test" on_create="quick_create">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="state"/></div>' + + '</t></templates>' + + '</kanban>', + archs: { + 'partner,false,form': '<form>' + + '<field name="state"/>' + + '</form>', + }, + mockRPC: function (route, args) { + if (args.method === 'name_create') { + return $.Deferred().reject({ + code: 200, + data: {}, + message: "Odoo server error", + }, $.Event()); + } + if (args.method === 'create') { + assert.deepEqual(args.args[0], {state: 'abc'}, + "should write the correct value for state"); + assert.deepEqual(args.kwargs.context, {default_state: 'abc', default_name: 'test'}, + "should send the correct default value for state"); + } + return this._super.apply(this, arguments); + }, + groupBy: ['state'], + }); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "there should be 1 record in first column"); + + kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click(); + kanban.$('.o_kanban_quick_create input').val('test').trigger('input'); + kanban.$('.o_kanban_add').click(); + + assert.strictEqual($('.modal .o_form_view.o_form_editable').length, 1, + "a form view dialog should have been opened (in edit)"); + assert.strictEqual($('.modal .o_field_widget[name=state]').val(), '"abc"', + "the correct default value for state should already be set"); + + $('.modal-footer .btn-primary').click(); + + assert.strictEqual($('.modal').length, 0, "the modal should be closed"); + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2, + "there should be 2 records in first column"); + + kanban.destroy(); + }); + QUnit.test('quick create record in empty grouped kanban', function (assert) { assert.expect(3); @@ -1726,6 +1840,327 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('quick create record in grouped on date(time) field', function (assert) { + assert.expect(6); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test" on_create="quick_create">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="display_name"/></div>' + + '</t></templates>' + + '</kanban>', + groupBy: ['date'], + intercepts: { + switch_view: function (ev) { + assert.deepEqual(_.pick(ev.data, 'res_id', 'view_type'), { + res_id: undefined, + view_type: 'form', + }, "should trigger an event to open the form view (twice)"); + }, + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 0, + "quick create should be disabled when grouped on a date field"); + + // clicking on CREATE in control panel should not open a quick create + kanban.$buttons.find('.o-kanban-button-new').click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0, + "should not have opened the quick create widget"); + + kanban.reload({groupBy: ['datetime']}); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 0, + "quick create should be disabled when grouped on a datetime field"); + + // clicking on CREATE in control panel should not open a quick create + kanban.$buttons.find('.o-kanban-button-new').click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0, + "should not have opened the quick create widget"); + + kanban.destroy(); + }); + + QUnit.test('quick create record feature is properly enabled/disabled at reload', function (assert) { + assert.expect(3); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test" on_create="quick_create">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="display_name"/></div>' + + '</t></templates>' + + '</kanban>', + groupBy: ['foo'], + }); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3, + "quick create should be enabled when grouped on a char field"); + + kanban.reload({groupBy: ['date']}); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 0, + "quick create should now be disabled (grouped on date field)"); + + kanban.reload({groupBy: ['bar']}); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 2, + "quick create should be enabled again (grouped on boolean field)"); + + kanban.destroy(); + }); + + QUnit.test('quick create record in grouped by char field', function (assert) { + assert.expect(4); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test" on_create="quick_create">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="display_name"/></div>' + + '</t></templates>' + + '</kanban>', + groupBy: ['foo'], + mockRPC: function (route, args) { + if (args.method === 'name_create') { + assert.deepEqual(args.kwargs.context, {default_foo: 'yop'}, + "should send the correct default value for foo"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3, + "quick create should be enabled when grouped on a char field"); + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "first column should contain 1 record"); + + kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click(); + kanban.$('.o_kanban_quick_create input').val('new record').trigger('input'); + kanban.$('.o_kanban_add').click(); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2, + "first column should now contain 2 records"); + + kanban.destroy(); + }); + + QUnit.test('quick create record in grouped by boolean field', function (assert) { + assert.expect(4); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test" on_create="quick_create">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="display_name"/></div>' + + '</t></templates>' + + '</kanban>', + groupBy: ['bar'], + mockRPC: function (route, args) { + if (args.method === 'name_create') { + assert.deepEqual(args.kwargs.context, {default_bar: true}, + "should send the correct default value for bar"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 2, + "quick create should be enabled when grouped on a boolean field"); + assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 3, + "second column (true) should contain 3 records"); + + kanban.$('.o_kanban_header:nth(1) .o_kanban_quick_add i').click(); + kanban.$('.o_kanban_quick_create input').val('new record').trigger('input'); + kanban.$('.o_kanban_add').click(); + + assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 4, + "second column (true) should now contain 4 records"); + + kanban.destroy(); + }); + + QUnit.test('quick create record in grouped on selection field', function (assert) { + assert.expect(4); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test" on_create="quick_create">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="display_name"/></div>' + + '</t></templates>' + + '</kanban>', + mockRPC: function (route, args) { + if (args.method === 'name_create') { + assert.deepEqual(args.kwargs.context, {default_state: 'abc'}, + "should send the correct default value for bar"); + } + return this._super.apply(this, arguments); + }, + groupBy: ['state'], + }); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3, + "quick create should be enabled when grouped on a selection field"); + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "first column (abc) should contain 1 record"); + + kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click(); + kanban.$('.o_kanban_quick_create input').val('new record').trigger('input'); + kanban.$('.o_kanban_add').click(); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2, + "first column (abc) should contain 2 records"); + + kanban.destroy(); + }); + + QUnit.test('quick create record in grouped by char field (within quick_create_view)', function (assert) { + assert.expect(6); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="foo"/></div>' + + '</t></templates>' + + '</kanban>', + archs: { + 'partner,some_view_ref,form': '<form>' + + '<field name="foo"/>' + + '</form>', + }, + groupBy: ['foo'], + mockRPC: function (route, args) { + if (args.method === 'create') { + assert.deepEqual(args.args[0], {foo: 'yop'}, + "should write the correct value for foo"); + assert.deepEqual(args.kwargs.context, {default_foo: 'yop'}, + "should send the correct default value for foo"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3, + "quick create should be enabled when grouped on a char field"); + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "first column should contain 1 record"); + + kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create input').val(), 'yop', + "should have set the correct foo value by default"); + kanban.$('.o_kanban_add').click(); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2, + "first column should now contain 2 records"); + + kanban.destroy(); + }); + + QUnit.test('quick create record in grouped by boolean field (within quick_create_view)', function (assert) { + assert.expect(6); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="bar"/></div>' + + '</t></templates>' + + '</kanban>', + archs: { + 'partner,some_view_ref,form': '<form>' + + '<field name="bar"/>' + + '</form>', + }, + groupBy: ['bar'], + mockRPC: function (route, args) { + if (args.method === 'create') { + assert.deepEqual(args.args[0], {bar: true}, + "should write the correct value for bar"); + assert.deepEqual(args.kwargs.context, {default_bar: true}, + "should send the correct default value for bar"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 2, + "quick create should be enabled when grouped on a boolean field"); + assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 3, + "second column (true) should contain 3 records"); + + kanban.$('.o_kanban_header:nth(1) .o_kanban_quick_add i').click(); + assert.ok(kanban.$('.o_kanban_quick_create .o_field_boolean input').is(':checked'), + "should have set the correct bar value by default"); + kanban.$('.o_kanban_add').click(); + + assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 4, + "second column (true) should now contain 4 records"); + + kanban.destroy(); + }); + + QUnit.test('quick create record in grouped by selection field (within quick_create_view)', function (assert) { + assert.expect(6); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban on_create="quick_create" quick_create_view="some_view_ref">' + + '<templates><t t-name="kanban-box">' + + '<div><field name="state"/></div>' + + '</t></templates>' + + '</kanban>', + archs: { + 'partner,some_view_ref,form': '<form>' + + '<field name="state"/>' + + '</form>', + }, + groupBy: ['state'], + mockRPC: function (route, args) { + if (args.method === 'create') { + assert.deepEqual(args.args[0], {state: 'abc'}, + "should write the correct value for state"); + assert.deepEqual(args.kwargs.context, {default_state: 'abc'}, + "should send the correct default value for state"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3, + "quick create should be enabled when grouped on a selection field"); + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1, + "first column (abc) should contain 1 record"); + + kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click(); + assert.strictEqual(kanban.$('.o_kanban_quick_create select').val(), '"abc"', + "should have set the correct state value by default"); + kanban.$('.o_kanban_add').click(); + + assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2, + "first column (abc) should now contain 2 records"); + + kanban.destroy(); + }); + QUnit.test('many2many_tags in kanban views', function (assert) { assert.expect(12); @@ -1796,6 +2231,72 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('Do not open record when clicking on `a` with `href`', function (assert) { + assert.expect(5); + + this.data.partner.records = [ + { id: 1, foo: 'yop' }, + ]; + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test">' + + '<templates>' + + '<t t-name="kanban-box">' + + '<div class="oe_kanban_global_click">' + + '<field name="foo"/>' + + '<div>' + + '<a class="o_test_link" href="#">test link</a>' + + '</div>' + + '</div>' + + '</t>' + + '</templates>' + + '</kanban>', + intercepts: { + // when clicking on a record in kanban view, + // it switches to form view. + switch_view: function () { + throw new Error("should not switch view"); + }, + }, + }); + + var $record = kanban.$('.o_kanban_record:not(.o_kanban_ghost)'); + assert.strictEqual($record.length, 1, + "should display a kanban record"); + + var $testLink = $record.find('a'); + assert.strictEqual($testLink.length, 1, + "should contain a link in the kanban record"); + assert.ok(!!$testLink[0].href, + "link inside kanban record should have non-empty href"); + + // Mocked views prevent accessing a link with href. This is intented + // most of the time, but not in this test which specifically needs to + // let the browser access a link with href. + kanban.$el.off('click', 'a'); + // Prevent the browser default behaviour when clicking on anything. + // This includes clicking on a `<a>` with `href`, so that it does not + // change the URL in the address bar. + // Note that we should not specify a click listener on 'a', otherwise + // it may influence the kanban record global click handler to not open + // the record. + $(document.body).on('click.o_test', function (ev) { + assert.notOk(ev.isDefaultPrevented(), + "should not prevented browser default behaviour beforehand"); + assert.strictEqual(ev.target, $testLink[0], + "should have clicked on the test link in the kanban record"); + ev.preventDefault(); + }); + + $testLink.click(); + + $(document.body).off('click.o_test'); + kanban.destroy(); + }); + QUnit.test('can drag and drop a record from one column to the next', function (assert) { assert.expect(9); @@ -2258,7 +2759,7 @@ QUnit.module('Views', { }); QUnit.test('delete a column in grouped on m2o', function (assert) { - assert.expect(33); + assert.expect(36); testUtils.patch(KanbanRenderer, { _renderGrouped: function () { @@ -3224,6 +3725,7 @@ QUnit.module('Views', { QUnit.test('resequence columns in grouped by m2o', function (assert) { assert.expect(7); + this.data.product.fields.sequence = {string: "Sequence", type: "integer"}; var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test var kanban = createView({ @@ -3237,12 +3739,6 @@ QUnit.module('Views', { '</t></templates>' + '</kanban>', groupBy: ['product_id'], - mockRPC: function (route) { - if (route === '/web/dataset/resequence') { - return $.when(); - } - return this._super.apply(this, arguments); - }, intercepts: { env_updated: function (event) { assert.deepEqual(event.data.env.ids, envIDs, @@ -3266,7 +3762,7 @@ QUnit.module('Views', { kanban.update({}, {reload: false}); // re-render without reloading assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 5, - "first column should be id 5 before resequencing"); + "first column should be id 5 after resequencing"); kanban.destroy(); }); @@ -4085,7 +4581,7 @@ QUnit.module('Views', { '<div><field name="foo"/></div>' + '</t></templates>' + '</kanban>', - groupBy: ['int_field'], + groupBy: ['foo'], mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { return $.when(true); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 63e7013c824..5a0d00b6f6d 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -3223,10 +3223,10 @@ QUnit.module('Views', { foo: { fields: {int_field: {string: "int_field", type: "integer", sortable: true}}, records: [ - {id: 1, int_field: 0}, - {id: 2, int_field: 1}, - {id: 3, int_field: 2}, - {id: 4, int_field: 3}, + {id: 1, int_field: 11}, + {id: 2, int_field: 12}, + {id: 3, int_field: 13}, + {id: 4, int_field: 14}, ] } }; @@ -3245,14 +3245,15 @@ QUnit.module('Views', { assert.deepEqual(args, { model: "foo", ids: [4, 3], - offset: 2, + offset: 13, field: "int_field", }); } if (moves === 1) { assert.deepEqual(args, { model: "foo", - ids: [1, 4, 2, 3], + ids: [4, 2], + offset: 12, field: "int_field", }); } @@ -3260,14 +3261,15 @@ QUnit.module('Views', { assert.deepEqual(args, { model: "foo", ids: [2, 4], - offset: 1, + offset: 12, field: "int_field", }); } if (moves === 3) { assert.deepEqual(args, { model: "foo", - ids: [1, 4, 2, 3], + ids: [4, 2], + offset: 12, field: "int_field", }); } @@ -3334,7 +3336,6 @@ QUnit.module('Views', { "should write the right field as sequence"); assert.deepEqual(args.ids, [4, 2, 3], "should write the sequence in correct order"); - return $.when(); } return this._super.apply(this, arguments); }, @@ -3464,13 +3465,16 @@ QUnit.module('Views', { '</tree>', mockRPC: function (route, args) { if (route === '/web/dataset/resequence') { + var _super = this._super.bind(this); assert.strictEqual(args.offset, 1, "should write the sequence starting from the lowest current one"); assert.strictEqual(args.field, 'int_field', "should write the right field as sequence"); assert.deepEqual(args.ids, [4, 2, 3], "should write the sequence in correct order"); - return $.when(def); + return $.when(def).then(function () { + return _super(route, args); + }); } return this._super.apply(this, arguments); }, |
