diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-11-29 20:06:16 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-11-29 20:06:16 +0100 |
| commit | ce4cc24621bf6430d7d3a2d469b375b59120c200 (patch) | |
| tree | 3837b5fe41ff88b24136ab53c871bc01f965cab1 /addons/web | |
| parent | 4d7aa97da38e497fef31a06543bfa5d2c8785708 (diff) | |
| parent | 82a1e1dcc33d7c123e50d3e1aacb5e127f57c521 (diff) | |
[MERGE] forward port branch 12.0 up to 82a1e1dcc33
Diffstat (limited to 'addons/web')
24 files changed, 492 insertions, 100 deletions
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index d0059a25a95..64cf2ca9ac5 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -525,7 +525,7 @@ class Home(http.Controller): request.env['res.users']._invalidate_session_cache() request.session.session_token = security.compute_session_token(request.session, request.env) - return http.redirect_with_hash(self._login_redirect(uid)) + return http.local_redirect(self._login_redirect(uid), keep_hash=True) class WebClient(http.Controller): diff --git a/addons/web/i18n/da.po b/addons/web/i18n/da.po index 5c4b7f55073..bb16198224c 100644 --- a/addons/web/i18n/da.po +++ b/addons/web/i18n/da.po @@ -3,27 +3,28 @@ # * web # # Translators: +# Kenneth Hansen <kenneth@vkdata.dk>, 2018 +# Jesper Carstensen <jc@danodoo.dk>, 2018 +# peso <peer.sommerlund@gmail.com>, 2018 +# Visti Kristensen <visti@vkdata.dk>, 2018 +# Jonathan Stein <cgs@image.dk>, 2018 # Martin Trigaux, 2018 # Hans Henrik Gabelgaard <hhg@gabelgaard.org>, 2018 -# Kenneth Hansen <kenneth@vkdata.dk>, 2018 +# jonas jensen <j.jensen@tcomp.dk>, 2018 # Morten Schou <ms@msteknik.dk>, 2018 -# Jesper Carstensen <jc@danodoo.dk>, 2018 # Jarnhold Nattestad <nattestads@gmail.com>, 2018 # Pernille Kristensen <pernillekristensen1994@gmail.com>, 2018 -# Sanne Kristensen <sanne@vkdata.dk>, 2018 # Ejner Sønniksen <ejner@vkdata.dk>, 2018 # lhmflexerp <lhm@flexerp.dk>, 2018 -# peso <peer.sommerlund@gmail.com>, 2018 -# jonas jensen <j.jensen@tcomp.dk>, 2018 -# Visti Kristensen <visti@vkdata.dk>, 2018 -# Jonathan Stein <cgs@image.dk>, 2018 +# Sanne Kristensen <sanne@vkdata.dk>, 2018 +# msgid "" msgstr "" "Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-10-08 06:49+0000\n" -"PO-Revision-Date: 2018-10-08 06:49+0000\n" -"Last-Translator: Jonathan Stein <cgs@image.dk>, 2018\n" +"PO-Revision-Date: 2018-08-24 09:33+0000\n" +"Last-Translator: Sanne Kristensen <sanne@vkdata.dk>, 2018\n" "Language-Team: Danish (https://www.transifex.com/odoo/teams/41243/da/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1247,7 +1248,7 @@ msgstr "Download xls" #: code:addons/web/static/src/xml/base.xml:1556 #, python-format msgid "Dropdown menu" -msgstr "" +msgstr "Dropdown menu" #. module: web #. openerp-web @@ -1641,7 +1642,7 @@ msgstr "Sortér efter: %s" #. module: web #: model:ir.model,name:web.model_ir_http msgid "HTTP Routing" -msgstr "" +msgstr "HTTP Routing" #. module: web #. openerp-web diff --git a/addons/web/i18n/en_GB.po b/addons/web/i18n/en_GB.po index d10e0618f04..c9419478555 100644 --- a/addons/web/i18n/en_GB.po +++ b/addons/web/i18n/en_GB.po @@ -1520,13 +1520,6 @@ msgstr "" #. module: web #. openerp-web -#: code:addons/web/static/src/js/views/form_relational_widgets.js:329 -#, fuzzy, python-format -msgid "No value found for the field %s for value %s" -msgstr "No value found for the field for value %s" - -#. module: web -#. openerp-web #: code:addons/web/static/src/js/framework/utils.js:144 #, python-format msgid "Node [%s] is not a JSONified XML node" diff --git a/addons/web/i18n/es_AR.po b/addons/web/i18n/es_AR.po index 4e28ef7be45..fbab2c0e3da 100644 --- a/addons/web/i18n/es_AR.po +++ b/addons/web/i18n/es_AR.po @@ -1533,13 +1533,6 @@ msgstr "" #. module: web #. openerp-web -#: code:addons/web/static/src/js/views/form_relational_widgets.js:329 -#, fuzzy, python-format -msgid "No value found for the field %s for value %s" -msgstr "No se ha encontrado ningún valor para el valor del campo " - -#. module: web -#. openerp-web #: code:addons/web/static/src/js/framework/utils.js:144 #, python-format msgid "Node [%s] is not a JSONified XML node" diff --git a/addons/web/i18n/es_EC.po b/addons/web/i18n/es_EC.po index d38d22b3403..c96276dc87a 100644 --- a/addons/web/i18n/es_EC.po +++ b/addons/web/i18n/es_EC.po @@ -1531,13 +1531,6 @@ msgstr "Sin registro seleccionado" #. module: web #. openerp-web -#: code:addons/web/static/src/js/views/form_relational_widgets.js:329 -#, fuzzy, python-format -msgid "No value found for the field %s for value %s" -msgstr "No se encuentran valores para el campo" - -#. module: web -#. openerp-web #: code:addons/web/static/src/js/framework/utils.js:144 #, python-format msgid "Node [%s] is not a JSONified XML node" diff --git a/addons/web/i18n/it.po b/addons/web/i18n/it.po index ef7ea2d7fe5..c246d4285d4 100644 --- a/addons/web/i18n/it.po +++ b/addons/web/i18n/it.po @@ -3,11 +3,9 @@ # * web # # Translators: -# Cécile Collart <cco@odoo.com>, 2018 # Cesare Cugnasco <cesare.cugnasco@gmail.com>, 2018 # Luca Tralli, 2018 # Pietro Della Notte <pdellanotte@gmail.com>, 2018 -# efraimbiffi <eab@odoo.com>, 2018 # Francesco Garganese <francesco.garganese@aeromnia.aero>, 2018 # Luigi Di Naro <gigidn@gmail.com>, 2018 # Giacomo Grasso <giacomo.grasso.82@gmail.com>, 2018 @@ -180,7 +178,7 @@ msgstr "'%s' non è un intero corretto" #: code:addons/web/static/src/js/fields/field_utils.js:548 #, python-format msgid "'%s' is not a correct monetary field" -msgstr "" +msgstr "\"%s\" non è un campo monetario corretto" #. module: web #. openerp-web @@ -230,7 +228,7 @@ msgstr "(senza etichetta)" #: code:addons/web/static/src/js/fields/field_utils.js:283 #, python-format msgid "1 record" -msgstr "" +msgstr "1 record" #. module: web #. openerp-web @@ -344,7 +342,7 @@ msgstr "" #: code:addons/web/static/src/xml/base.xml:1126 #, python-format msgid "Add Custom Filter" -msgstr "Aggiungi filtro" +msgstr "Aggiungi filtro personalizzato" #. module: web #. openerp-web @@ -358,14 +356,14 @@ msgstr "Aggiungi gruppo personalizzato" #: code:addons/web/static/src/xml/kanban.xml:49 #, python-format msgid "Add a Column" -msgstr "" +msgstr "Aggiungi una colonna" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:1130 #, python-format msgid "Add a condition" -msgstr "Aggiungi un filtro" +msgstr "Aggiungi una condizione" #. module: web #. openerp-web @@ -660,6 +658,8 @@ msgid "" "Binary fields can not be exported to Excel unless their content is " "base64-encoded. That does not seem to be the case for %s." msgstr "" +"Impossibile esportare %s. I campi binari devono avere il contenuto " +"codificato in base64 per essere esportati in Excel." #. module: web #. openerp-web @@ -1167,7 +1167,7 @@ msgstr "Descrizione" #: code:addons/web/static/src/xml/report.xml:18 #, python-format msgid "Discard" -msgstr "Annulla" +msgstr "Abbandona" #. module: web #. openerp-web @@ -1349,7 +1349,7 @@ msgstr "Errore, password non modificata." #: code:addons/web/static/src/xml/kanban.xml:62 #, python-format msgid "Esc to discard" -msgstr "" +msgstr "Esc per abbandonare" #. module: web #. openerp-web @@ -1391,14 +1391,14 @@ msgstr "" #: code:addons/web/static/src/js/views/list/list_controller.js:149 #, python-format msgid "Export" -msgstr "Esportazione" +msgstr "Esporta" #. module: web #. openerp-web #: code:addons/web/static/src/js/widgets/data_export.js:209 #, python-format msgid "Export Data" -msgstr "Esporta dati" +msgstr "Esportazione dati" #. module: web #. openerp-web @@ -1671,28 +1671,28 @@ msgstr "Premere INVIO per" #: code:addons/web/static/src/xml/base.xml:431 #, python-format msgid "Hit ENTER to CREATE" -msgstr "Premere INVIO per CREA" +msgstr "Premere INVIO per CREARE" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:423 #, python-format msgid "Hit ENTER to SAVE" -msgstr "Premere INVIO per SALVA" +msgstr "Premere INVIO per SALVARE" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:424 #, python-format msgid "Hit ESCAPE to DISCARD" -msgstr "" +msgstr "Premere ESCAPE per ABBANDONARE" #. module: web #. openerp-web #: code:addons/web/static/src/xml/colorpicker.xml:41 #, python-format msgid "Hue" -msgstr "" +msgstr "Tonalità" #. module: web #. openerp-web @@ -1772,7 +1772,7 @@ msgstr "" #: code:addons/web/static/src/js/tools/debug_manager.js:209 #, python-format msgid "JS Mobile Tests" -msgstr "" +msgstr "Test JS per cellulare" #. module: web #. openerp-web @@ -1900,7 +1900,7 @@ msgstr "Blu chiaro" #: code:addons/web/static/src/xml/colorpicker.xml:49 #, python-format msgid "Lightness %" -msgstr "" +msgstr "Luminosità %" #. module: web #. openerp-web @@ -2134,14 +2134,14 @@ msgstr "Altro" #: code:addons/web/static/src/xml/base.xml:1415 #, python-format msgid "Move Down" -msgstr "Muovi Giù" +msgstr "Sposta in basso" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:1414 #, python-format msgid "Move Up" -msgstr "Muovi Su" +msgstr "Sposta in alto" #. module: web #. openerp-web @@ -2403,7 +2403,7 @@ msgstr "Solo tu" #: code:addons/web/static/src/xml/colorpicker.xml:55 #, python-format msgid "Opacity %" -msgstr "" +msgstr "Opacità %" #. module: web #. openerp-web @@ -2550,14 +2550,14 @@ msgstr "pivot" #: code:addons/web/static/src/js/views/list/list_renderer.js:352 #, python-format msgid "Please click on the \"save\" button first." -msgstr "" +msgstr "Prima fare clic sul pulsante \"Salva\"." #. module: web #. openerp-web #: code:addons/web/static/src/js/widgets/data_export.js:177 #, python-format msgid "Please enter save field list name" -msgstr "Inserire il nome per l'elenco salvato" +msgstr "Inserire il nome dell'elenco campi da salvare" #. module: web #. openerp-web @@ -2571,7 +2571,7 @@ msgstr "Selezionare i campi da esportare..." #: code:addons/web/static/src/js/widgets/data_export.js:464 #, python-format msgid "Please select fields to save export list..." -msgstr "Selezionare l'elenco dei campi da esportare..." +msgstr "Selezionare i campi da salvare nell'elenco di esportazione..." #. module: web #. openerp-web @@ -2793,21 +2793,21 @@ msgstr "Ripristina" #: code:addons/web/static/src/xml/base.xml:240 #, python-format msgid "Run Click Everywhere Test" -msgstr "Esegui prova \"Fare clic ovunque\"" +msgstr "Esegui test \"Fare clic ovunque\"" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:239 #, python-format msgid "Run JS Mobile Tests" -msgstr "Esegui prove JS per dispositivo mobile" +msgstr "Esegui test JS per dispositivo mobile" #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:238 #, python-format msgid "Run JS Tests" -msgstr "Esegui prove JS" +msgstr "Esegui test JS" #. module: web #. openerp-web @@ -2877,14 +2877,14 @@ msgstr "Salva un record" #: code:addons/web/static/src/xml/base.xml:1443 #, python-format msgid "Save as:" -msgstr "Salva come:" +msgstr "Salvare come: " #. module: web #. openerp-web #: code:addons/web/static/src/xml/base.xml:1358 #, python-format msgid "Save current search" -msgstr "Salvare la ricerca corrente" +msgstr "Salva la ricerca corrente" #. module: web #. openerp-web @@ -2898,7 +2898,7 @@ msgstr "Salva predefinito" #: code:addons/web/static/src/xml/base.xml:1419 #, python-format msgid "Save fields list" -msgstr "Salva l'elenco dei campi" +msgstr "Salva elenco campi" #. module: web #. openerp-web @@ -2942,7 +2942,7 @@ msgstr "Cerca ancora..." #: code:addons/web/static/src/xml/base.xml:1231 #, python-format msgid "Search..." -msgstr "Cerca..." +msgstr "Ricerca..." #. module: web #. openerp-web @@ -3113,7 +3113,7 @@ msgstr "Accendi" #: code:addons/web/static/src/js/widgets/domain_selector.js:583 #, python-format msgid "Syntax error" -msgstr "" +msgstr "Errore di sintassi" #. module: web #. openerp-web @@ -3206,7 +3206,8 @@ msgid "" "The record has been modified, your changes will be discarded. Do you want to" " proceed?" msgstr "" -"Il documento è stato modificato, le modifiche verranno eliminate. Procedere?" +"Il documento è stato modificato, verranno abbandonate le modifiche. " +"Procedere?" #. module: web #. openerp-web @@ -3233,6 +3234,8 @@ msgid "" "There are too many rows (%s rows, limit: 65535) to export as Excel 97-2003 " "(.xls) format. Consider splitting the export." msgstr "" +"Impossibile esportare nel formato Excel 97-2003 (.xls) (%s righe, limite: " +"65535). Valutare di suddividere l'esportazione." #. module: web #. openerp-web @@ -3482,7 +3485,7 @@ msgstr "Senza titolo" #: code:addons/web/static/src/xml/base.xml:1393 #, python-format msgid "Update data (import-compatible export)" -msgstr "" +msgstr "Aggiornare i dati (compatibile con l'importazione)" #. module: web #. openerp-web @@ -3496,7 +3499,7 @@ msgstr "Aggiorna adesso" #: code:addons/web/static/src/xml/base.xml:1577 #, python-format msgid "Upgrade to enterprise" -msgstr "" +msgstr "Avanza a Enterprise" #. module: web #. openerp-web @@ -3542,7 +3545,7 @@ msgstr "Utilizzare come predefinita" #: code:addons/web/static/src/xml/base.xml:1387 #, python-format msgid "Use data in a spreadsheet (export all data)" -msgstr "" +msgstr "Usare i dati in un foglio di calcolo (esportare tutti i dati)" #. module: web #. openerp-web @@ -3584,14 +3587,14 @@ msgstr "Visualizza metadati" #: code:addons/web/static/src/xml/base.xml:606 #, python-format msgid "Warning" -msgstr "Avviso" +msgstr "Attenzione" #. module: web #. openerp-web #: code:addons/web/static/src/js/widgets/attach_document.js:87 #, python-format msgid "Warning : You have to save first before attaching a file." -msgstr "" +msgstr "Attenzione: prima di allegare un file è necessario salvare." #. module: web #: model_terms:ir.ui.view,arch_db:web.benchmark_suite @@ -3601,12 +3604,12 @@ msgstr "" #. module: web #: model_terms:ir.ui.view,arch_db:web.qunit_mobile_suite msgid "Web Mobile Tests" -msgstr "" +msgstr "Test web per dispositivo mobile" #. module: web #: model_terms:ir.ui.view,arch_db:web.qunit_suite msgid "Web Tests" -msgstr "" +msgstr "Test web" #. module: web #: model_terms:ir.ui.view,arch_db:web.external_layout_boxed @@ -3634,7 +3637,7 @@ msgstr "Ben fatto!" #: code:addons/web/static/src/xml/base.xml:1383 #, python-format msgid "What do you want to do?" -msgstr "Cosa vuoi fare?" +msgstr "Cosa si intende fare?" #. module: web #. openerp-web diff --git a/addons/web/i18n/mk.po b/addons/web/i18n/mk.po index 6fe04cbc033..6f55d6b2662 100644 --- a/addons/web/i18n/mk.po +++ b/addons/web/i18n/mk.po @@ -1532,13 +1532,6 @@ msgstr "" #. module: web #. openerp-web -#: code:addons/web/static/src/js/views/form_relational_widgets.js:329 -#, fuzzy, python-format -msgid "No value found for the field %s for value %s" -msgstr "Вредноста не е пронајдена во полето за вредност" - -#. module: web -#. openerp-web #: code:addons/web/static/src/js/framework/utils.js:144 #, python-format msgid "Node [%s] is not a JSONified XML node" diff --git a/addons/web/i18n/zh_CN.po b/addons/web/i18n/zh_CN.po index 484e8fd15a1..c1aa4a4d92c 100644 --- a/addons/web/i18n/zh_CN.po +++ b/addons/web/i18n/zh_CN.po @@ -1241,7 +1241,7 @@ msgstr "域名" #: code:addons/web/static/src/js/core/misc.js:16 #, python-format msgid "Don't leave yet,<br />it's still loading..." -msgstr "请不要离开,<br />它还在加载中..." +msgstr "请不要离开,<br />它仍在加载..." #. module: web #. openerp-web @@ -3068,14 +3068,14 @@ msgstr "特殊:" #: code:addons/web/static/src/js/core/misc.js:14 #, python-format msgid "Still loading..." -msgstr "大菜烹饪中..." +msgstr "仍在加载..." #. module: web #. openerp-web #: code:addons/web/static/src/js/core/misc.js:15 #, python-format msgid "Still loading...<br />Please be patient." -msgstr "大菜烹饪中...<br />稍安勿躁。" +msgstr "仍在加载...<br />请耐心等待。" #. module: web #. openerp-web @@ -3117,7 +3117,7 @@ msgstr "语法错误" #: code:addons/web/static/src/js/core/misc.js:18 #, python-format msgid "Take a minute to get a coffee,<br />because it's loading..." -msgstr "稍安勿躁,<br />大菜烹饪中..." +msgstr "花一分钟去喝杯咖啡,<br />因为它在加载..." #. module: web #. openerp-web @@ -3725,7 +3725,7 @@ msgstr "你不能让任何密码为空。" #, python-format msgid "" "You may not believe it,<br />but the application is actually loading..." -msgstr "稍安勿躁,<br />大菜烹饪中...." +msgstr "也许你不相信,<br />但是这个应用真的还在加载...." #. module: web #. openerp-web diff --git a/addons/web/static/lib/tempusdominus/tempusdominus.js b/addons/web/static/lib/tempusdominus/tempusdominus.js index 63daabbd6f7..a5dac48ac3a 100644 --- a/addons/web/static/lib/tempusdominus/tempusdominus.js +++ b/addons/web/static/lib/tempusdominus/tempusdominus.js @@ -563,10 +563,10 @@ var DateTimePicker = function ($, moment) { DateTimePicker.prototype._notifyEvent = function _notifyEvent(e) { // /!\ ODOO FIX: these next conditions have been modified by odoo // FIXME should write a test about the tricky case this handles - if (!e.date && !e.oldDate) { - return; - } if (e.type === DateTimePicker.Event.CHANGE) { + if (!e.date && !e.oldDate) { + return; + } // check _isUTC flag to ensure that we are not comparing apples and oranges var bothUTC = e.date && e.oldDate && e.date._isUTC === e.oldDate._isUTC; if (bothUTC && e.date.isSame(e.oldDate)) { diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js index 2a961596862..848e406c4b2 100644 --- a/addons/web/static/src/js/core/dom.js +++ b/addons/web/static/src/js/core/dom.js @@ -20,6 +20,7 @@ odoo.define('web.dom', function (require) { * something happens in the DOM. */ +var concurrency = require('web.concurrency'); var config = require('web.config'); var core = require('web.core'); var _t = core._t; @@ -40,7 +41,9 @@ function _notify(content, callbacks) { core.bus.trigger('DOM_updated', content); } -return { +var dom = { + DEBOUNCE: 400, + /** * Appends content in a jQuery object and optionnally triggers an event * @@ -209,6 +212,91 @@ return { return position; }, /** + * Protects a function which is to be used as a handler by preventing its + * execution for the duration of a previous call to it (including async + * parts of that call). + * + * Limitation: as the handler is ignored during async actions, + * the 'preventDefault' or 'stopPropagation' calls it may want to do + * will be ignored too. Using the 'preventDefault' and 'stopPropagation' + * arguments solves that problem. + * + * @param {function} fct + * The function which is to be used as a handler. If a promise + * is returned, it is used to determine when the handler's action is + * finished. Otherwise, the return is used as jQuery uses it. + * @param {function|boolean} preventDefault + * @param {function|boolean} stopPropagation + */ + makeAsyncHandler: function (fct, preventDefault, stopPropagation) { + // Create a deferred indicating if a previous call to this handler is + // still pending. + var def = $.when(); + + return function (ev) { + if (preventDefault === true || preventDefault && preventDefault()) { + ev.preventDefault(); + } + if (stopPropagation === true || stopPropagation && stopPropagation()) { + ev.stopPropagation(); + } + + if (def.state() === 'pending') { + // If a previous call to this handler is still pending, ignore + // the new call. + return; + } + var result = fct.apply(this, arguments); + def = $.when(result); + return result; + }; + }, + /** + * Creates a debounced version of a function to be used as a button click + * handler. Also improves the handler to disable the button for the time of + * the debounce and/or the time of the async actions it performs. + * + * Limitation: if two handlers are put on the same button, the button will + * become enabled again once any handler's action finishes (multiple click + * handlers should however not be binded to the same button). + * + * @param {function} fct + * The function which is to be used as a button click handler. If a + * promise is returned, it is used to determine when the button can be + * re-enabled. Otherwise, the return is used as jQuery uses it. + */ + makeButtonHandler: function (fct) { + // Fallback: if the final handler is not binded to a button, at least + // make it an async handler (also handles the case where some events + // might ignore the disabled state of the button). + fct = dom.makeAsyncHandler(fct); + + return function (ev) { + var result = fct.apply(this, arguments); + + var $button = $(ev.target).closest('.btn'); + if (!$button.length) { + return result; + } + + // Disable the button for the duration of the handler's action + // or at least for the duration of the click debounce. This makes + // a 'real' debounce creation useless. Also, during the debouncing + // part, the button is disabled without any visual effect. + $button.addClass('o_debounce_disabled'); + $.when(dom.DEBOUNCE && concurrency.delay(dom.DEBOUNCE)).then(function () { + $button.addClass('disabled').prop('disabled', true); + $button.removeClass('o_debounce_disabled'); + + return $.when(result).always(function () { + $button.removeClass('disabled').prop('disabled', false); + }); + }); + + return result; + }; + }, + /** * Prepends content in a jQuery object and optionnally triggers an event * * @param {jQuery} [$target] the node where content will be prepended @@ -460,4 +548,5 @@ return { } }, }; +return dom; }); diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 53fbf99a8cd..aac91081c3c 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2409,6 +2409,18 @@ var FieldProgressBar = AbstractField.extend({ this.$('.o_progressbar_value').focus().select(); } }, + /** + * The progress bar has more than one field/value to deal with + * i.e. max_value + * + * @override + * @private + */ + _reset: function () { + this._super.apply(this, arguments); + var new_max_value = this.recordData[this.nodeOptions.max_value]; + this.max_value = new_max_value !== undefined ? new_max_value : this.max_value; + }, isSet: function () { return true; }, diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index d9e15dab302..60b0275c8c8 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -564,6 +564,7 @@ var FieldMany2One = AbstractField.extend({ initial_ids: ids ? _.map(ids, function (x) { return x[0]; }) : undefined, initial_view: view, disable_multiple_selection: true, + no_create: !self.can_create, on_selected: function (records) { self.reinitialize(records[0]); self.activate(); diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index 8f9f167c69f..024216b5619 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -181,6 +181,7 @@ var AbstractView = Class.extend({ modelName: params.modelName, res_id: params.currentId, res_ids: params.ids, + orderedBy: params.context ? params.context.orderedBy : [], }; if (params.modelName) { this.loadParams.modelName = params.modelName; 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 345b76856b6..53aa2e56c5a 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -3674,7 +3674,7 @@ var BasicModel = AbstractModel.extend({ model: params.modelName, offset: params.offset || (type === 'record' ? _.indexOf(res_ids, res_id) : 0), openGroupByDefault: params.openGroupByDefault, - orderedBy: params.orderedBy || (params.context && params.context.orderedBy) || [], + orderedBy: params.orderedBy || [], orderedResIDs: params.orderedResIDs, parentID: params.parentID, rawContext: params.rawContext, @@ -4608,7 +4608,7 @@ var BasicModel = AbstractModel.extend({ if (orderData1 > orderData2) { return order.asc ? 1 : -1; } - return compareRecords(record1ID, record2ID, level + 1); + return compareRecords(resId1, resId2, level + 1); }; utils.stableSort(list.res_ids, compareRecords); } diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 7ba3010a3b5..b677abe377a 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -15,6 +15,7 @@ var DateWidget = Widget.extend({ 'change.datetimepicker': 'changeDatetime', 'change .o_datepicker_input': 'changeDatetime', 'input input': '_onInput', + 'show.datetimepicker': '_onDateTimePickerShow', }, /** * @override @@ -226,6 +227,18 @@ var DateWidget = Widget.extend({ //-------------------------------------------------------------------------- /** + * Reacts to the datetimepicker being shown + * Could set/verify our widget value + * And subsequently update the datetimepicker + * + * @private + */ + _onDateTimePickerShow: function () { + if (this.$input.val().length !== 0 && this.isValid()) { + this.$input.select(); + } + }, + /** * Prevents 'input' events triggered by the library to bubble up, as they * might have unwanted effects (like triggering 'field_changed' events in * the context of field widgets) diff --git a/addons/web/static/src/js/widgets/dropdown_menu.js b/addons/web/static/src/js/widgets/dropdown_menu.js index fdab9b82ebf..e92c714d838 100644 --- a/addons/web/static/src/js/widgets/dropdown_menu.js +++ b/addons/web/static/src/js/widgets/dropdown_menu.js @@ -15,6 +15,7 @@ var DropdownMenu = Widget.extend({ 'click .o_item_option': '_onOptionClick', 'click span.o_trash_button': '_onTrashClick', 'hidden.bs.dropdown': '_onBootstrapClose', + 'click .dropdown-item-text': '_onDropDownItemTextClick', }, /** @@ -200,6 +201,15 @@ var DropdownMenu = Widget.extend({ this._renderMenuItems(); }, /** + * Reacts to click on bootstrap's dropdown-item-text + * Protects against Bootstrap dropdown close from inside click + * + * @private + */ + _onDropDownItemTextClick: function (ev) { + ev.stopPropagation(); + }, + /** * @private * @param {MouseEvent} event */ diff --git a/addons/web/static/src/scss/ui.scss b/addons/web/static/src/scss/ui.scss index 46995aa9ba1..44d639805be 100644 --- a/addons/web/static/src/scss/ui.scss +++ b/addons/web/static/src/scss/ui.scss @@ -78,3 +78,8 @@ outline: 20px solid fade-out($o-brand-primary, 1.0); } } + +// Declare a similar class as 'disabled' but with no visual effect +.o_debounce_disabled { + pointer-events: none; +} diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 807b2ebadca..f176b4e0ab9 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -2933,6 +2933,38 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test('field date should select its content onclick when there is one', function (assert) { + assert.expect(2); + var done = assert.async(); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="display_name" /> ' + // Do not focus on the date field right away + '<field name="date" />' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + form.$el.on({ + 'show.datetimepicker': function () { + assert.ok($('.bootstrap-datetimepicker-widget').is(':visible'), + 'bootstrap-datetimepicker is visible'); + assert.strictEqual(window.getSelection().toString(), "02/03/2017", + 'The whole input of the date field should have been selected'); + done(); + } + }); + form.$('.o_input[name="date"]').mouseenter().trigger('focus'); + + form.destroy(); + }); + QUnit.module('FieldDatetime'); QUnit.test('datetime field in form view', function (assert) { @@ -5379,6 +5411,48 @@ QUnit.module('basic_fields', { form.destroy(); }); + + QUnit.module('FieldProgressBar'); + + QUnit.test('Field ProgressBar: max_value should update', function (assert) { + assert.expect(2); + + this.data.partner.records = this.data.partner.records.slice(0,1); + this.data.partner.records[0].qux = 2; + + this.data.partner.onchanges = { + display_name: function (obj) { + obj.int_field = 999; + obj.qux = 5; + } + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="display_name" />' + + '<field name="qux" invisible="1" />' + + '<field name="int_field" widget="progressbar" options="{\'current_value\': \'int_field\', \'max_value\': \'qux\'}" />' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '10 / 2', + 'The initial value of the progress bar should be correct'); + + // trigger the onchange + form.$('.o_input[name=display_name]').val('new name').trigger('input'); + + assert.strictEqual(form.$('.o_progressbar_value').text(), '999 / 5', + 'The value of the progress bar should be correct after the update'); + + form.destroy(); + }); }); }); }); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 49af80784c9..8d23dafe4f4 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -150,6 +150,83 @@ QUnit.module('relational_fields', { } }, function () { + QUnit.test('x2many default_order multiple fields', function (assert) { + assert.expect(7); + + this.data.partner.records = [ + {int_field: 10, id: 1, display_name: "record1"}, + {int_field: 12, id: 2, display_name: "record2"}, + {int_field: 11, id: 3, display_name: "record3"}, + {int_field: 12, id: 4, display_name: "record4"}, + {int_field: 10, id: 5, display_name: "record5"}, + {int_field: 10, id: 6, display_name: "record6"}, + {int_field: 11, id: 7, display_name: "record7"}, + ]; + + this.data.partner.records[0].p = [1, 7, 4, 5, 2, 6, 3]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="p" >' + + '<tree default_order="int_field,id">' + + '<field name="id"/>' + + '<field name="int_field"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + var $recordList = form.$('.o_field_x2many_list .o_data_row'); + var expectedOrderId = ['1', '5', '6', '3', '7', '2', '4']; + + _.each($recordList, function(record, index) { + var $record = $(record); + assert.strictEqual($record.find('.o_data_cell').eq(0).text(), expectedOrderId[index], + 'The record should be the right place. Index: ' + index); + }); + + form.destroy(); + }); + + + form.destroy(); + }); + + QUnit.test('one2many from a model that has been sorted', function (assert) { + assert.expect(1); + + /* On a standard list view, sort your records by a field + * Click on a record which contains a x2m with multiple records in it + * The x2m shouldn't take the orderedBy of the parent record (the one on the form) + */ + + this.data.partner.records[0].turtles = [3, 2]; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="turtles">' + + '<tree>' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + context: { + orderedBy: [{ + name: 'foo', + asc: false, + }] + }, + }); + + assert.strictEqual(form.$('.o_field_one2many[name=turtles] tbody').text().trim(), "kawablip", + 'The o2m should not have been sorted.'); QUnit.test('onchange on unloaded record clearing posterious change', function (assert) { // when we got onchange result for fields of record that were not // already available because they were in a inline view not already diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js index 769c179472a..3eaaae0a46f 100644 --- a/addons/web/static/tests/helpers/test_utils_mock.js +++ b/addons/web/static/tests/helpers/test_utils_mock.js @@ -13,6 +13,7 @@ odoo.define('web.test_utils_mock', function (require) { var basic_fields = require('web.basic_fields'); var config = require('web.config'); var core = require('web.core'); +var dom = require('web.dom'); var MockServer = require('web.MockServer'); var session = require('web.session'); @@ -154,6 +155,8 @@ function addMockEnvironment(widget, params) { // make sure the debounce value for input fields is set to 0 var initialDebounceValue = DebouncedField.prototype.DEBOUNCE; DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0; + var initialDOMDebounceValue = dom.DEBOUNCE; + dom.DEBOUNCE = 0; var initialSession, initialConfig, initialParameters, initialDebounce, initialThrottle; initialSession = _.extend({}, session); session.getTZOffset = function () { @@ -196,6 +199,7 @@ function addMockEnvironment(widget, params) { core.bus.trigger('clear_cache'); DebouncedField.prototype.DEBOUNCE = initialDebounceValue; + dom.DEBOUNCE = initialDOMDebounceValue; if (params.debounce === false) { _.debounce = initialDebounce; } diff --git a/addons/web/static/tests/views/form_benchmarks.js b/addons/web/static/tests/views/form_benchmarks.js index c6127f847ec..efd12741d66 100644 --- a/addons/web/static/tests/views/form_benchmarks.js +++ b/addons/web/static/tests/views/form_benchmarks.js @@ -1,4 +1,4 @@ -odoo.define('web.list_benchmarks', function (require) { +odoo.define('web.form_benchmarks', function (require) { "use strict"; var FormView = require('web.FormView'); @@ -38,7 +38,6 @@ QUnit.module('Form View', { this.run = function (assert, done, cb) { var data = this.data; var arch = this.arch; - var viewOptions = this.viewOptions; new Benchmark.Suite({}) .add('form', function () { var list = createView({ @@ -76,13 +75,13 @@ QUnit.module('Form View', { this.data.foo.records[0].many2many.push(i); } this.arch = - '<form string="Partners">' - '<field name="many2many">' - '<tree editable="top" limit="250">' - '<field name="char"/>' - '<field name="many2many" widget="many2many_tags" attrs="{\'readonly\': [(\'char\', \'==\', \'toto\')]}"/>' - '</tree>' - '</field>' + '<form string="Partners">' + + '<field name="many2many">' + + '<tree editable="top" limit="250">' + + '<field name="char"/>' + + '<field name="many2many" widget="many2many_tags" attrs="{\'readonly\': [(\'char\', \'==\', \'toto\')]}"/>' + + '</tree>' + + '</field>' + '</form>'; this.run(assert, done, function (form) { testUtils.form.clickEdit(form); diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js index e64ae72a9f4..0dbddbe1fae 100644 --- a/addons/web/static/tests/views/search_view_tests.js +++ b/addons/web/static/tests/views/search_view_tests.js @@ -975,5 +975,66 @@ QUnit.module('Search View', { form.destroy(); }); + QUnit.test('Customizing filter does not close the filter dropdown', function (assert) { + assert.expect(4); + var self = this; + + _.each(this.data.partner.records.slice(), function (rec) { + var copy = _.defaults({}, rec, {id: rec.id + 10 }); + self.data.partner.records.push(copy); + }); + + this.data.partner.fields.date_field.searchable = true; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="bar"/>' + + '</form>', + viewOptions: { + mode: 'edit', + }, + archs: { + 'partner,false,list': '<tree><field name="display_name"/></tree>', + 'partner,false,search': '<search><field name="date_field"/></search>', + }, + res_id: 1, + }); + + form.$('.o_input').click(); + $('.ui-autocomplete .ui-menu-item:contains(Search More)').mouseenter().click(); + + var $modal = $('.modal-dialog.modal-lg'); + assert.strictEqual($modal.length, 1, 'Modal Opened'); + + $modal.find('.o_search_options button:contains(Filters)').click(); + + var $filterDropDown = $modal.find('.o_dropdown_menu.show'); + + $filterDropDown.find('button:contains(Add Custom Filter)').click(); + + assert.ok($filterDropDown.find('button:contains(Add Custom Filter)').hasClass('o_open_menu'), + 'The right dropdown is open'); + + var $filterInputs = $filterDropDown.find('.o_input'); + + assert.strictEqual($filterInputs.length, 3, + 'The custom filter builder has 3 elements'); + + // We really are interested in the click event + // We do it twice on each input to make sure + // the parent dropdown doesn't react to any of it + _.each($filterInputs, function (input) { + var $input = $(input); + $input.click(); + $input.click(); + }); + + assert.ok($filterDropDown.is(':visible')); + + form.destroy(); + }); + }); }); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index 8898e520618..023a977ad56 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -478,6 +478,66 @@ QUnit.module('Views', { testUtils.mock.unpatch(ListController); parent.destroy(); }); + + QUnit.test('propagate can_create onto the search popup o2m', function (assert) { + assert.expect(3); + + this.data.instrument.records = [ + {id: 1, name: 'Tromblon1'}, + {id: 2, name: 'Tromblon2'}, + {id: 3, name: 'Tromblon3'}, + {id: 4, name: 'Tromblon4'}, + {id: 5, name: 'Tromblon5'}, + {id: 6, name: 'Tromblon6'}, + {id: 7, name: 'Tromblon7'}, + {id: 8, name: 'Tromblon8'}, + ]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="name"/>' + + '<field name="instrument" can_create="false"/>' + + '</form>', + res_id: 1, + archs: { + 'instrument,false,list': '<tree>'+ + '<field name="name"/>'+ + '</tree>', + 'instrument,false,search': '<search>'+ + '<field name="name"/>'+ + '</search>', + }, + viewOptions: { + mode: 'edit', + }, + + mockRPC: function(route, args) { + if (args.method === 'get_formview_id') { + return $.when(false); + } + return this._super(route, args); + }, + }); + + form.$('.o_field_widget[name="instrument"] .o_input').click(); + + assert.notOk($('.ui-autocomplete a:contains(Create and Edit)').length, + 'Create and edit not present in dropdown'); + + $('.ui-autocomplete a:contains(Search More)').trigger('mouseenter').trigger('click'); + + var $modal = $('.modal-dialog.modal-lg'); + + assert.strictEqual($modal.length, 1, 'Modal present'); + + assert.strictEqual($modal.find('.modal-footer button').text(), "Cancel", + 'Only the cancel button is present in modal'); + + form.destroy(); + }); }); }); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 1ce0a83f63b..10d887fe8e1 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -687,6 +687,16 @@ display: inherit; } </style> + + <script type="text/javascript" src="/web/static/tests/helpers/test_utils_create.js"></script> + <script type="text/javascript" src="/web/static/tests/helpers/test_utils_dom.js"></script> + <script type="text/javascript" src="/web/static/tests/helpers/test_utils_fields.js"></script> + <script type="text/javascript" src="/web/static/tests/helpers/test_utils_form.js"></script> + <script type="text/javascript" src="/web/static/tests/helpers/test_utils_graph.js"></script> + <script type="text/javascript" src="/web/static/tests/helpers/test_utils_kanban.js"></script> + <script type="text/javascript" src="/web/static/tests/helpers/test_utils_mock.js"></script> + <script type="text/javascript" src="/web/static/tests/helpers/test_utils_modal.js"></script> + <script type="text/javascript" src="/web/static/tests/helpers/test_utils_pivot.js"></script> <script type="text/javascript" src="/web/static/tests/helpers/test_utils.js"></script> <script type="text/javascript" src="/web/static/tests/helpers/mock_server.js"></script> |
