diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-03-20 11:45:11 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-03-20 11:45:11 +0100 |
| commit | e0345a4a3f4cfa8f4de8723c6b72b741b06483a1 (patch) | |
| tree | 5ad51dbebfd89cd7e56bf12df2da3ecf4d8e9509 /addons/web | |
| parent | e9db6b6d9de1e0589e8871ca2e5e8dde8ec8cab7 (diff) | |
| parent | 2835d299792ffa925d86ad3460057be713ac4fac (diff) | |
[MERGE] forward port branch 11.0 up to 2835d299792
Diffstat (limited to 'addons/web')
| -rw-r--r-- | addons/web/static/lib/ace/ace.odoo-custom.js | 9 | ||||
| -rw-r--r-- | addons/web/static/src/js/chrome/menu.js | 17 | ||||
| -rw-r--r-- | addons/web/static/src/js/chrome/web_client.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/basic_fields.js | 5 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/relational_fields.js | 29 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/basic/basic_model.js | 19 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/form/form_renderer.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/view_dialogs.js | 1 | ||||
| -rw-r--r-- | addons/web/static/src/less/kanban_view.less | 4 | ||||
| -rw-r--r-- | addons/web/static/src/less/kanban_view_mobile.less | 3 | ||||
| -rw-r--r-- | addons/web/static/src/xml/base.xml | 28 | ||||
| -rw-r--r-- | addons/web/static/tests/fields/basic_fields_tests.js | 76 | ||||
| -rw-r--r-- | addons/web/static/tests/fields/relational_fields_tests.js | 411 | ||||
| -rw-r--r-- | addons/web/static/tests/views/form_tests.js | 40 | ||||
| -rw-r--r-- | addons/web/static/tests/views/view_dialogs_tests.js | 122 |
15 files changed, 729 insertions, 43 deletions
diff --git a/addons/web/static/lib/ace/ace.odoo-custom.js b/addons/web/static/lib/ace/ace.odoo-custom.js index d8f84294316..49af24f5074 100644 --- a/addons/web/static/lib/ace/ace.odoo-custom.js +++ b/addons/web/static/lib/ace/ace.odoo-custom.js @@ -1173,15 +1173,16 @@ define("ace/lib/regexp", ["require", "exports", "module"], function(e, t, n) { if (!t.onCompositionEnd || t.$readOnly) return; var r = h; h = !1; - var i = setTimeout(function() { - i = null; + var i2 = setTimeout(function() { + i2 = null; var e = n.value.replace(/\x01/g, ""); if (h) return; e == r.lastValue ? w() : !r.lastValue && e && (w(), A(e)) }); k = function(n) { - return i && clearTimeout(i), n = n.replace(/\x01/g, ""), n == r.lastValue ? "" : (r.lastValue && i && t.undo(), n) - }, t.onCompositionEnd(), t.removeListener("mousedown", F), e.type == "compositionend" && r.range && t.selection.setRange(r.range) + return i2 && clearTimeout(i2), n = n.replace(/\x01/g, ""), n == r.lastValue ? "" : (r.lastValue && i2 && t.undo(), n) + }, t.onCompositionEnd(), t.removeListener("mousedown", F), e.type == "compositionend" && r.range && t.selection.setRange(r.range), + i.isChrome && i.isChrome >= 53 && O() }, I = o.delayedCall(j, 50); r.addListener(n, "compositionstart", B), i.isGecko ? r.addListener(n, "text", function() { diff --git a/addons/web/static/src/js/chrome/menu.js b/addons/web/static/src/js/chrome/menu.js index 1e5e2922186..1e253ba54f7 100644 --- a/addons/web/static/src/js/chrome/menu.js +++ b/addons/web/static/src/js/chrome/menu.js @@ -147,15 +147,22 @@ var Menu = Widget.extend({ }); }, /** - * Call open_menu with the first menu_item matching an action_id + * Call open_menu on a menu_item that matches the action_id + * + * If `menuID` is a match on this action, open this menu_item. + * Otherwise open the first menu_item that matches the action_id. * * @param {Number} id the action_id to match + * @param {Number} [menuID] a menu ID that may match with provided action */ - open_action: function (id) { + open_action: function (id, menuID) { var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]'); - var menu_id = $menu.data('menu'); - if (menu_id) { - this.open_menu(menu_id); + if (!(menuID && $menu.filter("[data-menu='" + menuID + "']").length)) { + // menuID doesn't match action, so pick first menu_item + menuID = $menu.data('menu'); + } + if (menuID) { + this.open_menu(menuID); } }, /** diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js index 76aadf49463..db440cd742f 100644 --- a/addons/web/static/src/js/chrome/web_client.js +++ b/addons/web/static/src/js/chrome/web_client.js @@ -135,7 +135,7 @@ return AbstractWebClient.extend({ self.action_manager.loadState(state, !!self._current_state).then(function () { var action = self.action_manager.getCurrentAction(); if (action) { - self.menu.open_action(action.id); + self.menu.open_action(action.id, state.menu_id); } }); } @@ -156,7 +156,7 @@ return AbstractWebClient.extend({ var completed = $.Deferred(); self.action_manager.doAction(result, { clear_breadcrumbs: true, - action_menu_id: self.menu.current_menu, + action_menu_id: options.id, }).fail(function() { self.menu.open_menu(options.previous_menu_id); }).always(function() { diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 75aa1acd164..4b46db8a8ff 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2007,8 +2007,9 @@ var FieldToggleBoolean = AbstractField.extend({ * @private */ _render: function () { - var className = this.value ? 'o_toggle_button_success' : 'text-muted'; - this.$('i').addClass('fa fa-circle ' + className); + this.$('i') + .toggleClass('o_toggle_button_success', !!this.value) + .toggleClass('text-muted', !this.value); var title = this.value ? this.attrs.options.active : this.attrs.options.inactive; this.$el.attr('title', title); }, diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index f81485a6b08..d5add00e38c 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -878,10 +878,15 @@ var FieldX2Many = AbstractField.extend({ this.pager = new Pager(this, this.value.count, this.value.offset + 1, this.value.limit, { single_page_hidden: true, withAccessKey: false, + validate: function () { + var isList = self.view.arch.tag === 'tree'; + // TODO: we should have some common method in the basic renderer... + return isList ? self.renderer.unselectRow() : $.when(); + }, }); this.pager.on('pager_changed', this, function (new_state) { - this.trigger_up('load', { - id: this.value.id, + self.trigger_up('load', { + id: self.value.id, limit: new_state.limit, offset: new_state.current_min - 1, on_success: function (value) { @@ -930,6 +935,7 @@ var FieldX2Many = AbstractField.extend({ * did not agree to discard. */ _saveLine: function (recordID) { + var self = this; var def = $.Deferred(); var fieldNames = this.renderer.canBeSaved(recordID); if (fieldNames.length) { @@ -941,7 +947,9 @@ var FieldX2Many = AbstractField.extend({ } else { this.renderer.setRowMode(recordID, 'readonly').done(def.resolve.bind(def)); } - return def; + return def.then(function () { + self.pager.updateState({ size: self.value.count }); + }); }, /** * Parses the 'columnInvisibleFields' attribute to search for the domains @@ -1178,7 +1186,20 @@ var FieldOne2Many = FieldX2Many.extend({ return this._super.apply(this, arguments).then(function () { if (ev && ev.target === self && ev.data.changes && self.view.arch.tag === 'tree') { if (ev.data.changes[self.name].operation === 'CREATE') { - var index = self.editable === 'top' ? 0 : self.value.data.length - 1; + var index = 0; + if (self.editable !== 'top') { + index = self.value.data.length - 1; + // we consider that a new record, created in the bottom, + // does not count as a record worth mentioning in the + // pager, at least not until the line has been saved. + // We prevent the pager from increasing its size, which + // means that the pager is not displayed when we just + // reach the limit. For example, if limit is 3, if we + // have 3 records, and we click on add, we will see the + // 4 records on the same page, but we do not want a + // pager. + self.pager.updateState({ size: self.value.count - 1}); + } var newID = self.value.data[index].id; self.renderer.editRecord(newID); } 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 21259671505..c6afa9c4921 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -359,6 +359,11 @@ var BasicModel = AbstractModel.extend({ elem._changes = null; elem._isDirty = false; } + elem.offset = 0; + if (elem.tempLimitIncrement) { + elem.limit -= elem.tempLimitIncrement; + delete elem.tempLimitIncrement; + } }); }, /** @@ -809,6 +814,7 @@ var BasicModel = AbstractModel.extend({ this.discardChanges(id, {rollback: false}); } } else if (element._changes) { + delete element.tempLimitIncrement; _.each(element._changes, function (change) { delete change.isNew; }); @@ -1640,6 +1646,12 @@ var BasicModel = AbstractModel.extend({ // handle multiple add: command[2] may be a dict of values (1 // record added) or an array of dict of values var data = _.isArray(command.ids) ? command.ids : [command.ids]; + + // Ensure the local data repository (list) boundaries can handle incoming records (data) + if (data.length + list.res_ids.length > list.limit) { + list.limit = data.length + list.res_ids.length; + } + var list_records = {}; _.each(data, function (d) { rec = self._makeDataPoint({ @@ -1684,6 +1696,10 @@ var BasicModel = AbstractModel.extend({ case 'CREATE': var options = {position: command.position}; def = this._addX2ManyDefaultRecord(list, options).then(function (id) { + if (command.position === 'bottom' && list.orderedResIDs.length >= list.limit) { + list.tempLimitIncrement = (list.tempLimitIncrement || 0) + 1; + list.limit += 1; + } // FIXME: hack for lunch widget, which does useless default_get and onchange if (command.data) { return self._applyChange(id, command.data); @@ -3824,7 +3840,8 @@ var BasicModel = AbstractModel.extend({ // generate the current count and res_ids list by applying the changes var currentCount = list.count; var currentResIDs = list.res_ids; - var upperBound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount; + var effectiveLimit = (list.limit || 0) - (list.tempLimitIncrement || 0); + var upperBound = effectiveLimit ? Math.min(list.offset + effectiveLimit, currentCount) : currentCount; var fieldNames = list.getFieldNames(); for (var i = list.offset; i < upperBound; i++) { var resId = currentResIDs[i]; 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 b289663fb19..b328b88f5e5 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -610,6 +610,10 @@ var FormRenderer = BasicRenderer.extend({ var $result = $('<div/>', {class: 'o_group'}); var colSize = Math.max(1, Math.round(12 / (parseInt(node.attrs.col, 10) || 2))); + if (node.attrs.string) { + var $sep = $('<div/>', {class: 'o_horizontal_separator'}).text(node.attrs.string); + $result.append($sep); + } $result.append(_.map(node.children, function (child) { if (child.tag === 'newline') { return $('<br/>'); diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index 1b74068d44d..d4ae709008c 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -252,6 +252,7 @@ var SelectCreateDialog = ViewDialog.extend({ } }, selection_changed: function (event) { + event.stopPropagation(); this.$footer.find(".o_select_button").prop('disabled', !event.data.selection.length); }, search: function (event) { diff --git a/addons/web/static/src/less/kanban_view.less b/addons/web/static/src/less/kanban_view.less index f53a5d185c3..34999256fa5 100644 --- a/addons/web/static/src/less/kanban_view.less +++ b/addons/web/static/src/less/kanban_view.less @@ -230,7 +230,7 @@ height: 20px; } .oe_kanban_bottom_left { - .o-flex(1, 0, auto); + .o-flex(1, 1, auto); > * { margin-right: 6px; @@ -243,7 +243,7 @@ } } .oe_kanban_bottom_right{ - .o-flex(0, 0, auto); + .o-flex(0, 1, auto); .oe_kanban_avatar { width: 20px; diff --git a/addons/web/static/src/less/kanban_view_mobile.less b/addons/web/static/src/less/kanban_view_mobile.less index 72ba087e0c2..d09b6968925 100644 --- a/addons/web/static/src/less/kanban_view_mobile.less +++ b/addons/web/static/src/less/kanban_view_mobile.less @@ -29,7 +29,8 @@ } } - &.o_kanban_small_column .o_kanban_group:not(.o_column_folded) { + // [class] to get same specificity as elsewhere (kanban_view.less) + &[class] .o_kanban_group:not(.o_column_folded) { .o-position-absolute(@top: @o-kanban-mobile-tabs-height, @left: 0, @bottom: 0); overflow-x: scroll; width: 100%; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index fd263a07eb6..3ef2a213398 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -209,7 +209,7 @@ <t t-name="WebClient.DebugManager"> <li class="o_debug_manager"> - <a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown"> + <a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="fa fa-bug"/> </a> <ul class="dropdown-menu o_debug_dropdown" role="menu"/> @@ -325,12 +325,12 @@ <t t-name="Sidebar"> <t t-foreach="widget.sections" t-as="section"> <div class="btn-group o_dropdown"> - <button t-if="section.name != 'buttons'" class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown"> + <button t-if="section.name != 'buttons'" class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <t t-if="section.name == 'files'" t-raw="widget.items[section.name].length || ''"/> <t t-esc="section.label"/> <span class="caret"/> </button> <t t-if="section.name == 'buttons'" t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname"> - <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url" target="_blank"> + <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url or '#'" target="_blank"> <t t-raw="item.label"/> </a> </t> @@ -352,7 +352,7 @@ <t t-raw="item.write_uid[1]"/> <t t-esc="item.write_date_string"/> </t> </t> - <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url"> + <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url or '#'"> <t t-raw="item.label"/> <span t-if="section.name == 'files' and widget.options.editable and !item.callback" class="fa fa-trash-o o_sidebar_delete_attachment" t-att-data-id="item.id" title="Delete this attachment"/> </a> @@ -469,7 +469,7 @@ </form> <t t-name="GraphView.buttons"> <div class="btn-group btn-group-sm" role="group"> - <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown"> + <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> Measures <span class="caret"/> </button> <ul class="dropdown-menu o_graph_measures_list"> @@ -498,7 +498,7 @@ </div> <t t-name="PivotView.buttons"> <div class="btn-group btn-group-sm" role="group"> - <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown"> + <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> Measures <span class="caret"/> </button> <ul class="dropdown-menu o_pivot_measures_list"> @@ -852,7 +852,7 @@ <t t-call="FieldStatus.content.button"/> </li> </ul> - <button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle" data-toggle="dropdown">More <span class="caret"/></button> + <button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">More <span class="caret"/></button> </t> <t t-foreach="selection_unfolded.reverse()" t-as="i"> <t t-call="FieldStatus.content.button"/> @@ -985,7 +985,7 @@ <ul class="dropdown-menu o_searchview_autocomplete" role="menu"></ul> </t> <div t-name="SearchView.FilterMenu" class="btn-group o_dropdown"> - <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown"> + <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="fa fa-filter"/> Filters <span class="caret"/> </button> <ul class="dropdown-menu o_filters_menu" role="menu"> @@ -1097,7 +1097,7 @@ </select> </t> <div t-name="SearchView.GroupByMenu" class="btn-group hidden-xs o_dropdown"> - <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown"> + <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="fa fa-bars"/> Group By <span class="caret"/> </button> <ul class="dropdown-menu o_group_by_menu" role="menu"> @@ -1118,7 +1118,7 @@ </li> </t> <div t-name="SearchView.FavoriteMenu" class="btn-group o_dropdown"> - <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown"> + <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="fa fa-star"/> Favorites <span class="caret"/> </button> <ul class="dropdown-menu o_favorites_menu" role="menu"> @@ -1247,8 +1247,8 @@ <span class="o_stat_text"><t t-esc="text"/></span> </t> <t t-name="toggle_button"> - <button type="button" class="o_icon_button" t-att-title="widget.string"> - <i t-attf-class="fa #{widget.icon}" t-att-title="widget.string"/> + <button type="button" class="o_icon_button" t-att-title="widget.string" t-att-aria-label="widget.string"> + <i class="fa fa-circle" t-att-title="widget.string"/> </button> </t> @@ -1262,8 +1262,8 @@ <t t-set="att_prev" t-value="{'accesskey': 'p'}" /> <t t-set="att_next" t-value="{'accesskey': 'n'}" /> </t> - <button type="button" class="fa fa-chevron-left btn btn-icon o_pager_previous" t-att="att_prev"/> - <button type="button" class="fa fa-chevron-right btn btn-icon o_pager_next" t-att="att_next"/> + <button type="button" class="fa fa-chevron-left btn btn-icon o_pager_previous" t-att="att_prev" aria-label="Previous"/> + <button type="button" class="fa fa-chevron-right btn btn-icon o_pager_next" t-att="att_next" aria-label="Next"/> </span> </div> diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 8488076b3f0..a60e3419bdb 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -387,7 +387,6 @@ QUnit.module('basic_fields', { '<field name="bar" widget="toggle_button" ' + 'options="{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}"/>' + '</tree>', - res_id: 2, }); assert.strictEqual(list.$('button i.fa.fa-circle.o_toggle_button_success').length, 4, @@ -411,6 +410,81 @@ QUnit.module('basic_fields', { list.destroy(); }); + QUnit.test('toggle_button in form view (edit mode)', function (assert) { + assert.expect(6); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="bar" widget="toggle_button" ' + + 'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' + + '</form>', + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.step('write'); + } + return this._super.apply(this, arguments); + }, + res_id: 2, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length, + 1, "should be green"); + + // click on the button to toggle the value + form.$('.o_field_widget[name=bar]').click(); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length, + 1, "should be gray"); + assert.verifySteps([]); + + // save + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length, + 1, "should still be gray"); + assert.verifySteps(['write']); + + form.destroy(); + }); + + QUnit.test('toggle_button in form view (readonly mode)', function (assert) { + assert.expect(4); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="bar" widget="toggle_button" ' + + 'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' + + '</form>', + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.step('write'); + } + return this._super.apply(this, arguments); + }, + res_id: 2, + }); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length, + 1, "should be green"); + + // click on the button to toggle the value + form.$('.o_field_widget[name=bar]').click(); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length, + 1, "should be gray"); + assert.verifySteps(['write']); + + form.destroy(); + }); QUnit.module('FieldFloat'); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 9b8df65ab29..dbdde1cb566 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -2078,7 +2078,7 @@ QUnit.module('relational_fields', { data: this.data, arch: '<form>' + '<field name="p">' + - '<tree editable="bttom">' + + '<tree editable="bottom">' + '<field name="trululu"/>' + '</tree>' + '</field>' + @@ -2173,6 +2173,92 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('many2many list add *many* records, remove, re-add', function (assert) { + assert.expect(5); + + this.data.partner.fields.timmy.domain = [['color', '=', 2]]; + this.data.partner.fields.timmy.onChange = true; + this.data.partner_type.fields.product_ids = {string: "Product", type: "many2many", relation: 'product'}; + + for (var i=0; i<50; i++) { + var new_record_partner_type = {id: 100+i, display_name: "batch" + i, color: 2}; + this.data.partner_type.records.push(new_record_partner_type); + } + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="timmy" widget="many2many">' + + '<tree>' + + '<field name="display_name"/>' + + '<field name="product_ids" widget="many2many_tags"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + archs: { + 'partner_type,false,list': '<tree><field name="display_name"/></tree>', + 'partner_type,false,search': '<search><field name="display_name"/><field name="color"/></search>', + }, + mockRPC: function (route, args) { + if (args.method === 'get_formview_id') { + assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id"); + return $.when(false); + } + return this._super(route, args); + }, + }); + + // First round: add 51 records in batch + form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + + var $modal = $('.modal-dialog.modal-lg'); + + assert.equal($modal.length, 1, + 'There should be one modal'); + + $modal.find('thead input[type=checkbox]').click(); //select all the records we created in batch + 'gold' + + $modal.find('.btn.btn-sm.btn-primary.o_select_button').click(); + + var m2m_records = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_data_cell:not(.o_many2many_tags_cell)'); + assert.equal(m2m_records.length, 51, + 'We should have added all the records present in the search view to the m2m field'); // the 50 in batch + 'gold' + + form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_save').click(); + + // Secound round: remove one record + form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_edit').click(); + var trash_buttons = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_list_record_delete'); + + trash_buttons.first().click(); + + var pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit'); + assert.equal(pager_limit.text(), '50', + 'We should have 50 records in the m2m field'); + + // Third round: re-add 1 records + form.$('.o_field_x2many_list_row_add a').click(); + + $modal = $('.modal-dialog.modal-lg'); + + assert.equal($modal.length, 1, + 'There should be one modal'); + + $modal.find('thead input[type=checkbox]').click(); + + $modal.find('.btn.btn-sm.btn-primary.o_select_button').click(); + + pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit'); + assert.equal(pager_limit.text(), '51', + 'We should have 51 records in the m2m field'); + + form.destroy(); + }); + QUnit.module('FieldOne2Many'); QUnit.test('one2many basic properties', function (assert) { @@ -2784,7 +2870,7 @@ QUnit.module('relational_fields', { }); QUnit.test('onchange followed by edition on the second page', function (assert) { - assert.expect(20); + assert.expect(12); var ids = []; for (var i=1; i<85; i++) { @@ -2854,10 +2940,32 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 2, "should display '#39' at the third line"); form.destroy(); + }); + + QUnit.test('onchange followed by edition on the second page (part 2)', function (assert) { + assert.expect(8); + + var ids = []; + for (var i=1; i<85; i++) { + var id = 10 + i; + ids.push(id); + this.data.turtle.records.push({ + id: id, + turtle_int: id/3|0, + turtle_foo: "#" + i, + }); + } + ids.splice(41, 0, 1, 2, 3); + this.data.partner.records[0].turtles = ids; + this.data.partner.onchanges = { + turtles: function (obj) { + obj.turtles = [[5]].concat(obj.turtles); + }, + }; // bottom order - form = createView({ + var form = createView({ View: FormView, model: 'partner', data: this.data, @@ -2889,16 +2997,16 @@ QUnit.module('relational_fields', { form.$('.o_field_x2many_list_row_add a').click(); - assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line"); + assert.strictEqual(form.$('.o_data_row').length, 41, "should display 41 records and the create line"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line"); - assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 39, "should display the create line at the last position"); + assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 40, "should display the create line at the last position"); form.$('.o_data_row input:first').val('value 3').trigger('input'); form.$('.o_field_x2many_list_row_add a').click(); - assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line"); + assert.strictEqual(form.$('.o_data_row').length, 42, "should display 42 records and the create line"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line"); - assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 39, "should display the create line at the last position"); + assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 41, "should display the create line at the last position"); form.destroy(); }); @@ -3979,6 +4087,37 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editable one2many list, pager is updated', function (assert) { + assert.expect(1); + + this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); + this.data.partner.records[0].turtles = [1,2,3,4]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a record, then click in form view to confirm it + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$el.click(); + + assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 5', + "pager should display the correct total"); + form.destroy(); + }); + + QUnit.test('one2many list (non editable): edition', function (assert) { assert.expect(12); @@ -4287,6 +4426,185 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editable one2many list, adding line when only one page', function (assert) { + assert.expect(1); + + this.data.partner.records[0].turtles=[1,2,3]; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a record, to reach the page size limit + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + + // the record currently being added should not count in the pager + assert.ok(!form.$('.o_cp_pager').is(':visible'), + "pager should not be visible"); + form.destroy(); + }); + + QUnit.test('editable one2many list, adding line, then discarding', function (assert) { + assert.expect(1); + + this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); + this.data.partner.records[0].turtles = [1,2,3,4]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a record, then discard + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$buttons.find('.o_form_button_cancel').click(); + + // confirm the discard operation + $('.modal .modal-footer .btn-primary').click(); // click on confirm + + assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4', + "pager should still be visible"); + form.destroy(); + }); + + QUnit.test('editable one2many list, required field and pager', function (assert) { + assert.expect(1); + + this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); + this.data.turtle.fields.turtle_foo.required = true; + this.data.partner.records[0].turtles = [1,2,3,4]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a (empty) record + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + + // go on next page. The new record is not valid and should be discarded + form.$('.o_pager_next').click(); + assert.strictEqual(form.$('tr.o_data_row').length, 1, + "should have 1 data rows"); + + form.destroy(); + }); + + QUnit.test('editable one2many list, required field, pager and confirm discard', function (assert) { + assert.expect(3); + + this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); + this.data.turtle.fields.turtle_foo.required = true; + this.data.partner.records[0].turtles = [1,2,3,4]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '<field name="turtle_int"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a record with a dirty state, but not valid + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('input[name="turtle_int"]').val(4321).trigger('input'); + + // go to next page. The new record is not valid, but dirty. we should + // see a confirm dialog + form.$('.o_pager_next').click(); + + assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4', + "pager should still display the correct total"); + + // click on cancel + $('.modal .modal-footer .btn-default').click(); // click on cancel + + assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4', + "pager should again display the correct total"); + assert.strictEqual(form.$('.o_field_one2many input.o_field_invalid').length, 1, + "there should be an invalid input in the one2many"); + form.destroy(); + }); + + QUnit.test('editable one2many list, adding, discarding, and pager', 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 editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a 4 records record (to make the pager appear) + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_x2many_list_row_add a').click(); + + // go on next page + form.$('.o_pager_next').click(); + + // discard + form.$buttons.find('.o_form_button_cancel').click(); + $('.modal .modal-footer .btn-primary').click(); + + assert.strictEqual(form.$('tr.o_data_row').length, 1, + "should have 1 data row"); + assert.ok(!form.$('.o_cp_pager').is(':visible'), + "pager should not be visible"); + + form.destroy(); + }); + QUnit.test('unselecting a line with missing required data', function (assert) { assert.expect(5); @@ -7602,6 +7920,42 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editing tabbed one2many (editable=bottom), again...', function (assert) { + assert.expect(1); + + this.data.partner.records[0].turtles = []; + for (var i = 0; i < 9; i++) { + var id = 100 + i; + this.data.turtle.records.push({id: id, turtle_foo: 'turtle' + (id-99)}); + this.data.partner.records[0].turtles.push(id); + } + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_data_row input[name="turtle_foo"]').val('rainbow dash').trigger('input'); + form.$('.o_x2m_control_panel .o_pager_next').click(); + form.$('.o_x2m_control_panel .o_pager_next').click(); + + assert.strictEqual(form.$('tr.o_data_row').length, 3, + "should have 3 data rows on the current page"); + form.destroy(); + }); + QUnit.test('editing tabbed one2many (editable=top)', function (assert) { assert.expect(15); @@ -8217,6 +8571,49 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('add a new line after limit is reached should behave nicely', function (assert) { + assert.expect(2); + + this.data.partner.records[0].turtles = [1,2,3]; + + this.data.partner.onchanges = { + turtles: function (obj) { + obj.turtles = [ + [5], + [1, 1, {turtle_foo: "yop"}], + [1, 2, {turtle_foo: "blip"}], + [1, 3, {turtle_foo: "kawa"}], + [0, obj.turtles[3][2], {turtle_foo: "abc"}], + ]; + }, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="turtles">' + + '<tree limit="3" editable="bottom">' + + '<field name="turtle_foo" required="1"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + form.$('.o_field_x2many_list_row_add a').click(); + assert.strictEqual(form.$('.o_data_row').length, 4, 'should have 4 data rows'); + form.$('.o_input[name="turtle_foo"]').val('a').trigger('input'); + assert.strictEqual(form.$('.o_data_row').length, 4, + 'should still have 4 data rows (the limit is increased to 4)'); + + form.destroy(); + }); + QUnit.test('onchange in a one2Many, but not in main record, with non inline view', function (assert) { assert.expect(6); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index e9b652659ff..192efa338bd 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -5202,6 +5202,46 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('outer and inner groups string attribute', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group string="parent group" class="parent_group">' + + '<group string="child group 1" class="group_1">' + + '<field name="bar"/>' + + '</group>' + + '<group string="child group 2" class="group_2">' + + '<field name="bar"/>' + + '</group>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + var $parentGroup = form.$('.parent_group'); + var $group1 = form.$('.group_1'); + var $group2 = form.$('.group_2'); + + assert.strictEqual(form.$('table.o_inner_group').length, 2, + "should contain two inner groups"); + assert.strictEqual($group1.find('.o_horizontal_separator').length, 1, + "inner group should contain one string separator"); + assert.strictEqual($group1.find('.o_horizontal_separator:contains(child group 1)').length, 1, + "first inner group should contain 'child group 1' string"); + assert.strictEqual($group2.find('.o_horizontal_separator:contains(child group 2)').length, 1, + "second inner group should contain 'child group 2' string"); + assert.strictEqual($parentGroup.find('> div.o_horizontal_separator:contains(parent group)').length, 1, + "outer group should contain 'parent group' string"); + + form.destroy(); + }); + QUnit.test('form group with newline tag inside', function (assert) { assert.expect(6); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index 6ec4f50ab4b..39484795f5a 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -5,6 +5,9 @@ var dialogs = require('web.view_dialogs'); var ListController = require('web.ListController'); var testUtils = require('web.test_utils'); var Widget = require('web.Widget'); +var FormView = require('web.FormView'); + +var createView = testUtils.createView; QUnit.module('Views', { beforeEach: function () { @@ -14,6 +17,7 @@ QUnit.module('Views', { display_name: { string: "Displayed name", type: "char" }, foo: {string: "Foo", type: 'char'}, bar: {string: "Bar", type: "boolean"}, + instrument: {string: 'Instruments', type: 'many2one', relation: 'instrument'}, }, records: [ {id: 1, foo: 'blip', display_name: 'blipblip', bar: true}, @@ -21,8 +25,34 @@ QUnit.module('Views', { {id: 3, foo: 'piou piou', display_name: "Jack O'Neill", bar: true}, ], }, + instrument: { + fields: { + name: {string: "name", type: "char"}, + badassery: {string: 'level', type: 'many2many', relation: 'badassery', domain: [['level', '=', 'Awsome']]}, + }, + }, + + badassery: { + fields: { + level: {string: 'level', type: "char"}, + }, + records: [ + {id: 1, level: 'Awsome'}, + ], + }, + + product: { + fields : { + name: {string: "name", type: "char" }, + partner : {string: 'Doors', type: 'one2many', relation: 'partner'}, + }, + records: [ + {id: 1, name: 'The end'}, + ], + }, }; }, + }, function () { QUnit.module('view_dialogs'); @@ -242,6 +272,98 @@ QUnit.module('Views', { parent.destroy(); }); + QUnit.test('SelectCreateDialog cascade x2many in create mode', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'product', + data: this.data, + arch: '<form>' + + '<field name="name"/>' + + '<field name="partner" widget="one2many_list" >' + + '<tree editable="top">' + + '<field name="display_name"/>' + + '<field name="instrument"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + archs: { + 'partner,false,form': '<form>' + + '<field name="name"/>' + + '<field name="instrument" widget="one2many_list" mode="tree"/>' + + '</form>', + + 'instrument,false,form': '<form>'+ + '<field name="name"/>'+ + '<field name="badassery">' + + '<tree>'+ + '<field name="level"/>'+ + '</tree>' + + '</field>' + + '</form>', + + 'badassery,false,list': '<tree>'+ + '<field name="level"/>'+ + '</tree>', + + 'badassery,false,search': '<search>'+ + '<field name="level"/>'+ + '</search>', + }, + + mockRPC: function(route, args) { + if (route === '/web/dataset/call_kw/partner/get_formview_id') { + return $.when(false); + } + if (route === '/web/dataset/call_kw/instrument/get_formview_id') { + return $.when(false); + } + if (route === '/web/dataset/call_kw/instrument/create') { + assert.deepEqual(args.args, [{badassery: [[6, false, [1]]], name: false}], + 'The method create should have been called with the right arguments'); + return $.when(false); + } + return this._super(route, args); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_widget .o_field_many2one[name=instrument] input').click(); + $('ul.ui-autocomplete.ui-front.ui-menu.ui-widget.ui-widget-content li.o_m2o_dropdown_option').first().click(); + + var $modal = $('.modal-dialog.modal-lg'); + + assert.equal($modal.length, 1, + 'There should be one modal'); + + $modal.find('.o_field_x2many_list_row_add a').click(); + + var $modals = $('.modal-dialog.modal-lg'); + + assert.equal($modals.length, 2, + 'There should be two modals'); + + var $second_modal = $modals.not($modal); + $second_modal.find('.o_list_view.table.table-condensed.table-striped.o_list_view_ungrouped .o_data_row input[type=checkbox]').click(); + + $second_modal.find('.o_select_button').click(); + + $modal = $('.modal-dialog.modal-lg'); + + assert.equal($modal.length, 1, + 'There should be one modal'); + + assert.equal($modal.find('.o_data_cell').text(), 'Awsome', + 'There should be one item in the list of the modal'); + + $modal.find('.btn.btn-sm.btn-primary').click(); + + form.destroy(); + }); + QUnit.test('SelectCreateDialog: save current search', function (assert) { assert.expect(4); |
