diff options
| author | Aaron Bohy <aab@odoo.com> | 2016-04-08 14:53:34 +0200 |
|---|---|---|
| committer | Géry Debongnie <ged@odoo.com> | 2016-04-21 15:21:39 +0200 |
| commit | 977db823c7c84e9edc7c8cf8384eb9cf47275b3a (patch) | |
| tree | c3312f745baf11d51f976a60d6e285480f557d4c /addons/web/static/src | |
| parent | e1bfd3fbda97ac7a64a585b07be161d5be606cf9 (diff) | |
[REF] web: stop duplicating web
Avoid duplicating web addon in enterprise by extracting a common basis.
Enterprise features stay in enterprise, but use that common basis.
Mainly:
- JS refactoring and linting
- Conversion of .sass into .less split into multiple files
- Templates cleaning and DOM simplification
- Re-generation of web.pot, and update of .po files
Diffstat (limited to 'addons/web/static/src')
80 files changed, 7006 insertions, 4504 deletions
diff --git a/addons/web/static/src/css/data_export.css b/addons/web/static/src/css/data_export.css deleted file mode 100644 index b9b885cd8e9..00000000000 --- a/addons/web/static/src/css/data_export.css +++ /dev/null @@ -1,89 +0,0 @@ -.openerp .oe_export { - width: 100%; -} -/* Customize according bootstrap3 */ -.openerp .oe_export label { - font-weight: normal; -} -/* End of customize */ -.openerp .oe_export_row tr{ - background-color: #FFFFFF; - font-size: 0.8em; - height: 22px; -} - -.openerp tr.ui-selected td { - background-color: #CCCCCC; -} - -.openerp .oe_export_requiredfield { - background-color: #D2D2FF; -} - -.openerp .oe_export_readonlyfield{ - background-color: #999999; -} - -.openerp .oe_export_row:hover{ - background-color: #F3F3F3; -} - -.openerp .oe_export_fields_selector_left { - width: 50%; -} - -.openerp div#left_field_panel { - overflow: scroll; - width: 100%; - height: 400px; - border: solid #999999 1px; -} -.openerp div#left_field_panel table { - width: 100%; -} - -.openerp .oe_export_fields_selector_center { - text-align: center; -} -.openerp .oe_export_fields_selector_center button { - white-space: nowrap; -} - -.openerp .oe_export_fields_selector_right { - width: 45%; - height: 400px; -} - -.openerp .oe_export_fields_selector_right select{ - width: 100%; - height: 100%; -} - -.openerp .oe_export_tree_header{ - border: 0.5px solid #E3E3E3; - text-align: left; - white-space: nowrap; - padding: 4px 5px; - background: url(/web/static/src/img/header.gif); -} - - -.openerp table.tree_grid{ - border: 1px solid #E3E3E3; - text-align: left; - white-space: nowrap; - background-color:#E3E3E3; - border-collapse: collapse; - width: 100%; -} - -.openerp table.tree_grid a:hover { - color: blue; - border: none; -} - -.openerp table.tree_grid a { - color: #5F5C5C; - border: none; - display: block; -} diff --git a/addons/web/static/src/css/full.css b/addons/web/static/src/css/full.css deleted file mode 100644 index 2692792d46d..00000000000 --- a/addons/web/static/src/css/full.css +++ /dev/null @@ -1,12 +0,0 @@ -/* This css contains the styling specific to the 'normal mode'. (as opposite to the embedded mode) */ -/* In embedded mode [1], this stylesheet won't be loaded. */ - -/* [1] The web client features an embedded mode in which the webclient */ -/* or a part of it can be started in any element of any webpage host. */ - -body { - padding: 0; - margin: 0; - overflow-y: scroll; - height: 100%; -} diff --git a/addons/web/static/src/css/ie.css b/addons/web/static/src/css/ie.css deleted file mode 100644 index a547120033e..00000000000 --- a/addons/web/static/src/css/ie.css +++ /dev/null @@ -1,29 +0,0 @@ -/** base */ -.openerp .oe_form label + .oe_form_field.oe_form_field_one2many { - display: block; -} -.openerp .oe_leftbar > div > div > div > div.oe_secondary_menus_container { - position: static; -} -.openerp .oe_application > div > .oe_view_manager > .oe_view_manager_wrapper > div > .oe_view_manager_body { - position: static; -} - -/** web_calendar */ -.fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */ -.fc-select-helper .fc-event-bg { - display: none; /* for IE6/7/8. nested opacity filters while dragging don't work */ -} - -/** web_graph */ -.openerp svg:not(:root) { /* width and height are not take in consideration */ - overflow: visible; -} -.graph_main_content.graph_chart_mode > div > svg { - min-height: 521px; -} - -/** web_kanban */ -.openerp .oe_kanban_column_cards { - min-height: 80px; -} diff --git a/addons/web/static/src/img/down-arrow.png b/addons/web/static/src/img/down-arrow.png Binary files differdeleted file mode 100644 index 0f473e13e84..00000000000 --- a/addons/web/static/src/img/down-arrow.png +++ /dev/null diff --git a/addons/web/static/src/js/abstract_web_client.js b/addons/web/static/src/js/abstract_web_client.js new file mode 100644 index 00000000000..31aa0d6f3dd --- /dev/null +++ b/addons/web/static/src/js/abstract_web_client.js @@ -0,0 +1,245 @@ +odoo.define('web.AbstractWebClient', function (require) { +"use strict"; + +var ActionManager = require('web.ActionManager'); +var core = require('web.core'); +var crash_manager = require('web.crash_manager'); +var Loading = require('web.Loading'); +var NotificationManager = require('web.notification').NotificationManager; +var session = require('web.session'); +var utils = require('web.utils'); +var Widget = require('web.Widget'); + +var _t = core._t; + +var WebClient = Widget.extend({ + custom_events: { + notification: function (e) { + if(this.notification_manager) { + this.notification_manager.notify(e.data.title, e.data.message, e.data.sticky); + } + }, + warning: function (e) { + if(this.notification_manager) { + this.notification_manager.warn(e.data.title, e.data.message, e.data.sticky); + } + }, + clear_uncommitted_changes: function (e) { + this.clear_uncommitted_changes().then(e.data.callback); + }, + }, + init: function(parent, client_options) { + this.client_options = {}; + this._super(parent); + this.origin = undefined; + if (client_options) { + _.extend(this.client_options, client_options); + } + this._current_state = null; + this.menu_dm = new utils.DropMisordered(); + this.action_mutex = new utils.Mutex(); + this.set('title_part', {"zopenerp": "Odoo"}); + }, + start: function() { + var self = this; + + this.on("change:title_part", this, this._title_changed); + this._title_changed(); + + return session.session_bind(this.origin) + .then(function () { + self.bind_events(); + return $.when( + self.set_action_manager(), + self.set_notification_manager(), + self.set_loading() + ); + }).then(function () { + if (session.session_is_valid()) { + return self.show_application(); + } else { + // database manager needs the webclient to keep going even + // though it has no valid session + return $.when(); + } + }).then(function () { + if (self.client_options.action) { + self.do_action(self.client_options.action); + delete(self.client_options.action); + } + core.bus.trigger('web_client_ready'); + }); + }, + bind_events: function() { + var self = this; + $('.oe_systray').show(); + this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() { + $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter'); + }); + this.$el.on('click', '.oe_dropdown_toggle', function(ev) { + ev.preventDefault(); + var $toggle = $(this); + var doc_width = $(document).width(); + var $menu = $toggle.siblings('.oe_dropdown_menu'); + $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu'); + var state = $menu.is('.oe_opened'); + setTimeout(function() { + // Do not alter propagation + $toggle.add($menu).toggleClass('oe_opened', !state); + if (!state) { + // Move $menu if outside window's edge + var offset = $menu.offset(); + var menu_width = $menu.width(); + var x = doc_width - offset.left - menu_width - 2; + if (x < 0) { + $menu.offset({ left: offset.left + x }).width(menu_width); + } + } + }, 0); + }); + core.bus.on('click', this, function(ev) { + $('.tooltip').remove(); + if (!$(ev.target).is('input[type=file]')) { + self.$('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened'); + } + }); + core.bus.on('set_full_screen', this, function (full_screen) { + this.set_content_full_screen(full_screen); + }); + core.bus.on('connection_lost', this, this.on_connection_lost); + core.bus.on('connection_restored', this, this.on_connection_restored); + + // crash manager integration + session.on('error', crash_manager, crash_manager.rpc_error); + window.onerror = function (message, file, line, col, error) { + var traceback = error ? error.stack : ''; + crash_manager.show_error({ + type: _t("Client Error"), + message: message, + data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback} + }); + }; + }, + set_action_manager: function() { + this.action_manager = new ActionManager(this, {webclient: this}); + return this.action_manager.appendTo(this.$('.o_main_content')); + }, + set_notification_manager: function() { + this.notification_manager = new NotificationManager(this); + return this.notification_manager.appendTo(this.$el); + }, + set_loading: function() { + this.loading = new Loading(this); + return this.loading.appendTo(this.$el); + }, + show_application: function() { + }, + clear_uncommitted_changes: function() { + var def = $.Deferred().resolve(); + core.bus.trigger('clear_uncommitted_changes', function chain_callbacks(callback) { + def = def.then(callback); + }); + return def; + }, + destroy_content: function() { + _.each(_.clone(this.getChildren()), function(el) { + el.destroy(); + }); + this.$el.children().remove(); + }, + // -------------------------------------------------------------- + // Window title handling + // -------------------------------------------------------------- + /** + Sets the first part of the title of the window, dedicated to the current action. + */ + set_title: function(title) { + this.set_title_part("action", title); + }, + /** + Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time + a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the + title of the window separated by '-'. + */ + set_title_part: function(part, title) { + var tmp = _.clone(this.get("title_part")); + tmp[part] = title; + this.set("title_part", tmp); + }, + _title_changed: function() { + var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; }); + var tmp = ""; + _.each(parts, function(part) { + var str = this.get("title_part")[part]; + if (str) { + tmp = tmp ? tmp + " - " + str : str; + } + }, this); + document.title = tmp; + }, + // -------------------------------------------------------------- + // do_* + // -------------------------------------------------------------- + /** + * When do_action is performed on the WebClient, forward it to the main ActionManager + * This allows to widgets that are not inside the ActionManager to perform do_action + */ + do_action: function() { + return this.action_manager.do_action.apply(this, arguments); + }, + do_reload: function() { + var self = this; + return this.session.session_reload().then(function () { + session.load_modules(true).then( + self.menu.proxy('do_reload')); + }); + }, + do_push_state: function(state) { + if ('title' in state) { + this.set_title(state.title); + delete state.title; + } + var url = '#' + $.param(state); + this._current_state = $.deparam($.param(state), false); // stringify all values + $.bbq.pushState(url); + this.trigger('state_pushed', state); + }, + // -------------------------------------------------------------- + // Connection notifications + // -------------------------------------------------------------- + on_connection_lost: function () { + this.connection_notification = this.notification_manager.notify( + _t('Connection lost'), + _t('Trying to reconnect...'), + true + ); + }, + on_connection_restored: function () { + if (this.connection_notification) { + this.connection_notification.destroy(); + this.notification_manager.notify( + _t('Connection restored'), + _t('You are back online'), + false + ); + this.connection_notification = false; + } + }, + // -------------------------------------------------------------- + // Scrolltop handling + // -------------------------------------------------------------- + get_scrollTop: function () { + }, + //-------------------------------------------------------------- + // Misc. + //-------------------------------------------------------------- + set_content_full_screen: function(fullscreen) { + $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll'); + this.$('.oe_webclient').toggleClass( + 'oe_content_full_screen', fullscreen); + }, +}); + +return WebClient; + +}); diff --git a/addons/web/static/src/js/action_manager.js b/addons/web/static/src/js/action_manager.js index 359b174a28d..be5e6c6c996 100644 --- a/addons/web/static/src/js/action_manager.js +++ b/addons/web/static/src/js/action_manager.js @@ -23,24 +23,17 @@ var Action = core.Class.extend({ this.title = action.display_name || action.name; }, /** - * Not implemented for client actions - * @return {Deferred} a rejected Deferred - */ - appendTo: function() { - return $.Deferred().reject(); - }, - /** * This method should restore this previously loaded action - * Calls on_reverse_breadcrumb callback if defined + * Calls on_reverse_breadcrumb_callback if defined * @return {Deferred} resolved when widget is enabled */ restore: function() { - if (this.on_reverse_breadcrumb) { - return this.on_reverse_breadcrumb(); + if (this.on_reverse_breadcrumb_callback) { + return this.on_reverse_breadcrumb_callback(); } }, /** - * Not implemented for functions + * There is nothing to detach in the case of a client function */ detach: function() { }, @@ -50,11 +43,16 @@ var Action = core.Class.extend({ destroy: function() { }, /** - * Sets the on_reverse_breadcrumb callback to be called when coming back to that action - * @param {Function} [on_reverse_breadcrumb] the callback + * Sets the on_reverse_breadcrumb_callback to be called when coming back to that action + * @param {Function} [callback] the callback + */ + set_on_reverse_breadcrumb: function(callback) { + this.on_reverse_breadcrumb_callback = callback; + }, + /** + * Not implemented for client actions */ - set_on_reverse_breadcrumb: function(on_reverse_breadcrumb) { - this.on_reverse_breadcrumb = on_reverse_breadcrumb; + set_scrollTop: function() { }, /** * Stores the DOM fragment of the action @@ -65,8 +63,10 @@ var Action = core.Class.extend({ }, /** * Not implemented for client actions + * @return {int} the number of pixels the webclient is scrolled when leaving the action */ - set_is_in_DOM: function() { + get_scrollTop: function() { + return 0; }, /** * @return {Object} the description of the action @@ -113,16 +113,6 @@ var WidgetAction = Action.extend({ }); }, /** - * Wraps the action's widget in a container and appends it to el - * @param {DocumentFragment} [el] where to append the widget - * @return {Deferred} resolved when the widget is appended - */ - appendTo: function(el) { - this.$client_action_container = $('<div>').addClass('oe_client_action'); - this.$client_action_container.appendTo(el); - return this.widget.appendTo(this.$client_action_container); - }, - /** * Restores WidgetAction by calling do_show on its widget */ restore: function() { @@ -132,18 +122,20 @@ var WidgetAction = Action.extend({ }); }, /** - * Detaches the client action's container from the DOM - * @return {jQuery} the action's container + * Detaches the action's widget from the DOM + * @return the widget's $el */ detach: function() { - return this.$client_action_container.detach(); + // Hack to remove badly inserted nvd3 tooltips ; should be removed when upgrading nvd3 lib + $('body > .nvtooltip').remove(); + + return framework.detach([{widget: this.widget}]); }, /** * Destroys the widget */ destroy: function() { this.widget.destroy(); - this.detach(); }, }); /** @@ -151,14 +143,6 @@ var WidgetAction = Action.extend({ */ var ViewManagerAction = WidgetAction.extend({ /** - * Appends the action's widget to el - * @param {DocumentFragment} [el] where to append the widget - * @return {Deferred} resolved when the widget is appended - */ - appendTo: function(el) { - return this.widget.appendTo(el); - }, - /** * Restores a ViewManagerAction * Switches to the requested view by calling select_view on the ViewManager * @param {int} [view_index] the index of the view to select @@ -170,27 +154,27 @@ var ViewManagerAction = WidgetAction.extend({ }); }, /** - * Sets is_in_DOM on this.widget - * @param {Boolean} [is_in_DOM] true iff the widget is attached in the DOM + * Sets the on_reverse_breadcrumb_callback and the scrollTop to apply when + * coming back to that action + * @param {Function} [callback] the callback + * @param {int} [scrollTop] the number of pixels to scroll */ - set_is_in_DOM: function(is_in_DOM) { - this.widget.is_in_DOM = is_in_DOM; + set_on_reverse_breadcrumb: function(callback, scrollTop) { + this._super(callback); + this.set_scrollTop(scrollTop); }, /** - * Detaches the view_manager from the DOM - * @return {jQuery} the view_manager's $el + * Sets the scroll position of the widgets's active_view + * @param {int} [scrollTop] the number of pixels to scroll */ - detach: function() { - // Hack to remove badly inserted nvd3 tooltips ; should be removed when upgrading nvd3 lib - $('body > .nvtooltip').remove(); - - return this.widget.$el.detach(); + set_scrollTop: function(scrollTop) { + this.widget.active_view.controller.set_scrollTop(scrollTop); }, /** - * Destroys the widget + * @return {int} the number of pixels the webclient is scrolled when leaving the action */ - destroy: function() { - this.widget.destroy(); + get_scrollTop: function() { + return this.widget.active_view.controller.get_scrollTop(); }, /** * @return {Array} array of Objects that will be interpreted to display the breadcrumbs @@ -214,13 +198,31 @@ var ViewManagerAction = WidgetAction.extend({ }); var ActionManager = Widget.extend({ - template: "ActionManager", - init: function(parent) { + template: 'ActionManager', + /** + * Called each time the action manager is attached into the DOM + */ + on_attach_callback: function() { + this.is_in_DOM = true; + if (this.inner_widget && this.inner_widget.on_attach_callback) { + this.inner_widget.on_attach_callback(); + } + }, + /** + * Called each time the action manager is detached from the DOM + */ + on_detach_callback: function() { + this.is_in_DOM = false; + if (this.inner_widget && this.inner_widget.on_detach_callback) { + this.inner_widget.on_detach_callback(); + } + }, + init: function(parent, options) { this._super(parent); this.action_stack = []; this.inner_action = null; this.inner_widget = null; - this.webclient = parent; + this.webclient = options && options.webclient; this.dialog = null; this.dialog_widget = null; this.on('history_back', this, this.proxy('history_back')); @@ -236,8 +238,15 @@ var ActionManager = Widget.extend({ this.select_action(action, index); }, 200, true)); - // Append the main control panel to the DOM (inside the ActionManager jQuery element) - this.main_control_panel.appendTo(this.$el); + // Listen to event "DOM_updated" to restore the scroll position + core.bus.on('DOM_updated', this, function() { + if (this.inner_action) { + this.trigger_up('scrollTo', {offset: this.inner_action.get_scrollTop() || 0}); + } + }); + + // Insert the main control panel into the DOM + return this.main_control_panel.insertBefore(this.$el); }, dialog_stop: function (reason) { if (this.dialog) { @@ -256,9 +265,9 @@ var ActionManager = Widget.extend({ */ push_action: function(widget, action_descr, options) { var self = this; - var old_widget = this.inner_widget; - var old_action = this.inner_action; var old_action_stack = this.action_stack; + var old_action = this.inner_action; + var old_widget = this.inner_widget; options = options || {}; // Empty action_stack if requested @@ -277,8 +286,8 @@ var ActionManager = Widget.extend({ } // Set on_reverse_breadcrumb callback on previous inner_action - if (old_action) { - old_action.set_on_reverse_breadcrumb(options.on_reverse_breadcrumb); + if (this.webclient && old_action) { + old_action.set_on_reverse_breadcrumb(options.on_reverse_breadcrumb, this.webclient.get_scrollTop()); } // Update action_stack (must be done before appendTo to properly @@ -295,25 +304,19 @@ var ActionManager = Widget.extend({ // render the inner_widget in a fragment, and append it to the // document only when it's ready var new_widget_fragment = document.createDocumentFragment(); - return $.when(this.inner_action.appendTo(new_widget_fragment)).done(function() { + return $.when(this.inner_widget.appendTo(new_widget_fragment)).done(function() { // Detach the fragment of the previous action and store it within the action if (old_action) { old_action.set_fragment(old_action.detach()); - old_action.set_is_in_DOM(false); } if (!widget.need_control_panel) { // Hide the main ControlPanel for widgets that do not use it self.main_control_panel.do_hide(); } - - framework.append(self.$el, new_widget_fragment, true); - self.inner_action.set_is_in_DOM(true); - - // Hide the old_widget as it will be removed from the DOM when it - // is destroyed - if (old_widget) { - old_widget.do_hide(); - } + framework.append(self.$el, new_widget_fragment, { + in_DOM: self.is_in_DOM, + callbacks: [{widget: self.inner_widget}], + }); if (options.clear_breadcrumbs) { self.clear_action_stack(old_action_stack); } @@ -325,6 +328,11 @@ var ActionManager = Widget.extend({ self.inner_widget = old_widget; }); }, + set_scrollTop: function(scrollTop) { + if (this.inner_action) { + this.inner_action.set_scrollTop(scrollTop); + } + }, get_breadcrumbs: function () { return _.flatten(_.map(this.action_stack, function (action) { return action.get_breadcrumbs(); @@ -368,7 +376,9 @@ var ActionManager = Widget.extend({ }, select_action: function(action, index) { var self = this; - return this.webclient.clear_uncommitted_changes().then(function() { + var def = this.webclient && this.webclient.clear_uncommitted_changes() || $.when(); + + return def.then(function() { // Set the new inner_action/widget and update the action stack var old_action = self.inner_action; var action_index = self.action_stack.indexOf(action); @@ -376,19 +386,21 @@ var ActionManager = Widget.extend({ self.inner_action = action; self.inner_widget = action.widget; - // Hide the ControlPanel if the widget doesn't use it - if (!self.inner_widget.need_control_panel) { - self.main_control_panel.do_hide(); - } - return $.when(action.restore(index)).done(function() { + // Hide the ControlPanel if the widget doesn't use it + if (!self.inner_widget.need_control_panel) { + self.main_control_panel.do_hide(); + } + // Attach the DOM of the action and restore the scroll position only if necessary if (action !== old_action) { // Clear the action stack (this also removes the current action from the DOM) self.clear_action_stack(to_destroy); // Append the fragment of the action to restore to self.$el - framework.append(self.$el, action.get_fragment(), true); - self.inner_action.set_is_in_DOM(true); + framework.append(self.$el, action.get_fragment(), { + in_DOM: self.is_in_DOM, + callbacks: [{widget: action.widget}], + }); } }); }).fail(function() { @@ -406,7 +418,7 @@ var ActionManager = Widget.extend({ } }, do_push_state: function(state) { - if (!this.webclient || !this.webclient.do_push_state || this.dialog) { + if (!this.webclient || this.dialog) { return; } state = state || {}; @@ -491,13 +503,6 @@ var ActionManager = Widget.extend({ res_id: state.id, view_type: state.view_type, }); - $.when(action_loaded || null).done(function() { - self.webclient.menu.is_bound.done(function() { - if (self.inner_action && self.inner_action.get_action_descr().id) { - self.webclient.menu.open_action(self.inner_action.get_action_descr().id); - } - }); - }); } } } else if (state.model && state.id) { @@ -524,9 +529,9 @@ var ActionManager = Widget.extend({ }); } - $.when(action_loaded || null).done(function() { + return $.when(action_loaded || null).done(function() { if (self.inner_widget && self.inner_widget.do_load_state) { - self.inner_widget.do_load_state(state, warm); + return self.inner_widget.do_load_state(state, warm); } }); }, @@ -628,7 +633,9 @@ var ActionManager = Widget.extend({ }); } - return this[type](action, options); + return $.when(this[type](action, options)).then(function() { + return action; + }); }, null_action: function() { this.dialog_stop(); @@ -645,6 +652,7 @@ var ActionManager = Widget.extend({ * @return {*} */ ir_actions_common: function(executor, options) { + var self = this; if (executor.action.target === 'new') { var pre_dialog = (this.dialog && !this.dialog.isDestroyed()) ? this.dialog : null; if (pre_dialog){ @@ -693,13 +701,16 @@ var ActionManager = Widget.extend({ this.dialog_widget.set_cp_bus(new core.Bus()); } this.dialog_widget.setParent(this.dialog); - this.dialog.open(); - - return this.dialog_widget.appendTo(this.dialog.$el); + + var fragment = document.createDocumentFragment(); + return this.dialog_widget.appendTo(fragment).then(function() { + self.dialog.open().$el.append(fragment); + if(options.state && self.dialog_widget.do_load_state) { + return self.dialog_widget.do_load_state(options.state); + } + }).then(function () { return executor.action; }); } - // Clear uncommitted changes on the current inner widget if there is one - var self = this; - var def = (this.inner_widget && this.webclient.clear_uncommitted_changes()) || $.when(); + var def = this.inner_action && this.webclient && this.webclient.clear_uncommitted_changes() || $.when(); return def.then(function() { self.dialog_stop(executor.action); return self.push_action(executor.widget(), executor.action, options); @@ -764,11 +775,11 @@ var ActionManager = Widget.extend({ }, ir_actions_server: function (action, options) { var self = this; - this.rpc('/web/action/run', { + return this.rpc('/web/action/run', { action_id: action.id, context: action.context || {} - }).done(function (action) { - self.do_action(action, options); + }).then(function (action) { + return self.do_action(action, options); }); }, ir_actions_report_xml: function(action, options) { @@ -818,6 +829,7 @@ var ActionManager = Widget.extend({ if (action.target === 'self') { framework.redirect(url); + return $.Deferred(); // The action is finished only when the redirection is done } else { window.open(url, '_blank'); } diff --git a/addons/web/static/src/js/apps.js b/addons/web/static/src/js/apps.js index 156fe9e10b7..5dd5b576fc4 100644 --- a/addons/web/static/src/js/apps.js +++ b/addons/web/static/src/js/apps.js @@ -1,4 +1,4 @@ -odoo.define('base.apps', function (require) { +odoo.define('web.Apps', function (require) { "use strict"; var core = require('web.core'); @@ -9,10 +9,9 @@ var web_client = require('web.web_client'); var Widget = require('web.Widget'); var _t = core._t; -var qweb = core.qweb; var apps_client = null; - +var qweb = core.qweb; var Apps = Widget.extend({ template: 'EmptyComponent', @@ -106,92 +105,98 @@ var Apps = Widget.extend({ } }, - start: function() { + _on_update_count: function(m) { var self = this; - return self.get_client(). - done(function(client) { - self.client = client; - - var qs = (session.debug ? 'debug&' : '') + 'db=' + client.dbname; - var u = client.origin + '/apps/embed/client?' + qs; - var css = {width: '100%', height: '400px'}; - self.$ifr = $('<iframe>').attr('src', u); - - $(window).on("message.apps", self.proxy('_on_message')); - - self.on('message:ready', self, function(m) { - var w = this.$ifr[0].contentWindow; - var act = { - type: 'ir.actions.client', - tag: this.remote_action_tag, - params: _.extend({}, this.params, { - db: this.session.db, - origin: this.session.origin, - }) - }; - w.postMessage({type:'action', action: act}, client.origin); + var count = m.count; + var get_upd_menu_id = function() { + if (_.isUndefined(self._upd_menu_id)) { + var IMD = new Model('ir.model.data'); + return IMD.call('get_object_reference', ['base', 'menu_module_updates']).then(function(r) { + var mid = r[1]; + if(r[0] !== 'ir.ui.menu') { + // invalid reference, return null + mid = null; + } + self._upd_menu_id = mid; + return mid; }); + } else { + return $.Deferred().resolve(self._upd_menu_id).promise(); + } + }; - self.on('message:set_height', self, function(m) { - this.$ifr.height(m.height); - }); + $.when(get_upd_menu_id()).done(function(menu_id) { + if (_.isNull(menu_id)) { + return; + } + var $menu = web_client.menu.$secondary_menus.find(_.str.sprintf('a[data-menu=%s]', menu_id)); + if ($menu.length === 0) { + return; + } + if (_.isUndefined(count)) { + count = 0; + } + var needupdate = $menu.find('#menu_counter'); + if (needupdate && needupdate.length !== 0) { + if (count > 0) { + needupdate.text(count); + } else { + needupdate.remove(); + } + } else if (count > 0) { + $menu.append(qweb.render("Menu.needaction_counter", {widget: {needaction_counter: count}})); + } + }); + }, - self.on('message:update_count', self, function(m) { - var count = m.count; - var get_upd_menu_id = function() { - if (_.isUndefined(self._upd_menu_id)) { - var IMD = new Model('ir.model.data'); - return IMD.call('get_object_reference', ['base', 'menu_module_updates']).then(function(r) { - var mid = r[1]; - if(r[0] !== 'ir.ui.menu') { - // invalid reference, return null - mid = null; - } - self._upd_menu_id = mid; - return mid; - }); - } else { - return $.Deferred().resolve(self._upd_menu_id).promise(); - } - }; - - $.when(get_upd_menu_id()).done(function(menu_id) { - if (_.isNull(menu_id)) { - return; - } - var $menu = web_client.menu.$secondary_menus.find(_.str.sprintf('a[data-menu=%s]', menu_id)); - if ($menu.length === 0) { - return; - } - if (_.isUndefined(count)) { - count = 0; - } - var needupdate = $menu.find('#menu_counter'); - if (needupdate && needupdate.length !== 0) { - if (count > 0) { - needupdate.text(count); - } else { - needupdate.remove(); - } - } else if (count > 0) { - $menu.append(qweb.render("Menu.needaction_counter", {widget: {needaction_counter: count}})); - } - }); - }); + start: function() { + var self = this; + var def = $.Deferred(); + self.get_client().then(function(client) { + self.client = client; + + var qs = (session.debug ? 'debug&' : '') + 'db=' + client.dbname; + var u = client.origin + '/apps/embed/client?' + qs; + var css = {width: '100%', height: '400px'}; + self.$ifr = $('<iframe>').attr('src', u); + + $(window).on("message.apps", self.proxy('_on_message')); + + self.on('message:ready', self, function(m) { + var w = this.$ifr[0].contentWindow; + var act = { + type: 'ir.actions.client', + tag: this.remote_action_tag, + params: _.extend({}, this.params, { + db: this.session.db, + origin: this.session.origin, + }) + }; + w.postMessage({type:'action', action: act}, client.origin); + }); - self.on('message:blockUI', self, function() { framework.blockUI(); }); - self.on('message:unblockUI', self, function() { framework.unblockUI(); }); - self.on('message:warn', self, function(m) {self.do_warn(m.title, m.message, m.sticky); }); - - self.$ifr.appendTo(self.$el).css(css).addClass('apps-client'); - }). - fail(function(client) { - self.do_warn(_t('Odoo Apps will be available soon'), _t('Showing locally available modules'), true); - self.rpc('/web/action/load', {action_id: self.failback_action_id}).done(function(action) { - self.do_action(action); - web_client.menu.open_action(action.id); - }); + self.on('message:set_height', self, function(m) { + this.$ifr.height(m.height); + }); + + self.on('message:update_count', self, self._on_update_count); + + self.on('message:blockUI', self, function() { framework.blockUI(); }); + self.on('message:unblockUI', self, function() { framework.unblockUI(); }); + self.on('message:warn', self, function(m) {self.do_warn(m.title, m.message, m.sticky); }); + + self.$ifr.appendTo(self.$el).css(css).addClass('apps-client'); + + def.resolve(); + }, function() { + self.do_warn(_t('Odoo Apps will be available soon'), _t('Showing locally available modules'), true); + return session.rpc('/web/action/load', {action_id: self.failback_action_id}).then(function(action) { + return self.do_action(action); + }).always(function () { + def.reject(); }); + }); + return def; }, }); @@ -202,4 +207,6 @@ var AppsUpdates = Apps.extend({ core.action_registry.add("apps", Apps); core.action_registry.add("apps.updates", AppsUpdates); +return Apps; + }); diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 34abca7ef03..666520b263d 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -199,7 +199,6 @@ process_jobs: function (jobs, services) { var job; var require; - var time; function process_job (job) { var require = make_require(job); diff --git a/addons/web/static/src/js/compatibility.js b/addons/web/static/src/js/compatibility.js index 5532c8c388e..7fa2d4733db 100644 --- a/addons/web/static/src/js/compatibility.js +++ b/addons/web/static/src/js/compatibility.js @@ -22,7 +22,6 @@ var form_relational = require('web.form_relational'); // necessary var form_widgets = require('web.form_widgets'); // necessary var framework = require('web.framework'); var ListView = require('web.ListView'); -var Menu = require('web.Menu'); var Model = require('web.DataModel'); var pyeval = require('web.pyeval'); var Registry = require('web.Registry'); @@ -96,7 +95,6 @@ openerp.web.FormView = FormView; openerp.web.json_node_to_xml = utils.json_node_to_xml; openerp.web.ListView = ListView; -openerp.web.Menu = Menu; openerp.web.Model = Model; openerp.web.normalize_format = time.strftime_to_moment_format; openerp.web.py_eval = pyeval.py_eval; @@ -208,7 +206,7 @@ ListView.include({ }).value(); } - return this._super(data); + return this._super(); }, /** * Returns the style for the provided record in the current view (from the diff --git a/addons/web/static/src/js/control_panel.js b/addons/web/static/src/js/control_panel.js index 5e2cc36a6a1..3a233564368 100644 --- a/addons/web/static/src/js/control_panel.js +++ b/addons/web/static/src/js/control_panel.js @@ -54,17 +54,15 @@ var ControlPanel = Widget.extend({ * @return {jQuery.Deferred} */ start: function() { - this.$title_col = this.$('.oe-cp-title'); - // Exposed jQuery nodesets this.nodes = { - $breadcrumbs: this.$('.oe-view-title'), - $buttons: this.$('.oe-cp-buttons'), - $pager: this.$('.oe-cp-pager'), - $searchview: this.$('.oe-cp-search-view'), - $searchview_buttons: this.$('.oe-search-options'), - $sidebar: this.$('.oe-cp-sidebar'), - $switch_buttons: this.$('.oe-cp-switch-buttons'), + $breadcrumbs: this.$('.breadcrumb'), + $buttons: this.$('.o_cp_buttons'), + $pager: this.$('.o_cp_pager'), + $searchview: this.$('.o_cp_searchview'), + $searchview_buttons: this.$('.o_search_options'), + $sidebar: this.$('.o_cp_sidebar'), + $switch_buttons: this.$('.o_cp_switch_buttons'), }; // By default, hide the ControlPanel and remove its contents from the DOM @@ -120,7 +118,7 @@ var ControlPanel = Widget.extend({ // Update the searchview and switch buttons this._update_search_view(status.searchview, status.search_view_hidden); - if (status.active_view_selector) { + if (status.active_view_selector) { this._update_switch_buttons(status.active_view_selector); } } @@ -182,22 +180,25 @@ var ControlPanel = Widget.extend({ */ _render_breadcrumbs: function (breadcrumbs) { var self = this; - return breadcrumbs.map(function (bc, index) { - return make_breadcrumb(bc, index === breadcrumbs.length - 1); + return self._render_breadcrumbs_li(bc, index, breadcrumbs.length); }); - - function make_breadcrumb (bc, is_last) { - var $bc = $('<li>') - .append(is_last ? _.escape(bc.title) : $('<a>').text(bc.title)) - .toggleClass('active', is_last); - if (!is_last) { - $bc.click(function () { - self.trigger("on_breadcrumb_click", bc.action, bc.index); - }); - } - return $bc; + }, + /** + * Private function that renders a breadcrumbs' li Jquery element + */ + _render_breadcrumbs_li: function (bc, index, length) { + var self = this; + var is_last = (index === length-1); + var $bc = $('<li>') + .append(is_last ? _.escape(bc.title) : $('<a>').text(bc.title)) + .toggleClass('active', is_last); + if (!is_last) { + $bc.click(function () { + self.trigger("on_breadcrumb_click", bc.action, bc.index); + }); } + return $bc; }, /** * Private function that updates the SearchView's visibility and extend the @@ -211,8 +212,10 @@ var ControlPanel = Widget.extend({ // have been appended to a jQuery node not in the DOM at SearchView initialization searchview.$buttons = this.nodes.$searchview_buttons; searchview.toggle_visibility(!is_hidden); - this.$title_col.toggleClass('col-md-6', !is_hidden).toggleClass('col-md-12', is_hidden); } + + this.nodes.$searchview.toggle(!is_hidden); + this.$el.toggleClass('o_breadcrumb_full', !!is_hidden); }, }); diff --git a/addons/web/static/src/js/framework/ajax.js b/addons/web/static/src/js/framework/ajax.js index 869e0214ad9..0301cd4e96c 100644 --- a/addons/web/static/src/js/framework/ajax.js +++ b/addons/web/static/src/js/framework/ajax.js @@ -2,8 +2,8 @@ odoo.define('web.ajax', function (require) { "use strict"; var core = require('web.core'); -var time = require('web.time'); var utils = require('web.utils'); +var time = require('web.time'); function genericJsonRpc (fct_name, params, fct) { var data = { @@ -14,6 +14,7 @@ function genericJsonRpc (fct_name, params, fct) { }; var xhr = fct(data); var result = xhr.pipe(function(result) { + core.bus.trigger('rpc:result', data, result); if (result.error !== undefined) { if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") { console.error("Server application error", JSON.stringify(result.error)); @@ -42,7 +43,7 @@ function jsonRpc(url, fct_name, params, settings) { contentType: 'application/json' })); }); -}; +} function jsonpRpc(url, fct_name, params, settings) { settings = settings || {}; @@ -116,7 +117,7 @@ function jsonpRpc(url, fct_name, params, settings) { return deferred; } }); -}; +} // helper function realSetTimeout (fct, millis) { @@ -140,7 +141,7 @@ function loadCSS(url) { 'type': 'text/css' })); } -}; +} function loadJS(url) { var def = $.Deferred(); @@ -165,7 +166,7 @@ function loadJS(url) { head.appendChild(script); } return def; -}; +} /** * Cooperative file download implementation, for ajaxy APIs. @@ -301,7 +302,7 @@ function post (controller_url, data) { var Def = $.Deferred(); var postData = new FormData(); - + $.each(data, function(i,val) { postData.append(i, val); }); @@ -311,7 +312,7 @@ function post (controller_url, data) { var xhr = new XMLHttpRequest(); if(xhr.upload) xhr.upload.addEventListener('progress', progressHandler(Def), false); - + var ajaxDef = $.ajax(controller_url, { xhr: function() {return xhr;}, data: postData, @@ -324,8 +325,6 @@ function post (controller_url, data) { return Def; } - - var loadXML = (function () { var loading = false; var urls = []; diff --git a/addons/web/static/src/js/framework/core.js b/addons/web/static/src/js/framework/core.js index 30138102663..f9ab78f1c8e 100644 --- a/addons/web/static/src/js/framework/core.js +++ b/addons/web/static/src/js/framework/core.js @@ -177,20 +177,16 @@ return { _lt: _lt, // registries - view_registry: new Registry(), - crash_registry: new Registry(), action_registry : new Registry(), - form_widget_registry: new Registry(), - form_tag_registry: new Registry(), + crash_registry: new Registry(), form_custom_registry: new Registry(), + form_tag_registry: new Registry(), + form_widget_registry: new Registry(), list_widget_registry: new Registry(), - search_widgets_registry: new Registry(), - search_filters_registry: new Registry(), - - // necessary to make the kanban view compatible between - // community and enterprise edition one2many_view_registry: new Registry(), - + search_filters_registry: new Registry(), + search_widgets_registry: new Registry(), + view_registry: new Registry(), }; diff --git a/addons/web/static/src/js/framework/crash_manager.js b/addons/web/static/src/js/framework/crash_manager.js index 4ab40f456bc..99d949ac311 100644 --- a/addons/web/static/src/js/framework/crash_manager.js +++ b/addons/web/static/src/js/framework/crash_manager.js @@ -40,22 +40,17 @@ var CrashManager = core.Class.extend({ if (!this.active) { return; } - if (this.$indicator){ + if (this.connection_lost) { return; } if (error.code == -32098) { - $.blockUI({ message: '' , overlayCSS: {'z-index': 9999, backgroundColor: '#FFFFFF', opacity: 0.0, cursor: 'wait'}}); - this.$indicator = $('<div class="oe_indicator">' + _t("Trying to reconnect... ") + '<i class="fa fa-refresh"></i></div>'); - this.$indicator.prependTo("body"); - var timeinterval = setInterval(function(){ + core.bus.trigger('connection_lost'); + this.connection_lost = true; + var timeinterval = setInterval(function() { ajax.jsonRpc('/web/webclient/version_info').then(function() { clearInterval(timeinterval); - self.$indicator.html(_t("You are back online")); - self.$indicator.delay(2000).fadeOut('slow',function(){ - $(this).remove(); - self.$indicator.remove(); - }); - $.unblockUI(); + core.bus.trigger('connection_restored'); + self.connection_lost = false; }); }, 2000); return; diff --git a/addons/web/static/src/js/framework/data.js b/addons/web/static/src/js/framework/data.js index a1b8e1602e9..592e402d927 100644 --- a/addons/web/static/src/js/framework/data.js +++ b/addons/web/static/src/js/framework/data.js @@ -320,6 +320,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, { this.index = null; this._sort = []; this._model = new Model(model, context); + this.orderer = new utils.DropMisordered(); }, previous: function () { this.index -= 1; @@ -408,10 +409,11 @@ var DataSet = Class.extend(mixins.PropertiesMixin, { read_slice: function (fields, options) { var self = this; options = options || {}; - return this._model.query(fields) + var query = this._model.query(fields) .limit(options.limit || false) .offset(options.offset || 0) - .all().done(function (records) { + .all(); + return this.orderer.add(query).done(function (records) { self.ids = _(records).pluck('id'); }); }, @@ -761,12 +763,6 @@ var BufferedDataSet = DataSetStatic.extend({ _.extend(cached.from_read, options.from_read); _.extend(cached.changes, options.changes); _.extend(cached.readonly_fields, options.readonly_fields); - // discard values from cached.changes that are in cached.from_read - _.each(cached.changes, function (v, k) { - if (cached.from_read[k] === v) { - delete cached.changes[k]; - } - }); if (options.to_create !== undefined) cached.to_create = options.to_create; if (options.to_delete !== undefined) cached.to_delete = options.to_delete; } diff --git a/addons/web/static/src/js/framework/misc.js b/addons/web/static/src/js/framework/misc.js index b8c5e5e927e..73345df94c0 100644 --- a/addons/web/static/src/js/framework/misc.js +++ b/addons/web/static/src/js/framework/misc.js @@ -211,6 +211,16 @@ if ('nv' in window) { nv.tooltip.cleanup = function () { $('.nvtooltip').remove(); }; + + // monkey patch nvd3 to prevent it to display a tooltip (position: absolute) with + // a negative `top`; with this patch the highest tooltip's position is still in the + // graph + var originalCalcTooltipPosition = nv.tooltip.calcTooltipPosition; + nv.tooltip.calcTooltipPosition = function () { + var container = originalCalcTooltipPosition.apply(this, arguments); + container.style.top = container.style.top.split('px')[0] < 0 ? 0 + 'px' : container.style.top; + return container; + }; } // Bootstrap customization @@ -303,11 +313,18 @@ $.extend( proto, { }); /** - * Private function that triggers an event on core.bus + * Private function to notify that something has been attached in the DOM * @param {htmlString or Element or Array or jQuery} [content] the content that * has been attached in the DOM + * @params {Array} [callbacks] array of {widget: w, callback_args: args} such + * that on_attach_callback() will be called on each w with arguments args */ -function _notify (content) { +function _notify (content, callbacks) { + _.each(callbacks, function(c) { + if (c.widget && c.widget.on_attach_callback) { + c.widget.on_attach_callback(c.callback_args); + } + }); core.bus.trigger('DOM_updated', content); } /** @@ -315,12 +332,14 @@ function _notify (content) { * @param {jQuery} [$target] the node where content will be appended * @param {htmlString or Element or Array or jQuery} [content] DOM element, * array of elements, HTML string or jQuery object to append to $target - * @param {jQuery} [trigger] true to trigger an event, false otherwise + * @param {Boolean} [options.in_DOM] true if $target is in the DOM + * @param {Array} [options.callbacks] array of objects describing the callbacks + * to perform (see _notify for a complete description) */ -function append ($target, content, trigger) { +function append ($target, content, options) { $target.append(content); - if (trigger) { - _notify(content); + if (options && options.in_DOM) { + _notify(content, options.callbacks); } } /** @@ -328,13 +347,37 @@ function append ($target, content, trigger) { * @param {jQuery} [$target] the node where content will be prepended * @param {htmlString or Element or Array or jQuery} [content] DOM element, * array of elements, HTML string or jQuery object to prepend to $target - * @param {jQuery} [trigger] true to trigger an event, false otherwise + * @param {Boolean} [options.in_DOM] true if $target is in the DOM + * @param {Array} [options.callbacks] array of objects describing the callbacks + * to perform (see _notify for a complete description) */ -function prepend ($target, content, trigger) { +function prepend ($target, content, options) { $target.prepend(content); - if (trigger) { - _notify(content); + if (options && options.in_DOM) { + _notify(content, options.callbacks); + } +} + +/** + * Detaches widgets from the DOM and performs their on_detach_callback() + * @param {Array} [to_detach] array of {widget: w, callback_args: args} such + * that w.$el will be detached and w.on_detach_callback(args) will be called + * @param {jQuery} [options.$to_detach] if given, detached instead of widgets' $el + * @return {jQuery} the detached elements + */ +function detach (to_detach, options) { + _.each(to_detach, function(d) { + if (d.widget.on_detach_callback) { + d.widget.on_detach_callback(d.callback_args); + } + }); + var $to_detach = options && options.$to_detach; + if (!$to_detach) { + $to_detach = $(_.map(to_detach, function(d) { + return d.widget.el; + })); } + return $to_detach.detach(); } /** @@ -358,6 +401,7 @@ return { redirect: redirect, append: append, prepend: prepend, + detach: detach, getPosition: getPosition, }; diff --git a/addons/web/static/src/js/framework/model.js b/addons/web/static/src/js/framework/model.js index fc1422d799a..dd516bb61ba 100644 --- a/addons/web/static/src/js/framework/model.js +++ b/addons/web/static/src/js/framework/model.js @@ -6,14 +6,12 @@ var session = require('web.session'); var Model = Class.extend({ /** - new openerp.web.Model([session,] model_name[, context[, domain]]) + new openerp.web.Model(name[, context[, domain]]) @constructs instance.web.Model @extends instance.web.Class - @param {openerp.web.Session} [session] The session object used to communicate with - the server. - @param {String} model_name name of the OpenERP model this object is bound to + @param {String} name name of the OpenERP model this object is bound to @param {Object} [context] @param {Array} [domain] */ diff --git a/addons/web/static/src/js/framework/session.js b/addons/web/static/src/js/framework/session.js index 71819cc6b7a..8a9269b8dff 100644 --- a/addons/web/static/src/js/framework/session.js +++ b/addons/web/static/src/js/framework/session.js @@ -236,7 +236,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { return d; }, load_qweb: function(mods) { - this.qweb_mutex.exec(function() { + this.qweb_mutex.exec(function () { return $.get('/web/webclient/qweb?mods=' + mods).then(function (doc) { if (!doc) { return; } qweb.add_template(doc); diff --git a/addons/web/static/src/js/framework/utils.js b/addons/web/static/src/js/framework/utils.js index d50647090d6..8d8d8dab6ee 100644 --- a/addons/web/static/src/js/framework/utils.js +++ b/addons/web/static/src/js/framework/utils.js @@ -423,7 +423,6 @@ function swap(array, elem1, elem2) { array[i1] = elem2; } - return { divmod: divmod, modf: modf, diff --git a/addons/web/static/src/js/framework/view.js b/addons/web/static/src/js/framework/view.js index fbbede916e5..d2cc73a4b71 100644 --- a/addons/web/static/src/js/framework/view.js +++ b/addons/web/static/src/js/framework/view.js @@ -27,6 +27,8 @@ var View = Widget.extend({ // multi_record is used to distinguish views displaying a single record // (e.g. FormView) from those that display several records (e.g. ListView) multi_record: true, + // indicates whether or not the view is mobile-friendly + mobile_friendly: false, // icon is the font-awesome icon to display in the view switcher icon: 'fa-question', // whether or not the view requires the model's fields to render itself @@ -45,11 +47,8 @@ var View = Widget.extend({ * @return {Deferred} */ start: function() { - // add css classes that reflect the (absence of) access rights - this.$el.addClass('oe_view') - .toggleClass('oe_cannot_create', !this.is_action_enabled('create')) - .toggleClass('oe_cannot_edit', !this.is_action_enabled('edit')) - .toggleClass('oe_cannot_delete', !this.is_action_enabled('delete')); + // add classname that reflect the (absence of) access rights + this.$el.toggleClass('o_cannot_create', !this.is_action_enabled('create')); return this._super().then(this.trigger.bind(this, 'view_loaded')); }, /** @@ -76,7 +75,7 @@ var View = Widget.extend({ // response handler var handler = function (action) { - if (action && action.constructor == Object) { + if (action && action.constructor === Object) { // filter out context keys that are specific to the current action. // Wrong default_* and search_default_* values will no give the expected result // Wrong group_by values will simply fail and forbid rendering of the destination view @@ -106,7 +105,7 @@ var View = Widget.extend({ if (action_data.special === 'cancel') { return handler({"type":"ir.actions.act_window_close"}); - } else if (action_data.type=="object") { + } else if (action_data.type === "object") { var args = [[record_id]]; if (action_data.args) { try { @@ -122,7 +121,7 @@ var View = Widget.extend({ return dataset.call_button(action_data.name, args).then(handler).then(function () { core.bus.trigger('do_reload_needaction'); }); - } else if (action_data.type=="action") { + } else if (action_data.type === "action") { return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), { active_model: dataset.model, active_ids: dataset.ids, @@ -144,27 +143,24 @@ var View = Widget.extend({ do_load_state: function (state, warm) { }, /** - * This function should render the buttons of the view, set this.$buttons to - * the produced jQuery element and define some listeners on it. - * This function should be called after start(). + * This function should render the action buttons of the view. + * It should be called after start(). * @param {jQuery} [$node] a jQuery node where the rendered buttons should be inserted * $node may be undefined, in which case the View can insert the buttons somewhere else */ render_buttons: function($node) { }, /** - * This function should instantiate and render the sidebar of the view, set this.sidebar to - * the instantiated Sidebar Widget and possibly add custom items in it. - * This function should be called after start(). + * This function should render the sidebar of the view. + * It should be called after start(). * @param {jQuery} [$node] a jQuery node where the sidebar should be inserted * $node may be undefined, in which case the View can insert the sidebar somewhere else */ render_sidebar: function($node) { }, /** - * This function should render the pager of the view, set this.$pager to - * the produced jQuery element and define some listeners on it. - * This function should be called after start(). + * This function should render the pager of the view. + * It should be called after start(). * @param {jQuery} [$node] a jQuery node where the pager should be inserted * $node may be undefined, in which case the View can insert the pager somewhere else */ @@ -200,6 +196,12 @@ var View = Widget.extend({ get_context: function () { return {}; }, + set_scrollTop: function(scrollTop) { + this.scrollTop = scrollTop; + }, + get_scrollTop: function() { + return this.scrollTop; + } }); return View; diff --git a/addons/web/static/src/js/framework/widget.js b/addons/web/static/src/js/framework/widget.js index a6b189dc832..25a30ae6511 100644 --- a/addons/web/static/src/js/framework/widget.js +++ b/addons/web/static/src/js/framework/widget.js @@ -32,7 +32,7 @@ var mixins = core.mixins; * }, * start: function() { * // stuff you want to make after the rendering, `this.$el` holds a correct value - * this.$el.find(".my_button").click(/* an example of event binding * /); + * this.$(".my_button").click(/* an example of event binding * /); * * // if you have some asynchronous operations, it's a good idea to return * // a promise in start() diff --git a/addons/web/static/src/js/menu.js b/addons/web/static/src/js/menu.js index 4d8671595bd..c244305b7aa 100644 --- a/addons/web/static/src/js/menu.js +++ b/addons/web/static/src/js/menu.js @@ -15,7 +15,7 @@ var Menu = Widget.extend({ this.data = {data:{children:[]}}; this.on("menu_bound", this, function() { // launch the fetch of needaction counters, asynchronous - var $all_menus = self.$el.parents('body').find('.oe_webclient').find('[data-menu]'); + var $all_menus = self.$el.parents('.o_web_client').find('.o_sub_menu').find('[data-menu]'); var all_menu_ids = _.map($all_menus, function (menu) {return parseInt($(menu).attr('data-menu'), 10);}); if (!_.isEmpty(all_menu_ids)) { this.do_load_needaction(all_menu_ids); @@ -34,7 +34,7 @@ var Menu = Widget.extend({ }, bind_menu: function() { var self = this; - this.$secondary_menus = this.$el.parents().find('.oe_secondary_menus_container'); + this.$secondary_menus = this.$el.parents().find('.o_sub_menu'); this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click); this.$el.on('click', 'a[data-menu]', function (event) { event.preventDefault(); @@ -160,7 +160,7 @@ var Menu = Widget.extend({ $sub_menu.show(); // Hide/Show the leftbar menu depending of the presence of sub-items - this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length); + this.$secondary_menus.toggle(!!$sub_menu.children().length); // Activate current menu item and show parents this.$secondary_menus.find('.active').removeClass('active'); diff --git a/addons/web/static/src/js/view_manager.js b/addons/web/static/src/js/view_manager.js index 0dfab67b4ea..6a8a162e144 100644 --- a/addons/web/static/src/js/view_manager.js +++ b/addons/web/static/src/js/view_manager.js @@ -14,7 +14,25 @@ var QWeb = core.qweb; var _t = core._t; var ViewManager = Widget.extend(ControlPanelMixin, { - template: "ViewManager", + className: "o_view_manager_content", + /** + * Called each time the view manager is attached into the DOM + */ + on_attach_callback: function() { + this.is_in_DOM = true; + if (this.active_view && this.active_view.controller.on_attach_callback) { + this.active_view.controller.on_attach_callback(); + } + }, + /** + * Called each time the view manager is detached from the DOM + */ + on_detach_callback: function() { + this.is_in_DOM = false; + if (this.active_view && this.active_view.controller.on_detach_callback) { + this.active_view.controller.on_detach_callback(); + } + }, /** * @param {Object} [dataset] * @param {Array} [views] List of [view_id, view_type[, fields_view]] @@ -34,12 +52,11 @@ var ViewManager = Widget.extend(ControlPanelMixin, { this.active_view = null; this.registry = core.view_registry; this.title = this.action.name; - this.is_in_DOM = false; // used to know if the view manager is attached in the DOM _.each(views, function (view) { var view_type = view[1] || view.view_type; var View = self.registry.get(view_type); if (!View) { - console.error("View type", "'"+view[1]+"'", "is not present in the view registry."); + console.error("View type", "'"+view_type+"'", "is not present in the view registry."); return; } var view_label = View.prototype.display_name; @@ -48,9 +65,9 @@ var ViewManager = Widget.extend(ControlPanelMixin, { button_label: _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}), controller: null, fields_view: view[2] || view.fields_view, - embedded_view: view.embedded_view, icon: View.prototype.icon, label: view_label, + mobile_friendly: View.prototype.mobile_friendly, multi_record: View.prototype.multi_record, options: view.options || {}, require_fields: View.prototype.require_fields, @@ -81,12 +98,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, { view.options = _.extend({ action: self.action, }, self.flags, self.flags[view.type], view.options); - view.$container = self.$(".oe-view-manager-view-" + view.type); }); - - this.$el.addClass("oe_view_manager_" + ((this.action && this.action.target) || 'current')); - - this.control_elements = {}; if (this.flags.search_view) { this.search_view_loaded = this.setup_search_view(); } @@ -224,33 +236,40 @@ var ViewManager = Widget.extend(ControlPanelMixin, { var view_control_elements = this.render_view_control_elements(); // Show the view - this.active_view.$container.show(); return $.when(view_controller.do_show(view_options)).done(function () { // Prepare the ControlPanel content and update it var cp_status = { - active_view_selector: '.oe-cp-switch-' + self.active_view.type, + active_view_selector: '.o_cp_switch_' + self.active_view.type, breadcrumbs: self.action_manager && self.action_manager.get_breadcrumbs(), - cp_content: _.extend({}, self.control_elements, view_control_elements), + cp_content: _.extend({}, self.searchview_elements, view_control_elements), hidden: self.flags.headless, searchview: self.searchview, search_view_hidden: view_controller.searchable === false || view_controller.searchview_hidden, }; self.update_control_panel(cp_status); - if (old_view) { - // Detach the old view but not ui-autocomplete elements to let - // jquery-ui garbage-collect them - old_view.$container.contents().not('.ui-autocomplete').detach(); - - // Hide old view (at first rendering, there is no view to hide) - if (self.active_view !== old_view) { - if (old_view.controller) old_view.controller.do_hide(); - if (old_view.$container) old_view.$container.hide(); + // Detach the old view and store it + if (old_view && old_view !== self.active_view) { + // Store the scroll position + if (self.action_manager && self.action_manager.webclient) { + old_view.controller.set_scrollTop(self.action_manager.webclient.get_scrollTop()); } + // Do not detach ui-autocomplete elements to let jquery-ui garbage-collect them + var $to_detach = self.$el.contents().not('.ui-autocomplete'); + old_view.$fragment = framework.detach([{widget: old_view.controller}], {$to_detach: $to_detach}); } - // Append the view fragment to its $container - framework.append(self.active_view.$container, view_fragment, self.is_in_DOM); + // If the user switches from a multi-record to a mono-record view, + // the action manager should be scrolled to the top. + if (old_view && old_view.controller.multi_record === true && view_controller.multi_record === false) { + view_controller.set_scrollTop(0); + } + + // Append the view fragment to self.$el + framework.append(self.$el, view_fragment, { + in_DOM: self.is_in_DOM, + callbacks: [{widget: view_controller}], + }); }); }, create_view: function(view, view_options) { @@ -282,39 +301,50 @@ var ViewManager = Widget.extend(ControlPanelMixin, { return this.switch_mode(view_type); }, /** - * Renders the switch buttons and adds listeners on them but does not append them to the DOM - * Sets $switch_buttons in control_elements to send to the ControlPanel - * @param {Object} [src] the source requesting the switch_buttons - * @param {Array} [views] the array of views + * Renders the switch buttons for multi- and mono-record views and adds + * listeners on them, but does not append them to the DOM + * Sets switch_buttons.$mono and switch_buttons.$multi to send to the ControlPanel */ render_switch_buttons: function() { - if (this.flags.views_switcher && this.view_order.length > 1) { - var self = this; + var self = this; - // Render switch buttons but do not append them to the DOM as this will - // be done later, simultaneously to all other ControlPanel elements - this.control_elements.$switch_buttons = $(QWeb.render('ViewManager.switch-buttons', {views: self.view_order})); + // Partition the views according to their multi-/mono-record status + var views = _.partition(this.view_order, function(view) { + return view.multi_record === true; + }); + var multi_record_views = views[0]; + var mono_record_views = views[1]; - // Create bootstrap tooltips - _.each(this.views, function(view) { - self.control_elements.$switch_buttons.siblings('.oe-cp-switch-' + view.type).tooltip(); - }); + // Inner function to render and prepare switch_buttons + var _render_switch_buttons = function(views) { + if (views.length > 1) { + var $switch_buttons = $(QWeb.render('ViewManager.switch-buttons', {views: views})); + // Create bootstrap tooltips + _.each(views, function(view) { + $switch_buttons.filter('.o_cp_switch_' + view.type).tooltip(); + }); + // Add onclick event listener + $switch_buttons.filter('button').click(_.debounce(function(event) { + var view_type = $(event.target).data('view-type'); + self.switch_mode(view_type); + }, 200, true)); + return $switch_buttons; + } + }; - // Add onclick event listener - this.control_elements.$switch_buttons.siblings('button').click(_.debounce(function(event) { - var view_type = $(event.target).data('view-type'); - self.switch_mode(view_type); - }, 200, true)); - } + // Render switch buttons but do not append them to the DOM as this will + // be done later, simultaneously to all other ControlPanel elements + this.switch_buttons = {}; + this.switch_buttons.$multi = _render_switch_buttons(multi_record_views); + this.switch_buttons.$mono = _render_switch_buttons(mono_record_views); }, /** - * Renders the control elements (buttons, sidebar, pager) of the current view - * This must be done when active_search is resolved (for KanbanViews) + * Renders the control elements (buttons, sidebar, pager) of the current view. * Fills this.active_view.control_elements dictionnary with the rendered - * elements and the adequate view switcher, to send to the ControlPanel + * elements and the adequate view switcher, to send to the ControlPanel. * Warning: it should be called before calling do_show on the view as the * sidebar is extended to listen on the load_record event triggered as soon - * as do_show is done (the sidebar should thus be instantiated before) + * as do_show is done (the sidebar should thus be instantiated before). */ render_view_control_elements: function() { if (!this.active_view.control_elements) { @@ -335,6 +365,15 @@ var ViewManager = Widget.extend(ControlPanelMixin, { elements = _.mapObject(elements, function($node) { return $node && $node.contents(); }); + // Use the adequate view switcher (mono- or multi-record) + if (this.switch_buttons) { + if (this.active_view.multi_record) { + elements.$switch_buttons = this.switch_buttons.$multi; + } else { + elements.$switch_buttons = this.switch_buttons.$mono; + } + } + // Store the rendered elements in the active_view to allow restoring them later this.active_view.control_elements = elements; } @@ -342,10 +381,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, { }, /** * Sets up the current viewmanager's search view. - * Sets $searchview and $searchview_buttons in control_elements to send to the ControlPanel - * - * @param {Number|false} view_id the view to use or false for a default one - * @returns {jQuery.Deferred} search view startup deferred + * Sets $searchview and $searchview_buttons in searchview_elements to send to the ControlPanel */ setup_search_view: function() { var self = this; @@ -361,6 +397,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, { search_defaults[match[1]] = value; } }); + var options = { hidden: this.flags.search_view === false, disable_custom_filters: this.flags.search_disable_custom_filters, @@ -374,8 +411,9 @@ var ViewManager = Widget.extend(ControlPanelMixin, { this.searchview.on('search_data', this, this.search.bind(this)); return $.when(this.searchview.appendTo($("<div>"))).done(function() { - self.control_elements.$searchview = self.searchview.$el; - self.control_elements.$searchview_buttons = self.searchview.$buttons.contents(); + self.searchview_elements = {}; + self.searchview_elements.$searchview = self.searchview.$el; + self.searchview_elements.$searchview_buttons = self.searchview.$buttons.contents(); }); }, /** diff --git a/addons/web/static/src/js/views/form_common.js b/addons/web/static/src/js/views/form_common.js index 1c86a86c1ec..b3d078dbc1a 100644 --- a/addons/web/static/src/js/views/form_common.js +++ b/addons/web/static/src/js/views/form_common.js @@ -215,7 +215,7 @@ var CompletionFieldMixin = { self._search_create_popup("search", data); }); }, - classname: 'oe_m2o_dropdown_option' + classname: 'o_m2o_dropdown_option' }); } // quick create @@ -228,7 +228,7 @@ var CompletionFieldMixin = { action: function() { self._quick_create(search_val); }, - classname: 'oe_m2o_dropdown_option' + classname: 'o_m2o_dropdown_option' }); } // create... @@ -238,14 +238,14 @@ var CompletionFieldMixin = { action: function() { self._search_create_popup("form", undefined, self._create_context(search_val)); }, - classname: 'oe_m2o_dropdown_option' + classname: 'o_m2o_dropdown_option' }); } else if (values.length === 0) { values.push({ label: _t("No results to show..."), action: function() {}, - classname: 'oe_m2o_dropdown_option' + classname: 'o_m2o_dropdown_option' }); } @@ -275,7 +275,7 @@ var CompletionFieldMixin = { // all search/create popup handling _search_create_popup: function(view, ids, context) { var self = this; - new SelectCreateDialog(this, { + new SelectCreateDialog(this, _.extend({}, (this.options || {}), { res_model: self.field.relation, domain: self.build_domain(), context: new data.CompoundContext(self.build_context(), context || {}), @@ -287,7 +287,7 @@ var CompletionFieldMixin = { self.add_id(element_ids[0]); self.focus(); } - }).open(); + })).open(); }, /** * To implement. @@ -474,7 +474,7 @@ var FormWidget = Widget.extend(InvisibilityChangerMixin, { debug: session.debug, widget: widget }); - }, + } }, options || {}); //only show tooltip if we are in debug or if we have a help to show, otherwise it will display //as empty @@ -666,16 +666,16 @@ var AbstractField = FormWidget.extend(FieldInterface, { this._check_css_flags(); }); - this.$translate = $(); // Enterprise compatibility + this.$translate = (_t.database.multi_lang && this.field.translate) ? $('<button/>', { + type: 'button', + }).addClass('o_field_translate fa fa-globe btn btn-link') : $(); }, + renderElement: function() { var self = this; this._super(); - if (this.field.translate && this.view) { - this.$el.addClass('oe_form_field_translatable'); - this.$el.find('.oe_field_translate').click(this.on_translate); - } - this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $(); + this.$el.addClass('o_form_field'); + this.$label = this.view ? this.view.$('label[for=' + this.id_for_label + ']') : $(); this.do_attach_tooltip(this, this.$label[0] || this.$el); if (session.debug) { this.$label.off('dblclick').on('dblclick', function() { @@ -707,16 +707,23 @@ var AbstractField = FormWidget.extend(FieldInterface, { }); this.render_value(); this._toggle_label(); + + if (this.view) { + this.$translate + .insertAfter(this.$el) + .on('click', _.bind(this.on_translate, this)); + } }, _toggle_label: function() { var empty = this.get('effective_readonly') && this.is_false(); + this.$label.toggleClass('o_form_label_empty', empty).toggleClass('o_form_label_false', this.get('effective_readonly') && this.get('value') === false); this.$el.toggleClass('o_form_field_empty', empty); }, /** * Private. Do not use. */ _set_required: function() { - this.$el.toggleClass('oe_form_required', this.get("required")); + this.$el.toggleClass('o_form_required', this.get("required")); }, set_value: function(value_) { this.set({'value': value_}); @@ -752,10 +759,11 @@ var AbstractField = FormWidget.extend(FieldInterface, { return this.get('value') === false; }, _check_css_flags: function() { - if (this.field.translate) { - this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create"); - } - this.$el.toggleClass('oe_form_invalid', !this.disable_utility_classes && !!this.field_manager.get('display_invalid_fields') && !this.is_valid()); + var show_translate = (!this.get('effective_readonly') && this.field_manager.get('actual_mode') !== "create"); + this.$translate.toggleClass('o_translate_active', !!show_translate); + + this.$el.add(this.$label) + .toggleClass('o_form_invalid', !this.disable_utility_classes && !!this.field_manager.get('display_invalid_fields') && !this.is_valid()); }, focus: function() { return false; @@ -770,11 +778,10 @@ var AbstractField = FormWidget.extend(FieldInterface, { self.do_action(r); }); }, - - set_dimensions: function (height, width) { + set_dimensions: function(height, width) { this.$el.css({ width: width, - minHeight: height + height: height, }); }, commit_value: function() { @@ -802,7 +809,7 @@ var ViewDialog = Dialog.extend({ // FIXME should use ViewManager options.dialogClass = options.dialogClass || ''; options.dialogClass += ' o_act_window'; - this._super(parent, _.clone(options)); + this._super(parent, $.extend(true, {}, options)); this.res_model = options.res_model || null; this.res_id = options.res_id || null; @@ -864,7 +871,7 @@ var FormViewDialog = ViewDialog.extend({ ]; if(!readonly) { - options.buttons.splice(0, 0, {text: _t("Save") + ((multi_select)? " " + _t(" & Close") : ""), classes: "btn-primary o_formdialog_save", click: function() { // o_formdialog_save class for web_tests! + options.buttons.splice(0, 0, {text: _t("Save") + ((multi_select)? " " + _t(" & Close") : ""), classes: "btn-primary", click: function() { self.view_form.onchanges_mutex.def.then(function() { if (!self.view_form.warning_displayed) { $.when(self.view_form.save()).done(function() { @@ -923,6 +930,7 @@ var FormViewDialog = ViewDialog.extend({ var fragment = document.createDocumentFragment(); self.view_form.appendTo(fragment).then(function () { self.view_form.do_show().then(function() { + self.view_form.autofocus(); _super().$el.append(fragment); }); }); @@ -941,7 +949,7 @@ var SelectCreateListView = ListView.extend({ this.popup.close(); }, do_select: function(ids, records) { - this._super(ids, records); + this._super.apply(this, arguments); this.popup.on_click_element(ids); } }); @@ -1022,7 +1030,7 @@ var SelectCreateDialog = ViewDialog.extend({ _.extend({'deletable': false, 'selectable': !self.options.disable_multiple_selection, 'import_enabled': false, - '$buttons': self.$footer, + '$buttons': self.$buttons, 'disable_editable_mode': true, 'pager': true, }, self.options.list_view_options || {})); @@ -1030,6 +1038,9 @@ var SelectCreateDialog = ViewDialog.extend({ e.cancel = true; }); self.view_list.popup = self; + self.view_list.on('list_view_loaded', self, function() { + this.on_view_list_loaded(); + }); var buttons = [ {text: _t("Cancel"), classes: "btn-default o_form_button_cancel", close: true} @@ -1076,6 +1087,7 @@ var SelectCreateDialog = ViewDialog.extend({ this.close(); return new FormViewDialog(this.__parentedParent, this.options).open(); }, + on_view_list_loaded: function() {}, }); var DomainEditorDialog = SelectCreateDialog.extend({ @@ -1095,11 +1107,11 @@ var DomainEditorDialog = SelectCreateDialog.extend({ }, get_domain: function (selected_ids) { - var group_domain = [], - domain; - if (this.$('input.oe_list_record_selector').prop('checked')) { + var group_domain = []; + var domain; + if (this.$('.o_list_record_selector input').prop('checked')) { if (this.view_list.grouped) { - var group_domain = _.chain(_.values(this.view_list.groups.children)) + group_domain = _.chain(_.values(this.view_list.groups.children)) .filter(function (child) { return child.records.length; }) .map(function (c) { return c.datagroup.domain;}) .value(); @@ -1118,6 +1130,11 @@ var DomainEditorDialog = SelectCreateDialog.extend({ } return this.dataset.domain.concat(group_domain).concat(domain || []); }, + + on_view_list_loaded: function() { + this.$('.o_list_record_selector input').prop('checked', true); + this.$footer.find(".o_selectcreatepopup_search_select").prop('disabled', false); + }, }); return { diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js index fc852108035..8e64599f470 100644 --- a/addons/web/static/src/js/views/form_relational_widgets.js +++ b/addons/web/static/src/js/views/form_relational_widgets.js @@ -6,7 +6,6 @@ var core = require('web.core'); var data = require('web.data'); var Dialog = require('web.Dialog'); var common = require('web.form_common'); -var FormView = require('web.FormView'); var ListView = require('web.ListView'); var Model = require('web.DataModel'); var session = require('web.session'); @@ -56,16 +55,15 @@ var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, com events: { 'keydown input': function (e) { switch (e.which) { - case $.ui.keyCode.UP: - case $.ui.keyCode.DOWN: - e.stopPropagation(); + case $.ui.keyCode.UP: + case $.ui.keyCode.DOWN: + e.stopPropagation(); } }, }, init: function(field_manager, node) { this._super(field_manager, node); common.CompletionFieldMixin.init.call(this); - this.set({'value': false}); this.display_value = {}; this.display_value_backup = {}; this.last_search = []; @@ -91,8 +89,9 @@ var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, com common.ReinitializeFieldMixin.initialize_field.call(this); }, initialize_content: function() { - if (!this.get("effective_readonly")) + if (!this.get("effective_readonly")) { this.render_editable(); + } }, destroy_content: function () { if (this.$dropdown) { @@ -125,16 +124,15 @@ var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, com }, render_editable: function() { var self = this; - this.$input = this.$el.find("input"); + this.$input = this.$("input"); this.init_error_displayer(); - self.$input.on('focus', function() { self.hide_error_displayer(); }); - this.$dropdown = this.$el.find(".oe_m2o_drop_down_button"); - this.$follow_button = $(".oe_m2o_cm_button", this.$el); + this.$dropdown = this.$(".o_dropdown_button"); + this.$follow_button = this.$(".o_external_button"); this.$follow_button.click(function(ev) { ev.preventDefault(); @@ -177,8 +175,7 @@ var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, com }; this.$input.keydown(input_changed); this.$input.change(input_changed); - this.$dropdown.click(function() { - self.$input.focus(); + this.$input.on('click', function() { if (self.$input.autocomplete("widget").is(":visible")) { self.$input.autocomplete("close"); } else { @@ -298,7 +295,6 @@ var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, com }); // set position for list of suggestions box this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } ); - this.$input.autocomplete("widget").openerpClass(); // used to correct a bug when selecting an element by pushing 'enter' in an editable list this.$input.keyup(function(e) { if (e.which === 13) { // ENTER @@ -311,7 +307,7 @@ var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, com }, render_value: function(no_recurse) { var self = this; - if (! this.get("value")) { + if (!this.get("value")) { this.display_string(""); return; } @@ -320,7 +316,7 @@ var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, com this.display_string(display); return; } - if (! no_recurse) { + if (!no_recurse) { var dataset = new data.DataSetStatic(this, this.field.relation, self.build_context()); var def = this.alive(dataset.name_get([self.get("value")])).done(function(data) { if (!data[0]) { @@ -341,38 +337,33 @@ var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, com } }, display_string: function(str) { - var self = this; if (!this.get("effective_readonly")) { this.$input.val(str.split("\n")[0]); this.current_display = this.$input.val(); - if (this.is_false()) { - this.$('.oe_m2o_cm_button').css({'display':'none'}); - } else { - this.$('.oe_m2o_cm_button').css({'display':'inline'}); - } + this.$follow_button.toggle(!this.is_false()); + this.$el.toggleClass('o_with_button', !!this.$follow_button && this.$follow_button.length > 0 && !this.is_false()); } else { - var lines = _.escape(str).split("\n"); - var link = ""; - var follow = ""; - link = lines[0]; - follow = _.rest(lines).join("<br />"); - if (follow) - link += "<br />"; - var $link = this.$el.find('.oe_form_uri') - .unbind('click') - .html(link); - if (! this.options.no_open) - $link.click(function () { - var context = self.build_context().eval(); - var model_obj = new Model(self.field.relation); - model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){ - self.do_action(action); - }); - return false; - }); - $(".oe_form_m2o_follow", this.$el).html(follow); + this.$el.html(_.escape(str).split("\n").join("<br/>")); + // Define callback to perform when clicking on the field + if (!this.options.no_open) { + // Remove potential previously added event handler + this.$el.off('click'); + // Ensure that the callback is performed only once even with multiple clicks + var execute_formview_action_once = _.once(this.execute_formview_action.bind(this)); + this.$el.click(function (ev) { + ev.preventDefault(); + execute_formview_action_once(); + }); + } } }, + execute_formview_action: function() { + var self = this; + var context = self.build_context().eval(); + (new Model(self.field.relation)).call('get_formview_action', [self.get("value"), context]).then(function(action) { + self.do_action(action); + }); + }, set_value: function(value_) { if (value_ instanceof Array) { this.display_value = {}; @@ -418,11 +409,6 @@ var FieldMany2One = common.AbstractField.extend(common.CompletionFieldMixin, com this.no_ed = false; return res; }, - set_dimensions: function (height, width) { - this._super(height, width); - if (!this.get("effective_readonly") && this.$input) - this.$input.css('height', height); - } }); /** @@ -586,40 +572,39 @@ var AbstractManyField = common.AbstractField.extend({ _.each(command_list, function(command) { self.mutex.exec(function() { var id = command[1]; - if (!id || _.isString(id) || _.isNumber(id)) { - switch (command[0]) { - case COMMANDS.CREATE: - var data = _.clone(command[2]); - delete data.id; - return dataset.create(data, internal_options).then(function (id) { + switch (command[0]) { + case COMMANDS.CREATE: + var data = _.clone(command[2]); + delete data.id; + return dataset.create(data, internal_options).then(function (id) { + dataset.ids.push(id); + res = id; + }); + case COMMANDS.UPDATE: + return dataset.write(id, command[2], internal_options).then(function () { + if (dataset.ids.indexOf(id) === -1) { dataset.ids.push(id); res = id; - }); - case COMMANDS.UPDATE: - return dataset.write(id, command[2], internal_options).then(function () { - if (dataset.ids.indexOf(id) === -1) { - dataset.ids.push(id); - res = id; - } - }); - case COMMANDS.FORGET: - return dataset.unlink([id]); - case COMMANDS.DELETE: - return dataset.unlink([id]); - case COMMANDS.LINK_TO: - if (dataset.ids.indexOf(id) === -1) { - return dataset.add_ids([id], internal_options); } - return; - case COMMANDS.DELETE_ALL: - return dataset.reset_ids([], {keep_read_data: true}); - case COMMANDS.REPLACE_WITH: - dataset.ids = []; - return dataset.alter_ids(command[2], internal_options); - } + }); + case COMMANDS.FORGET: + return dataset.unlink([id]); + case COMMANDS.DELETE: + return dataset.unlink([id]); + case COMMANDS.LINK_TO: + if (dataset.ids.indexOf(id) === -1) { + return dataset.add_ids([id], internal_options); + } + return; + case COMMANDS.DELETE_ALL: + return dataset.reset_ids([], {keep_read_data: true}); + case COMMANDS.REPLACE_WITH: + dataset.ids = []; + return dataset.alter_ids(command[2], internal_options); + default: + throw new Error("send_commands to '"+self.name+"' receive a non command value." + + "\n" + JSON.stringify(command_list)); } - throw new Error("send_commands to '"+self.name+"' receive a non command value." + - "\n" + JSON.stringify(command_list)); }); }); @@ -660,10 +645,6 @@ var AbstractManyField = common.AbstractField.extend({ if (record.to_create) { command_list.push(COMMANDS.create(values)); } else { - if (index === -1) { - // because the UPDATE below does not imply LINK_TO - command_list.push(COMMANDS.link_to(id)); - } command_list.push(COMMANDS.update(record.id, values)); } return; @@ -724,13 +705,12 @@ var FieldX2Many = AbstractManyField.extend({ }, start: function() { this._super.apply(this, arguments); - this.$el.addClass('oe_form_field'); - var self = this; this.load_views(); var destroy = function() { self.is_loaded = self.is_loaded.then(function() { + self.renderElement(); self.viewmanager.destroy(); return $.when(self.load_views()).done(function() { self.reload_current_view(); @@ -828,7 +808,8 @@ var FieldX2Many = AbstractManyField.extend({ }); }); $.async_when().done(function () { - self.alive( self.viewmanager.appendTo(self.$el)); + self.$el.addClass('o_view_manager_content'); + self.alive(self.viewmanager.attachTo(self.$el)); }); return def; }, @@ -881,6 +862,9 @@ var FieldX2Many = AbstractManyField.extend({ } return true; }, + is_false: function() { + return false; + }, }); var X2ManyDataSet = data.BufferedDataSet.extend({ @@ -891,10 +875,15 @@ var X2ManyDataSet = data.BufferedDataSet.extend({ self.context.add(context); }); return this.context; - } + }, }); var X2ManyViewManager = ViewManager.extend({ + custom_events: { + // Catch event scrollTo to prevent scrolling to the top when using the + // pager of List and Kanban views in One2Many fields + 'scrollTo': function() {}, + }, init: function(parent, dataset, views, flags, x2many_views) { // By default, render buttons and pager in X2M fields, but no sidebar flags = _.extend({}, flags, { @@ -984,6 +973,15 @@ var X2ManyListView = ListView.extend({ }); return valid; }, + render_pager: function($node, options) { + options = _.extend(options || {}, { + single_page_hidden: true, + }); + this._super($node, options); + }, + display_nocontent_helper: function () { + return false; + }, }); /** @@ -1008,7 +1006,7 @@ var X2ManyList = ListView.List.extend({ var $cell = $('<td>', { colspan: columns, - 'class': 'oe_form_field_x2many_list_row_add' + 'class': 'o_form_field_x2many_list_row_add' }).append( $('<a>', {href: '#'}).text(_t("Add an item")) .click(function (e) { @@ -1037,7 +1035,6 @@ var X2ManyList = ListView.List.extend({ }); var One2ManyListView = X2ManyListView.extend({ - _template: 'One2Many.listview', init: function () { this._super.apply(this, arguments); this.options = _.extend(this.options, { @@ -1177,7 +1174,7 @@ var One2ManyListView = X2ManyListView.extend({ var click_outside = ($target.closest('.ui-autocomplete,.btn,.modal-backdrop').length === 0); // Check if click inside the current list editable - var $o2m = $target.closest(".oe_list_editable"); + var $o2m = $target.closest(".o_list_editable"); if($o2m.length && $o2m[0] === this.el) { click_outside = false; } @@ -1251,39 +1248,19 @@ var One2ManyGroups = ListView.Groups.extend({ } }); -var One2ManyFormView = FormView.extend({ - form_template: 'One2Many.formview', - load_form: function(data) { - this._super(data); - var self = this; - this.$buttons.find('button.oe_form_button_create').click(function() { - self.save().done(self.on_button_new); - }); - }, - do_notify_change: function() { - if (this.dataset.parent_view) { - this.dataset.parent_view.do_notify_change(); - } else { - this._super.apply(this, arguments); - } - } -}); - var FieldOne2Many = FieldX2Many.extend({ init: function() { this._super.apply(this, arguments); this.x2many_views = { - form: One2ManyFormView, kanban: core.view_registry.get('one2many_kanban'), list: One2ManyListView, }; }, start: function() { - this.$el.addClass('oe_form_field_one2many'); + this.$el.addClass('o_form_field_one2many'); return this._super.apply(this, arguments); }, commit_value: function() { - var self = this; var view = this.viewmanager.active_view; if(view.type === "list" && view.controller.editable()) { return this.mutex.def.then(function () { @@ -1357,7 +1334,7 @@ var FieldMany2Many = FieldX2Many.extend({ }; }, start: function() { - this.$el.addClass('oe_form_field_many2many'); + this.$el.addClass('o_form_field_many2many'); return this._super.apply(this, arguments); } }); @@ -1373,7 +1350,7 @@ var FieldMany2ManyKanban = FieldMany2Many.extend({ }); var FieldMany2ManyTags = AbstractManyField.extend(common.CompletionFieldMixin, common.ReinitializeFieldMixin, { - className: "oe_form_field o_form_field_many2manytags", + className: "o_form_field_many2manytags", tag_template: "FieldMany2ManyTag", events: { @@ -1435,7 +1412,6 @@ var FieldMany2ManyTags = AbstractManyField.extend(common.CompletionFieldMixin, c } }, get_render_data: function(ids){ - var self = this; this.dataset.cancel_read(); var fields = this.fields.color ? ['display_name', 'name', 'color'] : ['display_name', 'name']; // TODO master: remove useless 'name' return this.dataset.read_ids(ids, fields); @@ -1475,26 +1451,30 @@ var FieldMany2ManyTags = AbstractManyField.extend(common.CompletionFieldMixin, c } return false; }, - set_dimensions: function (height, width) { - this._super(height, width); - var $input = this.$('input'); - if (!this.get("effective_readonly") && $input) { - $input.css('height', height); + set_dimensions: function(height, width) { + if(this.many2one) { + this.many2one.$el.css('height', 'auto'); + } + this.$el.css({ + width: width, + minHeight: height, + }); + if(this.many2one) { + this.many2one.$el.css('height', this.$el.height()); } }, open_color_picker: function(ev){ - this.mutex.exec(function(){ - if (this.fields.color) { - this.$color_picker = $(QWeb.render('FieldMany2ManyTag.colorpicker', { - 'widget': this, - 'tag_id': $(ev.currentTarget).data('id'), - })); - $(ev.currentTarget).append(this.$color_picker); - this.$color_picker.dropdown('toggle'); - this.$color_picker.attr("tabindex", 1).focus(); - } - }.bind(this)); + if (this.fields.color) { + this.$color_picker = $(QWeb.render('FieldMany2ManyTag.colorpicker', { + 'widget': this, + 'tag_id': $(ev.currentTarget).data('id'), + })); + + $(ev.currentTarget).append(this.$color_picker); + this.$color_picker.dropdown('toggle'); + this.$color_picker.attr("tabindex", 1).focus(); + } }, close_color_picker: function(){ this.$color_picker.remove(); @@ -1513,24 +1493,76 @@ var FieldMany2ManyTags = AbstractManyField.extend(common.CompletionFieldMixin, c var old_color = tag.data('color'); tag.removeClass('o_tag_color_' + old_color); tag.data('color', color); - tag.addClass('o_tag_color_' + color); + tag.addClass('o_tag_color_' + color); }); }, - }); /** * Widget for (many2many field) to upload one or more file in same time and display in list. * The user can delete his files. - * Options on attribute ; "blockui" {Boolean} block the UI or not - * during the file is uploading + * Options on attribute ; "blockui" {Boolean} block the UI or not during the file is uploading */ var FieldMany2ManyBinaryMultiFiles = AbstractManyField.extend(common.ReinitializeFieldMixin, { template: "FieldBinaryFileUploader", + events: { + 'click .o_attach': function(e) { + this.$('.o_form_input_file').click(); + }, + 'change .o_form_input_file': function(e) { + e.stopPropagation(); + + var $target = $(e.target); + var value = $target.val(); + + if(value !== '') { + if(this.data[0] && this.data[0].upload) { // don't upload more of one file in same time + return false; + } + + var filename = value.replace(/.*[\\\/]/, ''); + for(var id in this.get('value')) { + // if the files exits, delete the file before upload (if it's a new file) + if(this.data[id] && (this.data[id].filename || this.data[id].name) == filename && !this.data[id].no_unlink) { + this.ds_file.unlink([id]); + } + } + + if(this.node.attrs.blockui > 0) { // block UI or not + framework.blockUI(); + } + + // TODO : unactivate send on wizard and form + + // submit file + this.$('form.o_form_binary_form').submit(); + this.$(".oe_fileupload").hide(); + // add file on data result + this.data[0] = { + id: 0, + name: filename, + filename: filename, + url: '', + upload: true, + }; + } + }, + 'click .oe_delete': function(e) { + e.preventDefault(); + e.stopPropagation(); + + var file_id = $(e.currentTarget).data("id"); + if(file_id) { + var files = _.without(this.get('value'), file_id); + if(!this.data[file_id].no_unlink) { + this.ds_file.unlink([file_id]); + } + this.set({'value': files}); + } + }, + }, init: function(field_manager, node) { - this._super(field_manager, node); - this.field_manager = field_manager; - this.node = node; + this._super.apply(this, arguments); this.session = session; if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') { throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string); @@ -1541,40 +1573,39 @@ var FieldMany2ManyBinaryMultiFiles = AbstractManyField.extend(common.Reinitializ this.fileupload_id = _.uniqueId('oe_fileupload_temp'); $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this)); }, - initialize_content: function() { - this.$el.on('change', 'input.o_form_input_file', this.on_file_change ); - }, - get_file_url: function (attachment) { + get_file_url: function(attachment) { return '/web/content/' + attachment.id + '?download=true'; }, - read_name_values : function () { + read_name_values : function() { var self = this; // don't reset know values var ids = this.get('value'); - var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } ); + var _value = _.filter(ids, function(id) { return self.data[id] === undefined; }); // send request for get_name - if (_value.length) { - return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname', 'mimetype']]).then(function (datas) { - _.each(datas, function (data) { - data.no_unlink = true; - data.url = self.get_file_url(data); - self.data[data.id] = data; - }); - return ids; - }); + if(_value.length) { + return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname', 'mimetype']]) + .then(process_data); } else { return $.when(ids); } + + function process_data(datas) { + _.each(datas, function(data) { + data.no_unlink = true; + data.url = self.get_file_url(data); + self.data[data.id] = data; + }); + return ids; + } }, - render_value: function () { + render_value: function() { var self = this; this.read_name_values().then(function (ids) { - var render = $(QWeb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids})); - render.on('click', '.oe_delete', _.bind(self.on_file_delete, self)); - self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render ); + self.$('.oe_placeholder_files, .oe_attachments') + .replaceWith($(QWeb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}))); // reinit input type file - var $input = self.$('input.o_form_input_file'); + var $input = self.$('.o_form_input_file'); $input.after($input.clone(true)).remove(); self.$(".oe_fileupload").show(); @@ -1587,77 +1618,26 @@ var FieldMany2ManyBinaryMultiFiles = AbstractManyField.extend(common.Reinitializ }); }); }, - on_file_change: function (event) { - event.stopPropagation(); - var self = this; - var $target = $(event.target); - if ($target.val() !== '') { - var filename = $target.val().replace(/.*[\\\/]/,''); - // don't uplode more of one file in same time - if (self.data[0] && self.data[0].upload ) { - return false; - } - for (var id in this.get('value')) { - // if the files exits, delete the file before upload (if it's a new file) - if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) { - self.ds_file.unlink([id]); - } - } - - // block UI or not - if(this.node.attrs.blockui>0) { - framework.blockUI(); - } - - // TODO : unactivate send on wizard and form - - // submit file - this.$('form.o_form_binary_form').submit(); - this.$(".oe_fileupload").hide(); - // add file on data result - this.data[0] = { - 'id': 0, - 'name': filename, - 'filename': filename, - 'url': '', - 'upload': true - }; - } - }, - on_file_loaded: function (event, result) { - var files = this.get('value'); - - // unblock UI - if(this.node.attrs.blockui>0) { + on_file_loaded: function(e, result) { + if(this.node.attrs.blockui > 0) { // unblock UI framework.unblockUI(); } - if (result.error || !result.id ) { - this.do_warn( _t('Uploading Error'), result.error); + if(result.error || !result.id) { + this.do_warn(_t('Uploading Error'), result.error); delete this.data[0]; } else { - if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) { + if(this.data[0] && this.data[0].filename === result.filename && this.data[0].upload) { delete this.data[0]; } result.url = this.get_file_url(result); this.data[result.id] = result; var values = _.clone(this.get('value')); values.push(result.id); - this.set({'value': values}); + this.set({value: values}); } this.render_value(); }, - on_file_delete: function (event) { - event.stopPropagation(); - var file_id=$(event.target).closest('.oe_delete').data("id"); - if (file_id) { - var files = _.filter(this.get('value'), function (id) {return id != file_id;}); - if(!this.data[file_id].no_unlink) { - this.ds_file.unlink([file_id]); - } - this.set({'value': files}); - } - }, }); /* @@ -1672,7 +1652,7 @@ var FieldMany2ManyCheckBoxes = AbstractManyField.extend(common.ReinitializeField this.set("records", []); this.field_manager.on("view_content_has_changed", this, function() { var domain = new data.CompoundDomain(this.build_domain()).eval(); - if (! _.isEqual(domain, this.get("domain"))) { + if (!_.isEqual(domain, this.get("domain"))) { this.set("domain", domain); } }); @@ -1694,18 +1674,22 @@ var FieldMany2ManyCheckBoxes = AbstractManyField.extend(common.ReinitializeField }); }, render_value: function() { - this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this})); - var inputs = this.$("input"); - inputs.change(_.bind(this.from_dom, this)); - if (this.get("effective_readonly")) - inputs.attr("disabled", "true"); + this.$el.html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this})); + var $inputs = this.$("input"); + $inputs.change(_.bind(this.from_dom, this)); + if (this.get("effective_readonly")) { + $inputs.attr("disabled", "true"); + } }, from_dom: function() { var new_value = this.$("input:checked").map(function() { return +$(this).data("record-id"); }).get(); - if (! _.isEqual(new_value, this.get("value"))) { + if (!_.isEqual(new_value, this.get("value"))) { this.internal_set_value(new_value); } - } + }, + is_false: function() { + return false; + }, }); core.form_widget_registry @@ -1718,9 +1702,13 @@ core.form_widget_registry .add('many2many_binary', FieldMany2ManyBinaryMultiFiles) .add('many2many_checkboxes', FieldMany2ManyCheckBoxes); +core.one2many_view_registry + .add('list', One2ManyListView); + return { FieldMany2ManyTags: FieldMany2ManyTags, AbstractManyField: AbstractManyField, + FieldMany2One: FieldMany2One, }; }); diff --git a/addons/web/static/src/js/views/form_upgrade_widgets.js b/addons/web/static/src/js/views/form_upgrade_widgets.js new file mode 100644 index 00000000000..ce32ef0664b --- /dev/null +++ b/addons/web/static/src/js/views/form_upgrade_widgets.js @@ -0,0 +1,101 @@ +odoo.define('web.form_upgrade_widgets', function (require) { +"use strict"; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var form_widgets = require('web.form_widgets'); +var framework = require('web.framework'); +var Model = require('web.DataModel'); + +var _t = core._t; +var QWeb = core.qweb; + +/** + * This widget is intended to be used in config settings. + * When checked, an upgrade popup is showed to the user. + */ +var AbstractFieldUpgrade = { + events: { + 'click input': 'on_click_input', + }, + + start: function() { + this._super.apply(this, arguments); + + this.get_enterprise_label().after($("<span>", { + text: "Enterprise", + 'class': "label label-primary oe_inline" + })); + }, + + open_dialog: function() { + var message = $(QWeb.render('EnterpriseUpgrade')); + + var buttons = [ + { + text: _t("Upgrade now"), + classes: 'btn-primary', + close: true, + click: this.confirm_upgrade, + }, + { + text: _t("Cancel"), + close: true, + }, + ]; + + return new Dialog(this, { + size: 'medium', + buttons: buttons, + $content: $('<div>', { + html: message, + }), + title: _t("Odoo Enterprise"), + }).open(); + }, + + confirm_upgrade: function() { + new Model("res.users").call("search_count", [[["share", "=", false]]]).then(function(data) { + framework.redirect("https://www.odoo.com/odoo-enterprise/upgrade?num_users=" + data); + }); + }, + + get_enterprise_label: function() {}, + on_click_input: function() {}, +}; + +var UpgradeBoolean = form_widgets.FieldBoolean.extend(AbstractFieldUpgrade, { + template: "FieldUpgradeBoolean", + + get_enterprise_label: function() { + return this.$label; + }, + + on_click_input: function() { + if(this.$checkbox.prop("checked")) { + this.open_dialog().on('closed', this, function() { + this.$checkbox.prop("checked", false); + }); + } + }, +}); + +var UpgradeRadio = form_widgets.FieldRadio.extend(AbstractFieldUpgrade, { + get_enterprise_label: function() { + return this.$('label').last(); + }, + + on_click_input: function(event) { + if($(event.target).val() === 1) { + this.open_dialog().on('closed', this, function() { + this.$('input').first().prop("checked", true); + }); + } + }, +}); + +core.form_widget_registry + .add('upgrade_boolean', UpgradeBoolean) + .add('upgrade_radio', UpgradeRadio); + +}); diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js index 7a2cf428400..d2c7a21eb04 100644 --- a/addons/web/static/src/js/views/form_view.js +++ b/addons/web/static/src/js/views/form_view.js @@ -1,12 +1,14 @@ odoo.define('web.FormView', function (require) { "use strict"; +var common = require('web.form_common'); var core = require('web.core'); var crash_manager = require('web.crash_manager'); var data = require('web.data'); var Dialog = require('web.Dialog'); -var common = require('web.form_common'); +var FormRenderingEngine = require('web.FormRenderingEngine'); var Model = require('web.DataModel'); +var Pager = require('web.Pager'); var Sidebar = require('web.Sidebar'); var utils = require('web.utils'); var View = require('web.View'); @@ -14,7 +16,6 @@ var View = require('web.View'); var _t = core._t; var _lt = core._lt; var QWeb = core.qweb; -var Class = core.Class; /** * Properties: @@ -22,7 +23,7 @@ var Class = core.Class; * the mode used by the view. */ var FormView = View.extend(common.FieldManagerMixin, { - accesskey: "F", + className: "o_form_view", defaults: _.extend({}, View.prototype.defaults, { not_interactible_on_create: false, initial_mode: "view", @@ -34,8 +35,20 @@ var FormView = View.extend(common.FieldManagerMixin, { multi_record: false, // Indicates that this view is not searchable, and thus that no search view should be displayed. searchable: false, - template: "FormView", + /** + * Called each time the form view is attached into the DOM + */ + on_attach_callback: function() { + this.trigger('attached'); + this.autofocus(); + }, + /** + * Called each time the form view is detached from the DOM + */ + on_detach_callback: function() { + this.trigger('detached'); + }, init: function() { var self = this; this._super.apply(this, arguments); @@ -60,14 +73,11 @@ var FormView = View.extend(common.FieldManagerMixin, { this.__clicked_inside = false; this.__blur_timeout = null; this.rendering_engine = new FormRenderingEngine(this); - self.set({actual_mode: self.options.initial_mode}); + this.set({actual_mode: this.options.initial_mode}); this.has_been_loaded.done(function() { self._build_onchange_specs(); - self.check_actual_mode(); - self.on("change:actual_mode", self, self.check_actual_mode); self.on("change:actual_mode", self, self.toggle_buttons); self.on("change:actual_mode", self, self.toggle_sidebar); - self.on("change:actual_mode", self, self.do_update_pager); }); self.on("load_record", self, self.load_record); core.bus.on('clear_uncommitted_changes', this, function(chain_callbacks) { @@ -90,8 +100,7 @@ var FormView = View.extend(common.FieldManagerMixin, { this.rendering_engine.set_tags_registry(this.tags_registry); this.rendering_engine.set_widgets_registry(this.widgets_registry); this.rendering_engine.set_fields_view(this.fields_view); - var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container'); - this.rendering_engine.render_to($dest); + this.rendering_engine.render_to(this.$el); this.$el.on('mousedown.formBlur', function () { self.__clicked_inside = true; @@ -100,22 +109,14 @@ var FormView = View.extend(common.FieldManagerMixin, { this.has_been_loaded.resolve(); // Add bounce effect on button 'Edit' when click on readonly page view. - this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) { + this.$(".oe_title,.o_group").on('mouseup', function (e) { // 'mouseup' event because some widget need bootstrap click event to go up to body if(self.get("actual_mode") === "view" && self.$buttons && !$(e.target).is('[data-toggle]')) { - var $button = self.$buttons.find(".oe_form_button_edit"); + var $button = self.$buttons.find(".o_form_button_edit"); $button.openerpBounce(); e.stopPropagation(); core.bus.trigger('click', e); } }); - //bounce effect on red button when click on statusbar. - this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) { - if((self.get("actual_mode") === "view")) { - var $button = self.$el.find(".oe_highlight:not(.o_form_invisible)").css({'float':'left','clear':'none'}); - $button.openerpBounce(); - e.stopPropagation(); - } - }); return this._super(); }, destroy: function() { @@ -133,10 +134,10 @@ var FormView = View.extend(common.FieldManagerMixin, { * Set this.$buttons with the produced jQuery element * @param {jQuery} [$node] a jQuery node where the rendered buttons should be inserted * $node may be undefined, in which case the FormView inserts them into this.options.$buttons - * or into a div of its template + * if it exists */ render_buttons: function($node) { - this.$buttons = $('<div/>', {'class': 'oe_form_buttons'}); + this.$buttons = $('<div/>'); var $footer = this.$('footer'); if (this.options.action_buttons !== false || this.options.footer_to_buttons && $footer.children().length === 0) { @@ -148,23 +149,17 @@ var FormView = View.extend(common.FieldManagerMixin, { // Show or hide the buttons according to the view mode this.toggle_buttons(); - this.$buttons.on('click', '.oe_form_button_create', this.on_button_create); - this.$buttons.on('click', '.oe_form_button_edit', this.on_button_edit); - this.$buttons.on('click', '.oe_form_button_save', this.on_button_save); - this.$buttons.on('click', '.oe_form_button_cancel', this.on_button_cancel); + this.$buttons.on('click', '.o_form_button_create', this.on_button_create); + this.$buttons.on('click', '.o_form_button_edit', this.on_button_edit); + this.$buttons.on('click', '.o_form_button_save', this.on_button_save); + this.$buttons.on('click', '.o_form_button_cancel', this.on_button_cancel); - if ($node) { - this.$buttons.appendTo($node); - } else { - this.$('.oe_form_buttons').replaceWith(this.$buttons); - } + this.$buttons.appendTo($node); }, /** * Instantiate and render the sidebar if a sidebar is requested * Sets this.sidebar * @param {jQuery} [$node] a jQuery node where the sidebar should be inserted - * $node may be undefined, in which case the FormView inserts the sidebar in a - * div of its template **/ render_sidebar: function($node) { if (!this.sidebar && this.options.sidebar) { @@ -177,7 +172,6 @@ var FormView = View.extend(common.FieldManagerMixin, { this.is_action_enabled('create') && { label: _t('Duplicate'), callback: this.on_button_duplicate } ])); - $node = $node || this.$('.oe_form_sidebar'); this.sidebar.appendTo($node); // Show or hide the sidebar according to the view mode @@ -185,41 +179,33 @@ var FormView = View.extend(common.FieldManagerMixin, { } }, /** - * Render the pager according to the FormView.pager template and add listeners on it. - * Set this.$pager with the produced jQuery element - * @param {jQuery} [$node] a jQuery node where the rendered pager should be inserted + * Instantiate and render the pager and add listeners on it. + * Set this.pager + * @param {jQuery} [$node] a jQuery node where the pager should be inserted * $node may be undefined, in which case the FormView inserts the pager into this.options.$pager - * or into a div of its template */ render_pager: function($node) { if (this.options.pager) { var self = this; - if (this.$pager) { - this.$pager.remove(); - } - if (this.get("actual_mode") === "create") { - return; - } - this.$pager = $(QWeb.render("FormView.pager", {'widget': self})); - this.$pager.on('click','a[data-pager-action]',function() { - var $el = $(this); - if ($el.attr("disabled")) { - return; - } - var action = $el.data('pager-action'); - var def = $.when(self.execute_pager_action(action)); - $el.attr("disabled"); - def.always(function() { - $el.removeAttr("disabled"); + var options = { + validate: _.bind(this.can_be_discarded, this), + }; + + this.pager = new Pager(this, this.dataset.ids.length, this.dataset.index + 1, 1, options); + this.pager.on('pager_changed', this, function (new_state) { + this.pager.disable(); + this.dataset.index = new_state.current_min - 1; + this.trigger('pager_action_executed'); + $.when(this.reload()).then(function () { + self.pager.enable(); }); }); - this.do_update_pager(); - $node = $node || this.options.$pager; - if ($node) { - this.$pager.appendTo($node); - } else { - this.$('.oe_form_pager').replaceWith(this.$pager); + this.pager.appendTo($node = $node || this.options.$pager); + + // Hide the pager in create mode + if (this.get("actual_mode") === "create") { + this.pager.do_hide(); } } }, @@ -241,6 +227,16 @@ var FormView = View.extend(common.FieldManagerMixin, { this.sidebar.do_toggle(this.get("actual_mode") === "view"); } }, + update_pager: function() { + if (this.pager) { + // Hide the pager in create mode + if (this.get("actual_mode") === "create") { + this.pager.do_hide(); + } else { + this.pager.update_state({size: this.dataset.ids.length, current_min: this.dataset.index + 1}); + } + } + }, widgetFocused: function() { // Clear click flag if used to focus a widget this.__clicked_inside = false; @@ -281,11 +277,6 @@ var FormView = View.extend(common.FieldManagerMixin, { do_show: function (options) { var self = this; options = options || {}; - this._super(); - this.$el.css({ - opacity: '0', - filter: 'alpha(opacity = 0)' - }); this.$el.removeClass('oe_form_dirty'); var shown = this.has_been_loaded; @@ -307,10 +298,6 @@ var FormView = View.extend(common.FieldManagerMixin, { } return $.when(shown, this._super()).then(function() { self._actualize_mode(options.mode || self.options.initial_mode); - self.$el.css({ - opacity: '1', - filter: 'alpha(opacity = 100)' - }); core.bus.trigger('form_view_shown', self); }); }, @@ -324,6 +311,7 @@ var FormView = View.extend(common.FieldManagerMixin, { this.datarecord = record; this._actualize_mode(); this.set({ 'title' : record.id ? record.display_name : _t("New") }); + this.update_pager(); this.record_loaded = $.Deferred(); _(this.fields).each(function (field, f) { @@ -342,19 +330,17 @@ var FormView = View.extend(common.FieldManagerMixin, { self.rendering_engine.init_fields().then(function() { self.is_initialized.resolve(); self.record_loaded.resolve(); - self.do_update_pager(record.id === null || record.id === undefined); if (self.sidebar) { - self.sidebar.do_attachement_update(self.dataset, self.datarecord.id); + self.sidebar.do_attachement_update(self.dataset, self.datarecord.id); } if (record.id) { self.do_push_state({id:record.id}); } else { self.do_push_state({}); } - self.$el.removeClass('oe_form_dirty'); - self.autofocus(); + self.$el.removeClass('oe_form_dirty'); }); - }); + }); }, /** * Loads and sets up the default values for the model as the current @@ -370,7 +356,7 @@ var FormView = View.extend(common.FieldManagerMixin, { self.trigger('load_record', _.clone(r)); }); } - return self.trigger('load_record', {}); + return $.when().then(this.trigger.bind(this, 'load_record', {})); }, on_form_changed: function() { this.trigger("view_content_has_changed"); @@ -378,40 +364,6 @@ var FormView = View.extend(common.FieldManagerMixin, { do_notify_change: function() { this.$el.addClass('oe_form_dirty'); }, - execute_pager_action: function(action) { - var self = this; - return this.can_be_discarded().then(function() { - switch (action) { - case 'first': - self.dataset.index = 0; - break; - case 'previous': - self.dataset.previous(); - break; - case 'next': - self.dataset.next(); - break; - case 'last': - self.dataset.index = self.dataset.ids.length - 1; - break; - } - self.trigger('pager_action_executed'); - return self.reload(); - }); - }, - do_update_pager: function(hide_index) { - if (this.$pager) { - // Hide the pager in create mode or when there is only one record - var pager_visible = (this.get("actual_mode") !== "create") && (this.dataset.ids.length > 1); - this.$pager.toggle(pager_visible); - if (hide_index === true) { - this.$pager.find(".oe_form_pager_state").html(""); - } else { - this.$pager.find(".oe_form_pager_state").html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length)); - } - } - }, - _build_onchange_specs: function() { var self = this; var find = function(field_name, root) { @@ -664,15 +616,15 @@ var FormView = View.extend(common.FieldManagerMixin, { } else if (mode === "create") { mode = "edit"; } + + var viewMode = (mode === "view"); + this.$el.toggleClass('o_form_readonly', viewMode).toggleClass('o_form_editable', !viewMode); + this.render_value_defs = []; this.set({actual_mode: mode}); - }, - check_actual_mode: function(source, options) { - if(this.get("actual_mode") === "view") { - this.$el.removeClass('oe_form_editable').addClass('oe_form_readonly'); - } else { - this.$el.removeClass('oe_form_readonly').addClass('oe_form_editable'); - _.defer(_.bind(this.autofocus, this)); + + if(!viewMode) { + this.autofocus(); } }, autofocus: function() { @@ -699,7 +651,7 @@ var FormView = View.extend(common.FieldManagerMixin, { this.$('.oe_form_buttons').add(this.$buttons).find('button.o_disabled').removeClass('o_disabled').prop('disabled', false); this.is_disabled = false; }, - on_button_save: function(e) { + on_button_save: function() { var self = this; if (this.is_disabled) { return; @@ -716,7 +668,7 @@ var FormView = View.extend(common.FieldManagerMixin, { self.enable_button(); }); }, - on_button_cancel: function(event) { + on_button_cancel: function() { var self = this; this.can_be_discarded().then(function() { if (self.get('actual_mode') === 'create') { @@ -733,10 +685,11 @@ var FormView = View.extend(common.FieldManagerMixin, { }, on_button_new: function() { var self = this; - this.to_edit_mode(); return $.when(this.has_been_loaded).then(function() { return self.can_be_discarded().then(function() { - return self.load_defaults(); + return self.load_defaults().then(function() { + self.to_edit_mode(); + }); }); }); }, @@ -763,7 +716,8 @@ var FormView = View.extend(common.FieldManagerMixin, { if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) { self.dataset.unlink([self.datarecord.id]).done(function() { if (self.dataset.size()) { - self.execute_pager_action('next'); + self.reload(); + self.update_pager(); } else { self.do_action('history_back'); } @@ -790,7 +744,9 @@ var FormView = View.extend(common.FieldManagerMixin, { title: _t("Warning"), confirm_callback: function() { self.$el.removeClass('oe_form_dirty'); - def.resolve(); + this.on('closed', null, function() { // 'this' is the dialog widget + def.resolve(); + }); }, cancel_callback: function() { def.reject(); @@ -951,7 +907,7 @@ var FormView = View.extend(common.FieldManagerMixin, { this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids)); this.dataset.index = 0; } - this.do_update_pager(); + this.update_pager(); if (this.sidebar) { this.sidebar.do_attachement_update(this.dataset, this.datarecord.id); } @@ -1090,7 +1046,7 @@ var FormView = View.extend(common.FieldManagerMixin, { $box.remove(); }); // add content inside notification box on top of the sheet/form - var $target = this.$('.oe_form_sheet').length ? this.$('.oe_form_sheet') : this.$el; + var $target = this.$('.o_form_sheet_bg').length ? this.$('.o_form_sheet_bg') : this.$el; $target.prepend($box); }, open_defaults_dialog: function () { @@ -1153,7 +1109,7 @@ var FormView = View.extend(common.FieldManagerMixin, { var $defaults = d.$el.find('#formview_default_fields'); var field_to_set = $defaults.val(); if (!field_to_set) { - $defaults.parent().addClass('oe_form_invalid'); + $defaults.parent().addClass('o_form_invalid'); return; } var condition = d.$el.find('#formview_default_conditions').val(), @@ -1234,21 +1190,29 @@ var FormView = View.extend(common.FieldManagerMixin, { }, }); -/** - * Interface to be implemented by rendering engines for the form view. - */ -var FormRenderingEngineInterface = Class.extend({ - set_fields_view: function(fields_view) {}, - set_fields_registry: function(fields_registry) {}, - render_to: function($el) {}, +core.view_registry.add('form', FormView); + +return FormView; + }); + +odoo.define('web.FormRenderingEngine', function (require) { +"use strict"; + +var common = require('web.form_common'); +var core = require('web.core'); +var utils = require('web.utils'); + +var _t = core._t; +var QWeb = core.qweb; + /** * Default rendering engine for the form view. * * It is necessary to set the view using set_view() before usage. */ -var FormRenderingEngine = FormRenderingEngineInterface.extend({ +return core.Class.extend({ init: function(view) { this.view = view; }, @@ -1289,7 +1253,7 @@ var FormRenderingEngine = FormRenderingEngineInterface.extend({ $('board', doc).each(function() { $(this).attr('layout', $(this).attr('style')); }); - return $('<div class="oe_form"/>').append(utils.xml_to_str(doc)); + return $(utils.xml_to_str(doc)); }, render_to: function($target) { var self = this; @@ -1305,7 +1269,8 @@ var FormRenderingEngine = FormRenderingEngineInterface.extend({ this.labels = {}; this.process(this.$form); - this.$form.appendTo(this.$target); + this.$form.contents().appendTo(this.$target); + this.handle_common_properties(this.$target, this.$form); this.to_replace = []; @@ -1382,9 +1347,6 @@ var FormRenderingEngine = FormRenderingEngineInterface.extend({ args[0] = $tag; return fn.apply(self, args); } else { - if( tagname === 'header') { - $tag.addClass('o_statusbar_buttons'); - } // generic tag handling, just process children $tag.children().each(function() { self.process($(this)); @@ -1408,14 +1370,14 @@ var FormRenderingEngine = FormRenderingEngineInterface.extend({ process_sheet: function($sheet) { var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes()); this.handle_common_properties($new_sheet, $sheet); - var $dst = $new_sheet.find('.oe_form_sheet'); + var $dst = $new_sheet.find('.o_form_sheet'); $sheet.contents().appendTo($dst); $sheet.before($new_sheet).remove(); this.process($new_sheet); }, process_form: function($form) { if ($form.find('> sheet').length === 0) { - $form.addClass('oe_form_nosheet'); + $form.addClass('o_form_nosheet'); } var $new_form = this.render_element('FormRenderingForm', $form.getAttributes()); this.handle_common_properties($new_form, $form); @@ -1479,119 +1441,99 @@ var FormRenderingEngine = FormRenderingEngineInterface.extend({ }, process_group: function($group) { var self = this; - $group.children('field').each(function() { - self.preprocess_field($(this)); - }); - var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes()); - var $table; - if ($new_group.first().is('table.oe_form_group')) { - $table = $new_group; - } else if ($new_group.filter('table.oe_form_group').length) { - $table = $new_group.filter('table.oe_form_group').first(); - } else { - $table = $new_group.find('table.oe_form_group').first(); - } - var $tr, $td, - cols = parseInt($group.attr('col') || 2, 10), - row_cols = cols; + // Processes a group containing nested groups + // Renders a div for each nested group, into a div that is the outer group + // The nested groups's width is a percentage according to col and colspan attributes + var _process_outer_group = function(col) { + var $new_group = self.render_element('FormRenderingOuterGroup', $group.getAttributes()); + $group.children().each(function() { + var $child = $(this); + var tagName = $child[0].tagName.toLowerCase(); + var colspan = parseInt($child.attr('colspan') || 1, 10); - var children = []; - $group.children().each(function() { - var $child = $(this); - var colspan = parseInt($child.attr('colspan') || 1, 10); - var tagName = $child[0].tagName.toLowerCase(); - var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan); - var newline = tagName === 'newline'; - - // Note FME: those classes are used in layout debug mode - if ($tr && row_cols > 0 && (newline || row_cols < colspan)) { - $tr.addClass('oe_form_group_row_incomplete'); - if (newline) { - $tr.addClass('oe_form_group_row_newline'); + if (tagName === 'newline') { + $new_group.append($('<br>')); // Skip to the next line + return; } - } - if (newline) { - $tr = null; - return; - } - if (!$tr || row_cols < colspan) { - $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table); - row_cols = cols; - } else if (tagName==='group') { - // When <group> <group/><group/> </group>, we need a spacing between the two groups - $td.addClass('oe_group_right'); - } - row_cols -= colspan; + // compute child's classname from col and colspan attributes + $child.addClass('o_group_col_' + Math.min(Math.floor(colspan/col*12), 12)); + $new_group.append($child); - // invisibility transfer - var field_modifiers = JSON.parse($child.attr('modifiers') || '{}'); - var invisible = field_modifiers.invisible; - self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible}))); + children.push($child[0]); + }); + return $new_group; + }; + // Processes a group containing no nested group + // Renders an HTML table + var _process_inner_group = function(col) { + var $new_group = self.render_element('FormRenderingInnerGroup', $group.getAttributes()); + $new_group.find('td').attr('colspan', col); // Group's title spans all column + var row_col = col; + var $row = null; - $tr.append($td.append($child)); - children.push($child[0]); - }); - if (row_cols && $td) { - $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols); - } - $group.before($new_group).remove(); + $group.children().each(function() { + var $child = $(this); + var tagName = $child[0].tagName.toLowerCase(); + var colspan = parseInt($child.attr('colspan') || 1, 10); - $table.find('> tbody > tr').each(function() { - var to_compute = [], - row_cols = cols, - total = 100; - $(this).children().each(function() { - var $td = $(this), - $child = $td.children(':first'); - if ($child.attr('cell-class')) { - $td.addClass($child.attr('cell-class')); + if (tagName === 'newline') { + $row = null; // Start with a new row + return; } - switch ($child[0].tagName.toLowerCase()) { - case 'separator': - break; - case 'label': - if ($child.attr('for')) { - $td.attr('width', '1%').addClass('oe_form_group_cell_label'); - row_cols-= $td.attr('colspan') || 1; - total--; - } - break; - default: - var width = _.str.trim($child.attr('width') || ''), - iwidth = parseInt(width, 10); - if (iwidth) { - if (width.substr(-1) === '%') { - total -= iwidth; - width = iwidth + '%'; - } else { - // Absolute width - $td.css('min-width', width + 'px'); - } - $td.attr('width', width); - $child.removeAttr('width'); - row_cols-= $td.attr('colspan') || 1; - } else { - to_compute.push($td); - } - + row_col -= colspan; + if (!$row || row_col < 0) { // Append a new row to the group + $row = $('<tr>').appendTo($new_group); + row_col = col-colspan; } + $('<td>').attr('colspan', colspan).append($child).appendTo($row); + + children.push($child[0]); }); - if (row_cols) { - var unit = Math.floor(total / row_cols); - if (!$(this).is('.oe_form_group_row_incomplete')) { - _.each(to_compute, function($td) { - var width = parseInt($td.attr('colspan'), 10) * unit; - $td.attr('width', width + '%'); - total -= width; - }); - } - } + + // Some kind of an hack + // Add width on table's cell to avoid automatic layout of table which + // makes first col to take full width available (typically, first col contains label) + $new_group.find('tr').each(function() { + var $tr = $(this); + var nonlabel_width = 100/(col - $tr.find('td > label:first-child').length); + $tr.find('td').each(function() { + var $td = $(this); + var $child = $td.children(':first'); + if ($child.is("label")) { + $td.addClass("o_td_label"); + } else { + $td.css("width", (nonlabel_width*parseInt($td.attr('colspan')))+"%"); + } + }); + + }); + return $new_group; + }; + + // Preprocess field children + $group.children('field').each(function() { + self.preprocess_field($(this)); }); + + // Process group + var nested_groups = $group.children('group').length; // Detect possible nested groups + var children = []; + var col = parseInt($group.attr('col') || 2, 10); + var $new_group; + if (nested_groups) { + $new_group = _process_outer_group(col); + } else { + $new_group = _process_inner_group(col); + } + this.handle_common_properties($new_group, $group); + + // Process group's children + $group.before($new_group).remove(); _.each(children, function(el) { self.process($(el)); }); - this.handle_common_properties($new_group, $group); + return $new_group; }, process_notebook: function($notebook) { @@ -1658,17 +1600,8 @@ var FormRenderingEngine = FormRenderingEngineInterface.extend({ var dict = { string: $label.attr('string') || (field_orm || {}).string || '', help: $label.attr('help') || (field_orm || {}).help || '', - _for: name ? _.uniqueId('oe-field-input-') : undefined, + _for: name ? _.uniqueId('o_field_input_') : undefined, }; - var align = parseFloat(dict.align); - if (isNaN(align) || align === 1) { - align = 'right'; - } else if (align === 0) { - align = 'left'; - } else { - align = 'center'; - } - dict.align = align; var $new_label = this.render_element('FormRenderingLabel', dict); $label.before($new_label).remove(); this.handle_common_properties($new_label, $label); @@ -1698,8 +1631,4 @@ var FormRenderingEngine = FormRenderingEngineInterface.extend({ } }); -core.view_registry.add('form', FormView); - -return FormView; - }); diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js index 2a2a36cdaba..2aeaaa17111 100644 --- a/addons/web/static/src/js/views/form_widgets.js +++ b/addons/web/static/src/js/views/form_widgets.js @@ -5,17 +5,17 @@ var core = require('web.core'); var crash_manager = require('web.crash_manager'); var data = require('web.data'); var datepicker = require('web.datepicker'); +var dom_utils = require('web.dom_utils'); +var Priority = require('web.Priority'); var ProgressBar = require('web.ProgressBar'); var Dialog = require('web.Dialog'); var common = require('web.form_common'); var formats = require('web.formats'); var framework = require('web.framework'); var Model = require('web.DataModel'); -var Priority = require('web.Priority'); var pyeval = require('web.pyeval'); var session = require('web.session'); var utils = require('web.utils'); -var dom_utils = require('web.dom_utils'); var _t = core._t; var QWeb = core.qweb; @@ -119,24 +119,25 @@ var WidgetButton = common.FormWidget.extend({ var FieldChar = common.AbstractField.extend(common.ReinitializeFieldMixin, { template: 'FieldChar', - widget_class: 'oe_form_field_char', events: { - 'change input': 'store_dom_value', + 'change': 'store_dom_value', }, init: function (field_manager, node) { this._super(field_manager, node); this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1'; }, initialize_content: function() { - this.setupFocus(this.$('input')); + if(!this.get('effective_readonly') && !this.$input) { + this.$input = this.$el; + } + this.setupFocus(this.$el); + }, + destroy_content: function() { + this.$input = undefined; }, store_dom_value: function () { - if (!this.get('effective_readonly') - && this.$('input').length - && this.is_syntax_valid()) { - this.internal_set_value( - this.parse_value( - this.$('input').val())); + if (this.$input && this.is_syntax_valid()) { + this.internal_set_value(this.parse_value(this.$input.val())); } }, commit_value: function () { @@ -145,20 +146,19 @@ var FieldChar = common.AbstractField.extend(common.ReinitializeFieldMixin, { }, render_value: function() { var show_value = this.format_value(this.get('value'), ''); - if (!this.get("effective_readonly")) { - this.$el.find('input').val(show_value); + if (this.$input) { + this.$input.val(show_value); } else { if (this.password) { show_value = new Array(show_value.length + 1).join('*'); } - this.$(".oe_form_char_content").text(show_value); + this.$el.text(show_value); } }, is_syntax_valid: function() { - if (!this.get("effective_readonly") && this.$("input").size() > 0) { + if (this.$input) { try { - this.parse_value(this.$('input').val(), ''); - return true; + this.parse_value(this.$input.val(), ''); } catch(e) { return false; } @@ -175,31 +175,30 @@ var FieldChar = common.AbstractField.extend(common.ReinitializeFieldMixin, { return this.get('value') === '' || this._super(); }, focus: function() { - var input = this.$('input:first')[0]; - return input ? input.focus() : false; - }, - set_dimensions: function (height, width) { - this._super(height, width); - this.$('input').css({ - height: height, - width: width - }); - } + if (this.$input) { + return this.$input.focus(); + } + return false; + }, }); -var KanbanSelection = FieldChar.extend({ +var KanbanSelection = common.AbstractField.extend({ template: "FormSelection", - init: function (field_manager, node) { - this._super(field_manager, node); + events: { + 'click a': function(e) { + e.preventDefault(); + }, + 'mouseup a': function(e) { + e.stopPropagation(); + }, + 'click li': 'set_kanban_selection' }, start: function () { - var self = this; - this.states = []; - this._super.apply(this, arguments); // hook on form view content changed: recompute the states, because it may be related to the current stage - this.getParent().on('view_content_has_changed', self, function () { - self.render_value(); + this.view.on('view_content_has_changed', this, function () { + this.render_value(); }); + return this._super(); }, prepare_dropdown_selection: function() { var self = this; @@ -233,6 +232,7 @@ var KanbanSelection = FieldChar.extend({ this._super(); this.states = this.prepare_dropdown_selection(); var self = this; + // Adapt "FormSelection" var current_state = _.find(this.states, function(state) { return state.name === self.get('value'); @@ -248,7 +248,6 @@ var KanbanSelection = FieldChar.extend({ var $dropdown = this.$el.find('.dropdown-menu'); $dropdown.children().remove(); // remove old items $items.appendTo($dropdown); - this.$el.find('a').on('click', this.set_kanban_selection.bind(this)); }, /* setting the value: in view mode, perform an asynchronous call and reload the form view; in edit mode, use set_value to save the new value that will @@ -259,7 +258,7 @@ var KanbanSelection = FieldChar.extend({ var li = $(ev.target).closest('li'); if (li.length) { var value = String(li.data('value')); - if (this.view.get('actual_mode') == 'view') { + if (this.view.get('actual_mode') === 'view') { var write_values = {}; write_values[self.name] = value; return this.view.dataset._model.call( @@ -339,66 +338,35 @@ var FieldID = FieldChar.extend({ var FieldEmail = FieldChar.extend({ template: 'FieldEmail', - initialize_content: function() { - this._super(); - var $button = this.$el.find('button'); - $button.click(this.on_button_clicked); - this.setupFocus($button); + prefix: 'mailto', + init: function() { + this._super.apply(this, arguments); + this.clickable = true; }, render_value: function() { - if (!this.get("effective_readonly")) { - this._super(); - } else { - this.$el.find('a') - .attr('href', 'mailto:' + this.get('value')) - .text(this.get('value') || ''); - } - }, - on_button_clicked: function() { - if (!this.get('value') || !this.is_syntax_valid()) { - this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address")); - } else { - location.href = 'mailto:' + this.get('value'); + this._super(); + if (this.get("effective_readonly") && this.clickable) { + this.$el.attr('href', this.prefix + ':' + this.get('value')); } } }); -var FieldUrl = FieldChar.extend({ - template: 'FieldUrl', - initialize_content: function() { - this._super(); - var $button = this.$el.find('button'); - $button.click(this.on_button_clicked); - this.setupFocus($button); - }, +var FieldUrl = FieldEmail.extend({ render_value: function() { - if (!this.get("effective_readonly")) { - this._super(); - } else { + this._super(); + if(this.get("effective_readonly")) { var tmp = this.get('value'); var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp); if (!s) { tmp = "http://" + this.get('value'); } var text = this.get('value') ? this.node.attrs.text || tmp : ''; - this.$el.find('a').attr('href', tmp).text(text); - } - }, - on_button_clicked: function() { - if (!this.get('value')) { - this.do_warn(_t("Resource Error"), _t("This resource is empty")); - } else { - var url = $.trim(this.get('value')); - if(/^www\./i.test(url)) - url = 'http://'+url; - window.open(url); + this.$el.attr('href', tmp).text(text); } } }); var FieldFloat = FieldChar.extend({ - is_field_number: true, - widget_class: 'oe_form_field_float', init: function (field_manager, node) { this._super(field_manager, node); this.internal_set_value(0); @@ -408,19 +376,18 @@ var FieldFloat = FieldChar.extend({ this.digits = this.field.digits; } }, + initialize_content: function() { + this._super(); + this.$el.addClass('o_form_field_number'); + }, set_value: function(value_) { if (value_ === false || value_ === undefined) { - // As in GTK client, floats default to 0 - value_ = 0; + value_ = 0; // As in GTK client, floats default to 0 } if (this.digits !== undefined && this.digits.length === 2) { value_ = utils.round_decimals(value_, this.digits[1]); } - this._super.apply(this, [value_]); - }, - focus: function () { - var $input = this.$('input:first'); - return $input.length ? $input.select() : false; + this._super(value_); } }); @@ -474,7 +441,7 @@ var FieldCharDomain = common.AbstractField.extend(common.ReinitializeFieldMixin, } else { this.$('.o_count').text(_t('No selected record')); var $arrow = this.$('button span').detach(); - this.$('button').text(_('Select records ')).append($("<span/>").addClass('fa fa-arrow-right')); + this.$('button').text(_t('Select records ')).append($("<span/>").addClass('fa fa-arrow-right')); } }, on_click: function(event) { @@ -497,60 +464,58 @@ var FieldCharDomain = common.AbstractField.extend(common.ReinitializeFieldMixin, }, }); -var FieldDatetime = common.AbstractField.extend(common.ReinitializeFieldMixin, { - template: "FieldDatetime", +var FieldDate = common.AbstractField.extend(common.ReinitializeFieldMixin, { + tagName: "span", + className: "o_form_field_date", build_widget: function() { - return new datepicker.DateTimeWidget(this); + return new datepicker.DateWidget(this); }, - destroy_content: function() { + initialize_content: function() { if (this.datewidget) { this.datewidget.destroy(); this.datewidget = undefined; } - }, - initialize_content: function() { + if (!this.get("effective_readonly")) { this.datewidget = this.build_widget(); - this.datewidget.on('datetime_changed', this, _.bind(function() { + this.datewidget.on('datetime_changed', this, function() { this.internal_set_value(this.datewidget.get_value()); - }, this)); - this.datewidget.appendTo(this.$el); - this.setupFocus(this.datewidget.$input); + }); + + var self = this; + this.datewidget.appendTo('<div>').done(function() { + self.datewidget.$el.addClass(self.$el.attr('class')); + self.replaceElement(self.datewidget.$el); + self.datewidget.$input.addClass('o_form_input'); + self.setupFocus(self.datewidget.$input); + }); } }, render_value: function() { - if (!this.get("effective_readonly")) { - this.datewidget.set_value(this.get('value')); - } else { + if (this.get("effective_readonly")) { this.$el.text(formats.format_value(this.get('value'), this, '')); + } else { + this.datewidget.set_value(this.get('value')); } }, is_syntax_valid: function() { - if (!this.get("effective_readonly") && this.datewidget) { - return this.datewidget.is_valid(); - } - return true; + return this.get("effective_readonly") || !this.datewidget || this.datewidget.is_valid(); }, is_false: function() { return this.get('value') === '' || this._super(); }, focus: function() { - var input = this.datewidget && this.datewidget.$input[0]; - return input ? input.focus() : false; - }, - set_dimensions: function (height, width) { - this._super(height, width); if (!this.get("effective_readonly")) { - this.datewidget.$input.css('height', height); + return this.datewidget.$input.focus(); } - } + return false; + }, }); -var FieldDate = FieldDatetime.extend({ - template: "FieldDate", +var FieldDatetime = FieldDate.extend({ build_widget: function() { - return new datepicker.DateWidget(this); - } + return new datepicker.DateTimeWidget(this); + }, }); var FieldText = common.AbstractField.extend(common.ReinitializeFieldMixin, { @@ -566,46 +531,37 @@ var FieldText = common.AbstractField.extend(common.ReinitializeFieldMixin, { e.stopPropagation(); } }, - 'change textarea': 'store_dom_value', + 'change': 'store_dom_value', }, initialize_content: function() { - if (! this.get("effective_readonly")) { - this.$textarea = this.$el.find('textarea'); + if (!this.get("effective_readonly")) { this.auto_sized = false; - this.default_height = this.$textarea.css('height'); - if (this.default_height === '0px') this.default_height = '90px'; - if (this.get("effective_readonly")) { - this.$textarea.attr('disabled', 'disabled'); - } - this.setupFocus(this.$textarea); - } else { - this.$textarea = undefined; + this.setupFocus(this.$el); } }, commit_value: function () { - if (! this.get("effective_readonly") && this.$textarea) { + if (!this.get("effective_readonly")) { this.store_dom_value(); } return this._super(); }, store_dom_value: function () { - this.internal_set_value(formats.parse_value(this.$textarea.val(), this)); + this.internal_set_value(formats.parse_value(this.$el.val(), this)); }, render_value: function() { - if (! this.get("effective_readonly")) { - var show_value = formats.format_value(this.get('value'), this, ''); - this.$textarea.val(show_value); - dom_utils.autoresize(this.$textarea, {parent: this, min_height: parseInt(this.default_height)}); - } else { + if (this.get("effective_readonly")) { var txt = this.get("value") || ''; - this.$(".oe_form_text_content").text(txt); + this.$el.text(txt); + } else { + var show_value = formats.format_value(this.get('value'), this, ''); + this.$el.val(show_value); + dom_utils.autoresize(this.$el, {parent: this}); } }, is_syntax_valid: function() { - if (!this.get("effective_readonly") && this.$textarea) { + if (!this.get("effective_readonly")) { try { - formats.parse_value(this.$textarea.val(), this, ''); - return true; + formats.parse_value(this.$el.val(), this, ''); } catch(e) { return false; } @@ -616,51 +572,48 @@ var FieldText = common.AbstractField.extend(common.ReinitializeFieldMixin, { return this.get('value') === '' || this._super(); }, focus: function($el) { - var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0]; - return input ? input.focus() : false; - }, - set_dimensions: function (height, width) { - this._super(height, width); - if (!this.get("effective_readonly") && this.$textarea) { - this.$textarea.css({ - width: width, - minHeight: height - }); + if(!this.get("effective_readonly")) { + return this.$el.focus(); } + return false; + }, + set_dimensions: function(height, width) { + this.$el.css({ + width: width, + minHeight: height, + }); }, }); var FieldBoolean = common.AbstractField.extend({ template: 'FieldBoolean', + events: { + 'click': function() { + this.internal_set_value(this.$checkbox.prop('checked')); + } + }, start: function() { - var self = this; - this.$checkbox = $("input", this.$el); + this.$checkbox = this.$('input'); + + this.$checkbox.prop('disabled', this.get("effective_readonly")); + this.on("change:effective_readonly", this, function() { + this.$checkbox.prop('disabled', this.get("effective_readonly")); + }); + this.setupFocus(this.$checkbox); - this.$el.click(_.bind(function() { - this.internal_set_value(this.$checkbox.is(':checked')); - }, this)); - var check_readonly = function() { - self.$checkbox.prop('disabled', self.get("effective_readonly")); - self.click_disabled_boolean(); - }; - this.on("change:effective_readonly", this, check_readonly); - check_readonly.call(this); - this._super.apply(this, arguments); + + return this._super(); }, render_value: function() { - this.$checkbox[0].checked = this.get('value'); + this.$checkbox.prop('checked', this.get('value')); }, focus: function() { - var input = this.$checkbox && this.$checkbox[0]; - return input ? input.focus() : false; - }, - click_disabled_boolean: function(){ - var $disabled = this.$el.find('input[type=checkbox]:disabled'); - $disabled.each(function (){ - $(this).next('div').remove(); - $(this).closest("span").append($('<div class="boolean"></div>')); - }); - } + return this.$checkbox.focus(); + }, + set_dimensions: function(height, width) {}, // Checkboxes have a fixed height and width (even in list editable) + is_false: function() { + return false; + }, }); /** @@ -706,9 +659,7 @@ var FieldBooleanButton = common.AbstractField.extend({ }, }); -/** - The progressbar field expect a float from 0 to 100. -*/ +// The progressbar field expects a float from 0 to 100. var FieldProgressBar = common.AbstractField.extend(common.ReinitializeFieldMixin, { initialize_content: function() { if(this.progressbar) { @@ -734,96 +685,41 @@ var FieldProgressBar = common.AbstractField.extend(common.ReinitializeFieldMixin render_value: function() { this.progressbar.set_value(this.get('value')); }, + is_false: function() { + return false; + }, }); -/** - The PercentPie field expect a float from 0 to 100. -*/ +// The PercentPie field expects a float from 0 to 100. var FieldPercentPie = common.AbstractField.extend({ template: 'FieldPercentPie', - + start: function() { + this.$left_mask = this.$('.o_mask').first(); + this.$right_mask = this.$('.o_mask').last(); + this.$pie_value = this.$('.o_pie_value'); + return this._super(); + }, render_value: function() { - var value = this.get('value'), - formatted_value = Math.round(value || 0) + '%', - svg = this.$('svg')[0]; - - svg.innerHTML = ""; - nv.addGraph(function() { - var width = 42, height = 42; - var chart = nv.models.pieChart() - .width(width) - .height(height) - .margin({top: 0, right: 0, bottom: 0, left: 0}) - .donut(true) - .showLegend(false) - .showLabels(false) - .color(['#7C7BAD','#DDD']) - .donutRatio(0.62); - - chart.tooltip.enabled(false); - - d3.select(svg) - .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}]) - .transition() - .call(chart) - .attr('style', 'width: ' + width + 'px; height:' + height + 'px;'); - - d3.select(svg) - .append("text") - .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'}) - .style({"font-size": "10px", "font-weight": "bold"}) - .text(formatted_value); - - return chart; - }); - - } -}); + var value = this.get('value') || 0, degValue = 360*value/100; -/** - The FieldBarChart expectsa list of values (indeed) -*/ -var FieldBarChart = common.AbstractField.extend({ - template: 'FieldBarChart', + this.$right_mask.toggleClass('o_full', degValue >= 180); - render_value: function() { - var value = JSON.parse(this.get('value')); - var svg = this.$('svg')[0]; - svg.innerHTML = ""; - nv.addGraph(function() { - var width = 34, height = 34; - var chart = nv.models.discreteBarChart() - .x(function (d) { return d.tooltip; }) - .y(function (d) { return d.value; }) - .width(width) - .height(height) - .margin({top: 0, right: 0, bottom: 0, left: 0}) - .showValues(false) - .transition(350) - .showXAxis(false) - .showYAxis(false); - - chart.tooltip.enabled(false); - - d3.select(svg) - .datum([{key: 'values', values: value}]) - .transition() - .call(chart) - .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;'); - - nv.utils.windowResize(chart.update); - - return chart; - }); - + var leftDeg = 'rotate(' + ((degValue < 180)? 180 : degValue) + 'deg)'; + var rightDeg = 'rotate(' + ((degValue < 180)? degValue : 0) + 'deg)'; + this.$left_mask.css({transform: leftDeg, msTransform: leftDeg, mozTransform: leftDeg, webkitTransform: leftDeg}); + this.$right_mask.css({transform: rightDeg, msTransform: rightDeg, mozTransform: rightDeg, webkitTransform: rightDeg}); + + this.$pie_value.html(Math.round(value) + '%'); + }, + is_false: function() { + return false; } }); - var FieldSelection = common.AbstractField.extend(common.ReinitializeFieldMixin, { template: 'FieldSelection', events: { - 'change select': 'store_dom_value', + 'change': 'store_dom_value', }, init: function(field_manager, node) { this._super(field_manager, node); @@ -832,7 +728,7 @@ var FieldSelection = common.AbstractField.extend(common.ReinitializeFieldMixin, this.records_orderer = new utils.DropMisordered(); this.field_manager.on("view_content_has_changed", this, function() { var domain = new data.CompoundDomain(this.build_domain()).eval(); - if (! _.isEqual(domain, this.get("domain"))) { + if (!_.isEqual(domain, this.get("domain"))) { this.set("domain", domain); } }); @@ -869,27 +765,27 @@ var FieldSelection = common.AbstractField.extend(common.ReinitializeFieldMixin, // current row or switching to the next one) // * If the user presses [RETURN] with a select closed (side-effect: // also if the user opened the select and pressed [RETURN] without - // changing the selected value), takes the action as validating the - // row - var ischanging = false; - var $select = this.$el.find('select') - .change(function () { ischanging = true; }) - .click(function () { ischanging = false; }) - .keyup(function (e) { - if (e.which !== 13 || !ischanging) { return; } - e.stopPropagation(); - ischanging = false; - }); - this.setupFocus($select); + // changing the selected value), takes the action as validating the row + if(!this.get('effective_readonly')) { + var ischanging = false; + this.$el + .change(function () { ischanging = true; }) + .click(function () { ischanging = false; }) + .keyup(function (e) { + if (e.which !== 13 || !ischanging) { return; } + e.stopPropagation(); + ischanging = false; + }); + this.setupFocus(this.$el); + } }, commit_value: function () { this.store_dom_value(); return this._super(); }, store_dom_value: function () { - if (!this.get('effective_readonly') && this.$('select').length) { - var val = JSON.parse(this.$('select').val()); - this.internal_set_value(val); + if (!this.get('effective_readonly')) { + this.internal_set_value(JSON.parse(this.$el.val())); } }, set_value: function(value_) { @@ -901,28 +797,29 @@ var FieldSelection = common.AbstractField.extend(common.ReinitializeFieldMixin, var values = this.get("values"); values = [[false, this.node.attrs.placeholder || '']].concat(values); var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this); - if (! found) { + if (!found) { found = [this.get("value"), _t('Unknown')]; values = [found].concat(values); } - if (! this.get("effective_readonly")) { - this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values})); - this.$("select").val(JSON.stringify(found[0])); + if (!this.get("effective_readonly")) { + this.$el.empty(); + for(var i = 0 ; i < values.length ; i++) { + this.$el.append($('<option/>', { + value: JSON.stringify(values[i][0]), + html: values[i][1] + })) + } + this.$el.val(JSON.stringify(found[0])); } else { this.$el.text(found[1]); } }, focus: function() { - var input = this.$('select:first')[0]; - return input ? input.focus() : false; - }, - set_dimensions: function (height, width) { - this._super(height, width); - this.$('select').css({ - height: height, - width: width - }); - } + if (!this.get("effective_readonly")) { + return this.$el.focus(); + } + return false; + }, }); /** @@ -932,20 +829,20 @@ var FieldSelection = common.AbstractField.extend(common.ReinitializeFieldMixin, the time difference between UTC time and local time, in minutes. */ var TimezoneMismatch = FieldSelection.extend({ - initialize_content: function(){ + initialize_content: function() { this._super.apply(this, arguments); this.tz_offset_field = (this.options && this.options.tz_offset_field) || this.tz_offset_field || 'tz_offset'; this.set({"tz_offset": this.field_manager.get_field_value(this.tz_offset_field)}); this.on("change:tz_offset", this, this.render_value); }, - start: function(){ + start: function() { this._super.apply(this, arguments); // trigger a render_value when tz_offset field change this.field_manager.on("field_changed:" + this.tz_offset_field, this, function() { this.set({"tz_offset": this.field_manager.get_field_value(this.tz_offset_field)}); }); }, - check_timezone: function(){ + check_timezone: function() { var user_offset = this.get('tz_offset'); if (user_offset) { var offset = -(new Date().getTimezoneOffset()); @@ -956,17 +853,15 @@ var TimezoneMismatch = FieldSelection.extend({ } return false; }, - render_value: function(){ + render_value: function() { this._super.apply(this, arguments); + this.$label.next('.o_tz_warning').remove(); if(this.check_timezone()){ - this.$label.find('.oe_tz_warning').remove(); var options = _.extend({ delay: { show: 501, hide: 0 }, title: _t("Timezone Mismatch : The timezone of your browser doesn't match the selected one. The time in Odoo is displayed according to your field timezone."), }); - this.$label.css('white-space', 'normal'); - $(QWeb.render('WebClient.timezone_warning')).appendTo(this.$label); - this.$label.find('.oe_tz_warning').tooltip(options); + $('<span/>').addClass('fa fa-exclamation-triangle o_tz_warning').insertAfter(this.$label).tooltip(options); } } }); @@ -979,8 +874,8 @@ var LabelSelection = FieldSelection.extend({ render_value: function() { this._super.apply(this, arguments); if (this.get("effective_readonly")) { - var btn_class = this.classes[this.get('value')] || 'default'; - this.$el.wrapInner($('<span/>').addClass('label label-' + btn_class)); + var bt_class = this.classes[this.get('value')] || 'default'; + this.$el.html($('<span/>', {html: this.$el.html()}).addClass('label label-' + bt_class)); } }, }); @@ -1001,7 +896,6 @@ var FieldRadio = common.AbstractField.extend(common.ReinitializeFieldMixin, { this.uniqueId = _.uniqueId("radio"); }, initialize_content: function () { - this.on("change:effective_readonly", this, this.render_value); this.field_manager.on("view_content_has_changed", this, this.get_selection); this.get_selection(); }, @@ -1043,7 +937,7 @@ var FieldRadio = common.AbstractField.extend(common.ReinitializeFieldMixin, { def.resolve(); } return def.then(function () { - if (! _.isEqual(selection, self.selection)) { + if (!_.isEqual(selection, self.selection)) { self.selection = _.clone(selection); self.renderElement(); self.render_value(); @@ -1066,14 +960,16 @@ var FieldRadio = common.AbstractField.extend(common.ReinitializeFieldMixin, { }, render_value: function () { var self = this; - this.$el.toggleClass("oe_readonly", this.get('effective_readonly')); - this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true); - this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : ""); + if(this.get('effective_readonly')) { + this.$el.html(this.get('value')? this.get('value')[1] : ""); + } else { + this.$("input").filter(function() {return this.value == self.get_value();}).prop("checked", true); + } } }); var FieldReference = common.AbstractField.extend(common.ReinitializeFieldMixin, { - template: 'FieldReference', + className: 'o_row', init: function(field_manager, node) { this._super(field_manager, node); this.reference_ready = true; @@ -1086,9 +982,8 @@ var FieldReference = common.AbstractField.extend(common.ReinitializeFieldMixin, }, initialize_content: function() { var self = this; - var fm = new common.DefaultFieldManager(this); - this.fm = fm; - fm.extend_field_desc({ + this.fm = new common.DefaultFieldManager(this); + this.fm.extend_field_desc({ "selection": { selection: this.field_manager.get_field_desc(this.name).selection, type: "selection", @@ -1098,23 +993,23 @@ var FieldReference = common.AbstractField.extend(common.ReinitializeFieldMixin, type: "many2one", }, }); - this.selection = new FieldSelection(fm, { attrs: { + this.selection = new FieldSelection(this.fm, { attrs: { name: 'selection', modifiers: JSON.stringify({readonly: this.get('effective_readonly')}), }}); this.selection.on("change:value", this, this.on_selection_changed); - this.selection.appendTo(this.$(".oe_form_view_reference_selection")); + this.selection.appendTo(this.$el); this.selection .on('focused', null, function () {self.trigger('focused');}) .on('blurred', null, function () {self.trigger('blurred');}); var FieldMany2One = core.form_widget_registry.get('many2one'); - this.m2o = new FieldMany2One(fm, { attrs: { + this.m2o = new FieldMany2One(this.fm, { attrs: { name: 'Referenced Document', modifiers: JSON.stringify({readonly: this.get('effective_readonly')}), }}); this.m2o.on("change:value", this, this.data_changed); - this.m2o.appendTo(this.$(".oe_form_view_reference_m2o")); + this.m2o.appendTo(this.$el); this.m2o .on('focused', null, function () {self.trigger('focused');}) .on('blurred', null, function () {self.trigger('blurred');}); @@ -1151,6 +1046,9 @@ var FieldReference = common.AbstractField.extend(common.ReinitializeFieldMixin, this.m2o.do_toggle(!!this.get('value')[0]); this.reference_ready = true; }, + is_false: function() { + return !this.get_value(); + }, }); var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, { @@ -1161,7 +1059,7 @@ var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, { this.useFileAPI = !!window.FileReader; this.max_upload_size = 25 * 1024 * 1024; // 25Mo if (!this.useFileAPI) { - this.fileupload_id = _.uniqueId('oe_fileupload'); + this.fileupload_id = _.uniqueId('o_fileupload'); $(window).on(this.fileupload_id, function() { var args = [].slice.call(arguments).slice(1); self.on_file_uploaded.apply(self, args); @@ -1175,13 +1073,14 @@ var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, { this._super.apply(this, arguments); }, initialize_content: function() { - var self= this; - this.$('input.o_form_input_file').change(this.on_file_change); - this.$('button.oe_form_binary_file_save').click(this.on_save_as); - this.$('.oe_form_binary_file_clear').click(this.on_clear); - this.$('.oe_form_binary_file_edit').click(function() { - self.$('input.o_form_input_file').click(); + this.$inputFile = this.$('.o_form_input_file'); + this.$inputFile.change(this.on_file_change); + var self = this; + this.$('.o_select_file_button').click(function() { + self.$inputFile.click(); }); + this.$('.o_save_file_button').click(this.on_save_as); + this.$('.o_clear_file_button').click(this.on_clear); }, on_file_change: function(e) { var self = this; @@ -1202,26 +1101,28 @@ var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, { self.on_file_uploaded(file.size, file.name, file.type, data); }; } else { - this.$el.find('form.o_form_binary_form input[name=session_id]').val(this.session.session_id); - this.$el.find('form.o_form_binary_form').submit(); + this.$('form.o_form_binary_form input[name=session_id]').val(this.session.session_id); + this.$('form.o_form_binary_form').submit(); } - this.$el.find('.oe_form_binary_progress').show(); - this.$el.find('.oe_form_binary').hide(); + this.$('.o_form_binary_progress').show(); + this.$('button').hide(); } }, - on_file_uploaded: function(size, name, content_type, file_base64) { + on_file_uploaded: function(size, name) { if (size === false) { this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file")); // TODO: use openerp web crashmanager console.warn("Error while uploading file : ", name); } else { - this.filename = name; this.on_file_uploaded_and_valid.apply(this, arguments); } - this.$el.find('.oe_form_binary_progress').hide(); - this.$el.find('.oe_form_binary').show(); + this.$('.o_form_binary_progress').hide(); + this.$('button').show(); }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { + this.binary_value = true; + this.set_filename(name); + this.set_value(file_base64); }, on_save_as: function(ev) { var value = this.get('value'); @@ -1248,7 +1149,6 @@ var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, { 'error': c.rpc_error.bind(c) }); ev.stopPropagation(); - return false; } }, set_filename: function(value) { @@ -1262,129 +1162,95 @@ var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, { } }, on_clear: function() { - if (this.get('value') !== false) { - this.binary_value = false; - this.internal_set_value(false); - } - return false; + this.binary_value = false; + this.set_filename(''); + this.set_value(false); // FIXME do not really remove the value } }); var FieldBinaryFile = FieldBinary.extend({ template: 'FieldBinaryFile', initialize_content: function() { + var self = this; this._super(); if (this.get("effective_readonly")) { - var self = this; - this.$el.find('a').click(function(ev) { + this.$el.click(function(ev) { if (self.get('value')) { self.on_save_as(ev); } return false; }); + } else { + this.$input = this.$('.o_form_input').eq(0); + this.$input.on('click', function() { + self.$inputFile.click(); + }); } }, render_value: function() { - var show_value; - if (!this.get("effective_readonly")) { - if (this.node.attrs.filename) { - show_value = this.view.datarecord[this.node.attrs.filename] || ''; - } else { - show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : ''; + var filename = this.view.datarecord[this.node.attrs.filename]; + if (this.get("effective_readonly")) { + this.do_toggle(!!this.get('value')); + if (this.get('value')) { + this.$el.empty().append($("<span/>").addClass('fa fa-download')); + if (filename) { + this.$el.append(" " + filename); + } } - this.$el.find('input').eq(0).val(show_value); } else { - this.$el.find('a').toggle(!!this.get('value')); - if (this.get('value')) { - show_value = _t("Download"); - if (this.view) - show_value += " " + (this.view.datarecord[this.node.attrs.filename] || ''); - this.$el.find('a').text(show_value); + if(this.get('value')) { + this.$el.children().removeClass('o_hidden'); + this.$('.o_select_file_button').first().addClass('o_hidden'); + this.$input.val(filename || this.get('value')); + } else { + this.$el.children().addClass('o_hidden'); + this.$('.o_select_file_button').first().removeClass('o_hidden'); } } - }, - on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { - this.binary_value = true; - this.set_filename(name); - this.internal_set_value(file_base64); - var show_value = name + " (" + utils.human_size(size) + ")"; - this.$el.find('input').eq(0).val(show_value); - }, - on_clear: function() { - this._super.apply(this, arguments); - this.$el.find('input').eq(0).val(''); - this.set_filename(''); - }, - set_value: function(value_){ - var changed = value_ !== this.get_value(); - this._super.apply(this, arguments); - // Trigger value change if size is the same - if (!changed){ - this.trigger("change:value", this, { - oldValue: value_, - newValue: value_ - }); - } - } + } }); var FieldBinaryImage = FieldBinary.extend({ template: 'FieldBinaryImage', placeholder: "/web/static/src/img/placeholder.png", render_value: function() { - var self = this; - var url; - this.session = session; - if (this.get('value') && !utils.is_bin_size(this.get('value'))) { - url = 'data:image/png;base64,' + this.get('value'); - } else if (this.get('value')) { - var id = JSON.stringify(this.view.datarecord.id || null); - var field = this.name; - if (this.options.preview_image) - field = this.options.preview_image; - url = session.url('/web/image', { - model: this.view.dataset.model, - id: id, - field: field, - unique: (this.view.datarecord.__last_update || '').replace(/[^0-9]/g, ''), - }); - } else { - url = this.placeholder; + var url = this.placeholder; + if(this.get('value')) { + if(!utils.is_bin_size(this.get('value'))) { + url = 'data:image/png;base64,' + this.get('value'); + } else { + url = session.url('/web/image', { + model: this.view.dataset.model, + id: JSON.stringify(this.view.datarecord.id || null), + field: (this.options.preview_image)? this.options.preview_image : this.name, + unique: (this.view.datarecord.__last_update || '').replace(/[^0-9]/g, ''), + }); + } } - var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url })); - $($img).click(function(e) { + + var $img = $(QWeb.render("FieldBinaryImage-img", {widget: this, url: url})); + + var self = this; + $img.click(function(e) { if(self.view.get("actual_mode") == "view") { - var $button = $(".oe_form_button_edit"); + var $button = $(".o_form_button_edit"); $button.openerpBounce(); e.stopPropagation(); } }); - this.$el.find('> img').remove(); + this.$('> img').remove(); + if (self.options.size) { + $img.css("width", "" + self.options.size[0] + "px"); + $img.css("height", "" + self.options.size[1] + "px"); + } this.$el.prepend($img); - $img.load(function() { - if (! self.options.size) - return; - $img.css("max-width", "" + self.options.size[0] + "px"); - $img.css("max-height", "" + self.options.size[1] + "px"); - }); $img.on('error', function() { self.on_clear(); $img.attr('src', self.placeholder); self.do_warn(_t("Image"), _t("Could not display the selected image.")); }); }, - on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { - this.internal_set_value(file_base64); - this.binary_value = true; - this.render_value(); - this.set_filename(name); - }, - on_clear: function() { - this._super.apply(this, arguments); - this.render_value(); - this.set_filename(''); - }, - set_value: function(value_){ + set_value: function(value_) { var changed = value_ !== this.get_value(); this._super.apply(this, arguments); // By default, on binary images read, the server returns the binary size @@ -1397,7 +1263,16 @@ var FieldBinaryImage = FieldBinary.extend({ newValue: value_ }); } - } + }, + is_false: function() { + return false; + }, + set_dimensions: function(height, width) { + this.$el.css({ + maxWidth: width, + minHeight: height, + }); + }, }); var FieldStatus = common.AbstractField.extend({ @@ -1423,13 +1298,14 @@ var FieldStatus = common.AbstractField.extend({ }); this.get_selection(); if (this.options.clickable) { - this.$el.on('click','li[data-id]',this.on_click_stage); - } - if (this.$el.parent().is('header')) { - this.$el.after('<div class="oe_clear"/>'); + this.bind_stage_click(); } this._super(); }, + bind_stage_click: function () { + // This function is overriden in the enterprise webclient + this.$el.on('click','li[data-id]',this.on_click_stage); + }, set_value: function(value_) { if (value_ instanceof Array) { value_ = value_[0]; @@ -1468,15 +1344,15 @@ var FieldStatus = common.AbstractField.extend({ var fold_field = this.options.fold_field; var calculation = _.bind(function() { - if (this.field.type == "many2one") { - return self.get_distant_fields().then(function (fields) { + if (this.field.type === "many2one") { + return self.get_distant_fields().then(function () { return new data.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain")) .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) { var ids = _.pluck(records, 'id'); return self.dataset.name_get(ids).then(function (records_name) { _.each(records, function (record) { - var name = _.find(records_name, function (val) {return val[0] == record.id;})[1]; - if (fold_field && record[fold_field] && record.id != self.get('value')) { + var name = _.find(records_name, function (val) {return val[0] === record.id;})[1]; + if (fold_field && record[fold_field] && record.id !== self.get('value')) { selection_folded.push([record.id, name]); } else { selection_unfolded.push([record.id, name]); @@ -1492,7 +1368,7 @@ var FieldStatus = common.AbstractField.extend({ var select = this.field.selection; for(var i=0; i < select.length; i++) { var key = select[i][0]; - if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) { + if(key === this.get('value') || !this.options.visible || this.options.visible.indexOf(key) !== -1) { selection_unfolded.push(select[i]); } } @@ -1529,13 +1405,12 @@ var FieldStatus = common.AbstractField.extend({ return; } var val; - if (this.field.type == "many2one") { + if (this.field.type === "many2one") { val = parseInt($li.data("id"), 10); - } - else { + } else { val = $li.data("id"); } - if (val != self.get('value')) { + if (val !== self.get('value')) { this.view.recursive_save().done(function() { var change = {}; change[self.name] = val; @@ -1548,8 +1423,7 @@ var FieldStatus = common.AbstractField.extend({ }); var FieldMonetary = FieldFloat.extend({ - template: "FieldMonetary", - widget_class: 'oe_form_field_float oe_form_field_monetary', + template: 'FieldMonetary', init: function() { this._super.apply(this, arguments); this.set({"currency": false}); @@ -1567,6 +1441,23 @@ var FieldMonetary = FieldFloat.extend({ this.on("change:currency_info", this, this.update); return tmp; }, + initialize_content: function() { + if(!this.get('effective_readonly')) { + this.$input = this.$('input'); + this.add_symbol(); + } else { + this.$input = undefined; + } + + this._super(); + }, + add_symbol: function() { + var currency = this.get('currency_info'); + if(currency) { + var before = (currency.position === 'before'); + this.$el[(before)? 'prepend' : 'append']($('<span/>', {html: currency.symbol})); + } + }, get_currency_info: function() { var self = this; if (this.get("currency") === false) { @@ -1576,15 +1467,21 @@ var FieldMonetary = FieldFloat.extend({ return self.set({"currency_info": session.get_currency(self.get("currency"))}); }, update: function() { - if (this.view.options.is_list_editable){ + if (this.view.options.is_list_editable) { return; } else { return this.reinitialize(); } }, + render_value: function() { + this._super(); + if(this.get('effective_readonly')) { + this.add_symbol(); + } + }, get_digits_precision: function() { return this.node.attrs.digits || this.field.digits || (this.get('currency_info') && this.get('currency_info').digits); - }, + }, parse_value: function(val, def) { return formats.parse_value(val, {type: "float", digits: this.get_digits_precision()}, def); }, @@ -1625,7 +1522,6 @@ var StatInfo = common.AbstractField.extend({ this.$el.html(QWeb.render("StatInfo", options)); this.$el.addClass('o_stat_info'); }, - }); /** @@ -1633,14 +1529,18 @@ var StatInfo = common.AbstractField.extend({ switching between a green bullet / gray bullet. */ var FieldToggleBoolean = common.AbstractField.extend({ + template: "toggle_button", + events: { + 'click': 'set_toggle_button' + }, render_value: function () { - this.icon = this.get_value() ? 'gtk-yes.png' : 'gtk-normal.png'; - this.$el.html(QWeb.render("FieldToggleBoolean", {'widget': this})); - this.$('.oe_toggle_button').on('click', this.set_toggle_button.bind(this)); + var $img = this.$('img'); + var src = $img.attr('src'); + $img.attr('src', src.substr(0, src.lastIndexOf('/')+1) + (this.get_value() ? 'gtk-yes.png' : 'gtk-normal.png')); }, set_toggle_button: function () { var self = this; - var toggle_value = this.get_value() === false ? true: false; + var toggle_value = !this.get_value(); if (this.view.get('actual_mode') == 'view') { var rec_values = {}; rec_values[self.node.attrs.name] = toggle_value; @@ -1658,91 +1558,8 @@ var FieldToggleBoolean = common.AbstractField.extend({ reload_record: function () { this.view.reload(); }, -}); - -/** - This widget is intended to be used in config settings. - When checked, an upgrade popup is showed to the user. -*/ - -var AbstractFieldUpgrade = { - events: { - 'click input': 'on_click_input', - }, - - start: function() { - this._super.apply(this, arguments); - - this.get_enterprise_label().after($("<span>", { - text: "Enterprise", - 'class': "label label-primary oe_inline" - })); - }, - - open_dialog: function() { - var message = $(QWeb.render('EnterpriseUpgrade')); - - var buttons = [ - { - text: _t("Upgrade now"), - classes: 'btn-primary', - close: true, - click: this.confirm_upgrade, - }, - { - text: _t("Cancel"), - close: true, - }, - ]; - - return new Dialog(this, { - size: 'medium', - buttons: buttons, - $content: $('<div>', { - html: message, - }), - title: _t("Odoo Enterprise"), - }).open(); - }, - - confirm_upgrade: function() { - new Model("res.users").call("search_count", [[["share", "=", false]]]).then(function(data) { - framework.redirect("https://www.odoo.com/odoo-enterprise/upgrade?num_users=" + data); - }); - }, - - get_enterprise_label: function() {}, - on_click_input: function() {}, -}; - -var UpgradeBoolean = FieldBoolean.extend(AbstractFieldUpgrade, { - template: "FieldUpgradeBoolean", - - get_enterprise_label: function() { - return this.$label; - }, - - on_click_input: function(event) { - if(this.$checkbox.prop("checked")) { - this.open_dialog().on('closed', this, function() { - this.$checkbox.prop("checked", false); - }); - } - }, -}); - -var UpgradeRadio = FieldRadio.extend(AbstractFieldUpgrade, { - - get_enterprise_label: function() { - return this.$('label').last(); - }, - - on_click_input: function(event) { - if($(event.target).val() == 1) { - this.open_dialog().on('closed', this, function() { - this.$('input').first().prop("checked", true); - }); - } + is_false: function() { + return false; }, }); @@ -1770,7 +1587,6 @@ core.form_widget_registry .add('toggle_button', FieldToggleBoolean) .add('float', FieldFloat) .add('percentpie', FieldPercentPie) - .add('barchart', FieldBarChart) .add('integer', FieldFloat) .add('float_time', FieldFloat) .add('progressbar', FieldProgressBar) @@ -1782,9 +1598,7 @@ core.form_widget_registry .add('kanban_state_selection', KanbanSelection) .add('statinfo', StatInfo) .add('timezone_mismatch', TimezoneMismatch) - .add('label_selection', LabelSelection) - .add('upgrade_boolean', UpgradeBoolean) - .add('upgrade_radio', UpgradeRadio); + .add('label_selection', LabelSelection); /** @@ -1796,8 +1610,12 @@ core.form_widget_registry core.form_tag_registry.add('button', WidgetButton); return { + FieldBoolean: FieldBoolean, FieldChar: FieldChar, + FieldEmail: FieldEmail, FieldFloat: FieldFloat, + FieldRadio: FieldRadio, + FieldStatus: FieldStatus, FieldMonetary: FieldMonetary, WidgetButton: WidgetButton }; diff --git a/addons/web/static/src/js/views/graph_view.js b/addons/web/static/src/js/views/graph_view.js index 62ee1e62c23..c8ff16cd0f1 100644 --- a/addons/web/static/src/js/views/graph_view.js +++ b/addons/web/static/src/js/views/graph_view.js @@ -14,7 +14,7 @@ var _t = core._t; var QWeb = core.qweb; var GraphView = View.extend({ - className: 'oe_graph', + className: 'o_graph', display_name: _lt('Graph'), icon: 'fa-bar-chart', require_fields: true, @@ -54,12 +54,14 @@ var GraphView = View.extend({ if ($node) { var context = {measures: _.pairs(_.omit(this.measures, '__count__'))}; this.$buttons = $(QWeb.render('GraphView.buttons', context)); - this.$measure_list = this.$buttons.find('.oe-measure-list'); + this.$measure_list = this.$buttons.find('.o_graph_measures_list'); this.update_measure(); this.$buttons.find('button').tooltip(); this.$buttons.click(this.on_button_click.bind(this)); - this.$buttons.appendTo($node); + this.$buttons.find('.o_graph_button[data-mode="' + this.widget.mode + '"]').addClass('active'); + + this.$buttons.appendTo($node); } }, update_measure: function () { @@ -86,7 +88,7 @@ var GraphView = View.extend({ }, do_search: function (domain, context, group_by) { if (!this.widget) { - this.initial_groupbys = context.graph_groupbys || this.initial_groupbys; + this.initial_groupbys = context.graph_groupbys || (group_by.length ? group_by : this.initial_groupbys); this.widget = new GraphWidget(this, this.model, { measure: context.graph_measure || this.active_measure, mode: context.graph_mode || this.active_mode, @@ -112,14 +114,16 @@ var GraphView = View.extend({ }, on_button_click: function (event) { var $target = $(event.target); - if ($target.hasClass('oe-bar-mode')) {this.widget.set_mode('bar');} - if ($target.hasClass('oe-line-mode')) {this.widget.set_mode('line');} - if ($target.hasClass('oe-pie-mode')) {this.widget.set_mode('pie');} - if ($target.parents('.oe-measure-list').length) { - var parent = $target.parent(), - field = parent.data('field'); + if ($target.hasClass('o_graph_button')) { + this.widget.set_mode($target.data('mode')); + this.$buttons.find('.o_graph_button.active').removeClass('active'); + $target.addClass('active'); + } + else if ($target.parents('.o_graph_measures_list').length) { + var parent = $target.parent(); + var field = parent.data('field'); this.active_measure = field; - parent.toggleClass('selected'); + event.preventDefault(); event.stopPropagation(); this.update_measure(); this.widget.set_measure(this.active_measure); diff --git a/addons/web/static/src/js/views/graph_widget.js b/addons/web/static/src/js/views/graph_widget.js index a86ea35e449..e3786408125 100644 --- a/addons/web/static/src/js/views/graph_widget.js +++ b/addons/web/static/src/js/views/graph_widget.js @@ -14,6 +14,7 @@ var QWeb = core.qweb; var MAX_LEGEND_LENGTH = 25 * (1 + config.device.size_class); return Widget.extend({ + className: "o_graph_svg_container", init: function (parent, model, options) { this._super(parent); this.context = options.context; diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index d42d34a98d2..9214eb27152 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -8,6 +8,7 @@ var DataExport = require('web.DataExport'); var formats = require('web.formats'); var common = require('web.list_common'); var Model = require('web.DataModel'); +var Pager = require('web.Pager'); var pyeval = require('web.pyeval'); var session = require('web.session'); var Sidebar = require('web.Sidebar'); @@ -34,8 +35,7 @@ var row_decoration = [ var ListView = View.extend({ _template: 'ListView', - accesskey: 'L', - display_name: _lt('List'), + accesskey: "L", defaults: _.extend({}, View.prototype.defaults, { // records can be selected one by one selectable: true, @@ -54,8 +54,14 @@ var ListView = View.extend({ //whether the editable property of the view has to be disabled disable_editable_mode: false, }), + display_name: _lt('List'), events: { - 'click thead th.oe_sortable[data-id]': 'sort_by_column' + 'click thead th.o_column_sortable[data-id]': 'sort_by_column', + 'click .oe_view_nocontent': function() { + if (this.$buttons) { + this.$buttons.width(this.$buttons.width() + 1).openerpBounce(); + } + }, }, icon: 'fa-list-ul', @@ -92,8 +98,7 @@ var ListView = View.extend({ }); this.previous_colspan = null; - this.colors = null; - this.fonts = null; + this.decoration = null; this.columns = []; @@ -111,7 +116,6 @@ var ListView = View.extend({ this.groups.datagroup.sort = this.dataset._sort; } - this.page = 0; this.records.bind('change', function (event, record, key) { if (!_(self.aggregate_columns).chain() .pluck('name').contains(key).value()) { @@ -127,6 +131,7 @@ var ListView = View.extend({ this.options.$pager = false; } + this.options.deletable = this.options.deletable && this.is_action_enabled('delete'); this.name = "" + this.fields_view.arch.attrs.string; // the view's number of records per page (|| section) @@ -135,14 +140,26 @@ var ListView = View.extend({ (this.getParent().action || {}).limit || parseInt(this.fields_view.arch.attrs.limit, 10) || 80); + // the index of the first displayed record (starting from 1) + this.current_min = 1; - //Sort - var default_order = this.fields_view.arch.attrs.default_order, - unsorted = !this.dataset._sort.length; + // Sort + var default_order = this.fields_view.arch.attrs.default_order; + var unsorted = !this.dataset._sort.length; if (unsorted && default_order && !this.grouped) { this.dataset.set_sort(default_order.split(',')); } }, + willStart: function() { + // Retrieve the decoration defined on the model's list view + this.decoration = _.pick(this.fields_view.arch.attrs, function(value, key) { + return row_decoration.indexOf(key) >= 0; + }); + this.decoration = _.mapObject(this.decoration, function(value) { + return py.parse(py.tokenize(value)); + }); + return this._super(); + }, /** * Set a custom Group construct as the root of the List View. * @@ -171,43 +188,6 @@ var ListView = View.extend({ } }); }, - willStart: function() { - // Retrieve the decoration defined on the model's list view - this.decoration = _.pick(this.fields_view.arch.attrs, function(value, key) { - return row_decoration.indexOf(key) >= 0; - }); - this.decoration = _.mapObject(this.decoration, function(value) { - return py.parse(py.tokenize(value)); - }); - - this.setup_columns(this.fields_view.fields, this.grouped); - - return this._super(); - }, - start: function() { - var self = this; - this.$el.html(QWeb.render(this._template, this)); - this.$el.addClass(this.fields_view.arch.attrs['class']); - this.$el.addClass('oe_list o_list_view'); - - // Selecting records - this.$('.oe_list_record_selector').click(function() { - var checked = self.$('.oe_list_record_selector').prop('checked'); - self.$('.oe_list_record_selector input').prop('checked', checked || false); - var selection = self.groups.get_selection(); - $(self.groups).trigger('selected', [selection.ids, selection.records]); - }); - - if(this.dataset._sort.length){ - if(this.dataset._sort[0].indexOf('-') === -1){ - this.$el.find('th[data-id=' + this.dataset._sort[0] + ']').addClass("sortdown"); - }else { - this.$el.find('th[data-id=' + this.dataset._sort[0].split('-')[1] + ']').addClass("sortup"); - } - } - this.trigger('list_view_loaded', data, this.grouped); - return this._super(); - }, /** * Computes and returns the classnames for the provided record (from the * ``@decoration`` attribute) @@ -231,24 +211,58 @@ var ListView = View.extend({ return classnames; }, /** + * Renders the table itself and inserts its content + * Computes and displays the aggregates + * Hooks up the selection of records event + * Adds adequate classname on the sorted column + */ + load_list: function() { + var self = this; + + // Render the table and append its content + this.$el.html(QWeb.render(this._template, this)); + this.$el.addClass(this.fields_view.arch.attrs['class']); + if (this.grouped) { + this.$('.o_list_view').addClass('o_list_view_grouped'); + } + this.$('.o_list_view').append(this.groups.elements); + + // Compute the aggregates and display them in the list's footer + this.compute_aggregates(); + + // Head hook + // Selecting records + this.$('thead .o_list_record_selector input').click(function() { + self.$('tbody .o_list_record_selector input').prop('checked', $(this).prop('checked') || false); + var selection = self.groups.get_selection(); + $(self.groups).trigger('selected', [selection.ids, selection.records]); + }); + + // Sort + if (this.dataset._sort.length) { + if (this.dataset._sort[0].indexOf('-') === -1) { + this.$('th[data-id=' + this.dataset._sort[0] + ']').addClass("o-sort-down"); + } else { + this.$('th[data-id=' + this.dataset._sort[0].split('-')[1] + ']').addClass("o-sort-up"); + } + } + + this.trigger('list_view_loaded', data, this.grouped); + return $.when(); + }, + /** * Render the buttons according to the ListView.buttons template and * add listeners on it. * Set this.$buttons with the produced jQuery element * @param {jQuery} [$node] a jQuery node where the rendered buttons should be inserted * $node may be undefined, in which case the ListView inserts them into this.options.$buttons - * or into a div of its template + * if it exists */ render_buttons: function($node) { if (!this.$buttons) { this.$buttons = $(QWeb.render("ListView.buttons", {'widget': this})); - this.$buttons.find('.o_list_button_add').click(this.proxy('do_add_record')); - - if ($node) { - this.$buttons.appendTo($node); - } else { - this.$('.oe_list_buttons').replaceWith(this.$buttons); - } + this.$buttons.appendTo($node); } }, /** @@ -271,7 +285,7 @@ var ListView = View.extend({ this.is_action_enabled('delete') && { label: _t('Delete'), callback: this.do_delete_selected } ])); - $node = $node || this.options.$sidebar || this.$('.oe_list_sidebar'); + $node = $node || this.options.$sidebar; this.sidebar.appendTo($node); // Hide the sidebar by default (it will be shown as soon as a record is selected) @@ -279,72 +293,52 @@ var ListView = View.extend({ } }, /** - * Render the pager according to the ListView.pager template and add listeners on it. - * Set this.$pager with the produced jQuery element - * @param {jQuery} [$node] a jQuery node where the rendered pager should be inserted + * Instantiate and render the pager and add listeners on it. + * Set this.pager + * @param {jQuery} [$node] a jQuery node where the pager should be inserted * $node may be undefined, in which case the ListView inserts the pager into this.options.$pager - * or into a div of its template */ - render_pager: function($node) { - if (!this.$pager && this.options.pager) { - var self = this; - - this.$pager = $(QWeb.render("ListView.pager", {'widget': self})); - this.$pager - .on('click', 'a[data-pager-action]', function () { - var $this = $(this); - var max_page_index = Math.ceil(self.dataset.size() / self._limit) - 1; - switch ($this.data('pager-action')) { - case 'first': - self.page = 0; - break; - case 'last': - self.page = max_page_index; - break; - case 'next': - self.page += 1; - break; - case 'previous': - self.page -= 1; - break; + render_pager: function($node, options) { + if (!this.pager && this.options.pager) { + this.pager = new Pager(this, this.dataset.size(), 1, this._limit, options); + this.pager.appendTo($node || this.options.$pager); + + this.pager.on('pager_changed', this, function (new_state) { + var self = this; + var limit_changed = (this._limit !== new_state.limit); + + this._limit = new_state.limit; + this.current_min = new_state.current_min; + this.reload_content().then(function() { + // Reset the scroll position to the top on page changed only + if (!limit_changed) { + self.set_scrollTop(0); + self.trigger_up('scrollTo', {offset: 0}); } - if (self.page < 0) { - self.page = max_page_index; - } else if (self.page > max_page_index) { - self.page = 0; - } - self.reload_content(); - }).find('.oe_list_pager_state') - .click(function (e) { - e.stopPropagation(); - var $this = $(this); - - var $select = $('<select>') - .appendTo($this.empty()) - .click(function (e) {e.stopPropagation();}) - .append('<option value="80">80</option>' + - '<option value="200">200</option>' + - '<option value="500">500</option>' + - '<option value="2000">2000</option>' + - '<option value="NaN">' + _t("Unlimited") + '</option>') - .change(function () { - var val = parseInt($select.val(), 10); - self._limit = (isNaN(val) ? null : val); - self.page = 0; - self.reload_content(); - }).blur(function() { - $(this).trigger('change'); - }) - .val(self._limit || 'NaN'); - }); - this.configure_pager(this.dataset); - - $node = $node || this.options.$pager; - if ($node) { - this.$pager.appendTo($node); - } else { - this.$('.oe_list_pager').replaceWith(this.$pager); + }); + }); + } + }, + /** + * Updates the pager based on the provided dataset's information + * + * Horrifying side-effect: sets the dataset's data on this.dataset? + * + * @param {instance.web.DataSet} [dataset] + * @param {int} [current_min] the min pager value + */ + update_pager: function (dataset, current_min) { + this.dataset.ids = dataset.ids; + // Not exactly clean + if (dataset._length !== undefined) { + this.dataset._length = dataset._length; + } + if (this.pager && !this.grouped) { + var new_state = { size: this.dataset.size(), limit: this._limit }; + if (current_min) { + new_state.current_min = current_min; } + this.pager.update_state(new_state); } }, sort_by_column: function (e) { @@ -357,54 +351,14 @@ var ListView = View.extend({ return false; } this.dataset.sort(col_name); - if($column.hasClass("sortdown") || $column.hasClass("sortup")) { - $column.toggleClass("sortup sortdown"); + if($column.hasClass("o-sort-down") || $column.hasClass("o-sort-up")) { + $column.toggleClass("o-sort-up o-sort-down"); } else { - $column.addClass("sortdown"); - } - $column.siblings('.oe_sortable').removeClass("sortup sortdown"); - - this.reload_content(); - }, - /** - * Configures the ListView pager based on the provided dataset's information - * - * Horrifying side-effect: sets the dataset's data on this.dataset? - * - * @param {instance.web.DataSet} dataset - */ - configure_pager: function (dataset) { - this.dataset.ids = dataset.ids; - // Not exactly clean - if (dataset._length) { - this.dataset._length = dataset._length; + $column.addClass("o-sort-down"); } - if (this.$pager) { - if (this.grouped) { - // page count is irrelevant on grouped page, replace by limit - this.$pager.find('.oe-pager-buttons').hide(); - this.$pager.find('.oe_list_pager_state').text(this._limit || '∞'); - } else { - var total = dataset.size(); - var limit = this._limit || total; - this.$pager.find('.oe-pager-buttons').toggle(total > limit); - this.$pager.find('.oe_pager_value').toggle(total !== 0); - var spager = '-'; - if (total) { - var range_start = this.page * limit + 1; - var range_stop = range_start - 1 + limit; - if (this.records.length) { - range_stop = range_start - 1 + this.records.length; - } - if (range_stop > total) { - range_stop = total; - } - spager = _.str.sprintf(_t("%d-%d of %d"), range_start, range_stop, total); - } + $column.siblings('.o_column_sortable').removeClass("o-sort-up o-sort-down"); - this.$pager.find('.oe_list_pager_state').text(spager); - } - } + return this.reload_content(); }, /** * Sets up the listview's columns: merges view and fields data, move @@ -422,8 +376,7 @@ var ListView = View.extend({ return for_(id, fields[id], field); })); if (grouped) { - this.columns.unshift( - new ListView.MetaColumn('_group', _t("Group"))); + this.columns.unshift(new ListView.MetaColumn('_group')); } this.visible_columns = _.filter(this.columns, function (column) { @@ -468,27 +421,27 @@ var ListView = View.extend({ */ reload_content: synchronized(function () { var self = this; - self.$el.find('.oe_list_record_selector').prop('checked', false); + this.setup_columns(this.fields_view.fields, this.grouped); + this.$('tbody .o_list_record_selector input').prop('checked', false); this.records.reset(); var reloaded = $.Deferred(); - reloaded.then(function () { - self.configure_pager(self.dataset); - }); - this.$el.find('.oe_list_content').append( - this.groups.render(function () { - if (self.dataset.index === null) { - if (self.records.length) { - self.dataset.index = 0; - } - } else if (self.dataset.index >= self.records.length) { - self.dataset.index = self.records.length ? 0 : null; + this.groups.render(function () { + if (self.dataset.index === null) { + if (self.records.length) { + self.dataset.index = 0; + } + } else if (self.dataset.index >= self.records.length) { + self.dataset.index = self.records.length ? 0 : null; + } + self.load_list().then(function () { + if (!self.grouped && self.display_nocontent_helper()) { + self.no_result(); } - - self.compute_aggregates(); reloaded.resolve(); - })); + }); + }); this.do_push_state({ - page: this.page, + min: this.current_min, limit: this._limit }); return reloaded.promise(); @@ -531,8 +484,8 @@ var ListView = View.extend({ do_load_state: function(state, warm) { var reload = false; - if (state.page && this.page !== state.page) { - this.page = state.page; + if (state.min && this.current_min !== state.min) { + this.current_min = state.min; reload = true; } if (state.limit) { @@ -558,7 +511,7 @@ var ListView = View.extend({ * @param {Object} results results of evaluating domain and process for a search */ do_search: function (domain, context, group_by) { - this.page = 0; + this.current_min = 1; this.groups.datagroup = new DataGroup( this, this.model, domain, context, group_by); this.groups.datagroup.sort = this.dataset._sort; @@ -569,6 +522,10 @@ var ListView = View.extend({ this.no_leaf = !!context['group_by_no_leaf']; this.grouped = !!group_by; + // Hide the pager in grouped mode + if (this.pager && this.grouped) { + this.pager.do_hide(); + } return this.reload_content(); }, /** @@ -581,26 +538,25 @@ var ListView = View.extend({ return; } var self = this; + return $.when(this.dataset.unlink(ids)).done(function () { _(ids).each(function (id) { self.records.remove(self.records.get(id)); }); // Hide the table if there is no more record in the dataset - if (self.dataset.size() === 0) { + if (self.display_nocontent_helper()) { self.no_result(); } else { + // Load previous page if the current one is empty if (self.records.length === 0 && self.dataset.size() > 0) { - //Trigger previous manually to navigate to previous page, - //If all records are deleted on current page. - self.$pager.find('ul li:first a').trigger('click'); - } else if (self.dataset.size() === self._limit) { - //Reload listview to update current page with next page records - //because pager going to be hidden if dataset.size == limit + self.pager.previous(); + } + // Reload the list view if we are not on the last page + if (self.current_min + self._limit - 1 < self.dataset.size()) { self.reload(); - } else { - self.configure_pager(self.dataset); } } + self.update_pager(self.dataset); self.compute_aggregates(); }); }, @@ -613,7 +569,7 @@ var ListView = View.extend({ do_select: function (ids, records, deselected) { // uncheck header hook if at least one row has been deselected if (deselected) { - this.$('.oe_list_record_selector').prop('checked', false); + this.$('thead .o_list_record_selector input').prop('checked', false); } if (!ids.length) { @@ -810,13 +766,14 @@ var ListView = View.extend({ this.display_aggregates(aggregates); }, display_aggregates: function (aggregation) { - var $footer_cells = this.$el.find('.oe_list_footer'); + var $footer_cells = this.$('tfoot td'); _(this.aggregate_columns).each(function (column) { if (!column['function']) { return; } $footer_cells.filter(_.str.sprintf('[data-field=%s]', column.id)) + .addClass('o_list_number') .html(column.format(aggregation, { process_modifiers: false })); }); }, @@ -831,7 +788,7 @@ var ListView = View.extend({ */ get_active_domain: function () { var self = this; - if (this.$('.oe_list_record_selector').prop('checked')) { + if (this.$('thead .o_list_record_selector input').prop('checked')) { var search_view = this.getParent().searchview; var search_data = search_view.build_search_data(); return pyeval.eval_domains_and_contexts({ @@ -859,7 +816,7 @@ var ListView = View.extend({ pad_columns: function (count, options) { options = options || {}; // padding for action/pager header - var $first_header = this.$el.find('thead tr:first th'); + var $first_header = this.$('thead tr:first th'); var colspan = $first_header.attr('colspan'); if (colspan) { if (!this.previous_colspan) { @@ -882,7 +839,7 @@ var ListView = View.extend({ * Removes all padding columns of the table */ unpad_columns: function () { - this.$el.find('.oe_list_padding').remove(); + this.$('.oe_list_padding').remove(); if (this.previous_colspan) { this.$el .find('thead tr:first th') @@ -890,23 +847,20 @@ var ListView = View.extend({ this.previous_colspan = null; } }, + display_nocontent_helper: function () { + return (this.dataset.size() === 0); + }, no_result: function () { - this.$el.find('.oe_view_nocontent').remove(); - if (this.groups.group_by - || !this.options.action - || !this.options.action.help) { + this.$('.oe_view_nocontent').remove(); + if (this.groups.group_by || + !this.options.action || + !this.options.action.help) { return; } - this.$el.find('table:first').hide(); + this.$('table:first').hide(); this.$el.prepend( $('<div class="oe_view_nocontent">').html(this.options.action.help) ); - if (this.$buttons) { - var $buttons = this.$buttons; - this.$el.find('.oe_view_nocontent').click(function() { - $buttons.width($buttons.width() + 1).openerpBounce(); - }); - } } }); core.view_registry.add('list', ListView); @@ -975,9 +929,8 @@ ListView.List = Class.extend({ $row = self.$current.children( '[data-id=' + record.get('id') + ']'); } - var $newRow = $(self.render_record(record)); - $newRow.find('.oe_list_record_selector input').prop('checked', !!$row.find('.oe_list_record_selector input').prop('checked')); + $newRow.find('.o_list_record_selector input').prop('checked', !!$row.find('.o_list_record_selector input').prop('checked')); $row.replaceWith($newRow); }, 'add': function (ev, records, record, index) { @@ -1012,14 +965,14 @@ ListView.List = Class.extend({ */ e.preventDefault(); }) - .delegate('th.oe_list_record_selector', 'click', function (e) { + .delegate('td.o_list_record_selector', 'click', function (e) { e.stopPropagation(); var selection = self.get_selection(); var checked = $(e.currentTarget).find('input').prop('checked'); $(self).trigger( 'selected', [selection.ids, selection.records, ! checked]); }) - .delegate('td.oe_list_record_delete', 'click', function (e) { + .delegate('td.o_list_record_delete', 'click', function (e) { e.stopPropagation(); var $row = $(e.target).closest('tr'); $(self).trigger('deleted', [[self.row_id($row)]]); @@ -1028,7 +981,7 @@ ListView.List = Class.extend({ $('<input />').appendTo('body').focus().remove(); } }) - .delegate('td.oe_list_field_cell button', 'click', function (e) { + .delegate('td button', 'click', function (e) { e.stopPropagation(); var $target = $(e.currentTarget), field = $target.closest('td').data('field'), @@ -1158,7 +1111,7 @@ ListView.List = Class.extend({ } var cells = []; if (this.options.selectable) { - cells.push('<th class="oe_list_record_selector"></td>'); + cells.push('<td class="o_list_record_selector"></td>'); } _(this.columns).each(function(column) { if (column.invisible === '1') { @@ -1167,7 +1120,7 @@ ListView.List = Class.extend({ cells.push('<td title="' + column.string + '"> </td>'); }); if (this.options.deletable) { - cells.push('<td class="oe_list_record_delete"></td>'); + cells.push('<td class="o_list_record_delete"></td>'); } cells.unshift('<tr>'); cells.push('</tr>'); @@ -1187,7 +1140,7 @@ ListView.List = Class.extend({ return result; } var records = this.records; - this.$current.find('th.oe_list_record_selector input:checked') + this.$current.find('td.o_list_record_selector input:checked') .closest('tr').each(function () { var record = records.get($(this).data('id')); result.ids.push(record.get('id')); @@ -1206,14 +1159,12 @@ ListView.List = Class.extend({ return $(row).data('id'); }, /** - * Death signal, cleans up list display + * Death signal, cleans up records's callbacks */ on_records_reset: function () { _(this.record_callbacks).each(function (callback, event) { this.records.unbind(event, callback); }, this); - if (!this.$current) { return; } - this.$current.remove(); }, get_records: function () { return this.records.map(function (record) { @@ -1279,7 +1230,7 @@ ListView.Groups = Class.extend({ this.$row = null; this.children = {}; - this.page = 0; + this.pager = null; // group pager var self = this; this.records.bind('reset', function () { @@ -1315,51 +1266,46 @@ ListView.Groups = Class.extend({ } return red_letter_tboday; }, - make_paginator: function () { + /** + * Renders the group pager and append it to the group's header + */ + render_group_pager: function () { var self = this; - var $prev = $('<button type="button" data-pager-action="previous"><</button>') - .click(function (e) { - e.stopPropagation(); - if (self.page > 0) { - self.page -= 1; - } else { - self.page = self.max_page_index; - } - - self.$row.closest('tbody').next() - .replaceWith(self.render()); + if (this.datagroup.length > this.view._limit) { + this.pager = new Pager(this, this.datagroup.length, 1, this.view._limit); + this.pager.on('pager_changed', this, function (state) { + self.view._limit = state.limit; + self.current_min = state.current_min; + self.render().then(function() { + self.$row.closest('tbody').next().replaceWith($(self.elements)); + }); }); - var $next = $('<button type="button" data-pager-action="next">></button>') - .click(function (e) { - e.stopPropagation(); - if (self.page < self.max_page_index) { - self.page += 1; - } else { - self.page = 0; - } - self.$row.closest('tbody').next() - .replaceWith(self.render()); + var $last_cell = this.$row.children().last(); + $last_cell.addClass('o_group_pager'); + // Prevent group to fold when clicking on the pager + $last_cell.click(function(e) { + e.stopPropagation(); }); - this.$row.children().last() - .addClass('oe_list_group_pagination') - .append($prev) - .append('<span class="oe_list_pager_state"></span>') - .append($next); + this.pager.appendTo($last_cell); + } }, open: function (point_insertion) { - this.render().insertAfter(point_insertion); + this.render(); + $(this.elements).insertAfter(point_insertion); var no_subgroups = _(this.datagroup.group_by).isEmpty(), records_terminated = !this.datagroup.context['group_by_no_leaf']; if (no_subgroups && records_terminated) { - this.make_paginator(); + this.render_group_pager(); } }, close: function () { - this.$row.children().last().find('button').remove(); - this.$row.children().last().find('span').remove(); + if (this.pager) { + this.pager.destroy(); + } this.records.reset(); + this.$to_be_removed.remove(); }, /** * Prefixes ``$node`` with floated spaces in order to indent it relative @@ -1391,20 +1337,20 @@ ListView.Groups = Class.extend({ self.bind_child_events(child); child.datagroup = group; - var $row = child.$row = $('<tr class="oe_group_header">'); + var $row = child.$row = $('<tr class="o_group_header">'); if (group.openable && group.length) { $row.click(function (e) { if (!$row.data('open')) { $row.data('open', true) - .find('span.ui-icon') - .removeClass('ui-icon-triangle-1-e') - .addClass('ui-icon-triangle-1-s'); + .find('span.fa') + .removeClass('fa-caret-right') + .addClass('fa-caret-down'); child.open(self.point_insertion(e.currentTarget)); } else { $row.removeData('open') - .find('span.ui-icon') - .removeClass('ui-icon-triangle-1-s') - .addClass('ui-icon-triangle-1-e'); + .find('span.fa') + .removeClass('fa-caret-down') + .addClass('fa-caret-right'); child.close(); // force recompute the selection as closing group reset properties var selection = self.get_selection(); @@ -1414,7 +1360,7 @@ ListView.Groups = Class.extend({ } placeholder.appendChild($row[0]); - var $group_column = $('<th class="oe_list_group_name">').appendTo($row); + var $group_column = $('<th class="o_group_name">').appendTo($row); // Don't fill this if group_by_no_leaf but no group_by if (group.grouped_on) { var row_data = {}; @@ -1449,12 +1395,9 @@ ListView.Groups = Class.extend({ if (group.length && group.openable) { // Make openable if not terminal group & group_by_no_leaf - $group_column.prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">'); + $group_column.prepend('<span class="fa fa-caret-right" style="padding-right: 5px;">'); } else { - // Kinda-ugly hack: jquery-ui has no "empty" icon, so set - // wonky background position to ensure nothing is displayed - // there but the rest of the behavior is ui-icon's - $group_column.prepend('<span class="ui-icon" style="float: left; background-position: 150px 150px">'); + $group_column.prepend('<span class="fa">'); } } self.indent($group_column, group.level); @@ -1511,13 +1454,11 @@ ListView.Groups = Class.extend({ }); this.bind_child_events(list); - var view = this.view, - limit = view._limit, - page = this.datagroup.openable ? this.page : view.page; + var view = this.view; + var current_min = this.datagroup.openable ? this.current_min : view.current_min; var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field";}), 'name'); - var options = { offset: page * limit, limit: limit, context: {bin_size: true} }; - //TODO xmo: investigate why we need to put the setTimeout + var options = { offset: current_min - 1, limit: view._limit, context: {bin_size: true} }; return $.async_when().then(function() { return dataset.read_slice(fields, options).then(function (records) { // FIXME: ignominious hacks, parents (aka form view) should not send two ListView#reload_content concurrently @@ -1525,34 +1466,12 @@ ListView.Groups = Class.extend({ self.records.reset(null, {silent: true}); } if (!self.datagroup.openable) { - view.configure_pager(dataset); - } else { - if (dataset.size() == records.length) { - // only one page - self.$row.find('td.oe_list_group_pagination').find('button').remove(); - self.$row.find('td.oe_list_group_pagination').find('span').remove(); - } else { - self.max_page_index = Math.ceil(dataset.size() / limit) - 1; - self.$row - .find('.oe_list_pager_state') - .text(_.str.sprintf(_t("%(page)d/%(page_count)d"), { - page: page + 1, - page_count: self.max_page_index + 1 - })) - .end() - .find('button[data-pager-action=previous]') - .toggleClass('disabled', page === 0) - .end() - .find('button[data-pager-action=next]') - .toggleClass('disabled', page === self.max_page_index); - } + // Update the main list view pager + view.update_pager(dataset, current_min); } self.records.add(records, {silent: true}); list.render(); - if (_.isEmpty(records)) { - view.no_result(); - } return list; }); }); @@ -1580,7 +1499,7 @@ ListView.Groups = Class.extend({ helper: 'clone' }); if (sequence_field) { - list.$current.sortable('option', 'handle', '.oe_list_field_handle'); + list.$current.sortable('option', 'handle', '.o_row_handle'); } list.$current.sortable('option', { start: function (e, ui) { @@ -1631,7 +1550,7 @@ ListView.Groups = Class.extend({ var $el = $('<tbody>'); this.elements = [$el[0]]; - this.datagroup.list( + return this.datagroup.list( _(this.view.visible_columns).chain() .filter(function (column) { return column.tag === 'field';}) .pluck('name').value(), @@ -1640,7 +1559,7 @@ ListView.Groups = Class.extend({ self.render_groups(groups)); if (post_render) { post_render(); } }, function (dataset) { - self.render_dataset(dataset).then(function (list) { + return self.render_dataset(dataset).then(function (list) { self.children[null] = list; self.elements = [list.$current.replaceAll($el)[0]]; @@ -1650,7 +1569,6 @@ ListView.Groups = Class.extend({ self.view.trigger('view_list_rendered'); }); }); - return $el; }, /** * Returns the ids of all selected records for this group, and the records @@ -1669,8 +1587,11 @@ ListView.Groups = Class.extend({ return {ids: ids, records: records}; }, on_records_reset: function () { + this.$to_be_removed = $(this.elements); + _.each(this.children, function(child){ + this.$to_be_removed = this.$to_be_removed.add(child.$to_be_removed); + }.bind(this)); this.children = {}; - $(this.elements).remove(); }, get_records: function () { if (_(this.children).isEmpty()) { @@ -1725,13 +1646,12 @@ var DataGroup = Class.extend({ fields = _.unique((fields || []).concat(this.group_by)); } var query = this.model.query(fields).order_by(this.sort).group_by(this.group_by); - $.when(query).done(function (querygroups) { + return $.when(query).then(function (querygroups) { // leaf node if (!querygroups) { var ds = new data.DataSetSearch(self, self.model.name, self.model.context(), self.model.domain()); ds._sort = self.sort; - ifRecords(ds); - return; + return ifRecords(ds); } // internal node var child_datagroups = _(querygroups).map(function (group) { @@ -1763,7 +1683,7 @@ var StaticDataGroup = DataGroup.extend({ this.dataset = dataset; }, list: function (fields, ifGroups, ifRecords) { - ifRecords(this.dataset); + return ifRecords(this.dataset); } }); @@ -1811,6 +1731,10 @@ var Column = Class.extend({ C.prototype = this; return new C(aggregation_func, this[aggregation_func]); }, + heading: function () { + return _.escape(this.string); + }, + width: function () {}, /** * * @param row_data record whose values should be displayed in the cell @@ -1834,7 +1758,8 @@ var Column = Class.extend({ ? '' : options.value_if_empty; } - return this._format(row_data, options); + var f = this._format(row_data, options); + return (f !== '')? f : ' '; }, /** * Method to override in order to provide alternative HTML content for the @@ -1890,7 +1815,7 @@ var ColumnBoolean = Column.extend({ * @private */ _format: function (row_data, options) { - return _.str.sprintf('<input type="checkbox" %s readonly="readonly"/>', + return _.str.sprintf('<div class="o_checkbox"><input type="checkbox" %s disabled="disabled"/><span/></div>', row_data[this.id].value ? 'checked="checked"' : ''); } }); @@ -1965,14 +1890,19 @@ var ColumnHandle = Column.extend({ this._super.apply(this, arguments); // Handle overrides the field to not be form-editable. this.modifiers.readonly = true; + this.string = ""; // Don't display the column header + }, + heading: function () { + return '<span class="o_row_handle fa fa-arrows invisible"></span>'; }, + width: function () { return 1; }, /** * Return styling hooks for a drag handle * * @private */ _format: function (row_data, options) { - return '<i class="oe_list_handle fa fa-circle"/>'; + return '<span class="o_row_handle fa fa-arrows"/>'; } }); @@ -2042,8 +1972,9 @@ var ColumnUrl = Column.extend({ var ColumnMonetary = Column.extend({ _format: function (row_data, options) { + var options = pyeval.py_eval(this.options || '{}'); //name of currency field is defined either by field attribute, in view options or we assume it is named currency_id - var currency_field = (this.options && this.options.currency_field) || this.currency_field || 'currency_id'; + var currency_field = (_.isEmpty(options) === false && options.currency_field) || this.currency_field || 'currency_id'; var currency_id = row_data[currency_field] && row_data[currency_field].value[0]; var currency = session.get_currency(currency_id); var digits_precision = this.digits || (currency && currency.digits); diff --git a/addons/web/static/src/js/views/list_view_editable.js b/addons/web/static/src/js/views/list_view_editable.js index 8937b8bb36f..f4a4c695334 100644 --- a/addons/web/static/src/js/views/list_view_editable.js +++ b/addons/web/static/src/js/views/list_view_editable.js @@ -69,9 +69,9 @@ var Editor = Widget.extend({ if (!(arch.attrs && arch.attrs.version === "7.0")) { throw new Error("Editor delegate's #edition_view must be a version 7 view"); } - if (!/\boe_form_container\b/.test(arch.attrs['class'])) { + if (!/\bo_list_editable_form\b/.test(arch.attrs['class'])) { throw new Error("Editor delegate's #edition_view must have the class " + - "'boe_form_container' on its root element"); + "'o_list_editable_form' on its root element"); } return edition_view; }, @@ -183,12 +183,12 @@ ListView.include(/** @lends instance.web.ListView# */{ } }); this.on('edit:after', this, function () { - self.$el.add(self.$buttons).addClass('oe_editing'); + self.$el.add(self.$buttons).addClass('o-editing'); self.$('.ui-sortable').sortable('disable'); }); this.on('save:after cancel:after', this, function () { self.$('.ui-sortable').sortable('enable'); - self.$el.add(self.$buttons).removeClass('oe_editing'); + self.$el.add(self.$buttons).removeClass('o-editing'); }); }, destroy: function () { @@ -260,7 +260,7 @@ ListView.include(/** @lends instance.web.ListView# */{ this._super.apply(this, arguments); } }, - start: function () { + load_list: function (data, grouped) { // tree/@editable takes priority on everything else if present. var result = this._super.apply(this, arguments); @@ -271,10 +271,10 @@ ListView.include(/** @lends instance.web.ListView# */{ this.editor = new Editor(this); // Editor is not restartable due to formview not being restartable if(this.editable()) { - this.$el.addClass('oe_list_editable'); + this.$el.addClass('o_list_editable'); return $.when(result, this.editor.prependTo(this.$el).done(this.proxy('setup_events'))); } else { - this.$el.removeClass('oe_list_editable'); + this.$el.removeClass('o_list_editable'); } return result; }, @@ -366,11 +366,11 @@ ListView.include(/** @lends instance.web.ListView# */{ fields[field_name] = field; self.fields_for_resize.push({field: field, cell: cell}); }, options).then(function () { - $recordRow.addClass('oe_edition'); + $recordRow.addClass('o_row_edition'); self.resize_fields(); // Local function that returns true if field is visible and editable var is_focusable = function(field) { - return field && field.$el.is(':visible:not(.oe_readonly)'); + return field && field.$el.is(':visible:not(.o_readonly)'); }; var focus_field = options && options.focus_field ? options.focus_field : undefined; if (!is_focusable(fields[focus_field])) { @@ -423,16 +423,21 @@ ListView.include(/** @lends instance.web.ListView# */{ resize_field: function (field, cell) { var $cell = $(cell); field.set_dimensions($cell.outerHeight(), $cell.outerWidth()); - field.$el.addClass('o_temp_visible').css({top: 0, left: 0}).position({ - my: 'left top', - at: 'left top', - of: $cell, - }).removeClass('o_temp_visible'); + position_element(field.$el, 'left top'); + position_element(field.$translate, 'right top'); if(field.get('effective_readonly')) { - field.$el.addClass('oe_readonly'); + field.$el.addClass('o_readonly'); } if(field.widget == "handle") { - field.$el.addClass('oe_list_field_handle'); + field.$el.addClass('o_row_handle'); + } + + function position_element($el, pos) { + $el.addClass('o_temp_visible').css({top: 0, left: 0}).position({ + my: pos, + at: pos, + of: $cell, + }).removeClass('o_temp_visible'); } }, /** @@ -541,7 +546,7 @@ ListView.include(/** @lends instance.web.ListView# */{ var view = $.extend(true, {}, this.fields_view); view.arch.tag = 'form'; _.extend(view.arch.attrs, { - 'class': 'oe_form_container', + 'class': 'o_list_editable_form', version: '7.0' }); _(view.arch.children).chain() @@ -817,7 +822,7 @@ ListView.List.include(/** @lends instance.web.ListView.List# */{ var record_id = $(event.currentTarget).data('id'); return this.view.start_edition( ((record_id)? this.records.get(record_id) : null), { - focus_field: $(event.target).not(".oe_readonly").data('field'), + focus_field: $(event.target).not(".o_readonly").data('field'), }).fail(function() { return _super.apply(self, args); // The record can't be edited so open it in a modal (use-case: readonly mode) }); diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js index 9dc2c4c2cdd..c7710b86605 100644 --- a/addons/web/static/src/js/views/pivot_view.js +++ b/addons/web/static/src/js/views/pivot_view.js @@ -21,11 +21,11 @@ var QWeb = core.qweb; var PivotView = View.extend({ events: { - 'click .oe-opened': 'on_open_header_click', - 'click .oe-closed': 'on_closed_header_click', - 'click .o-field-menu': 'on_field_menu_selection', + 'click .o_pivot_header_cell_opened': 'on_open_header_click', + 'click .o_pivot_header_cell_closed': 'on_closed_header_click', + 'click .o_pivot_field_menu': 'on_field_menu_selection', 'click td': 'on_cell_click', - 'click .measure-row': 'on_measure_row_click', + 'click .o_pivot_measure_row': 'on_measure_row_click', }, display_name: _lt('Pivot'), icon: 'fa-table', @@ -100,6 +100,7 @@ var PivotView = View.extend({ if ((!this.active_measures.length) || this.fields_view.arch.attrs.display_quantity) { this.active_measures.push('__count__'); } + return $.when(fields_def, xlwt_def, this._super()).then(function (fields) { self.prepare_fields(fields); // add active measures to the measure list. This is very rarely necessary, but it @@ -116,14 +117,13 @@ var PivotView = View.extend({ }); }, start: function () { - this.$table_container = this.$('.o-pivot-table'); - this.$el.toggleClass('oe_enable_linking', this.enable_linking); + this.$el.toggleClass('o_enable_linking', this.enable_linking); var context = { fields: _.chain(this.groupable_fields).pairs().sortBy(function(f) { return f[1].string; }).value(), }; - this.$field_selection = this.$('.o-field-selection'); + this.$field_selection = this.$('.o_field_selection'); this.$field_selection.html(QWeb.render('PivotView.FieldSelection', context)); core.bus.on('click', this, function () { this.$field_selection.find('ul').first().hide(); @@ -161,11 +161,6 @@ var PivotView = View.extend({ render_sidebar: function($node) { if (this.xlwt_installed && $node && this.options.sidebar) { this.sidebar = new Sidebar(this, {editable: this.is_action_enabled('edit')}); - this.sidebar.add_items('other', [{ - label: _t("Download xls"), - callback: this.download_table.bind(this), - }]); - this.sidebar.appendTo($node); } }, @@ -207,11 +202,12 @@ var PivotView = View.extend({ }, do_show: function () { var self = this; + var _super = this._super.bind(this); this.do_push_state({}); - this.data_loaded.done(function () { + return this.data_loaded.done(function () { self.display_table(); + _super(); }); - return this._super(); }, get_context: function () { return !this.ready ? {} : { @@ -222,16 +218,17 @@ var PivotView = View.extend({ }, on_button_click: function (event) { var $target = $(event.target); - if ($target.hasClass('oe-pivot-flip')) { return this.flip();} - if ($target.hasClass('oe-pivot-expand-all')) {return this.expand_all();} - if ($target.parents('.oe-measure-list').length) { - var parent = $target.parent(), - field = parent.data('field'); + if ($target.hasClass('o_pivot_flip_button')) { return this.flip(); } + if ($target.hasClass('o_pivot_expand_button')) { return this.expand_all(); } + if ($target.parents('.o_pivot_measures_list').length) { + var parent = $target.parent(); + var field = parent.data('field'); parent.toggleClass('selected'); + event.preventDefault(); event.stopPropagation(); return this.toggle_measure(field); } - if ($target.hasClass('oe-pivot-download')) { + if ($target.hasClass('o_pivot_download')) { return this.download_table(); } }, @@ -252,21 +249,23 @@ var PivotView = View.extend({ this.expand_header(header, groupbys[header.path.length - 1]) .then(this.proxy('display_table')); } else { - var $test = $(event.target); - var pos = $test.position(); this.last_header_selected = id; + var $test = $(event.target); var $menu = this.$field_selection.find('ul').first(); - $menu.css('top', pos.top + $test.parent().height() - 2); - $menu.css('left', pos.left + event.offsetX); + var position = $test.position(); + $menu.css({ + top: position.top + $test.height(), + left: position.left + event.offsetX, + }); $menu.show(); event.stopPropagation(); } }, on_cell_click: function (event) { var $target = $(event.target); - if ($target.hasClass('oe-closed') - || $target.hasClass('oe-opened') - || $target.hasClass('oe-empty') + if ($target.hasClass('o_pivot_header_cell_closed') + || $target.hasClass('o_pivot_header_cell_opened') + || $target.hasClass('o_empty') || !this.enable_linking) { return; } @@ -297,11 +296,10 @@ var PivotView = View.extend({ col_id = $target.data('id'), measure = $target.data('measure'); - this.sort_rows(col_id, measure, $target.hasClass('o-sorted-asc')); + this.sort_rows(col_id, measure, $target.hasClass('o_pivot_measure_row_sorted_asc')); this.display_table(); }, sort_rows: function (col_id, measure, descending) { - console.log('order', descending); var self = this; traverse_tree(this.main_row.root, function (header) { header.children.sort(compare); @@ -321,6 +319,7 @@ var PivotView = View.extend({ } }, on_field_menu_selection: function (event) { + event.preventDefault(); var field = $(event.target).parent().data('field'), interval = $(event.target).data('interval'), header = this.headers[this.last_header_selected]; @@ -550,13 +549,22 @@ var PivotView = View.extend({ } return find_path_in_tree(root, path); }, + _clean_table: function () { + return this.$el + .find('table') + .remove() + .end() + .find('.oe_view_nocontent') + .remove() + .end(); + }, display_table: function () { if (!this.active_measures.length || !this.has_data) { - return this.$table_container.empty().append(QWeb.render('PivotView.nodata')); + return this._clean_table().append(QWeb.render('PivotView.nodata')); } var $fragment = $(document.createDocumentFragment()), $table = $('<table>') - .addClass('table table-hover table-condensed') + .addClass('table table-hover table-condensed table-bordered') .appendTo($fragment), $thead = $('<thead>').appendTo($table), $tbody = $('<tbody>').appendTo($table), @@ -572,8 +580,8 @@ var PivotView = View.extend({ $table.on('hover', 'td', function () { $table.find('col:eq(' + $(this).index()+')').toggleClass('hover'); }); - this.$table_container.empty().append($fragment); - this.$table_container.find('.oe-opened,.oe-closed').tooltip(); + this._clean_table().append($fragment); + this.$('.o_pivot_header_cell_opened,.o_pivot_header_cell_closed').tooltip(); }, draw_headers: function ($thead, headers) { var self = this, @@ -591,26 +599,27 @@ var PivotView = View.extend({ .text(cell.title) .attr('rowspan', cell.height) .attr('colspan', cell.width); - if (cell.total) { - $cell.addClass('oe-total'); - } if (i > 0) { $cell.attr('title', groupby_labels[i-1]); } if (cell.expanded !== undefined) { - $cell.addClass(cell.expanded ? 'oe-opened' : 'oe-closed'); + $cell.addClass(cell.expanded ? 'o_pivot_header_cell_opened' : 'o_pivot_header_cell_closed'); $cell.data('id', cell.id); } if (cell.measure) { - $cell.addClass('measure-row text-muted') - .text(this.measures[cell.measure].string) - .toggleClass('oe-total', cell.is_bold); + $cell.addClass('o_pivot_measure_row text-muted') + .text(this.measures[cell.measure].string); $cell.data('id', cell.id).data('measure', cell.measure); if (cell.id === this.sorted_column.id && cell.measure === this.sorted_column.measure) { - $cell.addClass('o-sorted o-sorted-' + this.sorted_column.order); + $cell.addClass('o_pivot_measure_row_sorted_' + this.sorted_column.order); } } $row.append($cell); + + $cell.toggleClass('hidden-xs', (cell.expanded !== undefined) || (cell.measure !== undefined && j < headers[i].length - this.active_measures.length)); + if (cell.height > 1) { + $cell.css('padding', 0); + } } $thead.append($row); } @@ -635,7 +644,7 @@ var PivotView = View.extend({ .text(rows[i].title) .data('id', rows[i].id) .css('padding-left', (5 + rows[i].indent * 30) + 'px') - .addClass(rows[i].expanded ? 'oe-opened' : 'oe-closed'); + .addClass(rows[i].expanded ? 'o_pivot_header_cell_opened' : 'o_pivot_header_cell_closed'); if (rows[i].indent > 0) $header.attr('title', groupby_labels[rows[i].indent - 1]); $header.appendTo($row); for (j = 0; j < length; j++) { @@ -643,12 +652,15 @@ var PivotView = View.extend({ $cell = $('<td>') .data('id', rows[i].id) .data('col_id', rows[i].col_ids[Math.floor(j / nbr_measures)]) - .toggleClass('oe-empty', !value) - .text(value); + .toggleClass('o_empty', !value) + .text(value) + .addClass('o_pivot_cell_value text-right'); if (((j >= length - this.active_measures.length) && display_total) || i === 0){ $cell.css('font-weight', 'bold'); } $row.append($cell); + + $cell.toggleClass('hidden-xs', j < length - this.active_measures.length); } $tbody.append($row); } diff --git a/addons/web/static/src/js/views/search_filters.js b/addons/web/static/src/js/views/search_filters.js index c6193488578..041cda35580 100644 --- a/addons/web/static/src/js/views/search_filters.js +++ b/addons/web/static/src/js/views/search_filters.js @@ -12,9 +12,9 @@ var _lt = core._lt; var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{ template: 'SearchView.extended_search.proposition', events: { - 'change .searchview_extended_prop_field': 'changed', - 'change .searchview_extended_prop_op': 'operator_changed', - 'click .searchview_extended_delete_prop': function (e) { + 'change .o_searchview_extended_prop_field': 'changed', + 'change .o_searchview_extended_prop_op': 'operator_changed', + 'click .o_searchview_extended_delete_prop': function (e) { e.stopPropagation(); this.getParent().remove_proposition(this); }, @@ -40,13 +40,13 @@ var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.Ext return this._super().done(this.proxy('changed')); }, changed: function() { - var nval = this.$(".searchview_extended_prop_field").val(); + var nval = this.$(".o_searchview_extended_prop_field").val(); if(this.attrs.selected === null || this.attrs.selected === undefined || nval != this.attrs.selected.name) { this.select_field(_.detect(this.fields, function(x) {return x.name == nval;})); } }, operator_changed: function (e) { - var $value = this.$('.searchview_extended_prop_value'); + var $value = this.$('.o_searchview_extended_prop_value'); switch ($(e.target).val()) { case '∃': case '∄': @@ -66,7 +66,7 @@ var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.Ext if(this.attrs.selected !== null && this.attrs.selected !== undefined) { this.value.destroy(); this.value = null; - this.$('.searchview_extended_prop_op').html(''); + this.$('.o_searchview_extended_prop_op').html(''); } this.attrs.selected = field; if(field === null || field === undefined) { @@ -80,9 +80,9 @@ var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.Ext _.each(this.value.operators, function(operator) { $('<option>', {value: operator.value}) .text(String(operator.text)) - .appendTo(self.$('.searchview_extended_prop_op')); + .appendTo(self.$('.o_searchview_extended_prop_op')); }); - var $value_loc = this.$('.searchview_extended_prop_value').show().empty(); + var $value_loc = this.$('.o_searchview_extended_prop_value').show().empty(); this.value.appendTo($value_loc); }, @@ -90,7 +90,7 @@ var ExtendedSearchProposition = Widget.extend(/** @lends instance.web.search.Ext if (this.attrs.selected === null || this.attrs.selected === undefined) return null; var field = this.attrs.selected, - op_select = this.$('.searchview_extended_prop_op')[0], + op_select = this.$('.o_searchview_extended_prop_op')[0], operator = op_select.options[op_select.selectedIndex]; return { @@ -155,7 +155,10 @@ ExtendedSearchProposition.Field = Widget.extend({ }); ExtendedSearchProposition.Char = ExtendedSearchProposition.Field.extend({ - template: 'SearchView.extended_search.proposition.char', + tagName: 'input', + attributes: { + type: 'text' + }, operators: [ {value: "ilike", text: _lt("contains")}, {value: "not ilike", text: _lt("doesn't contain")}, @@ -170,7 +173,7 @@ ExtendedSearchProposition.Char = ExtendedSearchProposition.Field.extend({ }); ExtendedSearchProposition.DateTime = ExtendedSearchProposition.Field.extend({ - template: 'SearchView.extended_search.proposition.empty', + tagName: 'span', operators: [ {value: "=", text: _lt("is equal to")}, {value: "!=", text: _lt("is not equal to")}, @@ -204,7 +207,11 @@ ExtendedSearchProposition.Date = ExtendedSearchProposition.DateTime.extend({ }); ExtendedSearchProposition.Integer = ExtendedSearchProposition.Field.extend({ - template: 'SearchView.extended_search.proposition.integer', + tagName: 'input', + attributes: { + type: 'number', + value: '0', + }, operators: [ {value: "=", text: _lt("is equal to")}, {value: "!=", text: _lt("is not equal to")}, @@ -280,7 +287,7 @@ ExtendedSearchProposition.Selection = ExtendedSearchProposition.Field.extend({ }); ExtendedSearchProposition.Boolean = ExtendedSearchProposition.Field.extend({ - template: 'SearchView.extended_search.proposition.empty', + tagName: 'span', operators: [ {value: "=", text: _lt("is true")}, {value: "!=", text: _lt("is false")} diff --git a/addons/web/static/src/js/views/search_inputs.js b/addons/web/static/src/js/views/search_inputs.js index 44b79999259..0263f270172 100644 --- a/addons/web/static/src/js/views/search_inputs.js +++ b/addons/web/static/src/js/views/search_inputs.js @@ -541,6 +541,7 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ return { category: _t("Filter"), icon: this.icon, + separator: _t(" or "), values: values, field: this }; @@ -617,6 +618,7 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ }); }, toggle_filter: function (e) { + e.preventDefault(); e.stopPropagation(); this.toggle(this.filters[Number($(e.target).parent().data('index'))]); }, @@ -686,6 +688,7 @@ var GroupbyGroup = FilterGroup.extend({ return { category: _t("Group By"), icon: this.icon, + separator: " > ", values: values, field: this.searchview._s_groupby }; diff --git a/addons/web/static/src/js/views/search_menus.js b/addons/web/static/src/js/views/search_menus.js index 9edb817c567..10ab44ef30c 100644 --- a/addons/web/static/src/js/views/search_menus.js +++ b/addons/web/static/src/js/views/search_menus.js @@ -15,12 +15,16 @@ return Widget.extend({ 'click li': function (event) { event.stopImmediatePropagation(); }, - 'click .oe-save-search a': function () { + 'click li a': function (event) { + event.preventDefault(); + }, + 'click .o_save_search a': function (event) { + event.preventDefault(); this.toggle_save_menu(); }, - 'click .oe-save-name button': 'save_favorite', + 'click .o_save_name button': 'save_favorite', 'hidden.bs.dropdown': 'close_menus', - 'keyup .oe-save-name input': function (ev) { + 'keyup .o_save_name input': function (ev) { if (ev.which === $.ui.keyCode.ENTER) { this.save_favorite(); } @@ -38,8 +42,8 @@ return Widget.extend({ start: function () { var self = this; this.$filters = {}; - this.$save_search = this.$('.oe-save-search'); - this.$save_name = this.$('.oe-save-name'); + this.$save_search = this.$('.o_save_search'); + this.$save_name = this.$('.o_save_name'); this.$inputs = this.$save_name.find('input'); this.$divider = this.$('.divider'); this.$inputs.eq(0).val(this.searchview.get_title()); @@ -116,7 +120,6 @@ return Widget.extend({ model_id: this.target_model, context: results.context, domain: results.domain, - sort: JSON.stringify(this.searchview.dataset._sort), is_default: default_filter, action_id: this.action_id, }; @@ -204,12 +207,11 @@ return Widget.extend({ this.$divider.show(); if (!(key in this.$filters)) { var $filter = $('<li>') - .addClass(filter.user_id ? 'oe_searchview_custom_private' - : 'oe_searchview_custom_public') - .toggleClass('oe_searchview_custom_default', filter.is_default) - .append($('<a>').text(filter.name)) + .addClass(filter.user_id ? 'o-searchview-custom-private' + : 'o-searchview-custom-public') + .append($('<a>', {'href': '#'}).text(filter.name)) .append($('<span>', { - class: 'fa fa-trash-o remove-filter', + class: 'fa fa-trash-o o-remove-filter', on: { click: function (event) { event.stopImmediatePropagation(); @@ -235,13 +237,6 @@ return Widget.extend({ } this.query.reset([this.facet_for(filter)], { preventSearch: preventSearch || false}); - - // Load sort settings on view - if (!_.isUndefined(filter.sort)){ - var sort_items = JSON.parse(filter.sort); - this.searchview.dataset.set_sort(sort_items); - } - this.$filters[this.key_for(filter)].addClass('selected'); }, remove_filter: function (filter, $filter, key) { @@ -277,16 +272,20 @@ var Widget = require('web.Widget'); return Widget.extend({ template: 'SearchView.FilterMenu', events: { - 'click .oe-add-filter': function () { + 'click .o_add_filter': function (event) { + event.preventDefault(); this.toggle_custom_filter_menu(); }, - 'click li': function (event) {event.stopImmediatePropagation();}, + 'click li': function (event) { + event.preventDefault(); + event.stopImmediatePropagation(); + }, 'hidden.bs.dropdown': function () { this.toggle_custom_filter_menu(false); }, - 'click .oe-add-condition': 'append_proposition', - 'click .oe-apply-filter': 'commit_search', - 'keyup .searchview_extended_prop_value': function (ev) { + 'click .o_add_condition': 'append_proposition', + 'click .o_apply_filter': 'commit_search', + 'keyup .o_searchview_extended_prop_value': function (ev) { if (ev.which === $.ui.keyCode.ENTER) { this.commit_search(); } @@ -301,10 +300,10 @@ return Widget.extend({ }, start: function () { var self = this; - this.$menu = this.$('.filters-menu'); - this.$add_filter = this.$('.oe-add-filter'); - this.$apply_filter = this.$('.oe-apply-filter'); - this.$add_filter_menu = this.$('.oe-add-filter-menu'); + this.$menu = this.$('.o_filters_menu'); + this.$add_filter = this.$('.o_add_filter'); + this.$apply_filter = this.$('.o_apply_filter'); + this.$add_filter_menu = this.$('.o_add_filter_menu'); _.each(this.filters, function (group) { if (group.is_visible()) { group.insertBefore(self.$add_filter); @@ -340,7 +339,7 @@ return Widget.extend({ .toggleClass('o_closed_menu', !self.custom_filters_open) .toggleClass('o_open_menu', self.custom_filters_open); self.$add_filter_menu.toggle(self.custom_filters_open); - self.$('.oe-filter-condition').toggle(self.custom_filters_open); + self.$('.o_filter_condition').toggle(self.custom_filters_open); }); }, append_proposition: function () { @@ -402,7 +401,8 @@ return Widget.extend({ 'hidden.bs.dropdown': function () { this.toggle_add_menu(false); }, - 'click .add-custom-group a': function () { + 'click .o_add_custom_group a': function (event) { + event.preventDefault(); this.toggle_add_menu(); }, }, @@ -413,13 +413,13 @@ return Widget.extend({ this.searchview = parent; }, start: function () { - this.$menu = this.$('.group-by-menu'); + this.$menu = this.$('.o_group_by_menu'); var divider = this.$menu.find('.divider'); _.invoke(this.groups, 'insertBefore', divider); if (this.groups.length) { divider.show(); } - this.$add_group = this.$menu.find('.add-custom-group'); + this.$add_group = this.$menu.find('.o_add_custom_group'); }, get_fields: function () { var self = this; @@ -435,9 +435,9 @@ return Widget.extend({ self.groupable_fields = _.sortBy(filter_group_field, 'string'); self.$menu.append(QWeb.render('GroupByMenuSelector', self)); - self.$add_group_menu = self.$('.oe-add-group'); - self.$group_selector = self.$('.oe-group-selector'); - self.$('.oe-select-group').click(function () { + self.$add_group_menu = self.$('.o_add_group'); + self.$group_selector = self.$('.o_group_selector'); + self.$('.o_select_group').click(function () { self.toggle_add_menu(false); var field = self.$group_selector.find(':selected').data('name'); self.add_groupby_to_menu(field); diff --git a/addons/web/static/src/js/views/search_view.js b/addons/web/static/src/js/views/search_view.js index 125b7c9c152..fa1794cdc7c 100644 --- a/addons/web/static/src/js/views/search_view.js +++ b/addons/web/static/src/js/views/search_view.js @@ -2,6 +2,7 @@ odoo.define('web.SearchView', function (require) { "use strict"; var AutoComplete = require('web.AutoComplete'); +var config = require('web.config'); var core = require('web.core'); var data_manager = require('web.data_manager'); var FavoriteMenu = require('web.FavoriteMenu'); @@ -9,9 +10,9 @@ var FilterMenu = require('web.FilterMenu'); var GroupByMenu = require('web.GroupByMenu'); var pyeval = require('web.pyeval'); var search_inputs = require('web.search_inputs'); -var utils = require('web.utils'); var View = require('web.View'); var Widget = require('web.Widget'); +var _t = core._t; var Backbone = window.Backbone; @@ -134,125 +135,32 @@ var InputView = Widget.extend({ template: 'SearchView.InputView', events: { focus: function () { this.trigger('focused', this); }, - blur: function () { this.$el.text(''); this.trigger('blurred', this); }, + blur: function () { this.$el.val(''); this.trigger('blurred', this); }, keydown: 'onKeydown', - paste: 'onPaste', - }, - getSelection: function () { - this.el.normalize(); - // get Text node - var root = this.el.childNodes[0]; - if (!root || !root.textContent) { - // if input does not have a child node, or the child node is an - // empty string, then the selection can only be (0, 0) - return {start: 0, end: 0}; - } - var range = window.getSelection().getRangeAt(0); - // In Firefox, depending on the way text is selected (drag, double- or - // triple-click) the range may start or end on the parent of the - // selected text node‽ Check for this condition and fixup the range - // note: apparently with C-a this can go even higher? - if (range.startContainer === this.el && range.startOffset === 0) { - range.setStart(root, 0); - } - if (range.endContainer === this.el && range.endOffset === 1) { - range.setEnd(root, root.length); - } - utils.assert(range.startContainer === root, - "selection should be in the input view"); - utils.assert(range.endContainer === root, - "selection should be in the input view"); - return { - start: range.startOffset, - end: range.endOffset - }; }, onKeydown: function (e) { - this.el.normalize(); - var sel; switch (e.which) { - // Do not insert newline, but let it bubble so searchview can use it - case $.ui.keyCode.ENTER: - e.preventDefault(); - break; + case $.ui.keyCode.BACKSPACE: + if(this.$el.val() === '') { + var preceding = this.getParent().siblingSubview(this, -1); + if (preceding && (preceding instanceof FacetView)) { + preceding.model.destroy(); + } + } + break; - // FIXME: may forget content if non-empty but caret at index 0, ok? - case $.ui.keyCode.BACKSPACE: - sel = this.getSelection(); - if (sel.start === 0 && sel.start === sel.end) { - e.preventDefault(); - var preceding = this.getParent().siblingSubview(this, -1); - if (preceding && (preceding instanceof FacetView)) { - preceding.model.destroy(); + case $.ui.keyCode.LEFT: // Stop propagation to parent if not at beginning of input value + if(this.el.selectionStart > 0) { + e.stopPropagation(); } - } - break; + break; - // let left/right events propagate to view if caret is at input border - // and not a selection - case $.ui.keyCode.LEFT: - sel = this.getSelection(); - if (sel.start !== 0 || sel.start !== sel.end) { - e.stopPropagation(); - } - break; - case $.ui.keyCode.RIGHT: - sel = this.getSelection(); - var len = this.$el.text().length; - if (sel.start !== len || sel.start !== sel.end) { - e.stopPropagation(); - } - break; + case $.ui.keyCode.RIGHT: // Stop propagation to parent if not at end of input value + if(this.el.selectionStart < this.$el.val().length) { + e.stopPropagation(); + } + break; } - }, - setCursorAtEnd: function () { - this.el.normalize(); - var sel = window.getSelection(); - sel.removeAllRanges(); - var range = document.createRange(); - // in theory, range.selectNodeContents should work here. In practice, - // MSIE9 has issues from time to time, instead of selecting the inner - // text node it would select the reference node instead (e.g. in demo - // data, company news, copy across the "Company News" link + the title, - // from about half the link to half the text, paste in search box then - // hit the left arrow key, getSelection would blow up). - // - // Explicitly selecting only the inner text node (only child node - // since we've normalized the parent) avoids the issue - range.selectNode(this.el.childNodes[0]); - range.collapse(false); - sel.addRange(range); - }, - onPaste: function () { - this.el.normalize(); - // In MSIE and Webkit, it is possible to get various representations of - // the clipboard data at this point e.g. - // window.clipboardData.getData('Text') and - // event.clipboardData.getData('text/plain') to ensure we have a plain - // text representation of the object (and probably ensure the object is - // pastable as well, so nobody puts an image in the search view) - // (nb: since it's not possible to alter the content of the clipboard - // — at least in Webkit — to ensure only textual content is available, - // using this would require 1. getting the text data; 2. manually - // inserting the text data into the content; and 3. cancelling the - // paste event) - // - // But Firefox doesn't support the clipboard API (as of FF18) - // although it correctly triggers the paste event (Opera does not even - // do that) => implement lowest-denominator system where onPaste - // triggers a followup "cleanup" pass after the data has been pasted - setTimeout(function () { - // Read text content (ignore pasted HTML) - var data = this.$el.text(); - if (!data) - return; - // paste raw text back in - this.$el.empty().text(data); - this.el.normalize(); - // Set the cursor at the end of the text, so the cursor is not lost - // in some kind of error-spawning limbo. - this.setCursorAtEnd(); - }.bind(this), 0); } }); @@ -262,7 +170,7 @@ var FacetView = Widget.extend({ 'focus': function () { this.trigger('focused', this); }, 'blur': function () { this.trigger('blurred', this); }, 'click': function (e) { - if ($(e.target).is('.oe_facet_remove')) { + if ($(e.target).hasClass('o_facet_remove')) { this.model.destroy(); return false; } @@ -272,10 +180,10 @@ var FacetView = Widget.extend({ 'keydown': function (e) { var keys = $.ui.keyCode; switch (e.which) { - case keys.BACKSPACE: - case keys.DELETE: - this.model.destroy(); - return false; + case keys.BACKSPACE: + case keys.DELETE: + this.model.destroy(); + return false; } } }, @@ -290,9 +198,12 @@ var FacetView = Widget.extend({ }, start: function () { var self = this; - var $e = this.$('> span:last-child'); + var $e = this.$('.o_facet_values').last(); return $.when(this._super()).then(function () { - return $.when.apply(null, self.model.values.map(function (value) { + return $.when.apply(null, self.model.values.map(function (value, index) { + if (index > 0) { + $('<span/>', {html: self.model.get('separator') || _t(" or ")}).addClass('o_facet_values_sep').appendTo($e); + } return new FacetValueView(self, value).appendTo($e); })); }); @@ -319,49 +230,34 @@ var FacetValueView = Widget.extend({ }); var SearchView = View.extend({ - defaults: _.extend({}, View.prototype.defaults, { - hidden: false, - disable_filters: false, - disable_groupby: false, - disable_favorites: false, - disable_custom_filters: false, - }), events: { - // focus last input if view itself is clicked - 'click': function (e) { - if (e.target === this.$('.oe_searchview_facets')[0]) { - this.$('.oe_searchview_input:last').focus(); - } - }, - // search button - 'click div.oe_searchview_search': function (e) { - e.stopImmediatePropagation(); - this.do_search(); - }, - 'click .oe_searchview_unfold_drawer': function (e) { - e.stopImmediatePropagation(); - $(e.target).toggleClass('fa-caret-down fa-caret-up'); + 'click .o_searchview_more': function (e) { + $(e.target).toggleClass('fa-search-plus fa-search-minus'); localStorage.visible_search_menu = (localStorage.visible_search_menu !== 'true'); this.toggle_buttons(); }, - 'keydown .oe_searchview_input, .oe_searchview_facet': function (e) { + 'keydown .o_searchview_input, .o_searchview_facet': function (e) { switch(e.which) { - case $.ui.keyCode.LEFT: - this.focusPreceding(e.target); - e.preventDefault(); - break; - case $.ui.keyCode.RIGHT: - if (!this.autocomplete.is_expandable()) { - this.focusFollowing(e.target); - } - e.preventDefault(); - break; + case $.ui.keyCode.LEFT: + this.focusPreceding(e.target); + e.preventDefault(); + break; + case $.ui.keyCode.RIGHT: + if(!this.autocomplete.is_expandable()) { + this.focusFollowing(e.target); + } + e.preventDefault(); + break; } }, - 'autocompleteopen': function () { - this.$el.autocomplete('widget').css('z-index', 9999); - }, }, + defaults: _.extend({}, View.prototype.defaults, { + hidden: false, + disable_filters: false, + disable_groupby: false, + disable_favorites: false, + disable_custom_filters: false, + }), template: "SearchView", /** @@ -377,9 +273,9 @@ var SearchView = View.extend({ */ init: function() { this._super.apply(this, arguments); - this.title = this.options.action && this.options.action.name; - this.action_id = this.options.action && this.options.action.id; this.query = undefined; + this.title = this.options.action && this.options.action.name; + this.action_id = this.options.action && this.options.action.id; this.search_fields = []; this.filters = []; this.groupbys = []; @@ -408,15 +304,14 @@ var SearchView = View.extend({ this.do_hide(); } this.toggle_visibility(false); - this.$facets_container = this.$('div.oe_searchview_facets'); this.setup_global_completion(); this.query = new SearchQuery() .on('add change reset remove', this.proxy('do_search')) .on('change', this.proxy('renderChangedFacets')) .on('add reset remove', this.proxy('renderFacets')); - this.$('.oe_searchview_unfold_drawer') - .toggleClass('fa-caret-down', !this.visible_filters) - .toggleClass('fa-caret-up', this.visible_filters); + this.$('.o_searchview_more') + .toggleClass('fa-search-minus', this.visible_filters) + .toggleClass('fa-search-plus', !this.visible_filters); var menu_defs = []; this.prepare_search_inputs(); if (this.$buttons) { @@ -516,8 +411,8 @@ var SearchView = View.extend({ if (this.$buttons) { this.$buttons.toggle(!this.headless && is_visible && this.visible_filters); } - if (!this.headless && is_visible) { - this.$('div.oe_searchview_input').last().focus(); + if (!config.device.touch && config.device.size_class >= config.device.SIZES.SM) { + this.$('input').focus(); } }, toggle_buttons: function (is_visible) { @@ -535,7 +430,7 @@ var SearchView = View.extend({ source: this.proxy('complete_global_search'), select: this.proxy('select_completion'), get_search_string: function () { - return self.$('div.oe_searchview_input').text(); + return self.$('.o_searchview_input').val().trim(); }, }); this.autocomplete.appendTo(this.$el); @@ -569,10 +464,11 @@ var SearchView = View.extend({ */ select_completion: function (e, ui) { e.preventDefault(); - var input_index = _(this.input_subviews).indexOf( - this.subviewForRoot( - this.$('div.oe_searchview_input:focus')[0])); - this.query.add(ui.item.facet, {at: input_index / 2}); + if(ui.item.facet.values && ui.item.facet.values.length && String(ui.item.facet.values[0].value).trim() !== "") { + this.query.add(ui.item.facet); + } else { + this.query.trigger('add'); + } }, subviewForRoot: function (subview_root) { return _(this.input_subviews).detect(function (subview) { @@ -609,34 +505,21 @@ var SearchView = View.extend({ _.invoke(this.input_subviews, 'destroy'); this.input_subviews = []; - var i = new InputView(this); - started.push(i.appendTo(this.$facets_container)); - this.input_subviews.push(i); this.query.each(function (facet) { var f = new FacetView(this, facet); - started.push(f.appendTo(self.$facets_container)); + started.push(f.appendTo(self.$el)); self.input_subviews.push(f); - - var i = new InputView(this); - started.push(i.appendTo(self.$facets_container)); - self.input_subviews.push(i); }, this); + var i = new InputView(this); + started.push(i.appendTo(self.$el)); + self.input_subviews.push(i); _.each(this.input_subviews, function (childView) { childView.on('focused', self, self.proxy('childFocused')); childView.on('blurred', self, self.proxy('childBlurred')); }); $.when.apply(null, started).then(function () { - if (options && options.focus_input === false) return; - var input_to_focus; - // options.at: facet inserted at given index, focus next input - // otherwise just focus last input - if (!options || typeof options.at !== 'number') { - input_to_focus = _.last(self.input_subviews); - } else { - input_to_focus = self.input_subviews[(options.at + 1) * 2]; - } - input_to_focus.$el.focus(); + _.last(self.input_subviews).$el.focus(); }); }, childFocused: function () { @@ -726,230 +609,3 @@ _.extend(SearchView, { return SearchView; }); - -odoo.define('web.AutoComplete', function (require) { -"use strict"; - -var Widget = require('web.Widget'); - -return Widget.extend({ - template: "SearchView.autocomplete", - - // Parameters for autocomplete constructor: - // - // parent: this is used to detect keyboard events - // - // options.source: function ({term:query}, callback). This function will be called to - // obtain the search results corresponding to the query string. It is assumed that - // options.source will call callback with the results. - // options.select: function (ev, {item: {facet:facet}}). Autocomplete widget will call - // that function when a selection is made by the user - // options.get_search_string: function (). This function will be called by autocomplete - // to obtain the current search string. - init: function (parent, options) { - this._super(parent); - this.$input = parent.$el; - this.source = options.source; - this.select = options.select; - this.get_search_string = options.get_search_string; - - this.current_result = null; - - this.searching = true; - this.search_string = ''; - this.current_search = null; - }, - start: function () { - var self = this; - this.$input.on('keyup', function (ev) { - if (ev.which === $.ui.keyCode.RIGHT) { - self.searching = true; - ev.preventDefault(); - return; - } - if (ev.which === $.ui.keyCode.ENTER) { - if (self.search_string.length) { - self.select_item(ev); - } - return; - } - var search_string = self.get_search_string(); - if (self.search_string !== search_string) { - if (search_string.length) { - self.search_string = search_string; - self.initiate_search(search_string); - } else { - self.close(); - } - } - }); - this.$input.on('keypress', function (ev) { - self.search_string = self.search_string + String.fromCharCode(ev.which); - if (self.search_string.length) { - self.searching = true; - var search_string = self.search_string; - self.initiate_search(search_string); - } else { - self.close(); - } - }); - this.$input.on('keydown', function (ev) { - switch (ev.which) { - case $.ui.keyCode.ENTER: - - // TAB and direction keys are handled at KeyDown because KeyUp - // is not guaranteed to fire. - // See e.g. https://github.com/aef-/jquery.masterblaster/issues/13 - case $.ui.keyCode.TAB: - if (self.search_string.length) { - self.select_item(ev); - } - break; - case $.ui.keyCode.DOWN: - self.move('down'); - self.searching = false; - ev.preventDefault(); - break; - case $.ui.keyCode.UP: - self.move('up'); - self.searching = false; - ev.preventDefault(); - break; - case $.ui.keyCode.RIGHT: - self.searching = false; - var current = self.current_result; - if (current && current.expand && !current.expanded) { - self.expand(); - self.searching = true; - } - ev.preventDefault(); - break; - case $.ui.keyCode.ESCAPE: - self.close(); - self.searching = false; - break; - } - }); - }, - initiate_search: function (query) { - if (query === this.search_string && query !== this.current_search) { - this.search(query); - } - }, - search: function (query) { - var self = this; - this.current_search = query; - this.source({term:query}, function (results) { - if (results.length) { - self.render_search_results(results); - self.focus_element(self.$('li:first-child')); - } else { - self.close(); - } - }); - }, - render_search_results: function (results) { - var self = this; - var $list = this.$('ul'); - $list.empty(); - var render_separator = false; - results.forEach(function (result) { - if (result.is_separator) { - if (render_separator) - $list.append($('<li>').addClass('oe-separator')); - render_separator = false; - } else { - var $item = self.make_list_item(result).appendTo($list); - result.$el = $item; - render_separator = true; - } - }); - this.show(); - }, - make_list_item: function (result) { - var self = this; - var $li = $('<li>') - .hover(function () {self.focus_element($li);}) - .mousedown(function (ev) { - if (ev.button === 0) { // left button - self.select(ev, {item: {facet: result.facet}}); - self.close(); - } else { - ev.preventDefault(); - } - }) - .data('result', result); - if (result.expand) { - var $expand = $('<span class="oe-expand">').text('▶').appendTo($li); - $expand.mousedown(function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - if (result.expanded) - self.fold(); - else - self.expand(); - }); - result.expanded = false; - } - if (result.indent) $li.addClass('oe-indent'); - $li.append($('<span>').html(result.label)); - return $li; - }, - expand: function () { - var self = this; - var current_result = this.current_result; - current_result.expand(this.get_search_string()).then(function (results) { - (results || [{label: '(no result)'}]).reverse().forEach(function (result) { - result.indent = true; - var $li = self.make_list_item(result); - current_result.$el.after($li); - }); - current_result.expanded = true; - current_result.$el.find('span.oe-expand').html('▼'); - }); - }, - fold: function () { - var $next = this.current_result.$el.next(); - while ($next.hasClass('oe-indent')) { - $next.remove(); - $next = this.current_result.$el.next(); - } - this.current_result.expanded = false; - this.current_result.$el.find('span.oe-expand').html('▶'); - }, - focus_element: function ($li) { - this.$('li').removeClass('oe-selection-focus'); - $li.addClass('oe-selection-focus'); - this.current_result = $li.data('result'); - }, - select_item: function (ev) { - if (this.current_result.facet) { - this.select(ev, {item: {facet: this.current_result.facet}}); - this.close(); - } - }, - show: function () { - this.$el.show(); - }, - close: function () { - this.current_search = null; - this.search_string = ''; - this.searching = true; - this.$el.hide(); - }, - move: function (direction) { - var $next; - if (direction === 'down') { - $next = this.$('li.oe-selection-focus').nextAll(':not(.oe-separator)').first(); - if (!$next.length) $next = this.$('li:first-child'); - } else { - $next = this.$('li.oe-selection-focus').prevAll(':not(.oe-separator)').first(); - if (!$next.length) $next = this.$('li:not(.oe-separator)').last(); - } - this.focus_element($next); - }, - is_expandable: function () { - return !!this.$('.oe-selection-focus .oe-expand').length; - }, -}); -}); diff --git a/addons/web/static/src/js/views/tree_view.js b/addons/web/static/src/js/views/tree_view.js index 6246a1494f5..aeb206257fe 100644 --- a/addons/web/static/src/js/views/tree_view.js +++ b/addons/web/static/src/js/views/tree_view.js @@ -87,7 +87,7 @@ var TreeView = View.extend({ return; } - var $select = self.$el.find('select') + var $select = self.$('select') .change(function () { var $option = $(this).find(':selected'); self.getdata($option.val(), $option.data('children')); @@ -155,8 +155,8 @@ var TreeView = View.extend({ children_ids = record[self.children_field]; _(children_ids).each(function(childid) { - if (self.$el.find('[id=treerow_' + childid + '][data-row-parent-id='+ record_id +']').length ) { - if (self.$el.find('[id=treerow_' + childid + '][data-row-parent-id='+ record_id +']').is(':hidden')) { + if (self.$('[id=treerow_' + childid + '][data-row-parent-id='+ record_id +']').length ) { + if (self.$('[id=treerow_' + childid + '][data-row-parent-id='+ record_id +']').is(':hidden')) { is_loaded = -1; } else { is_loaded++; @@ -164,7 +164,7 @@ var TreeView = View.extend({ } }); if (is_loaded === 0) { - if (!$this.parent().hasClass('oe_open')) { + if (!$this.parent().hasClass('o_open')) { self.getdata(record_id, children_ids); } } else { @@ -180,7 +180,7 @@ var TreeView = View.extend({ _(records).each(function (record) { self.records[record.id] = record; }); - var $curr_node = self.$el.find('#treerow_' + id); + var $curr_node = self.$('#treerow_' + id); var children_rows = QWeb.render('TreeView.rows', { 'records': records, 'children_field': self.children_field, @@ -192,10 +192,10 @@ var TreeView = View.extend({ 'row_parent_id': id }); if ($curr_node.length) { - $curr_node.addClass('oe_open'); + $curr_node.addClass('o_open'); $curr_node.after(children_rows); } else { - self.$el.find('tbody').html(children_rows); + self.$('tbody').html(children_rows); } }); }, @@ -229,11 +229,11 @@ var TreeView = View.extend({ // show & hide the contents showcontent: function (curnode,record_id, show) { - curnode.parent('tr').toggleClass('oe_open', show); + curnode.parent('tr').toggleClass('o_open', show); _(this.records[record_id][this.children_field]).each(function (child_id) { - var $child_row = this.$el.find('[id=treerow_' + child_id + '][data-row-parent-id='+ curnode.data('id') +']'); - if ($child_row.hasClass('oe_open')) { - $child_row.toggleClass('oe_open',show); + var $child_row = this.$('[id=treerow_' + child_id + '][data-row-parent-id='+ curnode.data('id') +']'); + if ($child_row.hasClass('o_open')) { + $child_row.toggleClass('o_open',show); this.showcontent($child_row, child_id, false); } $child_row.toggle(show); diff --git a/addons/web/static/src/js/web_client.js b/addons/web/static/src/js/web_client.js index 640449d74b3..213aa70eed4 100644 --- a/addons/web/static/src/js/web_client.js +++ b/addons/web/static/src/js/web_client.js @@ -1,177 +1,26 @@ odoo.define('web.WebClient', function (require) { "use strict"; -var ActionManager = require('web.ActionManager'); +var AbstractWebClient = require('web.AbstractWebClient'); var core = require('web.core'); -var crash_manager = require('web.crash_manager'); var data = require('web.data'); var data_manager = require('web.data_manager'); var framework = require('web.framework'); -var Loading = require('web.Loading'); var Menu = require('web.Menu'); var Model = require('web.DataModel'); -var NotificationManager = require('web.notification').NotificationManager; var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); var UserMenu = require('web.UserMenu'); -var utils = require('web.utils'); -var Widget = require('web.Widget'); -var _t = core._t; - -var WebClient = Widget.extend({ +return AbstractWebClient.extend({ events: { 'click .oe_logo_edit_admin': 'logo_edit', - 'click .oe_logo img': 'on_logo_click', - }, - custom_events: { - 'notification': function (e) { - if(this.notification_manager) { - this.notification_manager.notify(e.data.title, e.data.message, e.data.sticky); - } - }, - 'warning': function (e) { - if(this.notification_manager) { - this.notification_manager.warn(e.data.title, e.data.message, e.data.sticky); - } - }, - }, - - init: function(parent, client_options) { - this.client_options = {}; - this._super(parent); - this.origin = undefined; - if (client_options) { - _.extend(this.client_options, client_options); - } - this._current_state = null; - this.menu_dm = new utils.DropMisordered(); - this.action_mutex = new utils.Mutex(); - this.set('title_part', {"zopenerp": "Odoo"}); - }, - start: function() { - var self = this; - this.on("change:title_part", this, this._title_changed); - this._title_changed(); - - core.bus.on('web_client_toggle_bars', this, function () { - this.toggle_bars.apply(this, arguments); - }); - - document.body.classList.add('o_web_client'); - return session.session_bind(this.origin).then(function() { - self.bind_events(); - return self.show_common(); - }).then(function() { - if (session.session_is_valid()) { - self.show_application(); - } - if (self.client_options.action) { - self.action_manager.do_action(self.client_options.action); - delete(self.client_options.action); - } - core.bus.trigger('web_client_ready'); - }); - }, - bind_events: function() { - var self = this; - $('.oe_systray').show(); - this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() { - $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter'); - }); - this.$el.on('click', '.oe_dropdown_toggle', function(ev) { + 'click .oe_logo img': function(ev) { ev.preventDefault(); - var $toggle = $(this); - var doc_width = $(document).width(); - var $menu = $toggle.siblings('.oe_dropdown_menu'); - $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu'); - var state = $menu.is('.oe_opened'); - setTimeout(function() { - // Do not alter propagation - $toggle.add($menu).toggleClass('oe_opened', !state); - if (!state) { - // Move $menu if outside window's edge - var offset = $menu.offset(); - var menu_width = $menu.width(); - var x = doc_width - offset.left - menu_width - 2; - if (x < 0) { - $menu.offset({ left: offset.left + x }).width(menu_width); - } - } - }, 0); - }); - core.bus.on('click', this, function(ev) { - $('.tooltip').remove(); - if (!$(ev.target).is('input[type=file]')) { - self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened'); - } - }); - core.bus.on('set_full_screen', this, function (full_screen) { - this.set_content_full_screen(full_screen); - }); - }, - on_logo_click: function(ev) { - ev.preventDefault(); - return this.clear_uncommitted_changes().then(function() { - framework.redirect("/web" + (core.debug ? "?debug" : "")); - }); - }, - show_common: function() { - var self = this; - session.on('error', crash_manager, crash_manager.rpc_error); - self.notification_manager = new NotificationManager(this); - self.notification_manager.appendTo(self.$('.openerp')); - self.loading = new Loading(self); - self.loading.appendTo(self.$('.openerp_webclient_container')); - self.action_manager = new ActionManager(self); - self.action_manager.replace(self.$('.oe_application')); - - window.onerror = function (message, file, line, col, error) { - var traceback = error ? error.stack : ''; - crash_manager.show_error({ - type: _t("Client Error"), - message: message, - data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback} + return this.clear_uncommitted_changes().then(function() { + framework.redirect("/web" + (core.debug ? "?debug" : "")); }); - }; - - }, - toggle_bars: function(value) { - this.$('tr:has(td.navbar),.oe_leftbar').toggle(value); - }, - clear_uncommitted_changes: function() { - var def = $.Deferred().resolve(); - core.bus.trigger('clear_uncommitted_changes', function chain_callbacks(callback) { - def = def.then(callback); - }); - return def; - }, - /** - Sets the first part of the title of the window, dedicated to the current action. - */ - set_title: function(title) { - this.set_title_part("action", title); - }, - /** - Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time - a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the - title of the window separated by '-'. - */ - set_title_part: function(part, title) { - var tmp = _.clone(this.get("title_part")); - tmp[part] = title; - this.set("title_part", tmp); - }, - _title_changed: function() { - var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; }); - var tmp = ""; - _.each(parts, function(part) { - var str = this.get("title_part")[part]; - if (str) { - tmp = tmp ? tmp + " - " + str : str; - } - }, this); - document.title = tmp; + }, }, show_application: function() { var self = this; @@ -186,9 +35,8 @@ var WebClient = Widget.extend({ // Create the user menu (rendered client-side) self.user_menu = new UserMenu(self); - var user_menu_loaded = self.user_menu.appendTo(this.$el.parents().find('.oe_user_menu_placeholder')); - self.user_menu.on('user_logout', self, self.on_logout); - self.user_menu.do_update(); + var $user_menu_placeholder = $('body').find('.oe_user_menu_placeholder').show(); + var user_menu_loaded = self.user_menu.appendTo($user_menu_placeholder); // Create the systray menu (rendered server-side) self.systray_menu = new SystrayMenu(self); @@ -208,6 +56,9 @@ var WebClient = Widget.extend({ delete(self.client_options.action_post_login); } }, + toggle_bars: function(value) { + this.$('tr:has(td.navbar),.oe_leftbar').toggle(value); + }, update_logo: function() { var company = session.company_id; var img = session.url('/web/binary/company_logo' + '?db=' + session.db + (company ? '&company=' + company : '')); @@ -237,37 +88,12 @@ var WebClient = Widget.extend({ }); return false; }, - /** - * When do_action is performed on the WebClient, forward it to the main ActionManager - * This allows to widgets that are not inside the ActionManager to perform do_action - */ - do_action: function() { - return this.action_manager.do_action.apply(this, arguments); - }, - destroy_content: function() { - _.each(_.clone(this.getChildren()), function(el) { - el.destroy(); - }); - this.$el.children().remove(); - }, - do_reload: function() { - var self = this; - return this.session.session_reload().then(function () { - session.load_modules(true).then( - self.menu.proxy('do_reload')); }); - }, - on_logout: function() { - var self = this; - this.clear_uncommitted_changes().then(function() { - self.action_manager.do_action('logout'); - }); - }, bind_hashchange: function() { var self = this; $(window).bind('hashchange', this.on_hashchange); var state = $.bbq.getState(true); - if (_.isEmpty(state) || state.action == "login") { + if (_.isEmpty(state) || state.action === "login") { self.menu.is_bound.done(function() { new Model("res.users").call("read", [session.uid, ["action_id"]]).done(function(data) { if(data.action_id) { @@ -277,7 +103,8 @@ var WebClient = Widget.extend({ var first_menu_id = self.menu.$el.find("a:first").data("menu"); if(first_menu_id) { self.menu.menu_click(first_menu_id); - } } + } + } }); }); } else { @@ -295,19 +122,16 @@ var WebClient = Widget.extend({ }); } else { state._push_me = false; // no need to push state back... - this.action_manager.do_load_state(state, !!this._current_state); + this.action_manager.do_load_state(state, !!this._current_state).then(function () { + var action = self.action_manager.get_inner_action(); + if (action) { + self.menu.open_action(action.action_descr.id); + } + }); } } this._current_state = stringstate; }, - do_push_state: function(state) { - this.set_title(state.title); - delete state.title; - var url = '#' + $.param(state); - this._current_state = $.deparam($.param(state), false); // stringify all values - $.bbq.pushState(url); - this.trigger('state_pushed', state); - }, on_menu_action: function(options) { var self = this; return this.menu_dm.add(data_manager.load_action(options.action_id)) @@ -337,13 +161,6 @@ var WebClient = Widget.extend({ }); }); }, - set_content_full_screen: function(fullscreen) { - $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll'); - this.$('.oe_webclient').toggleClass( - 'oe_content_full_screen', fullscreen); - }, }); -return WebClient; - }); diff --git a/addons/web/static/src/js/widgets/auto_complete.js b/addons/web/static/src/js/widgets/auto_complete.js new file mode 100644 index 00000000000..883206ae4c2 --- /dev/null +++ b/addons/web/static/src/js/widgets/auto_complete.js @@ -0,0 +1,220 @@ +odoo.define('web.AutoComplete', function (require) { +"use strict"; + +var Widget = require('web.Widget'); + +return Widget.extend({ + template: "SearchView.autocomplete", + + // Parameters for autocomplete constructor: + // + // parent: this is used to detect keyboard events + // + // options.source: function ({term:query}, callback). This function will be called to + // obtain the search results corresponding to the query string. It is assumed that + // options.source will call callback with the results. + // options.select: function (ev, {item: {facet:facet}}). Autocomplete widget will call + // that function when a selection is made by the user + // options.get_search_string: function (). This function will be called by autocomplete + // to obtain the current search string. + init: function (parent, options) { + this._super(parent); + this.$input = parent.$el; + this.source = options.source; + this.select = options.select; + this.get_search_string = options.get_search_string; + + this.current_result = null; + + this.searching = true; + this.search_string = ''; + this.current_search = null; + }, + start: function () { + var self = this; + this.$input.on('keyup', function (ev) { + if (ev.which === $.ui.keyCode.RIGHT) { + self.searching = true; + ev.preventDefault(); + return; + } + if (ev.which === $.ui.keyCode.ENTER) { + if (self.search_string.length) { + self.select_item(ev); + } + return; + } + var search_string = self.get_search_string(); + if (self.search_string !== search_string) { + if (search_string.length) { + self.search_string = search_string; + self.initiate_search(search_string); + } else { + self.close(); + } + } + }); + this.$input.on('keypress', function (ev) { + self.search_string = self.search_string + String.fromCharCode(ev.which); + if (self.search_string.length) { + self.searching = true; + var search_string = self.search_string; + self.initiate_search(search_string); + } else { + self.close(); + } + }); + this.$input.on('keydown', function (ev) { + switch (ev.which) { + case $.ui.keyCode.ENTER: + + // TAB and direction keys are handled at KeyDown because KeyUp + // is not guaranteed to fire. + // See e.g. https://github.com/aef-/jquery.masterblaster/issues/13 + case $.ui.keyCode.TAB: + if (self.search_string.length) { + self.select_item(ev); + } + break; + case $.ui.keyCode.DOWN: + self.move('down'); + self.searching = false; + ev.preventDefault(); + break; + case $.ui.keyCode.UP: + self.move('up'); + self.searching = false; + ev.preventDefault(); + break; + case $.ui.keyCode.RIGHT: + self.searching = false; + var current = self.current_result; + if (current && current.expand && !current.expanded) { + self.expand(); + self.searching = true; + } + ev.preventDefault(); + break; + case $.ui.keyCode.ESCAPE: + self.close(); + self.searching = false; + break; + } + }); + }, + initiate_search: function (query) { + if (query === this.search_string && query !== this.current_search) { + this.search(query); + } + }, + search: function (query) { + var self = this; + this.current_search = query; + this.source({term:query}, function (results) { + if (results.length) { + self.render_search_results(results); + self.focus_element(self.$('li:first-child')); + } else { + self.close(); + } + }); + }, + render_search_results: function (results) { + var self = this; + var $list = this.$el; + $list.empty(); + results.forEach(function (result) { + var $item = self.make_list_item(result).appendTo($list); + result.$el = $item; + }); + this.show(); + }, + make_list_item: function (result) { + var self = this; + var $li = $('<li>') + .hover(function () {self.focus_element($li);}) + .mousedown(function (ev) { + if (ev.button === 0) { // left button + self.select(ev, {item: {facet: result.facet}}); + self.close(); + } + }) + .data('result', result); + if (result.expand) { + var $expand = $('<a class="o-expand" href="#">').appendTo($li); + $expand.mousedown(function (ev) { + ev.stopPropagation(); + ev.preventDefault(); // to prevent dropdown from closing + if (result.expanded) { + self.fold(); + } else { + self.expand(); + } + }); + $expand.click(function(ev) { + ev.preventDefault(); // to prevent url from changing due to href="#" + }); + result.expanded = false; + } + if (result.indent) $li.addClass('o-indent'); + $li.append($('<a href="#">').html(result.label)); + return $li; + }, + expand: function () { + var self = this; + var current_result = this.current_result; + current_result.expand(this.get_search_string()).then(function (results) { + (results || [{label: '(no result)'}]).reverse().forEach(function (result) { + result.indent = true; + var $li = self.make_list_item(result); + current_result.$el.after($li); + }); + self.current_result.expanded = true; + self.current_result.$el.find('a.o-expand').removeClass('o-expand').addClass('o-expanded'); + }); + }, + fold: function () { + var $next = this.current_result.$el.next(); + while ($next.hasClass('o-indent')) { + $next.remove(); + $next = this.current_result.$el.next(); + } + this.current_result.expanded = false; + this.current_result.$el.find('a.o-expanded').removeClass('o-expanded').addClass('o-expand'); + }, + focus_element: function ($li) { + this.$('li').removeClass('o-selection-focus'); + $li.addClass('o-selection-focus'); + this.current_result = $li.data('result'); + }, + select_item: function (ev) { + if (this.current_result.facet) { + this.select(ev, {item: {facet: this.current_result.facet}}); + this.close(); + } + }, + show: function () { + this.$el.show(); + }, + close: function () { + this.current_search = null; + this.search_string = ''; + this.searching = true; + this.$el.hide(); + }, + move: function (direction) { + var $next; + if (direction === 'down') { + $next = this.$('li.o-selection-focus').next(); + if (!$next.length) $next = this.$('li').first(); + } else { + $next = this.$('li.o-selection-focus').prev(); + if (!$next.length) $next = this.$('li').last(); + } + this.focus_element($next); + }, + is_expandable: function () { + return !!this.$('.o-selection-focus .o-expand').length; + }, +}); +}); diff --git a/addons/web/static/src/js/widgets/change_password.js b/addons/web/static/src/js/widgets/change_password.js index 658c7ee8e7d..367cf42e680 100644 --- a/addons/web/static/src/js/widgets/change_password.js +++ b/addons/web/static/src/js/widgets/change_password.js @@ -13,7 +13,7 @@ var ChangePassword = Widget.extend({ // FIXME start: function() { var self = this; web_client.set_title(_t("Change Password")); - var $button = self.$el.find('.oe_form_button'); + var $button = self.$('.oe_form_button'); $button.appendTo(this.getParent().$footer); $button.eq(1).click(function(){ self.$el.parents('.modal').modal('hide'); diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js index a73020ce9b2..4b87d45ff4a 100644 --- a/addons/web/static/src/js/widgets/data_export.js +++ b/addons/web/static/src/js/widgets/data_export.js @@ -11,212 +11,326 @@ var QWeb = core.qweb; var _t = core._t; var DataExport = Dialog.extend({ - template: 'ExportTreeView', + template: 'ExportDialog', events: { - 'click #add_field': function () { + 'click .o_expand_parent': function(e) { + this.on_expand_action(this.records[$(e.target).closest('.o_export_tree_item').data('id')]); + }, + 'click .o_export_tree_item': function(e) { + e.stopPropagation(); + var $elem = $(e.currentTarget); + + var row_index = $elem.prevAll('.o_export_tree_item').length; + var row_index_level = $elem.parents('.o_export_tree_item').length; + + if(e.shiftKey && row_index_level === this.row_index_level) { + var minIndex = Math.min(row_index, this.row_index); + var maxIndex = Math.max(row_index, this.row_index); + + this.$records.filter(function() { return ($elem.parent()[0] === $(this).parent()[0]); }) + .slice(minIndex, maxIndex+1) + .addClass('o_selected') + .filter(':not(:last)') + .each(process_children); + } + + this.row_index = row_index; + this.row_index_level = row_index_level; + + if(e.ctrlKey) { + $elem.toggleClass('o_selected').focus(); + } else if(e.shiftKey) { + $elem.addClass('o_selected').focus(); + } else { + this.$(".o_selected").removeClass("o_selected") + $elem.addClass("o_selected").focus(); + } + + function process_children() { + var $this = $(this); + if($this.hasClass('open')) { + $this.children('.o_export_tree_item') + .addClass('o_selected') + .each(process_children); + } + } + }, + 'dblclick .o_export_tree_item': function(e) { + var $elem = $(e.currentTarget); + $elem.removeClass('o_selected'); + this.add_field($elem.data('id'), $elem.find('.o_tree_column').first().text()); + }, + 'keydown .o_export_tree_item': function(e) { + e.stopPropagation(); + var $elem = $(e.currentTarget); + var record = this.records[$elem.data('id')]; + + switch(e.keyCode || e.which) { + case $.ui.keyCode.LEFT: + if ($elem.hasClass('open')) { + this.on_expand_action(record); + } + break; + case $.ui.keyCode.RIGHT: + if (!$elem.hasClass('open')) { + this.on_expand_action(record); + } + break; + case $.ui.keyCode.UP: + var $prev = $elem.prev('.o_export_tree_item'); + if($prev.length === 1) { + while($prev.hasClass('open')) { + $prev = $prev.children('.o_export_tree_item').last(); + } + } else { + $prev = $elem.parent('.o_export_tree_item'); + if($prev.length === 0) { + break; + } + } + + $elem.removeClass("o_selected").blur(); + $prev.addClass("o_selected").focus(); + break; + case $.ui.keyCode.DOWN: + var $next; + if($elem.hasClass('open')) { + $next = $elem.children('.o_export_tree_item').first(); + } else { + $next = $elem.next('.o_export_tree_item'); + if($next.length === 0) { + $next = $elem.parent('.o_export_tree_item').next('.o_export_tree_item'); + if($next.length === 0) { + break; + } + } + } + + $elem.removeClass("o_selected").blur(); + $next.addClass("o_selected").focus(); + break; + } + }, + + 'click .o_add_field': function() { var self = this; - this.$('#field-tree-structure tr.ui-selected') - .removeClass('ui-selected') - .find('a').each(function () { - var id = $(this).attr('id').split('-')[1]; - var string = $(this).attr('string'); - self.add_field(id, string); + this.$('.o_field_tree_structure .o_selected') + .removeClass('o_selected') + .each(function() { + var $this = $(this); + self.add_field($this.data('id'), $this.children('.o_tree_column').text()); }); }, - 'click #remove_field': function () { - this.$('#fields_list option:selected').remove(); + 'click .o_remove_field': function() { + this.$fields_list.find('option:selected').remove(); + }, + 'click .o_remove_all_field': function() { + this.$fields_list.empty(); + }, + 'click .o_move_up': function() { + var $selected_rows = this.$fields_list.find('option:selected'); + + var $prev_row = $selected_rows.first().prev(); + if($prev_row.length){ + $prev_row.before($selected_rows.detach()); + } + }, + 'click .o_move_down': function () { + var $selected_rows = this.$fields_list.find('option:selected'); + + var $next_row = $selected_rows.last().next(); + if($next_row.length){ + $next_row.after($selected_rows.detach()); + } }, - 'click #remove_all_field': function () { - this.$('#fields_list').empty(); + + 'click .o_toggle_save_list': function(e) { + e.preventDefault(); + + var $saveList = this.$(".o_save_list"); + if($saveList.is(':empty')) { + $saveList.append(QWeb.render('Export.SaveList')); + } else { + if($saveList.is(':hidden')) { + $saveList.show(); + $saveList.find(".o_export_list_input").val(""); + } else { + $saveList.hide(); + } + } + }, + 'click .o_save_list > button': function(e) { + var $saveList = this.$(".o_save_list"); + + var value = $saveList.find("input").val(); + if(!value) { + Dialog.alert(this, _t("Please enter save field list name")); + return; + } + + var fields = this.get_fields(); + if (fields.length === 0) { + return; + } + + $saveList.hide(); + + var self = this; + this.exports.create({ + name: value, + resource: this.dataset.model, + export_fields: _.map(fields, function (field) { + return [0, 0, {name: field}]; + }), + }).then(function(export_list_id) { + if(!export_list_id) { + return; + } + var $select = self.$(".o_exported_lists_select"); + if($select.length === 0 || $select.is(":hidden")) { + self.show_exports_list(); + } + $select.append(new Option(value, export_list_id)); + }); }, - 'click #export_new_list': 'on_show_save_list', - 'click #move_up':'on_click_move_up', - 'click #move_down':'on_click_move_down', }, init: function(parent, dataset) { - var self = this; var options = { + title: _t("Export Data"), buttons: [ - {text: _t("Close"), click: function () { self.$el.parents('.modal').modal('hide'); }}, - {text: _t("Export To File"), click: function () { self.on_click_export_data(); }} + {text: _t("Export To File"), click: this.export_data, classes: "btn-primary"}, + {text: _t("Close"), close: true}, ], - close: function () { self.close();} }; this._super(parent, options); this.records = {}; this.dataset = dataset; - this.exports = new data.DataSetSearch( - this, 'ir.exports', this.dataset.get_context()); - this.set_title(_t("Export Data")); + this.exports = new data.DataSetSearch(this, 'ir.exports', this.dataset.get_context()); + + this.row_index = 0; + this.row_index_level = 0; }, start: function() { var self = this; - this._super.apply(this, arguments); + var waitFor = [this._super.apply(this, arguments)]; + + this.$fields_list = this.$('.o_fields_list'); + this.$import_compat_radios = this.$('.o_import_compat input'); + + waitFor.push(this.rpc('/web/export/formats', {}).then(do_setup_export_formats)); var got_fields = new $.Deferred(); - this.$el.find('#import_compat').change(function() { - self.$el.find('#fields_list').empty(); - self.$el.find('#field-tree-structure').remove(); - var import_comp = self.$el.find("#import_compat").val(); + this.$import_compat_radios.change(function(e) { + self.$fields_list.empty(); + self.$('.o_field_tree_structure').remove(); + self.rpc("/web/export/get_fields", { model: self.dataset.model, - import_compat: !!import_comp, + import_compat: !!$(e.target).val(), }).done(function (records) { got_fields.resolve(); self.on_show_data(records); }); - }).change(); + }).eq(0).change(); + waitFor.push(got_fields); - var got_domain = this.getParent().get_active_domain().then(function (domain) { + waitFor.push(this.getParent().get_active_domain().then(function (domain) { if (domain === undefined) { self.ids_to_export = self.getParent().get_selected_ids(); self.domain = self.dataset.domain; - } - else { + } else { self.ids_to_export = false; self.domain = domain; } self.on_show_domain(); - }); + })); - return $.when( - got_fields, - got_domain, - this.rpc('/web/export/formats', {}).done(this.do_setup_export_formats), - this.show_exports_list()); - }, - on_click_move_up: function () { - var prev_row = this.$('#fields_list option:selected').first().prev(); - if(prev_row.length){ - var selected_rows = this.$('#fields_list option:selected').detach(); - prev_row.before(selected_rows); - } - }, - on_click_move_down: function () { - var next_row = this.$('#fields_list option:selected').last().next(); - if(next_row.length){ - var selected_rows = this.$('#fields_list option:selected').detach(); - next_row.after(selected_rows); + waitFor.push(this.show_exports_list()); + + return $.when.apply($, waitFor); + + function do_setup_export_formats(formats) { + var $fmts = self.$('.o_export_format'); + + _.each(formats, function(format, i) { + var $radio = $('<input/>', {type: 'radio', value: format.tag, name: 'o_export_format_name'}); + var $label = $('<label/>', {html: format.label}); + + if (format.error) { + $radio.prop('disabled', true); + $label.html(_.str.sprintf("%s — %s", format.label, format.error)); + } + + $fmts.append($("<div/>").append($radio, $label)); + }); + + self.$export_format_inputs = $fmts.find('input'); + self.$export_format_inputs.first().prop('checked', true); } }, - do_setup_export_formats: function (formats) { - var $fmts = this.$el.find('#export_format'); - _(formats).each(function (format) { - var opt = new Option(format.label, format.tag); - if (format.error) { - opt.disabled = true; - opt.replaceChild( - document.createTextNode( - _.str.sprintf("%s — %s", format.label, format.error)), - opt.childNodes[0]); - } - $fmts.append(opt); - }); - }, show_exports_list: function() { - var self = this; - if (self.$el.find('#saved_export_list').is(':hidden')) { - self.$el.find('#ExistsExportList').show(); + if (this.$('.o_exported_lists_select').is(':hidden')) { + this.$('.o_exported_lists').show(); return $.when(); } + + var self = this; return this.exports.read_slice(['name'], { domain: [['resource', '=', this.dataset.model]] - }).done(function (export_list) { + }).then(function (export_list) { if (!export_list.length) { return; } - self.$el.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list})); - self.$el.find('#saved_export_list').change(function() { - self.$el.find('#fields_list option').remove(); - var export_id = self.$el.find('#saved_export_list option:selected').val(); - if (export_id) { - self.rpc('/web/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id, 10)}).done(self.do_load_export_field); + self.$('.o_exported_lists').append(QWeb.render('Export.SavedList', {'existing_exports': export_list})); + self.$('.o_exported_lists_select').on('change', function() { + self.$fields_list.empty(); + var export_id = self.$('.o_exported_lists_select option:selected').val(); + if(export_id) { + self.rpc('/web/export/namelist', { + model: self.dataset.model, + export_id: parseInt(export_id, 10), + }).then(do_load_export_field); } }); - self.$el.find('#delete_export_list').click(function() { - var select_exp = self.$el.find('#saved_export_list option:selected'); - if (select_exp.val()) { + self.$('.o_delete_exported_list').click(function() { + var select_exp = self.$('.o_exported_lists_select option:selected'); + if(select_exp.val()) { self.exports.unlink([parseInt(select_exp.val(), 10)]); select_exp.remove(); - self.$el.find("#fields_list option").remove(); - if (self.$el.find('#saved_export_list option').length <= 1) { - self.$el.find('#ExistsExportList').hide(); + self.$fields_list.empty(); + if (self.$('.o_exported_lists_select option').length <= 1) { + self.$('.o_exported_lists').hide(); } } }); }); - }, - do_load_export_field: function(field_list) { - var export_node = this.$el.find("#fields_list"); - _(field_list).each(function (field) { - export_node.append(new Option(field.label, field.name)); - }); - }, - on_show_save_list: function() { - var self = this; - var current_node = self.$el.find("#savenewlist"); - if (!(current_node.find("label")).length) { - current_node.append(QWeb.render('ExportNewList')); - current_node.find("#add_export_list").click(function() { - var value = current_node.find("#savelist_name").val(); - if (value) { - self.do_save_export_list(value); - } else { - alert(_t("Please enter save field list name")); - } + + function do_load_export_field(field_list) { + _.each(field_list, function (field) { + self.$fields_list.append(new Option(field.label, field.name)); }); - } else { - if (current_node.is(':hidden')) { - current_node.show(); - current_node.find("#savelist_name").val(""); - } else { - current_node.hide(); - } } }, - do_save_export_list: function(value) { - var self = this; - var fields = self.get_fields(); - if (!fields.length) { + on_expand_action: function(record) { + if(!record['children']) { return; } - this.exports.create({ - name: value, - resource: this.dataset.model, - export_fields: _(fields).map(function (field) { - return [0, 0, {name: field}]; - }) - }).then(function (export_list_id) { - if (!export_list_id) { - return; - } - if (!self.$el.find("#saved_export_list").length || self.$el.find("#saved_export_list").is(":hidden")) { - self.show_exports_list(); - } - self.$el.find("#saved_export_list").append( new Option(value, export_list_id) ); - }); - this.on_show_save_list(); - }, - on_click: function(id, record) { - var self = this; - if (!record['children']) { - return; - } - var model = record['params']['model'], - prefix = record['params']['prefix'], - name = record['params']['name'], - exclude_fields = []; - if (record['relation_field']) { + + var model = record['params']['model']; + var prefix = record['params']['prefix']; + var name = record['params']['name']; + var exclude_fields = []; + if(record['relation_field']) { exclude_fields.push(record['relation_field']); } - if (!record.loaded) { - var import_comp = self.$el.find("#import_compat").val(); - self.rpc("/web/export/get_fields", { + if(!record.loaded) { + var self = this; + this.rpc("/web/export/get_fields", { model: model, prefix: prefix, parent_name: name, - import_compat: Boolean(import_comp), + import_compat: !!this.$import_compat_radios.filter(':checked').val(), parent_field_type : record['field_type'], exclude: exclude_fields }).done(function(results) { @@ -224,200 +338,90 @@ var DataExport = Dialog.extend({ self.on_show_data(results, record.id); }); } else { - self.showcontent(record.id); + this.show_content(record.id); } }, on_show_domain: function() { - this.$el.find('tr').first().find('td').append(QWeb.render('ExportTreeView-Domain', {'record': this})); + this.$('p').first() + .after(QWeb.render('Export.DomainMessage', {record: this})); }, - on_show_data: function(result, after) { + on_show_data: function(records, expansion) { var self = this; - - if (after) { - var current_tr = self.$el.find("tr[id='treerow-" + after + "']"); - current_tr.addClass('open'); - current_tr.find('img').attr('src','/web/static/src/img/collapse.gif'); - current_tr.after(QWeb.render('ExportTreeView-Secondary.children', {'fields': result})); + + if(expansion) { + this.$('.o_export_tree_item[data-id="' + expansion + '"]') + .addClass('open') + .find('.o_expand_parent') + .toggleClass('fa-plus fa-minus') + .next() + .after(QWeb.render('Export.TreeItems', {'fields': records})); } else { - self.$el.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result})); + this.$('.o_left_field_panel').empty().append( + $("<div/>").addClass('o_field_tree_structure') + .append(QWeb.render('Export.TreeItems', {'fields': records})) + ); } - _.each(result, function(record) { - self.records[record.id] = record.value; - if (record.required) { - var required_fld = self.$el.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); - required_fld.addClass("oe_export_requiredfield"); - } - self.$el.find("img[id='parentimg-" + record.id +"']").click(function() { - self.on_click(this.id, record); - }); - self.$el.find("tr[id='treerow-" + record.id + "']").click(function(e) { - if (e.shiftKey) { - var frst_click, scnd_click = ''; - if (self.row_index === 0) { - self.row_index = this.rowIndex; - frst_click = self.$el.find("tr[id^='treerow-']")[self.row_index-1]; - $(frst_click).addClass("ui-selected"); - } else { - var i; - if (this.rowIndex >=self.row_index) { - for (i = (self.row_index-1); i < this.rowIndex; i++) { - scnd_click = self.$el.find("tr[id^='treerow-']")[i]; - if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { - $(scnd_click).addClass("ui-selected"); - } - } - } else { - for (i = (self.row_index-1); i >= (this.rowIndex-1); i--) { - scnd_click = self.$el.find("tr[id^='treerow-']")[i]; - if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { - $(scnd_click).addClass("ui-selected"); - } - } - } - } - } - self.row_index = this.rowIndex; - - self.$el.find("tr[id='treerow-" + record.id + "']").keyup(function() { - self.row_index = 0; - }); - var o2m_selection = self.$el.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); - if ($(o2m_selection).hasClass("oe_export_readonlyfield")) { - return false; - } - if (e.ctrlKey) { - if ($(this).hasClass('ui-selected')) { - $(this).removeClass('ui-selected').find('a').blur(); - } else { - $(this).addClass('ui-selected').find('a').focus(); - } - } else if (!e.shiftKey) { - self.$el.find("tr.ui-selected") - .removeClass("ui-selected").find('a').blur(); - $(this).addClass("ui-selected").find('a').focus(); - } - return false; - }); - - self.$el.find("tr[id='treerow-" + record.id + "']").keydown(function(e) { - var keyCode = e.keyCode || e.which; - var arrow = {left: 37, up: 38, right: 39, down: 40 }; - var elem; - switch (keyCode) { - case arrow.left: - if ($(this).hasClass('open')) { - self.on_click(this.id, record); - } - break; - case arrow.right: - if (!$(this).hasClass('open')) { - self.on_click(this.id, record); - } - break; - case arrow.up: - elem = this; - $(elem).removeClass("ui-selected"); - while (!$(elem).prev().is(":visible")) { - elem = $(elem).prev(); - } - if (!$(elem).prev().find('#tree-column').hasClass("oe_export_readonlyfield")) { - $(elem).prev().addClass("ui-selected"); - } - $(elem).prev().find('a').focus(); - break; - case arrow.down: - elem = this; - $(elem).removeClass("ui-selected"); - while(!$(elem).next().is(":visible")) { - elem = $(elem).next(); - } - if (!$(elem).next().find('#tree-column').hasClass("oe_export_readonlyfield")) { - $(elem).next().addClass("ui-selected"); - } - $(elem).next().find('a').focus(); - break; - } - }); - self.$el.find("tr[id='treerow-" + record.id + "']").dblclick(function() { - var $o2m_selection = self.$el.find("tr[id^='treerow-" + record.id + "']").find('#tree-column'); - if (!$o2m_selection.hasClass("oe_export_readonlyfield")) { - self.add_field(record.id, $(this).find("a").attr("string")); - } - }); - }); - self.$el.find('#fields_list').mouseover(function(event) { - if (event.relatedTarget) { - if (event.relatedTarget.attributes['id'] && event.relatedTarget.attributes['string']) { - var field_id = event.relatedTarget.attributes["id"]["value"]; - if (field_id && field_id.split("-")[0] === 'export') { - if (!self.$el.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) { - self.add_field(field_id.split("-")[1], event.relatedTarget.attributes["string"]["value"]); - } - } - } - } + _.extend(this.records, _.object(_.pluck(records, 'id'), records)); + this.$records = this.$(".o_export_tree_item"); + this.$records.each(function(i, el) { + var $elem = $(el); + $elem.find('.o_tree_column').first().toggleClass('o_required', !!self.records[$elem.data('id')].required); }); }, - showcontent: function(id) { - // show & hide the contents - var $this = this.$el.find("tr[id='treerow-" + id + "']"); - var is_open = $this.hasClass('open'); + show_content: function(id) { + var $this = this.$('.o_export_tree_item[data-id="' + id + '"]'); $this.toggleClass('open'); + var is_open = $this.hasClass('open'); - var first_child = $this.find('img'); - if (is_open) { - first_child.attr('src', '/web/static/src/img/expand.gif'); - } else { - first_child.attr('src', '/web/static/src/img/collapse.gif'); - } - var child_field = this.$el.find("tr[id^='treerow-" + id +"/']"); + $this.children('.o_expand_parent').toggleClass('fa-minus', !!is_open).toggleClass('fa-plus', !is_open); + + var $child_field = $this.find('.o_export_tree_item'); var child_len = (id.split("/")).length + 1; - for (var i = 0; i < child_field.length; i++) { - var $child = $(child_field[i]); - if (is_open) { + for (var i = 0 ; i < $child_field.length ; i++) { + var $child = $child_field.eq(i); + if(!is_open) { $child.hide(); - } else if (child_len == (child_field[i].id.split("/")).length) { + } else if(child_len === $child_field.eq(i).data('id').split("/").length) { if ($child.hasClass('open')) { $child.removeClass('open'); - $child.find('img').attr('src', '/web/static/src/img/expand.gif'); + $child.children('.o_expand_parent').removeClass('fa-minus').addClass('fa-plus'); } $child.show(); } } }, add_field: function(field_id, string) { - var field_list = this.$el.find('#fields_list'); - field_id = this.records[field_id] || field_id; - if (this.$el.find("#fields_list option[value='" + field_id + "']") - && !this.$el.find("#fields_list option[value='" + field_id + "']").length) { - field_list.append(new Option(string, field_id)); + var $field_list = this.$('.o_fields_list'); + if($field_list.find("option[value='" + field_id + "']").length === 0) { + $field_list.append(new Option(string, field_id)); } }, get_fields: function() { - var export_fields = this.$("#fields_list option").map(function() { + var $export_fields = this.$(".o_fields_list option").map(function() { return $(this).val(); }).get(); - if (!export_fields.length) { - alert(_t("Please select fields to save export list...")); + if($export_fields.length === 0) { + Dialog.alert(this, _t("Please select fields to save export list...")); } - return export_fields; + return $export_fields; }, - on_click_export_data: function() { + export_data: function() { var self = this; - var exported_fields = this.$el.find('#fields_list option').map(function () { - // DOM property is textContent, but IE8 only knows innerText - return {name: self.records[this.value] || this.value, - label: this.textContent || this.innerText}; + var exported_fields = this.$('.o_fields_list option').map(function () { + return { + name: (self.records[this.value] || this).value, + label: this.textContent || this.innerText // DOM property is textContent, but IE8 only knows innerText + }; }).get(); if (_.isEmpty(exported_fields)) { - alert(_t("Please select fields to export...")); + Dialog.alert(this, _t("Please select fields to export...")); return; } exported_fields.unshift({name: 'id', label: 'External ID'}); - var export_format = this.$el.find("#export_format").val(); + var export_format = this.$export_format_inputs.filter(':checked').val(); framework.blockUI(); this.session.get_file({ @@ -428,16 +432,14 @@ var DataExport = Dialog.extend({ ids: this.ids_to_export, domain: this.domain, context: this.dataset.context, - import_compat: !!this.$el.find("#import_compat").val(), + import_compat: !!this.$import_compat_radios.filter(':checked').val(), })}, complete: framework.unblockUI, error: crash_manager.rpc_error.bind(crash_manager), }); }, - close: function() { - this._super(); - } }); return DataExport; + }); diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js index 74a534ec40e..d95509bb465 100644 --- a/addons/web/static/src/js/widgets/debug_manager.js +++ b/addons/web/static/src/js/widgets/debug_manager.js @@ -1,6 +1,8 @@ odoo.define('web.DebugManager', function (require) { "use strict"; +var ActionManager = require('web.ActionManager'); +var common = require('web.form_common'); var core = require('web.core'); var Dialog = require('web.Dialog'); var formats = require('web.formats'); @@ -8,210 +10,691 @@ var framework = require('web.framework'); var Model = require('web.Model'); var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); +var Tour = require('web.Tour'); var utils = require('web.utils'); var ViewManager = require('web.ViewManager'); +var WebClient = require('web.WebClient'); var Widget = require('web.Widget'); var QWeb = core.qweb; var _t = core._t; -if (core.debug) { - var DebugManager = Widget.extend({ - template: "WebClient.DebugManager", - events: { - "click .oe_debug_button": "render_dropdown", - "click .js_debug_dropdown li": "on_debug_click", - }, - start: function() { - this._super(); - this.$dropdown = this.$(".js_debug_dropdown"); - }, - /** - * Updates its attributes according to the inner_widget of the ActionManager - */ - _update: function() { - this.view_manager = odoo.__DEBUG__.services['web.web_client'].action_manager.get_inner_widget(); - if (!this.view_manager instanceof ViewManager) { return; } - this.dataset = this.view_manager.dataset; - this.active_view = this.view_manager.active_view; - if (!this.active_view) { return; } - this.view = this.active_view.controller; - return true; +var ADD = function (id) { return [4, id, false]; }, + REMOVE = function (id) { return [3, id, false]; }; +/** + * DebugManager base + general features (applicable to any context) + */ +var DebugManager = Widget.extend({ + template: "WebClient.DebugManager", + events: { + "click a[data-action]": "perform_callback", + "mouseover .o_debug_dropdowns > li:not(.open)": function(e) { + // Open other dropdowns on mouseover + var $opened = this.$('.o_debug_dropdowns > li.open'); + if($opened.length) { + $opened.removeClass('open'); + $(e.currentTarget).addClass('open').find('> a').focus(); + } }, - /** - * Renders the DebugManager dropdown - */ - render_dropdown: function() { - var self = this; + }, + init: function () { + this._super.apply(this, arguments); + // 15 fps, only actually call after sequences of queries + this._update_stats = _.throttle( + this._update_stats.bind(this), + 1000/15, {leading: false}); + this._events = null; + if (document.querySelector('meta[name=debug]')) { + this._events = []; + } + }, + start: function () { + core.bus.on('action_pushed', this, this.update); + core.bus.on('rpc:result', this, function (req, resp) { + this._debug_events(resp.debug); + }); + this.on('update-stats', this, this._update_stats); + var init; + if ((init = document.querySelector('meta[name=debug]'))) { + this._debug_events(JSON.parse(init.getAttribute('value'))); + } - // Empty the previously rendered dropdown - this.$dropdown.empty(); + this.$dropdown = this.$(".o_debug_dropdown"); + // falsy if can't write to user or couldn't find technical features + // group, otherwise features group id + this._features_group = null; + // whether group is currently enabled for current user + this._has_features = false; + // whether the current user is an administrator + this._is_admin = false; + return $.when( + new Model('res.users').call('check_access_rights', {operation: 'write', raise_exception: false}), + this.session.user_has_group('base.group_no_one'), + new Model('ir.model.data').call('xmlid_to_res_id', {xmlid: 'base.group_no_one'}), + this.session.user_has_group('base.group_system'), + this._super() + ).then(function (can_write_user, has_group_no_one, group_no_one_id, is_admin) { + this._features_group = can_write_user && group_no_one_id; + this._has_features = has_group_no_one; + this._is_admin = is_admin; + return this.update(); + }.bind(this)); + }, + leave_debug_mode: function () { + var qs = $.deparam.querystring(); + delete qs.debug; + window.location.search = '?' + $.param(qs); + }, /** + * Calls the appropriate callback when clicking on a Debug option + */ + perform_callback: function (evt) { + evt.preventDefault(); + var params = $(evt.target).data(); + var callback = params.action; - // Attempt to retrieve the inner_widget of the ActionManager - if (!this._update()) { - // Disable the button when not available - console.warn("DebugManager is not available"); - return; + if (callback && this[callback]) { + // Perform the callback corresponding to the option + this[callback](params, evt); + } else { + console.warn("No handler for ", callback); + } + }, + + _debug_events: function (events) { + if (!this._events) { return; } + if (events && events.length) { + this._events.push(events); + } + this.trigger('update-stats', this._events); + }, + requests_clear: function () { + if (!this._events) { return; } + this._events = []; + this.trigger('update-stats', this._events); + }, + _update_stats: function (rqs) { + var requests = 0, rtime = 0, queries = 0, qtime = 0; + for(var r = 0; r < rqs.length; ++r) { + for (var i = 0; i < rqs[r].length; i++) { + var event = rqs[r][i]; + var query_start, request_start; + switch (event[0]) { + case 'request-start': + request_start = event[3] * 1e3; + break; + case 'request-end': + ++requests; + rtime += (event[3] * 1e3 - request_start) | 0; + break; + case 'sql-start': + query_start = event[3] * 1e3; + break; + case 'sql-end': + ++queries; + qtime += (event[3] * 1e3 - query_start) | 0; + break; + } } + } + this.$('#debugmanager_requests_stats').text( + _.str.sprintf(_t("%d requests (%d ms) %d queries (%d ms)"), + requests, rtime, queries, qtime)); + }, + show_timelines: function () { + if (this._overlay) { + this._overlay.destroy(); + this._overlay = null; + return; + } + this._overlay = new RequestsOverlay(this); + this._overlay.appendTo(document.body); + }, - this.session.user_has_group('base.group_system').then(function(is_admin) { - // Render the dropdown and append it - var dropdown_content = QWeb.render('WebClient.DebugDropdown', { - widget: self, - active_view: self.active_view, - view: self.view, - action: self.view_manager.action, - searchview: self.view_manager.searchview, - is_admin: is_admin, - }); - $(dropdown_content).appendTo(self.$dropdown); - }); - }, - /** - * Calls the appropriate callback when clicking on a Debug option - */ - on_debug_click: function (evt) { - evt.preventDefault(); - - var params = $(evt.target).data(); - var callback = params.action; - - if (callback && this[callback]) { - // Perform the callback corresponding to the option - this[callback](params, evt); - } else { - console.warn("No debug handler for ", callback); + /** + * Update the debug manager: reinserts all "universal" controls + */ + update: function () { + this.$dropdown + .empty() + .append(QWeb.render('WebClient.DebugManager.Global', { + manager: this, + })); + return $.when(); + }, + start_tour: function() { + var dialog = new Dialog(this, { + title: 'Tours', + $content: QWeb.render('WebClient.DebugManager.ToursDialog', { + tours: Tour.tours + }), + }).open(); + + dialog.$('.o_start_tour').on('click', function(e) { + e.preventDefault(); + odoo.__DEBUG__.services['web.Tour'].run($(e.target).data('id')); + }); + }, + select_view: function () { + var self = this; + new common.SelectCreateDialog(this, { + res_model: 'ir.ui.view', + title: _t('Select a view'), + disable_multiple_selection: true, + on_selected: function (element_ids) { + new Model('ir.ui.view') + .query(['name', 'model', 'type']) + .filter([['id', '=', element_ids[0]]]) + .first() + .then(function (view) { + self.do_action({ + type: 'ir.actions.act_window', + name: view.name, + res_model: view.model, + views: [[view.id, view.type]] + }); + }); } - }, - get_metadata: function() { - var ds = this.dataset; - ds.call('get_metadata', [this.view.get_selected_ids()]).done(function(result) { - new Dialog(this, { - title: _.str.sprintf(_t("Metadata (%s)"), ds.model), - size: 'medium', - $content: QWeb.render('WebClient.DebugViewLog', { - perm : result[0], - format : formats.format_value - }) - }).open(); - }); - }, - toggle_layout_outline: function() { - this.view.rendering_engine.toggle_layout_debugging(); - }, - set_defaults: function() { - this.view.open_defaults_dialog(); - }, - perform_js_tests: function() { - this.do_action({ - name: _t("JS Tests"), - target: 'new', - type : 'ir.actions.act_url', - url: '/web/tests?mod=*' + }).open(); + }, + perform_js_tests: function () { + this.do_action({ + name: _t("JS Tests"), + target: 'new', + type: 'ir.actions.act_url', + url: '/web/tests?mod=*' + }); + }, + toggle_technical_features: function () { + if (!this._features_group) { return; } + var command = this._has_features ? REMOVE(this._features_group) : ADD(this._features_group); + new Model('res.users').call('write', [session.uid, { + groups_id: [command] + }]).then(function () { + window.location.reload(); + }); + }, + split_assets: function() { + window.location = $.param.querystring(window.location.href, 'debug=assets'); + }, +}); + +/** + * DebugManager features depending on having an action, and possibly a model + * (window action) + */ +DebugManager.include({ + /** + * Updates current action (action descriptor) on tag = action, + */ + update: function (tag, descriptor) { + if (tag === 'action') { + this._action = descriptor; + } + return this._super().then(function () { + this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.Action', { + manager: this, + action: this._action + })); + }.bind(this)); + }, + edit: function (params, evt) { + this.do_action({ + res_model: params.model, + res_id: params.id, + name: evt.target.text, + type: 'ir.actions.act_window', + views: [[false, 'form']], + view_mode: 'form', + target: 'new', + flags: {action_buttons: true, headless: true} + }); + }, + get_view_fields: function () { + var self = this; + var model = this._action.res_model; + new Model(model).call('fields_get', { + attributes: ['string', 'searchable', 'required', 'readonly', 'type', 'store', 'sortable', 'relation', 'help'] + }).done(function (fields) { + new Dialog(self, { + title: _.str.sprintf(_t("Fields of %s"), model), + $content: $(QWeb.render('WebClient.DebugManager.Action.Fields', { + fields: fields + })) + }).open(); + }); + }, + manage_filters: function () { + this.do_action({ + res_model: 'ir.filters', + name: _t('Manage Filters'), + views: [[false, 'list'], [false, 'form']], + type: 'ir.actions.act_window', + context: { + search_default_my_filters: true, + search_default_model_id: this._action.res_model + } + }); + }, + edit_workflow: function () { + return this.do_action({ + res_model: 'workflow', + name: _t('Edit Workflow'), + domain: [['osv', '=', this._action.res_model]], + views: [[false, 'list'], [false, 'form'], [false, 'diagram']], + type: 'ir.actions.act_window', + view_type: 'list', + view_mode: 'list' + }); + }, + translate: function() { + var model = this._action.res_model; + new Model("ir.translation") + .call('get_technical_translations', [model]) + .then(this.do_action); + } +}); + +/** + * DebugManager features depending on having a form view or single record. + * These could theoretically be split, but for now they'll be considered one + * and the same. + */ +DebugManager.include({ + start: function () { + this._can_edit_views = false; + return $.when( + this._super(), + new Model('ir.ui.view').call( + 'check_access_rights', {operation: 'write', raise_exception: false} + ).then(function (ar) { + this._can_edit_views = ar; + }.bind(this)) + ); + }, + update: function (tag, descriptor, widget) { + switch (tag) { + case 'action': + if (!(widget instanceof ViewManager)) { + this._active_view = null; + this._view_manager = null; + break; + } + this._view_manager = widget; + widget.on('switch_mode', this, function () { + this.update('view', null, widget); }); - }, - get_view_fields: function() { - var self = this; - self.dataset.call('fields_get', [false, {}]).done(function (fields) { - var $root = $('<dl>'); - _(fields).each(function (attributes, name) { - $root.append($('<dt>').append($('<h4>').text(name))); - var $attrs = $('<dl>').appendTo($('<dd>').appendTo($root)); - _(attributes).each(function (def, name) { - if (def instanceof Object) { - def = JSON.stringify(def); - } - $attrs - .append($('<dt>').text(name)) - .append($('<dd style="white-space: pre-wrap;">').text(def)); - }); + case 'view': + this._active_view = widget.active_view; + } + + return this._super(tag, descriptor).then(function () { + this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.View', { + manager: this, + action: this._action, + view: this._active_view, + can_edit: this._can_edit_views, + searchview: this._view_manager && this._view_manager.searchview, + })); + }.bind(this)); + }, + + get_metadata: function() { + var ds = this._view_manager.dataset; + ds.call('get_metadata', [this._active_view.controller.get_selected_ids()]).done(function(result) { + new Dialog(this, { + title: _.str.sprintf(_t("Metadata (%s)"), ds.model), + size: 'medium', + $content: QWeb.render('WebClient.DebugViewLog', { + perm : result[0], + format : formats.format_value + }) + }).open(); + }); + }, + set_defaults: function() { + this._active_view.controller.open_defaults_dialog(); + }, + fvg: function() { + var dialog = new Dialog(this, { title: _t("Fields View Get") }).open(); + $('<pre>').text(utils.json_node_to_xml( + this._active_view.controller.fields_view.arch, true) + ).appendTo(dialog.$el); + }, + print_workflow: function() { + var ids = this._active_view.controller.get_selected_ids(); + framework.blockUI(); + var action = { + context: { active_ids: ids }, + report_name: "workflow.instance.graph", + datas: { + model: this._view_manager.dataset.model, + id: ids[0], + nested: true, + } + }; + this.session.get_file({ + url: '/web/report', + data: {action: JSON.stringify(action)}, + complete: framework.unblockUI + }); + }, +}); +function make_context(width, height, fn) { + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + // make e.layerX/e.layerY imitate e.offsetX/e.offsetY. + canvas.style.position = 'relative'; + var ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = false; + ctx.mozImageSmoothingEnabled = false; + ctx.oImageSmoothingEnabled = false; + ctx.webkitImageSmoothingEnabled = false; + fn && fn(ctx); + return ctx; +} +var RequestsOverlay = Widget.extend({ + template: 'WebClient.DebugManager.RequestsOverlay', + TRACKS: 8, + TRACK_WIDTH: 9, + events: { + mousemove: function (e) { + this.$tooltip.hide(); + } + }, + init: function () { + this._super.apply(this, arguments); + this._render = _.throttle( + this._render.bind(this), + 1000/15, {leading: false} + ); + }, + start: function () { + var _super = this._super(); + this.$tooltip = this.$('div.o_debug_tooltip'); + this.getParent().on('update-stats', this, this._render); + this._render(); + return _super; + }, + tooltip: function (text, start, end, x, y) { + // x and y are hit point with respect to the viewport. To know where + // this hit point is with respect to the overlay, subtract the offset + // between viewport and overlay, then add scroll factor of overlay + // (which isn't taken in account by the viewport). + // + // Normally the viewport overlay should sum offsets of all + // offsetParents until we reach `null` but in this case the overlay + // should have been added directly to the body, which should have an + // offset of 0. + + var top = y - this.el.offsetTop + this.el.scrollTop + 1; + var left = x - this.el.offsetLeft + this.el.scrollLeft + 1; + this.$tooltip.css({top: top, left: left}).show()[0].innerHTML = ['<p>', text, ' (', (end - start), 'ms)', '</p>'].join(''); + }, + + _render: function () { + var $summary = this.$('header'), + w = $summary[0].clientWidth, + $requests = this.$('.o_debug_requests'); + $summary.find('canvas').attr('width', w); + var tracks = document.getElementById('o_debug_requests_summary'); + + _.invoke(this.getChildren(), 'destroy'); + + var requests = this.getParent()._events; + var bounds = this._get_bounds(requests); + // horizontal scaling factor for summary + var scale = w / (bounds.high - bounds.low); + + // store end-time of "current" requests, to find out which track a + // request should go in, just look for the first track whose end-time + // is smaller than the new request's start time. + var track_ends = _(this.TRACKS).times(_.constant(-Infinity)); + + var ctx = tracks.getContext('2d'); + ctx.lineWidth = this.TRACK_WIDTH; + for (var i = 0; i < requests.length; i++) { + var request = requests[i]; + // FIXME: is it certain that events in the request are sorted by timestamp? + var rstart = Math.floor(request[0][3] * 1e3); + var rend = Math.ceil(request[request.length - 1][3] * 1e3); + // find free track for current request + for(var track=0; track < track_ends.length; ++track) { + if (track_ends[track] < rstart) { break; } + } + // FIXME: display error message of some sort? Re-render with larger area? Something? + if (track >= track_ends.length) { + console.warn("could not find an empty summary track"); + continue; + } + // set new track end + track_ends[track] = rend; + ctx.save(); + ctx.translate(Math.floor((rstart - bounds.low) * scale), track * (this.TRACK_WIDTH + 1)); + this._draw_request(request, ctx, 0, scale); + ctx.restore(); + new RequestDetails(this, request, scale).appendTo($requests); + } + }, + _draw_request: function (request, to_context, step, hscale, handle_event) { + // have one draw surface for each event type: + // * no need to alter context from one event to the next, each surface + // gets its own color for all its lifetime + // * surfaces can be blended in a specified order, which means events + // can be drawn in any order, no need to care about z-index while + // serializing events to the surfaces + var surfaces = { + request: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { + ctx.strokeStyle = 'blue'; + ctx.fillStyle = '#88f'; + ctx.lineJoin = 'round'; + ctx.lineWidth = 1; + }), + //func: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { + // ctx.strokeStyle = 'gray'; + // ctx.lineWidth = to_context.lineWidth; + // ctx.translate(0, initial_offset); + //}), + sql: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { + ctx.strokeStyle = 'red'; + ctx.fillStyle = '#f88'; + ctx.lineJoin = 'round'; + ctx.lineWidth = 1; + }), + template: make_context(to_context.canvas.width, to_context.canvas.height, function (ctx) { + ctx.strokeStyle = 'green'; + ctx.fillStyle = '#8f8'; + ctx.lineJoin = 'round'; + ctx.lineWidth = 1; + }) + }; + // apply scaling manually so zooming in improves display precision + var stacks = {}, start = Math.floor(request[0][3] * 1e3 * hscale); + var event_idx = 0; + + var rect_width = to_context.lineWidth; + for (var i = 0; i < request.length; i++) { + var type, m, event = request[i]; + var tag = event[0], timestamp = Math.floor(event[3] * 1e3 * hscale) - start; + + if (m = /(\w+)-start/.exec(tag)) { + type = m[1]; + if (!(type in stacks)) { stacks[type] = []; } + handle_event && handle_event(event_idx, timestamp, event); + stacks[type].push({ + timestamp: timestamp, + idx: event_idx++ }); - new Dialog(self, { - title: _.str.sprintf(_t("Model %s fields"), self.dataset.model), - $content: $root - }).open(); - }); - }, - fvg: function() { - var dialog = new Dialog(this, { title: _t("Fields View Get") }).open(); - $('<pre>').text(utils.json_node_to_xml(this.view.fields_view.arch, true)).appendTo(dialog.$el); - }, - manage_filters: function() { - this.do_action({ - res_model: 'ir.filters', - name: _t('Manage Filters'), - views: [[false, 'list'], [false, 'form']], - type: 'ir.actions.act_window', - context: { - search_default_my_filters: true, - search_default_model_id: this.dataset.model - } - }); - }, - translate: function() { - new Model("ir.translation") - .call('get_technical_translations', [this.dataset.model]) - .then(this.do_action); + } else if (m = /(\w+)-end/.exec(tag)) { + type = m[1]; + var stack = stacks[type]; + var estart = stack.pop(), duration = Math.ceil(timestamp - estart.timestamp); + handle_event && handle_event(estart.idx, timestamp, event); + + var surface = surfaces[type]; + if (!surface) { continue; } // FIXME: support for unknown event types + + var y = step * estart.idx; + // path rectangle for the current event on the relevant surface + surface.rect(estart.timestamp + 0.5, y + 0.5, duration || 1, rect_width); + } + } + // add each layer to the main canvas + var keys = ['request', /*'func', */'template', 'sql']; + for (var j = 0; j < keys.length; ++j) { + // stroke and fill all rectangles for the relevant surface/context + var ctx = surfaces[keys[j]]; + ctx.fill(); + ctx.stroke(); + to_context.drawImage(ctx.canvas, 0, 0); + } + }, + /** + * Returns first and last events in milliseconds + * + * @param requests + * @returns {{low: number, high: number}} + * @private + */ + _get_bounds: function (requests) { + var low = +Infinity; + var high =-+Infinity; + + for (var i = 0; i < requests.length; i++) { + var request = requests[i]; + for (var j = 0; j < request.length; j++) { + var event = request[j]; + var timestamp = event[3]; + low = Math.min(low, timestamp); + high = Math.max(high, timestamp); + } + } + return {low: Math.floor(low * 1e3), high: Math.ceil(high * 1e3)}; + } +}); +var RequestDetails = Widget.extend({ + events: { + click: function () { + this._open = !this._open; + this.render(); }, - edit: function(params, evt) { - this.do_action({ - res_model : params.model, - res_id : params.id, - name: evt.target.text, - type : 'ir.actions.act_window', - view_type : 'form', - view_mode : 'form', - views : [[false, 'form']], - target : 'new', - flags : { - action_buttons : true, - headless: true, + 'mousemove canvas': function (e) { + e.stopPropagation(); + var y = e.y || e.offsetY || e.layerY; + if (!y) { return; } + var event = this._payloads[Math.floor(y / this._REQ_HEIGHT)]; + if (!event) { return; } + + this.getParent().tooltip(event.payload, event.start, event.stop, e.clientX, e.clientY); + } + }, + init: function (parent, request, scale) { + this._super.apply(this, arguments); + this._request = request; + this._open = false; + this._scale = scale; + this._REQ_HEIGHT = 20; + }, + start: function () { + this.el.style.borderBottom = '1px solid black'; + this.render(); + return this._super(); + }, + render: function () { + var request_cell_height = this._REQ_HEIGHT, TITLE_WIDTH = 200; + var request = this._request; + var req_start = request[0][3] * 1e3; + var req_duration = request[request.length - 1][3] * 1e3 - req_start; + var height = request_cell_height * (this._open ? request.length / 2 : 1); + var cell_center = request_cell_height / 2; + var ctx = make_context(210 + Math.ceil(req_duration * this._scale), height, function (ctx) { + ctx.lineWidth = cell_center; + }); + this.$el.empty().append(ctx.canvas); + var payloads = this._payloads = []; + + // lazy version: if the render is single-line (!this._open), the extra + // content will be discarded when the text canvas gets pasted onto the + // main canvas. An improvement would be to not do text rendering + // beyond the first event for "closed" requests events… then again + // that makes for more regular rendering profile? + var text_ctx = make_context(TITLE_WIDTH, height, function (ctx) { + ctx.font = '12px sans-serif'; + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + ctx.translate(0, cell_center); + }); + + ctx.save(); + ctx.translate(TITLE_WIDTH + 10, ((request_cell_height/4)|0)); + + this.getParent()._draw_request(request, ctx, this._open ? request_cell_height : 0, this._scale, function (idx, timestamp, event) { + if (/-start$/g.test(event[0])) { + payloads.push({ + payload: event[2], + start: timestamp, + stop: null + }); + + // we want ~200px wide, assume the average character is at + // least 4px wide => there can be *at most* 49 characters + var title = event[2]; + title = title.replace(/\s+$/, ''); + title = title.length <= 50 ? title : ('…' + title.slice(-49)); + while (text_ctx.measureText(title).width > 200) { + title = '…' + title.slice(2); } + text_ctx.fillText(title, TITLE_WIDTH, request_cell_height * idx); + } else if (/-end$/g.test(event[0])) { + payloads[idx].stop = timestamp; + } + }); + ctx.restore(); + // add the text layer to the main canvas + ctx.drawImage(text_ctx.canvas, 0, 0); + } +}); + +if (core.debug) { + SystrayMenu.Items.push(DebugManager); + + WebClient.include({ + start: function() { + var self = this; + return this._super().then(function () { + // Override push_action so that it triggers an event each time a new action is pushed + // The DebugManager listens to this event to keep itself up-to-date + var push_action = self.action_manager.push_action; + self.action_manager.push_action = function(widget, descr) { + return push_action.apply(self.action_manager, arguments).then(function () { + core.bus.trigger('action_pushed', 'action', descr, widget); + }); + }; }); }, - edit_workflow: function() { - return this.do_action({ - res_model : 'workflow', - name: _t('Edit Workflow'), - domain : [['osv', '=', this.dataset.model]], - views: [[false, 'list'], [false, 'form'], [false, 'diagram']], - type : 'ir.actions.act_window', - view_type : 'list', - view_mode : 'list' - }); - }, - print_workflow: function() { - if (this.view.get_selected_ids && this.view.get_selected_ids().length == 1) { - framework.blockUI(); - var action = { - context: { active_ids: this.view.get_selected_ids() }, - report_name: "workflow.instance.graph", - datas: { - model: this.dataset.model, - id: this.view.get_selected_ids()[0], - nested: true, - } - }; - this.session.get_file({ - url: '/web/report', - data: {action: JSON.stringify(action)}, - complete: framework.unblockUI + }); + + Dialog.include({ + open: function() { + var self = this; + + var parent = self.getParent(); + if (parent instanceof ActionManager && parent.dialog_widget) { + // Instantiate the DebugManager and insert it into the DOM once dialog is opened + this.opened(function() { + self.debug_manager = new DebugManager(self); + var $header = self.$modal.find('.modal-header'); + return self.debug_manager.prependTo($header).then(function() { + self.debug_manager.update('action', parent.dialog_widget.action, parent.dialog_widget); + }); }); - } else { - this.do_warn("Warning", "No record selected."); } - }, - split_assets: function() { - window.location = $.param.querystring(window.location.href, 'debug=assets'); - }, - leave_debug: function() { - window.location.search="?"; + + return this._super.apply(this, arguments); }, }); - - SystrayMenu.Items.push(DebugManager); - - return DebugManager; } +return DebugManager; + }); diff --git a/addons/web/static/src/js/widgets/loading.js b/addons/web/static/src/js/widgets/loading.js index 4ccabcc199e..645287ecf80 100644 --- a/addons/web/static/src/js/widgets/loading.js +++ b/addons/web/static/src/js/widgets/loading.js @@ -9,7 +9,8 @@ var Widget = require('web.Widget'); var _t = core._t; var Loading = Widget.extend({ - template: _t("Loading"), + template: "Loading", + init: function(parent) { this._super(parent); this.count = 0; diff --git a/addons/web/static/src/js/widgets/pager.js b/addons/web/static/src/js/widgets/pager.js index 21f3957ca97..d9b37dc889a 100644 --- a/addons/web/static/src/js/widgets/pager.js +++ b/addons/web/static/src/js/widgets/pager.js @@ -28,6 +28,7 @@ var Pager = Widget.extend({ * @param {int} [limit] the number of elements per page * @param {boolean} [options.can_edit] editable feature of the pager * @param {boolean} [options.single_page_hidden] (not) to display the pager if only one page + * @param {function} [options.validate] callback returning a Deferred to validate changes */ init: function (parent, size, current_min, limit, options) { this.state = { @@ -43,6 +44,9 @@ var Pager = Widget.extend({ this.options = _.defaults({}, options, { can_edit: true, // editable single_page_hidden: false, // displayed even if there is a single page + validate: function() { + return $.Deferred().resolve(); + }, }); this._super(parent); }, @@ -101,7 +105,7 @@ var Pager = Widget.extend({ _edit: function() { if (this.options.can_edit) { var self = this; - var $input = $('<input>').val(this.$value.html()); + var $input = $('<input>', {type: 'text', value: this.$value.html()}); this.$value.html($input); $input.focus(); @@ -129,23 +133,26 @@ var Pager = Widget.extend({ * @param {jQuery} [$input] the jQuery element containing the new state */ _save: function($input) { - var value = $input.val().split("-"); - var min = utils.confine(parseInt(value[0], 10), 1, this.state.size); - var max = utils.confine(parseInt(value[1], 10), 1, this.state.size); - - if (!isNaN(min)) { - this.state.current_min = min; - if (!isNaN(max)) { - this.state.limit = utils.confine(max-min+1, 1, this.state.size); - } else { - // The state has been given as a single value -> set the limit to 1 - this.state.limit = 1; + var self = this; + this.options.validate().then(function() { + var value = $input.val().split("-"); + var min = utils.confine(parseInt(value[0], 10), 1, self.state.size); + var max = utils.confine(parseInt(value[1], 10), 1, self.state.size); + + if (!isNaN(min)) { + self.state.current_min = min; + if (!isNaN(max)) { + self.state.limit = utils.confine(max-min+1, 1, self.state.size); + } else { + // The state has been given as a single value -> set the limit to 1 + self.state.limit = 1; + } + self.trigger('pager_changed', _.clone(self.state)); } - this.trigger('pager_changed', _.clone(this.state)); - } - - // Render the pager's new state (removes the input) - this._render(); + }).always(function() { + // Render the pager's new state (removes the input) + self._render(); + }); }, /** @@ -153,23 +160,26 @@ var Pager = Widget.extend({ * @param {int} [direction] the action (previous or next) on the pager */ _change_selection: function (direction) { - var size = this.state.size; - var current_min = this.state.current_min; - var limit = this.state.limit; - - // Compute the new current_min - current_min = (current_min + limit*direction); - if (current_min > size) { - current_min = 1; - } else if ((current_min < 1) && (limit === 1)) { - current_min = size; - } else if ((current_min < 1) && (limit > 1)) { - current_min = size - ((size % limit) || limit) + 1; - } + var self = this; + this.options.validate().then(function() { + var size = self.state.size; + var current_min = self.state.current_min; + var limit = self.state.limit; + + // Compute the new current_min + current_min = (current_min + limit*direction); + if (current_min > size) { + current_min = 1; + } else if ((current_min < 1) && (limit === 1)) { + current_min = size; + } else if ((current_min < 1) && (limit > 1)) { + current_min = size - ((size % limit) || limit) + 1; + } - this.state.current_min = current_min; - this.trigger('pager_changed', _.clone(this.state)); - this._render(); + self.state.current_min = current_min; + self.trigger('pager_changed', _.clone(self.state)); + self._render(); + }); }, /** diff --git a/addons/web/static/src/js/widgets/sidebar.js b/addons/web/static/src/js/widgets/sidebar.js index 9ea15be128b..e2222526320 100644 --- a/addons/web/static/src/js/widgets/sidebar.js +++ b/addons/web/static/src/js/widgets/sidebar.js @@ -6,7 +6,6 @@ var data = require('web.data'); var Dialog = require('web.Dialog'); var framework = require('web.framework'); var pyeval = require('web.pyeval'); -var session = require('web.session'); var Widget = require('web.Widget'); var QWeb = core.qweb; @@ -37,6 +36,7 @@ var Sidebar = Widget.extend({ start: function() { var self = this; this._super(this); + this.$el.addClass('btn-group'); this.redraw(); this.$el.on('click','.dropdown-menu li a', function(event) { var section = $(this).data('section'); @@ -53,16 +53,19 @@ var Sidebar = Widget.extend({ }); }, redraw: function() { - var self = this; - self.$el.html(QWeb.render('Sidebar', {widget: self})); + this.$el.html(QWeb.render('Sidebar', {widget: this})); // Hides Sidebar sections when item list is empty - this.$('.oe_form_dropdown_section').each(function() { - $(this).toggle(!!$(this).find('li').length); + this.$('.o_dropdown').each(function() { + if (!$(this).find('li').length) { + $(this).hide(); + } }); - self.$("[title]").tooltip({ + this.$("[title]").tooltip({ delay: { show: 500, hide: 0} }); + this.$('.o_sidebar_add_attachment .o_form_binary_form').change(this.on_attachment_changed); + this.$('.o_sidebar_delete_attachment').click(this.on_attachment_delete); }, /** * For each item added to the section: @@ -104,7 +107,6 @@ var Sidebar = Widget.extend({ items[i] = { label: items[i]['name'], action: items[i], - classname: 'oe_sidebar_' + type }; } self.add_items(type=='print' ? 'print' : 'other', items); @@ -184,16 +186,14 @@ var Sidebar = Widget.extend({ }); this.items.files = attachments; this.redraw(); - this.$('.oe_sidebar_add_attachment .o_form_input_file').change(this.on_attachment_changed); - this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete); }, on_attachment_changed: function(e) { var $e = $(e.target); if ($e.val() !== '') { - this.$el.find('form.o_form_binary_form').submit(); + this.$('form.o_form_binary_form').submit(); $e.parent().find('input[type=file]').prop('disabled', true); $e.parent().find('button').prop('disabled', true).find('img, span').toggle(); - this.$('.oe_sidebar_add_attachment a').text(_t('Uploading...')); + this.$('.o_sidebar_add_attachment a').text(_t('Uploading...')); framework.blockUI(); } }, @@ -202,11 +202,16 @@ var Sidebar = Widget.extend({ e.stopPropagation(); var self = this; var $e = $(e.currentTarget); - if (confirm(_t("Do you really want to delete this attachment ?"))) { - (new data.DataSet(this, 'ir.attachment')).unlink([parseInt($e.attr('data-id'), 10)]).done(function() { - self.do_attachement_update(self.dataset, self.model_id); - }); - } + var options = { + confirm_callback: function () { + new data.DataSet(this, 'ir.attachment') + .unlink([parseInt($e.attr('data-id'), 10)]) + .done(function() { + self.do_attachement_update(self.dataset, self.model_id); + }); + } + }; + Dialog.confirm(this, _t("Do you really want to delete this attachment ?"), options); } }); diff --git a/addons/web/static/src/js/widgets/switch_company_menu.js b/addons/web/static/src/js/widgets/switch_company_menu.js index 2879a0a2b0d..89dc6ae29b3 100644 --- a/addons/web/static/src/js/widgets/switch_company_menu.js +++ b/addons/web/static/src/js/widgets/switch_company_menu.js @@ -1,45 +1,46 @@ odoo.define('web.SwitchCompanyMenu', function(require) { - "use strict"; +"use strict"; - var Model = require('web.Model'); - var session = require('web.session'); - var SystrayMenu = require('web.SystrayMenu'); - var Widget = require('web.Widget'); - - var SwitchCompanyMenu = Widget.extend({ - template: 'SwitchCompanyMenu', - willStart: function() { - if (!session.user_companies) { - return $.Deferred().reject(); - } - return this._super(); - }, - start: function() { - var self = this; - this.$el.on('click', '.dropdown-menu li a[data-menu]', _.debounce(function(ev) { - ev.preventDefault(); - var company_id = $(ev.currentTarget).data('company-id'); - new Model('res.users').call('write', [[session.uid], {'company_id': company_id}]).then(function() { - location.reload(); - }); - }, 1500, true)); - - self.$('.oe_topbar_name').text(session.user_companies.current_company[1]); +var Model = require('web.Model'); +var session = require('web.session'); +var SystrayMenu = require('web.SystrayMenu'); +var Widget = require('web.Widget'); - var companies_list = ''; - _.each(session.user_companies.allowed_companies, function(company) { - var a = ''; - if(company[0] == session.user_companies.current_company[0]) { - a = '<i class="fa fa-check o_current_company"></i>'; - } else { - a = '<span class="o_company"/>'; - } - companies_list += '<li><a href="#" data-menu="company" data-company-id="' + company[0] + '">' + a + company[1] + '</a></li>'; +var SwitchCompanyMenu = Widget.extend({ + template: 'SwitchCompanyMenu', + willStart: function() { + if (!session.user_companies) { + return $.Deferred().reject(); + } + return this._super(); + }, + start: function() { + var self = this; + this.$el.on('click', '.dropdown-menu li a[data-menu]', _.debounce(function(ev) { + ev.preventDefault(); + var company_id = $(ev.currentTarget).data('company-id'); + new Model('res.users').call('write', [[session.uid], {'company_id': company_id}]).then(function() { + location.reload(); }); - self.$('.dropdown-menu').html(companies_list); - return this._super(); - }, - }); + }, 1500, true)); + + self.$('.oe_topbar_name').text(session.user_companies.current_company[1]); + + var companies_list = ''; + _.each(session.user_companies.allowed_companies, function(company) { + var a = ''; + if (company[0] === session.user_companies.current_company[0]) { + a = '<i class="fa fa-check o_current_company"></i>'; + } else { + a = '<span class="o_company"/>'; + } + companies_list += '<li><a href="#" data-menu="company" data-company-id="' + company[0] + '">' + a + company[1] + '</a></li>'; + }); + self.$('.dropdown-menu').html(companies_list); + return this._super(); + }, +}); + +SystrayMenu.Items.push(SwitchCompanyMenu); - SystrayMenu.Items.push(SwitchCompanyMenu); }); diff --git a/addons/web/static/src/js/widgets/user_menu.js b/addons/web/static/src/js/widgets/user_menu.js index 31728dfa813..edeb059790a 100644 --- a/addons/web/static/src/js/widgets/user_menu.js +++ b/addons/web/static/src/js/widgets/user_menu.js @@ -4,7 +4,7 @@ odoo.define('web.UserMenu', function (require) { var core = require('web.core'); var Dialog = require('web.Dialog'); var framework = require('web.framework'); -var Model = require('web.DataModel'); +var Model = require('web.Model'); var session = require('web.session'); var Widget = require('web.Widget'); @@ -13,10 +13,6 @@ var QWeb = core.qweb; var UserMenu = Widget.extend({ template: "UserMenu", - init: function(parent) { - this._super(parent); - this.update_promise = $.Deferred().resolve(); - }, start: function() { var self = this; this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) { @@ -26,32 +22,28 @@ var UserMenu = Widget.extend({ f($(this)); } }); - this.$el.parent().show(); - return this._super.apply(this, arguments); + return this._super.apply(this, arguments).then(function () { + return self.do_update(); + }); }, do_update: function () { var self = this; - var fct = function() { - var $avatar = self.$el.find('.oe_topbar_avatar'); - $avatar.attr('src', $avatar.data('default-src')); - if (!session.uid) - return; - var func = new Model("res.users").get_func("read"); - return self.alive(func(session.uid, ["name"])).then(function(res) { - var topbar_name = res.name; - if(session.debug) - topbar_name = _.str.sprintf("%s (%s)", topbar_name, session.db); - self.$el.find('.oe_topbar_name').text(topbar_name); - if (!session.debug) { - topbar_name = _.str.sprintf("%s (%s)", topbar_name, session.db); - } - var avatar_src = session.url('/web/image', {model:'res.users', field: 'image_small', id: session.uid}); - $avatar.attr('src', avatar_src); + var $avatar = self.$('.oe_topbar_avatar'); + $avatar.attr('src', $avatar.data('default-src')); + if (!session.uid) { + return $.when(); + } + var func = new Model("res.users").get_func("read"); + return self.alive(func(session.uid, ["name"])).then(function(res) { + var topbar_name = res.name; + if(session.debug) { + topbar_name = _.str.sprintf("%s (%s)", topbar_name, session.db); + } + self.$('.oe_topbar_name').text(topbar_name); - core.bus.trigger('resize'); // Re-trigger the reflow logic - }); - }; - this.update_promise = this.update_promise.then(fct, fct); + var avatar_src = session.url('/web/image', {model:'res.users', field: 'image_small', id: session.uid}); + $avatar.attr('src', avatar_src); + }); }, on_menu_documentation: function () { window.open('https://www.odoo.com/documentation/user', '_blank'); @@ -83,36 +75,42 @@ var UserMenu = Widget.extend({ }, on_menu_settings: function() { var self = this; - this.getParent().clear_uncommitted_changes().then(function() { - self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) { - result.res_id = session.uid; - self.getParent().action_manager.do_action(result); - }); + this.trigger_up('clear_uncommitted_changes', { + callback: function() { + self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) { + result.res_id = session.uid; + self.do_action(result); + }); + }, }); }, on_menu_account: function() { - this.getParent().clear_uncommitted_changes().then(function() { - var P = new Model('ir.config_parameter'); - P.call('get_param', ['database.uuid']).then(function(dbuuid) { - var state = { - 'd': session.db, - 'u': window.location.protocol + '//' + window.location.host, - }; - var params = { - response_type: 'token', - client_id: dbuuid || '', - state: JSON.stringify(state), - scope: 'userinfo', - }; - framework.redirect('https://accounts.odoo.com/oauth2/auth?'+$.param(params)); - }).fail(function(result, ev){ - ev.preventDefault(); - framework.redirect('https://accounts.odoo.com/account'); - }); + this.trigger_up('clear_uncommitted_changes', { + callback: function() { + var P = new Model('ir.config_parameter'); + P.call('get_param', ['database.uuid']).then(function(dbuuid) { + var state = { + 'd': session.db, + 'u': window.location.protocol + '//' + window.location.host, + }; + var params = { + response_type: 'token', + client_id: dbuuid || '', + state: JSON.stringify(state), + scope: 'userinfo', + }; + framework.redirect('https://accounts.odoo.com/oauth2/auth?'+$.param(params)); + }).fail(function(result, ev){ + ev.preventDefault(); + framework.redirect('https://accounts.odoo.com/account'); + }); + }, }); }, on_menu_logout: function() { - this.trigger('user_logout'); + this.trigger_up('clear_uncommitted_changes', { + callback: this.do_action.bind(this, 'logout'), + }); }, }); diff --git a/addons/web/static/src/less/bootstrap_overridden.less b/addons/web/static/src/less/bootstrap_overridden.less new file mode 100644 index 00000000000..39df64ac86e --- /dev/null +++ b/addons/web/static/src/less/bootstrap_overridden.less @@ -0,0 +1,103 @@ +// ------------------------------------------------------------------ +// Bootstrap variables +// ------------------------------------------------------------------ + +//== Grid +@grid-gutter-width: @odoo-horizontal-padding*2; + +//== Colors +@brand-primary: @odoo-brand-primary; + +//== Titles +@font-size-h1: @odoo-font-size-base + 13px; +@font-size-h2: @odoo-font-size-base + 7px; +@font-size-h3: @odoo-font-size-base + 3px; +@font-size-h4: @odoo-font-size-base + 2px; +@font-size-h5: @odoo-font-size-base + 1px; +@font-size-h6: @odoo-font-size-base; + +//== Scaffolding +@body-bg: @odoo-brand-secondary; +@text-color: @odoo-main-text-color; +@link-color: @odoo-brand-optional; +@link-hover-decoration: none; + +//== Typography +@font-family-sans-serif: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif; +@font-size-base: @odoo-font-size-base; +@headings-font-weight: bold; + +//== Components +@component-active-bg: @gray-lighter; +@component-active-color: @odoo-brand-optional; + +//== Badges +@badge-bg: @odoo-brand-optional; +@badge-color: white; +@badge-line-height: @line-height-base; +@badge-font-weight: normal; + +//== Breadcrumbs +@breadcrumb-active-color: #777777; +@breadcrumb-bg: @odoo-control-panel-background-color; +@breadcrumb-color: #777777; +@breadcrumb-padding-horizontal: 0; +@breadcrumb-padding-vertical: 0; + +//== Buttons +@btn-default-bg: white; +@btn-default-border: #cccccc; +@btn-default-color: @odoo-main-text-color; +@btn-primary-bg: @odoo-brand-optional; +@btn-primary-border: @odoo-brand-optional; +@btn-primary-color: white; + +//== Modals +@modal-sm: 300px; +@modal-md: 650px; +@modal-lg: 980px; +@modal-inner-padding: @odoo-horizontal-padding; +@modal-title-padding: @odoo-horizontal-padding; + +//== Navs +@nav-link-padding: 10px 15px; +@nav-pills-border-radius: 0; +@nav-pills-active-link-hover-color: white; +@nav-pills-active-link-hover-bg: @odoo-brand-optional; +@nav-tabs-active-link-hover-bg: white; + +//== Navbar +@navbar-height: @odoo-navbar-height - 2; +@navbar-margin-bottom: 0; +@navbar-inverse-bg: #222222; +@navbar-inverse-link-color: #9d9d9d; +@navbar-inverse-link-hover-bg: #222222; +@navbar-inverse-toggle-hover-bg: #222222; +@navbar-border-radius: 0; + +//== Tables +@table-bg-accent: #efeff8; // FIXME: expose + + +// ------------------------------------------------------------------ +// Bootstrap components rules (non exposed features variables) +// ------------------------------------------------------------------ + +//== Badges +.badge { + margin: 1px 2px 1px 0; + padding-top: 0; + padding-bottom: 0; +} + +//== Buttons +.btn-icon { + .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border); +} + +//== Printing improvements +@media print { + .table-responsive { + overflow-x: initial; + } +} diff --git a/addons/web/static/src/less/control_panel.less b/addons/web/static/src/less/control_panel.less new file mode 100644 index 00000000000..e2635af43d4 --- /dev/null +++ b/addons/web/static/src/less/control_panel.less @@ -0,0 +1,74 @@ +.o_control_panel { + .o-webclient-padding(@top: 10px, @bottom: 10px); + background-color: @odoo-control-panel-background-color; + border-bottom: 1px solid @odoo-main-color-muted; + .breadcrumb { + font-size: 18px; + margin: 0px; + > li > a { + color: @odoo-brand-optional; + cursor: pointer; + &:hover { + color: darken(@odoo-brand-optional, 10%); + text-decoration: none; + } + } + } + + // Sidebar + .o_cp_sidebar { + padding-right: 10px; + + .o_hidden_input_file { + position: relative; + input.o_form_input_file { + position: absolute; + top: 1px; + opacity: 0; + width: 100%; + height: 26px; + } + .o_form_binary_form span { + padding: 3px 25px; + color: @btn-default-color; + } + .o_form_binary_form:hover { + background-color: @table-bg-hover; + } + } + .o_sidebar_delete_attachment { + padding: 0px; + position: absolute; + top: 5px; + right: 10px; + } + } + + // Pager + .o_cp_pager { + text-align: center; + padding-left: 5px; + .user-select(none); + } + + // View Switcher + .o_cp_switch_buttons { + padding-left: 5px; + } +} + +.o_x2m_control_panel { + .o_cp_buttons > div { + margin-top: 5px; + } +} + +// Pager input (to edit pager values) +.o_pager_value > input { + display: inline-block; + width: 60px; + text-align: right; + &:focus { + outline: none; + } +} diff --git a/addons/web/static/src/less/control_panel_layout.less b/addons/web/static/src/less/control_panel_layout.less new file mode 100644 index 00000000000..92f8397bca4 --- /dev/null +++ b/addons/web/static/src/less/control_panel_layout.less @@ -0,0 +1,70 @@ +@breadcrumb-height: 30px; + +.o_control_panel { + .o-flex-display(); + .o-flex-flow(row, wrap); + + .breadcrumb { + width: 50%; + + > li { + .o_text_overflow; + max-width: 90%; + display: inline-block; + } + } + + &.o_breadcrumb_full { + .breadcrumb { + width: 100%; + } + } + + .o_cp_searchview { + width: 50%; + min-height: @breadcrumb-height; + } + + .btn.o_enable_searchview { + width: 10%; + margin-left: 2%; + padding: 0px; + } + + .o_cp_left { + width: 50%; + margin-top: 5px; + .o-flex-display(); + .o-justify-content(space-between); + } + + .o_cp_right { + width: 50%; + margin-top: 5px; + .o-flex-display(); + + .o_cp_pager { + margin: auto 0 auto auto; + } + } +} + +.o_x2m_control_panel { + .o-flex-display(); + .o-flex-flow(row, wrap); + + .o_cp_buttons { + .o-flex-display(); + margin-right: auto; + } + .o_cp_pager { + .o-flex-display(); + margin-left: auto; + } +} + +@media print { + .o_control_panel { + display: none; + } +} diff --git a/addons/web/static/src/less/data_export.less b/addons/web/static/src/less/data_export.less new file mode 100644 index 00000000000..3f596bea27d --- /dev/null +++ b/addons/web/static/src/less/data_export.less @@ -0,0 +1,107 @@ +.o_web_client .o_export { + + .o-flex(0, 1, 1000px); + .o-flex-display(); + .o-flex-flow(column, nowrap); + + > p, > .row { + .o-flex(0, 0, auto); + } + + label { + vertical-align: top; + font-weight: normal; + } + + .o_import_compat, .o_export_format { + display: inline-block; + + > div { + margin-left: 5px; + .o-inline-flex-display(); + .o-align-items(center); + + > label, > input { + margin: 0 0 0 5px; + } + } + } + + .o_export_panel { + .o-flex(1, 0, 0); + position: relative; + + > div { + height: 100%; + .o-flex-display(); + .o-flex-flow(column, nowrap); + .o-justify-content(center); + + &.o_left_panel { // Needed because of chrome + .o-position-absolute(@top: 0, @bottom: 0, @left: 0, @right: 57%); + } + &.o_center_panel { // Needed because of chrome + .o-position-absolute(@top: 0, @bottom: 0, @left: 43%, @right: 43%); + } + &.o_right_panel { // Needed because of chrome + .o-position-absolute(@top: 0, @bottom: 0, @left: 57%, @right: 0); + } + + > h4, > div { + .o-flex(0, 0, auto); + } + + > button { + .o-flex(0, 0, auto); + margin-top: 10px; + } + + .o_left_field_panel, .o_fields_list { + border: 1px solid @odoo-brand-lightsecondary; + overflow: auto; + .o-flex(1, 0, 0); + } + + .o_export_tree_item { + cursor: pointer; + position: relative; + padding-left: 20px; + .user-select(none); + + &.o_selected > .o_tree_column { + background-color: @odoo-brand-primary; + color: white; + } + + .o_expand_parent { + .o-position-absolute(@top: 4px, @left: 5px); + } + .o_tree_column { + display: block; + + &.o_required { + border-bottom: 2px solid @odoo-main-text-color; + } + } + + &:focus { + outline: none; + } + } + + .o_save_list, .o_exported_lists { + .o-flex-display(); + .o-align-items(baseline); + + > label, > button { + margin: 0 0 5px; + .o-flex(0, 0, auto); + } + > input, > select { + width: 0; + .o-flex(1, 0, auto); + } + } + } + } +} diff --git a/addons/web/static/src/less/datepicker.less b/addons/web/static/src/less/datepicker.less new file mode 100644 index 00000000000..271cd357d15 --- /dev/null +++ b/addons/web/static/src/less/datepicker.less @@ -0,0 +1,59 @@ +@odoo-datepicker-week-color: #8f8f8f; +@odoo-datepicker-week-bg-color: lighten(@odoo-datepicker-week-color, 30%); + +.o_datepicker { + position: relative; + + .o_datepicker_input { + width: 100%; + } + + .o_datepicker_button { + .o-position-absolute(2px, 4px); + cursor: pointer; + &:after { + .o-caret-down; + } + } +} + +.datepicker { + .table-condensed { + > thead { + color: white; + background-color: @odoo-brand-primary; + + > tr { + &:first-child { + th:hover { + color: white; + background-color: darken(@odoo-brand-primary, 10%); + } + } + + &:last-child { + color: @odoo-datepicker-week-color; + background-color: @odoo-datepicker-week-bg-color; + } + > th { + border-radius: 0; + } + } + } + + > tbody { + > tr { + > td { + &.active, .active { + background-color: @odoo-brand-optional; + border-radius: 100px; + } + + &.today:before { + border-bottom-color: @odoo-brand-optional; + } + } + } + } + } +} diff --git a/addons/web/static/src/less/debug_manager.less b/addons/web/static/src/less/debug_manager.less new file mode 100644 index 00000000000..74d20dda43a --- /dev/null +++ b/addons/web/static/src/less/debug_manager.less @@ -0,0 +1,8 @@ +.modal { + .o_debug_manager { + position: relative; + list-style: none; + margin: 2px 10px 2px 0; + float: left; + } +} diff --git a/addons/web/static/src/less/dropdown.less b/addons/web/static/src/less/dropdown.less new file mode 100644 index 00000000000..5cc6ddbe759 --- /dev/null +++ b/addons/web/static/src/less/dropdown.less @@ -0,0 +1,79 @@ +.li-a-padding { + padding: 3px @odoo-dropdown-hpadding; +} + +.ui-autocomplete { // Copy style of bootstrap dropdown + background-image: none; + background-color: @dropdown-bg; + border: 1px solid @dropdown-border; + font-family: @font-family-serif; + font-size: @font-size-base; + padding: 5px 0px; + box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.176); + + .ui-menu-item { + padding: 0; + > a { + display: block; + color: @dropdown-link-color; + + .li-a-padding + } + + &.ui-state-focus { + background-image: none; + background-color: @odoo-brand-primary; + border: none; + font-weight: normal; + margin: 0; + } + &.o_m2o_dropdown_option { + padding-left: 20px; + > a { + color: @odoo-brand-primary; + } + } + } +} + +.dropdown-menu { + .o_inline_dropdown { + position: relative; + + &> .dropdown-menu { + top: 0; + left: 100%; + } + &:hover > .dropdown-menu { + display: block; + } + } +} + +.dropdown-menu-mixin { + .open .dropdown-menu > li { + position: relative; // to allow absolute positionning inside the li + > a { + &:focus, &:active, &:focus:active { + outline: none; + } + + .li-a-padding; + } + + &.dropdown-header { + .li-a-padding; + color: @odoo-main-text-color; + + @media (max-width: @screen-xs-max) { + color: white; + text-decoration: underline; + } + } + } +} + +.dropdown-menu-mixin(); +.navbar-nav { + .dropdown-menu-mixin(); +} diff --git a/addons/web/static/src/less/dropdown_extra.less b/addons/web/static/src/less/dropdown_extra.less new file mode 100644 index 00000000000..ec38e6d5292 --- /dev/null +++ b/addons/web/static/src/less/dropdown_extra.less @@ -0,0 +1,63 @@ +.ui-autocomplete .ui-menu-item { + &.ui-state-focus { + background-color: @odoo-brand-primary; + } + &.o_m2o_dropdown_option > a { + color: @odoo-brand-primary; + } + &.ui-state-focus > a, + &.ui-state-focus.o_m2o_dropdown_option > a { + color: white; + } +} + +@tag-border: #afafb6; +.oe_dropdown, .oe_dropdown_toggle { + position: relative; + cursor: pointer; +} +.oe_dropdown_toggle { + color: #2B2B2B; + font-weight: normal; +} +.oe_dropdown_menu { + display: none; + position: absolute; + z-index: 3; + margin: 0; + border: 1px solid @tag-border; + background: white; + padding: 4px 0; + min-width: 140px; + text-align: left; + box-shadow: 0 1px 4px rgba(0,0,0,0.3); + &.oe_opened { + display: block; + } + > li { + list-style-type: none; + float: none; + display: block; + position: relative; + margin: 0; + padding: 2px 8px; + &:hover { + .o-vertical-gradient(#f0f0fa, #eeeef6); + box-shadow: none; + } + > a { + white-space: nowrap; + display: block; + color: #4c4c4c; + text-decoration: none; + &:hover { + text-decoration: none; + } + } + } +} + +// Buttons used as dropdown toggler +.o_dropdown_toggler_btn { + .btn-default; +} diff --git a/addons/web/static/src/less/enterprise_compatibility.less b/addons/web/static/src/less/enterprise_compatibility.less deleted file mode 100644 index 71dc441abbd..00000000000 --- a/addons/web/static/src/less/enterprise_compatibility.less +++ /dev/null @@ -1,467 +0,0 @@ -// ------------------------------------------------------------------ -// Odoo -// ------------------------------------------------------------------ - -// This file is necessary to make the community edition of the Odoo web -// client compatible with the enterprise edition. - -// The problem is that we have two web clients in parallel, and every -// addons should behave properly with both. - -@brand-primary: @odoo-brand-primary; - -.oe-view-manager { - background-color: @odoo-webclient-background-color !important; -} - -.o_kanban_view { - background-color: @odoo-webclient-background-color; - - .o_kanban_config i, .o_kanban_quick_add i { - color: @odoo-main-text-color !important; - &:hover { - background-color: darken(@odoo-webclient-background-color, 10%) !important; - } - } - .o_column_title, .o_kanban_title, .o_kanban_add_column, .o_column_unfold { - color: @odoo-main-text-color !important; - } - - .o_column_folded { - background-color: darken(@odoo-webclient-background-color, 5%) !important; - border-color: darken(@odoo-webclient-background-color, 10%) !important; - } - - .o_column_quick_create { - background-color: darken(@odoo-webclient-background-color, 10%) !important; - } - - &.o_kanban_grouped .o_kanban_group .o_kanban_header { - margin-bottom: 2px; // For the buttons not to overlap first record's box shaddow on hover - } -} - -.oe_list { - background-color: white !important; -} - -// Buttons and links -@link-hover-decoration: none; -.btn, .btn-group > .btn { - &:hover, &:active, &:focus, &:active:focus { - outline: none; - } -} -.btn-icon { - .btn-default; -} -.oe_button { - .btn-default; - &.oe_highlight, &.btn-primary { - .btn-primary; - } - &.oe_link, &.btn-link { - .btn-link; - } -} -.oe_highlight { - .btn-primary; -} -.oe_link { - .btn-link; -} - -.openerp .oe_avatar { - float: left; -} - -.openerp .oe_button_box { - float: right; -} - - -// Pager input (to edit pager values) -.o_pager_value > input { - width: 60px; - text-align: right; - &:focus { - outline: none; - } -} - -.oe_form_field_one2many, .oe_form_field_many2many { - .oe-view-manager { - background-color: white; - - .o_kanban_view { - background-color: white; - - .o_kanban_record { - box-shadow: none; - } - } - } -} - -// Buttons containing only an icon -.o_icon_button { - background-color: transparent; - border: 0; - padding: 0; - outline: none; -} - -// Stars -.o_priority { - display: inline-block; - padding: 0; - margin: 0; - vertical-align: middle; - > .o_priority_star { - display: inline-block; - font-size: 1.35em; - &:hover, &:focus { - text-decoration: none; - outline: none; - } - - + .o_priority_star { - padding-left: 5px; - } - - &.fa-star-o { - color: @odoo-main-color-muted; - } - &.fa-star { - color: gold; - } - } -} - -.oe_title { - .o_priority > .o_priority_star { - font-size: inherit; - } -} - -.o_text_overflow { - display: block; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: top; // To update display context changed by overflow:hidden -} - -.oe-x2m-control-panel { - background-color: white; -} - -div.o_checkbox { - display: inline-block; - vertical-align: sub; - > input + span { - display: none; - } -} - -.openerp .oe_formview { - .oe_button_box { - .oe_stat_button { - text-align: left; - - .o_button_icon { - width: 30%; - display: inline-block; - vertical-align: middle; - } - - .o_stat_info { // contains the value and text - display: inline-block; - vertical-align: middle; - max-width: 70%; - line-height: 1.2; - - .o_stat_value { - .o_text_overflow; - font-weight: 700; - color: @odoo-brand-primary; - } - .o_stat_text { - .o_text_overflow; - font-size: small; - - .o_stat_value { - display: inline; - } - } - } - - // Some buttons only display text and directly use the WidgetButton template instead of StatInfo - > span { - .o_stat_info; // layout - font-size: small; - white-space: normal; // text on several lines if needed - } - } - - &, & + .oe_avatar { - + .oe_title { - width: 400px; - } - } - } - - .oe_avatar { - > img { - max-width: 90px; - max-height: 90px; - } - - + .oe_title { - padding-left: 95px; - } - } - - .o_row { - > * { - width: auto!important; // oe_inline - } - > div { - display: inline-block; - + div { - margin-left: 5px; - } - } - } - - .oe_form label { - font-size: 13px; - } - - .oe_form_group .oe_form_group_cell:not(.oe_form_group_cell_label) label { - font-weight: normal; - } - - .o_form_field_many2manytags { - .o-inline-flex-display(); - .o-flex-flow(row, wrap); - .o-align-items(flex-start); - - .badge { - cursor: pointer; - .o-flex(0, 0, auto); - margin: 0 3px 3px 0; - color: #FFF; - font-size: 12px; - .o_delete { - cursor: pointer; - } - > ul { - @colored-tag-width: 45px; - width: 5*@colored-tag-width + 3px; - > li { - float: left; - color: black; - > span { - border-radius: 6px; - width: @colored-tag-width; - height: 30px; - display: inline-block; - border: 2px solid white; - cursor: pointer; - padding: 0; - - &:hover { - opacity: 0.8; - } - } - } - > li:last-child { - margin-top: 7px; - cursor: auto; - } - } - } - - padding: 0; - > .oe_form_field_many2one { - padding: 0; - } - } - - &.oe_form_editable .o_form_field_many2manytags { - border: 1px solid #CCC; - border-radius: 3px; - - &.o_form_field_empty { - display: none; - } - - > .oe_form_field { - .o-flex(1, 0, 100px); - - input { - border: none; - } - } - } - - .o_address_format { - width: 100%; - - > .oe_form_field { - width: auto; - display: block; - } - - > .o_address_street, > .o_address_country { - clear: left; - width: 100%; - } - > .o_address_city, > .o_address_state, > .o_address_zip { - float: left; - margin-right: 2%; - } - > .o_address_zip { - margin-right: 0; - } - &.o_zip_city { - > .o_address_zip { - margin-right: 2%; - } - > .o_address_city { - margin-right: 0; - } - > .o_address_state { - clear: left; - margin-right: 0; - } - } - &.o_city_state { - > .o_address_state { - margin-right: 0; - } - > .o_address_zip { - clear: left; - margin-right: 0; - } - } - } - &.oe_form_editable .o_address_format { - > .o_address_city { - width: 38%; - } - > .o_address_state { - width: 33%; - } - > .o_address_zip { - width: 25%; - } - &.o_zip_city { - > .o_address_zip { - width: 38%; - } - > .o_address_city { - width: 60%; - } - > .o_address_state { - width: 100%; - } - } - &.o_city_state { - > .o_address_city { - width: 50%; - } - > .o_address_state { - width: 48%; - } - > .o_address_zip { - width: 100%; - } - } - } -} - -// Many2many tags -.o_form_field_many2manytags { - .o_tag_color_0 { - background-color: #4D4D4D; - } - .o_tag_color_1 { - background-color: #61BD4F; - } - .o_tag_color_2 { - background-color: #F2D600; - } - .o_tag_color_3 { - background-color: #FFAB4A; - } - .o_tag_color_4 { - background-color: #EB5A46; - } - .o_tag_color_5 { - background-color: #A24689; - } - .o_tag_color_6 { - background-color: #0079BF; - } - .o_tag_color_7 { - background-color: #00C2E0; - } - .o_tag_color_8 { - background-color: #4CD98E; - } - .o_tag_color_9 { - background-color: #FF80CE; - } - .o_tag_color_10 { - background-color: #B6BBBF; - } -} - -// Search options dropdowns -.o-search-options-dropdown-custom-li() { - margin: 3px 25px; - width: auto; - max-width: 250px; - -} -// Open and closed carets in dropdowns -.o_closed_menu { - position: relative; - a:before { - .o-caret-right; - .o-position-absolute(@top: 50%, @left: 12px); - margin-top: (-@caret-width-base); - } -} -.o_open_menu { - position: relative; - a:before { - .o-caret-down; - .o-position-absolute(@top: 50%, @left: 9px); - margin-top: (-@caret-width-base); - } -} - -// Favorites Menu -> Add to my dashboard -.o_favorites_menu { - .o_add_to_dashboard_input { - .form-control(); - } - .o_add_to_dashboard_button { - .btn(); - .btn-default(); - } -} - -// Because the `web_planner` module has to work with Odoo community and enterprise without -// code duplication, fine-tune the display of it in the community webclient here -#PlannerDialog { - top: 35px; - -webkit-box-shadow: 0 3px 9px #666666; - box-shadow: 0 3px 9px #666666; - padding-bottom: 44px; -} - diff --git a/addons/web/static/src/less/fonts.less b/addons/web/static/src/less/fonts.less new file mode 100644 index 00000000000..c6b19a2ba2f --- /dev/null +++ b/addons/web/static/src/less/fonts.less @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------ +// Lato +// ------------------------------------------------------------------ +@lato-font-path: '../fonts/lato'; + +.lato-font(@type, @weight, @style) { + @font-face { + font-family: 'Lato'; + src: url('@{lato-font-path}/Lato-@{type}-webfont.eot'); + src: url('@{lato-font-path}/Lato-@{type}-webfont.eot?#iefix') format('embedded-opentype'), + url('@{lato-font-path}/Lato-@{type}-webfont.woff') format('woff'), + url('@{lato-font-path}/Lato-@{type}-webfont.ttf') format('truetype'), + url('@{lato-font-path}/Lato-@{type}-webfont.svg#Lato') format('svg'); + font-weight: @weight; + font-style: @style; + } + + @font-face { + font-family: 'Lato-@{type}'; + src: url('@{lato-font-path}/Lato-@{type}-webfont.eot'); + src: url('@{lato-font-path}/Lato-@{type}-webfont.eot?#iefix') format('embedded-opentype'), + url('@{lato-font-path}/Lato-@{type}-webfont.woff') format('woff'), + url('@{lato-font-path}/Lato-@{type}-webfont.ttf') format('truetype'), + url('@{lato-font-path}/Lato-@{type}-webfont.svg#Roboto') format('svg'); + } +} + +.lato-font-pair(@type, @weight) { + .lato-font('@{type}', @weight, normal); + .lato-font('@{type}Ita', @weight, italic); +} + +.lato-font-pair('Hai', 100); +.lato-font-pair('Lig', 300); +.lato-font-pair('Reg', 400); +.lato-font-pair('Bol', 700); +.lato-font-pair('Bla', 900); diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less new file mode 100644 index 00000000000..d534976ee1c --- /dev/null +++ b/addons/web/static/src/less/form_view.less @@ -0,0 +1,468 @@ +.o_form_view { + background-color: @odoo-view-background-color; + + // Utility classes (+ oe_edit_only, oe_read_only, o_label_nowrap, o_text_overflow) + .o_form_invisible, .o_form_field_empty { + display: none!important; + } + .oe_form_box_info { + .bg-info; + .o-webclient-padding(@top: 5px, @bottom: 5px); + > p { + margin: auto; + } + } + .oe_text_center { + text-align: center; + } + .oe_link { + .btn-link; + } + .oe_grey { + opacity: 0.5; + } + + // Readonly specific rules + &.o_form_readonly { + .oe_edit_only { + .o_form_invisible; + } + + .o_form_uri { // FIXME maybe global? + display: inline-block; + + color: @odoo-main-text-color; + &:first-line { + color: @link-color; + } + &:hover { + color: darken(@odoo-main-text-color, 1%); // hack required on chrome + &:first-line { + color: darken(@link-color, 15%); + } + } + } + } + + // Editable specific rules + &.o_form_editable { + .oe_read_only { + .o_form_invisible; + } + + .o_form_field_many2manytags { + .o-input(); + + > .o_form_field { + .o-flex(1, 0, 100px); + max-width: 100%; + + > .o_form_input_dropdown > .o_form_input { + border-style: none!important; + } + } + } + + .o_form_field_monetary { + .o-input(); + .o-align-items(baseline); + + > .o_form_input { + border-style: none!important; + width: 100px; + .o-flex(1, 0, auto); + } + } + } + + // Button box + .oe_button_box { + .oe_stat_button { + &:not(:hover) .o_stat_info > .o_hover { + display: none !important; + } + &:hover .o_stat_info > .o_not_hover { + display: none !important + } + } + } + + // Title + .oe_title { + .o_priority > .o_priority_star { + font-size: inherit; + } + } + + // Groups + .o_group { + &.o_label_nowrap { + .o_form_label { + white-space: nowrap; + } + } + + .o_form_label { + font-weight: normal; + } + .o_td_label .o_form_label { + font-weight: bold; + margin-right: 0px; + } + } + &.oe_form_configuration { + .o_group .o_form_label { + white-space: nowrap; + } + } + + // Separators + .o_horizontal_separator { + font-size: @font-size-h2; + margin: @odoo-form-spacing-unit 0; + &:empty { + height: @odoo-form-spacing-unit * 2; + } + } + + // Labels + .o_form_label { + font-size: @font-size-base; // The label muse have the same size whatever their position + line-height: @line-height-base; + margin-right: @odoo-form-spacing-unit; + &.o_form_invalid { + color: @brand-danger; + } + } + + // Tags + .o_form_field_many2manytags { + .badge { + cursor: pointer; + color: white; + font-size: 12px; + border: 0; + + > ul { + @colored-tag-width: 45px; + width: 5*@colored-tag-width + 3px; + > li { + float: left; + color: black; + > span { + border-radius: 6px; + width: @colored-tag-width; + height: 30px; + display: inline-block; + border: 2px solid white; + cursor: pointer; + padding: 0; + + &:hover { + opacity: 0.8; + } + } + } + > li:last-child { + margin-top: 7px; + cursor: auto; + } + } + } + } + + .o_form_invalid { + &.o_form_input, .o_form_input, &.o_form_textarea, .o_form_textarea { + border-color: @brand-danger; + } + } + + // Boolean + .o_form_field_boolean { + margin: 0 @odoo-form-spacing-unit 0 0; + } + + // Numbers + .o_form_field_number { + white-space: nowrap; + } + + // Radio buttons + .o_form_field_radio .o_form_radio { + outline: none; + } + + // Translate icon + .o_field_translate { + padding: 0; + display: none; + cursor: pointer; + width: 12px; + vertical-align: top; + &::before { // seems to be useful for some old browser + position: relative; + } + &.o_translate_active { + display: inline-block; + } + } + + // Dropdowns input (many2one, ...) + .o_form_input_dropdown { + .o-flex(1, 0, auto); + position: relative; + + > input { + padding-right: 15px; + cursor: pointer; + } + + .o_dropdown_button { + pointer-events: none; + &:after { + .o-caret-down; + } + } + } + + // Many2many tags + .badge .o_delete { + cursor: pointer; + } + + // Image + .o_form_field_image { + position: relative; + + .o_form_image_controls { + .o-position-absolute(0, 0); + width: 100%; + + color: white; + background-color: @odoo-brand-optional; + opacity: 0; + transition: opacity ease 400ms; + + > .fa { + margin: 5px; + cursor: pointer; + } + } + + &:hover .o_form_image_controls { + opacity: 0.8; + } + } + + // Input loading/file + .o_form_binary_progress, .o_hidden_input_file .o_form_input_file { + display: none; + } + + // Timezone widget warning + .o_tz_warning { + color: @brand-danger; + cursor: pointer; + } + + // One2Many Kanban views + .o_form_field .o_kanban_view.o_kanban_ungrouped { + padding: 0; + .o_kanban_record { + box-shadow: none; + } + } + + // One2Many List views + .o_form_field .o_list_view { + margin-bottom: 10px; + + > tfoot > tr > td { + padding: 3px; + color: @odoo-main-text-color; + } + } + &.o_form_readonly .o_form_field .o_list_view .o_row_handle { + display: none; // Hide the handler in non-edit mode + } + + // Percent pie field + .o_form_field_percent_pie { + .o_pie { + position: relative; + border-radius: 100%; + overflow: hidden; + background-color: @odoo-brand-optional; + + &:after { // Outside pie border to go over border-radius irregularities + content: " "; + .o-position-absolute(0, 0); + width: 100%; + height: 100%; + border-radius: 100%; + } + + .o_mask { + .o-position-absolute(0, 0); + .o-transform-origin(0); + width: 50%; + height: 100%; + background-color: @odoo-view-background-color; + &.o_full { + background-color: inherit; + } + } + + .o_pie_value { + .o-flex-display(); + .o-justify-content(center); + .o-align-items(center); + + border-radius: 100%; + + font-weight: 700; + color: @odoo-brand-primary; + } + } + } + + // Attachments + .oe_attachment { + display: inline-block; + border: 1px solid @odoo-brand-lightsecondary; + margin: 0 @odoo-form-spacing-unit @odoo-form-spacing-unit 0; + padding: @odoo-form-spacing-unit; + } + + // Specific style classes + .o_group.o_inner_group.oe_subtotal_footer { + > tbody > tr > td { + padding: 0; + + &.o_td_label { + text-align: right; + } + + .o_form_label { + padding-right: 20px; + + min-width: 0; + white-space: nowrap; + &:after { + content: ":"; + } + } + + .o_form_field { + text-align: right; + width: 100%; + } + } + + .oe_subtotal_footer_separator { + width: 100%; + text-align: right; + border-top: 1px solid @odoo-brand-secondary; + font-weight: bold; + font-size: 1.3em; + } + } + + .o_address_format { + width: 100%; + .o_address_street, .o_address_country { + .o-flex-display; + } + + .o_address_city { + margin-right: 2%; + } + .o_address_state { + margin-right: 2%; + } + &.o_zip_city { + .o_address_zip { + margin-right: 2%; + } + .o_address_city { + margin-right: 0; + } + .o_address_state { + display: block; + margin-right: 0; + } + } + &.o_city_state { + .o_address_state { + margin-right: 0; + } + .o_address_zip { + display: block; + margin-right: 0; + } + } + > span.o_form_field { + width: auto; + } + } + &.o_form_editable .o_address_format { + input.o_address_city { + width: 38%; + } + div.o_address_state { + width: 33%; + } + input.o_address_zip { + width: 25%; + } + &.o_zip_city { + .o_address_zip { + width: 38%; + } + .o_address_city { + width: 60%; + } + .o_address_state { + width: 100%; + } + } + &.o_city_state { + .o_address_city { + width: 50%; + } + .o_address_state { + width: 48%; + } + .o_address_zip { + width: 100%; + } + } + } +} + +// Overriden style when form view in modal +.modal .modal-dialog { + .o_form_view { + .o_form_sheet { + box-shadow: none; + width: 100%; + margin: 0 auto; + max-width: none; + } + } + + .o_form_invisible { + display: none!important; + } +} + +// FIXME bootstrap modal : 1050 +.ui-autocomplete { + z-index: 1051; +} + +// Buttons in ControlPanel +.o_control_panel .o_form_buttons_view > button:first-child { + float: left; // Unfortunately needed for the bounce effect + margin-right: 4px; +} diff --git a/addons/web/static/src/less/form_view_extra.less b/addons/web/static/src/less/form_view_extra.less new file mode 100644 index 00000000000..300724e2819 --- /dev/null +++ b/addons/web/static/src/less/form_view_extra.less @@ -0,0 +1,265 @@ +.o_form_view { + .o_form_sheet_bg { + background: url(/web/static/src/img/form_sheetbg.png); + border-bottom: 1px solid #ddd; + .o_form_sheet { + border: 1px solid #c8c8d3; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + } + } + + // Header containing status/workflow buttons + > header { + border-bottom: 1px solid #cacaca; + .o-vertical-gradient(#fcfcfc, #dedede); + > button:not(.btn-primary):not(.btn-link):not(.oe_highlight) { + .btn-default; + } + } + &.o_form_nosheet > header { + margin: -16px -16px 0 -16px; + } + ul.oe_form_status, ul.oe_form_status_clickable { + display: inline-block; + margin: 0; + padding: 0 18px 0 0; + li { + display: inline-block; + list-style-type: none; + // It is probably a bad idea to modify this margin + margin: 0 -18px 0 0; + padding: 0; + height: 30px; + .o-vertical-gradient(#fcfcfc, #dedede); + &:first-child > .label { + border-left: 1px solid #cacaca; + padding-left: 14px; + } + &:last-child { + border-right: 1px solid #cacaca; + > .label { + padding-right: 14px; + } + > .arrow { + display: none; + } + } + > .label { + color: #4c4c4c; + text-shadow: 0 1px 1px #fcfcfc, 0 -1px 1px #dedede; + padding: 7px; + display: inline-block; + padding-left: 24px; + margin: 0; + position: relative; + // Customize for statusbar label + line-height: normal; + font-size: 100%; + font-weight: normal; + // End of customize + } + > .arrow { + width: 17px; + height: 30px; + display: inline-block; + vertical-align: top; + overflow: hidden; + margin-left: -5px; + span { + position: relative; + width: 24px; + height: 24px; + display: inline-block; + margin-left: -12px; + margin-top: 3px; + border-radius: 3px; + box-shadow: -1px 1px 2px rgba(255,255,255,0.2), inset -1px 1px 1px rgba(0,0,0,0.2); + .o-skew-gradient(#fcfcfc, #dedede); + .o-transform(rotate(45deg)); + } + } + } + ul.oe_dropdown_menu { + display: none; + padding: 0; + min-width: 0; + &.oe_opened { + display: block; + } + li { + margin: 0; + width: 100%; + span.label { + padding-left: 14px; + } + } + } + li.oe_active { + .o-vertical-gradient(lighten(@odoo-brand-primary, 15%), darken(@odoo-brand-primary, 15%)); + > .arrow span { + .o-skew-gradient(lighten(@odoo-brand-primary, 15%), darken(@odoo-brand-primary, 15%)); + } + > .label { + color: white; + text-shadow: 0 1px 1px #729fcf, 0 -1px 1px #3465a4; + } + } + } + ul.oe_form_status_clickable { + li { + cursor: pointer; + &:hover { + .o-vertical-gradient(darken(#fcfcfc, 8%), darken(#dedede, 8%)); + > .label { + text-shadow: 0 -1px 1px #fcfcfc, 0 1px 1px #dedede; + } + > .arrow span { + .o-vertical-gradient(darken(#fcfcfc, 8%), darken(#dedede, 8%)); + } + } + > .label { + color: @link-color; + } + } + li.oe_active { + &:hover { + .o-vertical-gradient(@odoo-brand-primary, darken(@odoo-brand-primary, 30%)); + > .label { + text-shadow: 0 -1px 1px #729fcf, 0 1px 1px #3465a4; + } + > .arrow span { + .o-skew-gradient(@odoo-brand-primary, darken(@odoo-brand-primary, 30%)); + } + } + } + } + + // Button box + .oe_button_box { + .oe_stat_button { + border-radius: 0; + border-color: #ccc; + background: white; + outline: none; + &:hover { + background-color: #e6e6e6; + } + .o_button_icon { + color: #7C7BAD; + font-size: 24px; + } + + .o_stat_info { // contains the value and text + .o_stat_value { + .o-text-overflow(block); + font-weight: 700; + color: @odoo-brand-primary; + } + .o_stat_text { + .o-text-overflow(block); + font-size: small; + + .o_stat_value { + display: inline; + } + } + } + + // Some buttons only display text and directly use the WidgetButton template instead of StatInfo + > span { + font-size: small; + white-space: normal; // text on several lines if needed + } + } + } + + // Avatar + .oe_avatar > img { + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); + border: none; + } + + // Groups + .o_group { + .o_td_label { + border-right: 1px solid #ddd; + } + } + + // All required form fields + .o_form_required { + &.o_form_input, .o_form_input, &.o_form_textarea, .o_form_textarea { + background-color: #D2D2FF; + } + } + + .o_form_field.o_form_field_many2one .o_external_button { + padding: 0; + margin-left: 2px; + font-size: 19px; + color: #7C7BAD; + border: none; + &:hover { + background-color: transparent; + } + } + + // Dropdowns input carret + .o_form_input_dropdown .o_dropdown_button { + .o-position-absolute(2px, 4px); + } + + // Separators + .o_horizontal_separator { + color: @odoo-brand-optional; + font-weight: bold; + } + + // Translate icon + .o_field_translate { + margin-left: -15px; + } + + // Percent pie field + .o_form_field_percent_pie { + @pie-dimension: 34px; + @pie-ring-width: 4px; + + .o_pie { + width: @pie-dimension; + height: @pie-dimension; + margin-left: 5px; + + &:after { // Outside pie border to go over border-radius irregularities + border: 1px solid @odoo-brand-primary; + } + + .o_pie_value { + .o-position-absolute(@pie-ring-width, @pie-ring-width); + width: @pie-dimension - 2 * @pie-ring-width; + height: @pie-dimension - 2 * @pie-ring-width; + border: 1px solid white; + background-color: white; + } + } + } + + // Specific style classes + .o_group.o_inner_group.oe_subtotal_footer { + .oe_subtotal_footer_separator { + border-top: 1px solid #cacaca; + } + .o_td_label { + border-right: none; + } + } +} + +// Overriden style when form view in modal +.modal .modal-dialog { + .o_form_view { + .o_form_sheet_bg, .o_form_sheet { + border: none; + } + } +} diff --git a/addons/web/static/src/less/form_view_layout.less b/addons/web/static/src/less/form_view_layout.less new file mode 100644 index 00000000000..e6aa989ca12 --- /dev/null +++ b/addons/web/static/src/less/form_view_layout.less @@ -0,0 +1,298 @@ +.o_form_view { + .oe_inline { + width: auto!important; + @media (min-width: @screen-xs-min) { + &.o_inner_group { + width: 1px!important; + } + } + } + .oe_left { + .oe_inline; + float: left!important; + } + .oe_right { + .oe_inline; + float: right!important; + } + + @media (min-width: @screen-xs-min) { + .o_row { + &, &.o_form_field { // Some field may want to use o_row as root and these rules must prevalue + .o-flex-display(); + width: auto!important; + } + + .o-align-items(baseline); + min-width: 50px; + margin: 0 -@odoo-form-spacing-unit/2; + + > div, > span, > button, > label, > a, > input { // > * did not add a level of priority to the rule + .o-flex(0, 0, auto); + width: auto!important; + margin-right: @odoo-form-spacing-unit/2; + margin-left: @odoo-form-spacing-unit/2; + } + + > .o_row { + margin: 0; + } + > .btn { + padding-top: 0; + padding-bottom: 0; + } + > .o_form_field_boolean { + .o-align-self(center); + } + } + + .o_row .o_form_field_many2one, .o_form_field_many2one.oe_inline { + &.o_with_button:not(:last-child) { // If invisible element as last child, this line has no effect but that's not very important + margin-right: (35px + (@odoo-form-spacing-unit/2)); + } + } + } + + .o_row > div > .o_form_field { + width: 100%; + } + + // Readonly specific rules + &.o_form_readonly { + .o_row { + display: inline-block; + > div { + display: inline-block; + } + } + } + + // Editable specific rules + &.o_form_editable { + @media (min-width: @screen-xs-min) { + .o_row { + > .o_form_field, > div { + .o-flex(1, 1, auto); + width: 0!important; // 3rd flex argument does not work with input (must be auto and real width 0) + + &.o_form_field_boolean, &.o_priority { + .o-flex(0, 0, auto); + width: auto!important; + } + } + } + } + + .oe_title { + max-width: @container-sm - (2 * @odoo-horizontal-padding); + } + } + + // Title + .oe_title { + > h1, > h2, > h3 { + width: 100%; // Needed because inline-block when is a hx.o_row + margin-top: 0; + margin-bottom: 0; + line-height: inherit; + } + } + + // Groups + .o_group { + display: inline-block; + width: 100%; + margin: 10px 0; + + // o_group contains nested groups + .o-generate-groups(@odoo-form-group-cols); + .o-generate-groups(@n, @i: 1) when (@i =< @n) { + .o_group_col_@{i} { + display: inline-block; + width: floor(100% / 12 * @i); + vertical-align: top; + } + .o-generate-groups(@n, @i + 1); + } + + &.o_inner_group { + display: inline-table; + + > tbody > tr > td { + &.o_td_label { + width: 0%; + padding: 0 15px 0 0; + min-width: 150px; + } + vertical-align: top; + + span, .o_form_field_boolean, .oe_avatar, .o_form_uri { + &.o_form_field { + width: auto; + } + } + } + } + + .o_form_field { + width: 100%; + + > .btn { + .o-flex(0, 0, auto); + padding: 0 10px; + } + } + } + + // Notebooks + .o_notebook { + clear: both; // For the notebook not to have alongside floating elements + margin-top: @odoo-form-spacing-unit * 2; + } + + // Labels + .o_form_label { + margin: 0; + } + + // Form fields + .o_form_field { + display: inline-block; + margin-bottom: @odoo-form-spacing-unit; + } + + // Block fields + .note-editor, .oe_form_field_html, .oe_form_field_html_text, .oe_form_field_text, .o_view_manager_content { + display: block; + } + + // Flex fields + .o_form_field_many2one, .o_form_field_radio, .o_form_field_many2manytags, .o_form_field_percent_pie, .o_form_field_monetary, .o_form_field_binary_file { + .o-inline-flex-display(); + > span, > button { + .o-flex(0, 0, auto); + } + } + + .o_form_field, .btn { + .o_form_field { + margin-bottom: 0px; + } + } + + // Input in field + .o_form_field { + .o_form_input_dropdown { + width: 100%; + } + .o_form_input { + display: inline-block; + } + } + + // Textarea + .o_form_textarea { + white-space: pre-wrap; + width: 100%; + &.oe_inline { + width: 100%!important; + @media (min-width: @screen-xs-min) { + width: 45%!important; + } + } + } + + // Radio buttons + .o_form_field_radio { + .o_radio_item { + .o-flex(0, 0, auto); + .o-flex-display(); + .o-align-items(center); + + > input { + .o-flex(0, 0, auto); + margin: 0 @odoo-form-spacing-unit 0 0; + } + > label { + margin: 0; + } + } + + &.o_horizontal { + .o_radio_item { + margin-right: @odoo-form-spacing-unit * 2; + } + } + + &.o_vertical { + .o-flex-flow(column, nowrap); + .o_radio_item { + margin-bottom: @odoo-form-spacing-unit; + } + } + } + + // One2Many, Many2Many outside of group + .o_form_field { + &.o_view_manager_content, &.o_form_field_one2many, &.o_form_field_many2many { + width: 100%; + > div { + width: 100%; + } + } + } + + // Many2many tags + .o_form_field_many2manytags { + .o-flex-flow(row, wrap); + .o-align-items(baseline); + .badge { + .o-flex(0, 0, auto); + > .o_badge_text { + .o-text-overflow(inline-block); + max-width: 200px; + } + } + } + + // Percent pie + .o_form_field_percent_pie { + .o-align-items(center); + > .o_pie { + margin-right: 10px; + } + } + + // Kanban selection + .o_form_sheet > .o_kanban_selection { + float: right; + } + + // Specific style classes + .o_group.o_inner_group.oe_subtotal_footer { + .oe_right; + } +} + +// Overriden style when form view in modal +.modal .modal-dialog { + .o_form_view { + .o_form_sheet_bg { + padding: 0; + } + .oe_chatter { + display: none; + } + } + &:not(.modal-lg) .o_form_view { + .o_group { + width: 100%; + } + } +} + +@media print { + .oe_button_box { + display: none !important; + } +} diff --git a/addons/web/static/src/less/form_view_layout_extra.less b/addons/web/static/src/less/form_view_layout_extra.less new file mode 100644 index 00000000000..0845977ef4d --- /dev/null +++ b/addons/web/static/src/less/form_view_layout_extra.less @@ -0,0 +1,125 @@ +@sheet-max-width: 860px; +@sheet-min-width: 650px; +@sheet-padding: 16px; + +.o_form_view { + &.o_form_nosheet { + padding: 16px; + &.oe_form_nomargin { + margin: 0; + } + } + .o_form_sheet_bg { + position: relative; + padding: @sheet-padding 0; + .o_form_sheet { + min-width: @sheet-min-width; + max-width: @sheet-max-width; + min-height: 330px; + background: white; + padding: 16px; + margin: 0 auto; + .ui-tabs { + margin: 0 -16px; + } + .oe_notebook_page { + padding: 0 16px; + } + } + } + + // Header containing status/workflow buttons + > header { + padding-left: 14px; + .clearfix(); + ul:not(.oe_tooltip_technical):not(.oe_dropdown_menu) { + display: inline-block; + float: right; + } + > button { + margin: 0 2px; + } + > span { + margin-left: 4px; + } + } + + // Button box + .oe_button_box { + width: 400px; + text-align: right; + float: right; + .oe_stat_button { + text-align: left; + width: 132px !important; + height: 40px; + margin: 0px -1px -1px 0px; + padding: 0; + > div { + text-align: left; + padding: 0; + line-height: 120%; + vertical-align: middle; + } + .o_button_icon { + display: inline-block; + width: 30%; + padding: 0px 3px; + width: 37px; + text-align: center; + vertical-align: middle; + } + + .o_stat_info { // contains the value and text + display: inline-block; + max-width: 70%; + line-height: 1.2; + } + + // Some buttons only display text and directly use the WidgetButton template instead of StatInfo + > span { + .o_stat_info; // layout + } + } + + &, & + .oe_avatar { + + .oe_title { + width: 400px; + } + } + } + + // Groups + .o_group { + .o_td_label + td { + padding: 2px 36px 2px 8px; + } + .o_form_field.o_text_overflow { + width: 1px!important; // hack to make the table layout believe it is a small element (so that the table does not grow too much) ... + min-width: 100%; // ... but in fact it takes the whole table space + } + } + + // Avatar + .oe_avatar { + float: left; + > img { + max-height: 90px; + max-width: 90px; + margin-bottom: 10px; + } + + .oe_title { + padding-left: 95px; + margin-left: 5px; + } + } + + // Chatter + .oe_chatter { + display: block; + min-width: @sheet-min-width + 2* @sheet-padding; + max-width: @sheet-max-width + 2* @sheet-padding; + margin: 0 auto; + padding: 16px 16px 48px; + } +} diff --git a/addons/web/static/src/less/graph_view.less b/addons/web/static/src/less/graph_view.less new file mode 100644 index 00000000000..c0d460ab3f1 --- /dev/null +++ b/addons/web/static/src/less/graph_view.less @@ -0,0 +1,18 @@ +.o_graph { + height: 100%; + + .o_graph_svg_container { + height: 100%; + overflow: auto; + svg { + background-color: @odoo-view-background-color; + } + } +} + +// ------------------------------------------------------------------ +// Control panel buttons +// ------------------------------------------------------------------ +.o_graph_measures_list { + .o-selected-li; +}
\ No newline at end of file diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less new file mode 100644 index 00000000000..a8b4130269c --- /dev/null +++ b/addons/web/static/src/less/list_view.less @@ -0,0 +1,220 @@ +@odoo-list-group-header-color: lighten(@odoo-list-grey, 10%); +.o_list_view { + cursor: pointer; + background-color: @odoo-view-background-color; + margin-bottom: 0px; + + &.table { + td, th { + vertical-align: middle; + } + } + + thead { + color: @odoo-main-text-color; + background-color: #eee; + border-bottom: 1px solid #cacaca; + > tr > th { + border-left: 1px solid #dfdfdf; + &:not(.o_column_sortable):hover { + cursor: default; + } + } + } + + tfoot { + cursor: default; + color: @odoo-list-footer-color; + background-color: @odoo-list-footer-bg-color; + font-weight: @odoo-list-footer-font-weight; + border-top: 2px solid #cacaca; + border-bottom: 1px solid #cacaca; + } + + .o_column_sortable { + .user-select(none); // Prevent unwanted selection while sorting + + &.o-sort-up:after { + font-family: FontAwesome; + margin-left: 5px; + content: "\f0d7"; + } + &.o-sort-down:after { + font-family: FontAwesome; + margin-left: 5px; + content: "\f0d8"; + } + } + + .o_list_record_selector { + width: 1px; // to prevent the column to expand + > input { + margin-left: 3px; + .hidden-xs(); + } + } + + .o_list_record_delete { + width: 13px; + } + + .o_handle_cell { + width: 1px; // to prevent the column to expand + .o_row_handle { + cursor: ns-resize; + } + } + + // Decoration of the row + .text-bf { + font-weight: bold; + } + .text-it { + font-style: italic; + } + .text-muted { + color: lighten(@text-muted, 10%); + } + + // Grouped list views + tbody > tr.o_group_header { + .o-vertical-gradient(#fcfcfc, #dedede); + + .o_group_name { + white-space: nowrap; + } + .o_group_pager { + cursor: text; + .user-select(none); + text-align: right; + .o_pager_previous, .o_pager_next { + background-color: @odoo-list-group-header-color; + &:hover { + background-color: darken(@odoo-list-group-header-color, 10%); + } + } + } + } + tbody + tbody { + border-top: none; // Override bootstrap for grouped list views + } + + .o_list_number { + text-align: right; + white-space: nowrap; + } + .o_list_text { + white-space: pre-wrap; + } +} + +.o_list_editable { + position: relative; + + .o_temp_visible { + opacity: 0; + display: block!important; + } + + .o_list_view { + .o_row_edition > td { + visibility: hidden; // Hide row's content during edition + &.o_list_record_selector, &.o_list_record_delete, &.o_list_button { + visibility: visible; + } + + &.o_readonly { + visibility: visible; + color: transparent; + } + } + } + + .o_form_view.o_list_editable_form { + padding: 0; + > .o_form_field { + padding: 0; + position: absolute; + white-space: nowrap; + &.o_form_textarea { + white-space: pre-wrap; + } + .o_form_field, .badge { // Many2Many tags + display: block; + float: left; + white-space: normal; + max-width: 100%; // Fix a chrome bug where badge would not break on multiple lines + } + + &.o_readonly { + background-color: transparent; + padding: 5px; + } + } + > .o_field_translate { + position: absolute; + width: 16px; // to have some space on the right + } + .o_form_input_dropdown { + height: 100%; + } + .o_form_input { + height: 100%; + padding-left: 5px; + padding-right: 5px; + } + .o_form_input, .o_form_textarea, .o_form_field_many2manytags, .o_form_field_monetary { + border-style: solid; + } + .o_form_field_monetary .o_form_input { + width: 0; + } + .o_checkbox { + padding: 7px 0 0 5px; + background: transparent; + } + .o_form_textarea { + min-width: 0; + } + .o_with_button .o_form_input_dropdown { + width: 1%; // Hack so that the sibling button is not overlapped + } + .o_form_input_dropdown, .o_datepicker { + > .o_form_input { + padding-right: 15px; // To prevent caret overlapping + } + > .o_dropdown_button, .o_datepicker_button { + .o-position-absolute(20%, 5px); + } + } + .o_form_field > .o_external_button { + padding: 0; + background-color: inherit; + margin-left: 3px; + } + .o_row_handle { + visibility: hidden; // Hide sequence when editing + } + .o_form_field_boolean { + width: auto!important; // Override the width computed in JS (needed for Chrome) + } + .o_form_field_number { + text-align: right; + } + } +} + +// Buttons in ControlPanel +.o_list_buttons { + .o_list_button_save, .o_list_button_discard { + display: none; + } + &.o-editing { // for list view editable + .o_list_button_add { + display: none + } + .o_list_button_save, .o_list_button_discard { + display: inline-block + } + } +} diff --git a/addons/web/static/src/less/modal.less b/addons/web/static/src/less/modal.less index 9ff0453c4dd..bca950796fc 100644 --- a/addons/web/static/src/less/modal.less +++ b/addons/web/static/src/less/modal.less @@ -3,6 +3,10 @@ .modal-content { border-radius: 0; + .modal-header .o_subtitle { + margin-left: 10px; + } + .modal-body { &.o_act_window { padding: 0; @@ -20,7 +24,7 @@ } } - .oe-view-manager-content { + .o_view_manager_content { position: static; // Reset position for view managers in modals (default value: absolute) } } diff --git a/addons/web/static/src/less/navbar.less b/addons/web/static/src/less/navbar.less index 8dcacd1c45c..50015db9955 100644 --- a/addons/web/static/src/less/navbar.less +++ b/addons/web/static/src/less/navbar.less @@ -1,6 +1,5 @@ .o_main_navbar { - position: relative; height: @odoo-navbar-height; border: 1px solid @odoo-navbar-inverse-link-hover-bg; background-color: @odoo-brand-primary; @@ -66,12 +65,6 @@ left: auto; right: 0; } - .o_user_menu { - float: right; - .oe_topbar_avatar { - height: 15px; - } - } } } @@ -150,13 +143,6 @@ } } } - - .o_menu_brand { - float: none; - margin: 0; - border-bottom: 1px solid @odoo-navbar-inverse-link-hover-bg; - color: transparent; - } } } diff --git a/addons/web/static/src/less/pivot_view.less b/addons/web/static/src/less/pivot_view.less new file mode 100644 index 00000000000..2fa6e1a6f44 --- /dev/null +++ b/addons/web/static/src/less/pivot_view.less @@ -0,0 +1,79 @@ +.o_pivot { + height: 100%; + overflow: auto; + + // Inform the user that he may click on a cell to be redirected to a list view of the + // items corresponding to the clicked cell + &.o_enable_linking { + .o_pivot_cell_value:hover { + color: @odoo-brand-optional; + cursor: pointer; + } + } + + table { + background-color: @odoo-view-background-color; + width: auto; // bootstrap override + + th { + font-weight: normal; // bootstrap override + background-color: lighten(@odoo-brand-secondary, 40%); + } + + .o-pivot-header-cell() { + background-color: lighten(@odoo-brand-secondary, 40%); + cursor: pointer; + white-space: nowrap; + .user-select(none); + &:hover { + background-color: lighten(@odoo-brand-secondary, 30%); + } + } + + .o_pivot_measure_row { + .o-pivot-header-cell; + } + + .o_pivot_header_cell_closed { + .o-pivot-header-cell; + &::before { + font-family: FontAwesome; + content: ""; + margin-right: 8px; + } + } + + .o_pivot_header_cell_opened { + .o-pivot-header-cell; + &::before { + font-family: FontAwesome; + content: ""; + margin-right: 8px; + } + } + + .o_pivot_measure_row_sorted_asc::after { + .o-caret-down; + margin-left: 5px; + } + + .o_pivot_measure_row_sorted_desc::after { + .o-caret-up; + margin-left: 5px; + } + } + + .o_pivot_field_selection::after { + .o-caret-right; + position: absolute; + right: 6px; + top: 9px; + } +} + +// ------------------------------------------------------------------ +// Control panel buttons +// ------------------------------------------------------------------ +.o_pivot_measures_list { + .o-selected-li; +}
\ No newline at end of file diff --git a/addons/web/static/src/less/progress_bar.less b/addons/web/static/src/less/progress_bar.less new file mode 100644 index 00000000000..25a89d15ecf --- /dev/null +++ b/addons/web/static/src/less/progress_bar.less @@ -0,0 +1,36 @@ +.o_progressbar { + + > div { + display: inline-block; + } + + .o_progressbar_title { + white-space: nowrap; + padding-right: 10px; + } + + .o_progress { + width: 100px; + height: 15px; + vertical-align: middle; + + border: 1px solid lighten(@odoo-brand-secondary, 25%); + overflow: hidden; + + background-color: white; + &.o_progress_overflow { + background-color: @odoo-brand-secondary; + } + + .o_progressbar_complete { + background-color: @odoo-brand-optional; + height: 100%; + } + } + + .o_progressbar_value { + width: 100px; + white-space: nowrap; + padding-left: 10px; + } +} diff --git a/addons/web/static/src/less/search_view.less b/addons/web/static/src/less/search_view.less new file mode 100644 index 00000000000..a34905cb929 --- /dev/null +++ b/addons/web/static/src/less/search_view.less @@ -0,0 +1,209 @@ +.o-search-options-dropdown-custom-li() { + margin: 3px 25px; + width: auto; + max-width: 250px; +} + +.o_searchview { + .o-flex-display(); + .o-flex-flow(row, wrap); + .o-align-items(flex-end); + position: relative; + padding: 0 20px 1px 0; + + .o_searchview_facet { + @o-searchview-facet-remove-width: 18px; + + .o-flex(0, 0, auto); + max-width: 100%; + .o-flex-display(); + position: relative; + margin: 1px 3px 0 0; + + .o_searchview_facet_label { + .o-flex(0, 0, auto); + .o-text-overflow(); + padding: 0 3px; + color: white; + + .o-flex-display(); + .o-align-items(center); + } + + .o_facet_values { + padding: 0 @o-searchview-facet-remove-width 0 5px; + .o_facet_values_sep { + font-style: italic; + } + } + .o_facet_remove { + .o-flex(0, 0, auto); + .o-flex-display(); + .o-justify-content(center); + .o-align-items(center); + .o-position-absolute(0, 0, 0); + width: @o-searchview-facet-remove-width; + cursor: pointer; + } + } + + .o_searchview_input { + width: 100px; + .o-flex(1, 0, auto); + } + + .o_searchview_more { + font-size: 16px; + cursor: pointer; + } + + .o_searchview_autocomplete { + .o-position-absolute(100%, @left: auto); + width: 100%; + + li { + padding-left: 25px; + position: relative; + &.o-indent { + padding-left: 50px; + } + a { + display: inline-block; + padding-left: 0px; + padding-right: 0px; + &:hover { + background-color: inherit; + } + &.o-expand, &.o-expanded { // caret + .o-position-absolute(@left: 6px); + padding: 3px; + } + &.o-expand:before { + .o-caret-right; + } + &.o-expanded:before { + .o-caret-down; + } + } + } + } +} + +// Searchview and Sidebar dropdowns +// FIXME: move me out of here +.o_dropdown { + white-space: nowrap; // prevent the arrow from going under the dropdown when too little space + .dropdown-toggle:first-child { + // apply the padding to the first element so that when the `favorites` menu goes under + // the `filters` menu, they're still aligned + padding-right: 10px; + } + + display: inline-block; + cursor: pointer; + .user-select(none); // Prevent text selection when toggling the dropdowns + > a { // dropdown togglers + color: @odoo-main-text-color; + &:hover { + text-decoration: none; + font-weight: bold; + color: darken(@odoo-main-text-color, 10%); + } + } + &.open > a { + font-weight: bold; + color: darken(@odoo-main-text-color, 10%); + } +} + +// Searchview dropdowns +.o_search_options { + // Open and closed carets in dropdowns + .o_closed_menu { + position: relative; + a:before { + .o-caret-right; + .o-position-absolute(@top: 50%, @left: 10px); + margin-top: (-@caret-width-base); + } + } + .o_open_menu { + position: relative; + a:before { + .o-caret-down; + .o-position-absolute(@top: 50%, @left: 10px); + margin-top: (-@caret-width-base); + } + } + + // Filters + .o_filters_menu { + .o-selected-li; + + .o_filter_condition { + .o-search-options-dropdown-custom-li; + margin-bottom: 8px; + .o_or_filter { // or between conditions + display: none; // hidden for the first condition + .o-position-absolute(@top: 7px, @left: -18px); + } + } + .o_filter_condition + .o_filter_condition { + .o_or_filter { + display: block; // displayed from second condition + } + } + .o_searchview_extended_prop_op { + margin: 3px 0px; // vertical gap between selects + } + .o_searchview_extended_prop_value { // date fields: datepicker position + .datepickerbutton { + .o-position-absolute(3px, -20px); + cursor: pointer; + } + } + .o_searchview_extended_delete_prop { // delete condition button position + .o-position-absolute(3px, -18px); + cursor: pointer; + } + .o_add_filter_menu { // apply and add buttons + display: none; // Needed by the webclient + .o-search-options-dropdown-custom-li; + } + } + + // Group By + .o_group_by_menu { + .o-selected-li; + + .divider {display: none;} // hide the divider by default (no group) + .o_add_group { // add button + display: none; // Needed by the webclient + .o-search-options-dropdown-custom-li; + } + } + + // Favorites + .o_favorites_menu { + .o-selected-li; + + .divider {display: none;} // hide the divider by default (no favorite) + .o-searchview-custom-private, .o-searchview-custom-public { + .o-remove-filter { + .o-position-absolute(50%, 12px); + margin-top: -6px; + cursor: pointer; + } + } + .o-searchview-custom-public a:after { + font-family: FontAwesome; + content: "\f0c0"; + margin-left: 5px; + font-weight: normal; + } + .o_save_name { // save button + display: none; // hidden by default + .o-search-options-dropdown-custom-li; + } + } +} diff --git a/addons/web/static/src/less/search_view_extra.less b/addons/web/static/src/less/search_view_extra.less new file mode 100644 index 00000000000..0ef7b8dd1dd --- /dev/null +++ b/addons/web/static/src/less/search_view_extra.less @@ -0,0 +1,44 @@ +.o_searchview { + background-color: white; + border: 1px solid #ccc; + border-radius: @border-radius-base; + padding: 1px 25px 3px 5px; + + .o_searchview_facet { + border: 1px solid @gray-light; + background: lighten(@odoo-brand-primary, 35%); + + .o_searchview_facet_label { + background-color: @gray-light; + } + + .o_facet_remove { + color: @gray-light; + &:hover { + color: @gray; + } + } + } + + .o_searchview_input { + border: none; + padding: 4px 0 0 0; + } + .o_searchview_more { + .o-position-absolute(@top: 6px, @right: 5px); + } + .o_searchview_autocomplete li.o-selection-focus { + background-color: @odoo-brand-primary; + > a { + color: white; + } + } +} + +// Search options: inputs and selects style +.o_search_options { + input[type="text"], select { + .form-control(); + } + +} diff --git a/addons/web/static/src/less/tooltip.less b/addons/web/static/src/less/tooltip.less new file mode 100644 index 00000000000..dad63e374fa --- /dev/null +++ b/addons/web/static/src/less/tooltip.less @@ -0,0 +1,57 @@ + +.tooltip { + padding: 0; + margin: 0; + opacity: 1; + + .tooltip-inner { + background-color: @odoo-tooltip-background-color; + color: @odoo-tooltip-color; + border-radius: @border-radius-base; + box-shadow: 0px 0px 1px 1px @odoo-brand-secondary; + + padding: 5px; + + text-align: left; + + .oe_tooltip_string { + background-color: @odoo-brand-primary; + color: white; + font-weight: bold; + padding: 5px; + } + .oe_tooltip_help { + white-space: pre-line; + padding: 5px 5px 0; + } + .oe_tooltip_technical { + padding: 0; + margin: 5px 0 5px 20px; + list-style-type: circle; + + .oe_tooltip_technical_title { + font-weight: bold; + } + } + } + + .tooltip-arrow { + -moz-transform: scale(0.9999); + } + &.top, &.top-left, &.top-right { + .tooltip-arrow { + border-top-color: @odoo-tooltip-arrow-color!important; + } + } + &.bottom, &.bottom-left, &.bottom-right { + .tooltip-arrow { + border-bottom-color: @odoo-tooltip-arrow-color!important; + } + } + &.left .tooltip-arrow { + border-left-color: @odoo-tooltip-arrow-color!important; + } + &.right .tooltip-arrow { + border-right-color: @odoo-tooltip-arrow-color!important; + } +} diff --git a/addons/web/static/src/less/tree_view.less b/addons/web/static/src/less/tree_view.less new file mode 100644 index 00000000000..681cfb33c80 --- /dev/null +++ b/addons/web/static/src/less/tree_view.less @@ -0,0 +1,50 @@ +.o_treeview_table { + width: 100%; + background-color: white; + border-collapse: collapse; + + thead { + color: @odoo-main-text-color; + background-color: @odoo-list-grey; + > tr > th { + padding: 10px; + } + } + + tbody { + > tr { + border: 1px solid @odoo-list-grey; + &:hover { + background-color: lighten(@odoo-list-grey, 15%); + } + > td { + border: 1px solid @odoo-list-grey; + cursor: pointer; + height: 40px; + padding: 0 4px; + &.oe_number { + text-align: right; + } + } + } + .treeview-tr, .treeview-td { + &.o_treeview_first > span { + padding-left: 16px; + } + } + .treeview-tr.o_treeview_first { + position: relative; + &:before { + position: absolute; + top: 13px; + .o-caret-right(6px); + } + } + .o_open .treeview-tr.o_treeview_first { + &:before { + top: 16px; + .o-caret-down(6px); + } + } + } +}
\ No newline at end of file diff --git a/addons/web/static/src/less/utils.less b/addons/web/static/src/less/utils.less index 302f79edf03..836ae6453dc 100644 --- a/addons/web/static/src/less/utils.less +++ b/addons/web/static/src/less/utils.less @@ -116,14 +116,6 @@ background-size: @size; } -// Background gradient -.o-linear-gradient(@deg: 45deg, @start-color: #555, @end-color: #333) { - background-repeat: repeat-x; - background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); - background-image: -o-linear-gradient(@deg, @start-color, @end-color); - background-image: linear-gradient(@deg, @start-color, @end-color); -} - // ------------------------------------------------------------------ // Dropdowns // ------------------------------------------------------------------ @@ -221,3 +213,40 @@ text-overflow: ellipsis; vertical-align: top; // To update display context changed by overflow:hidden } + +// ------------------------------------------------------------------ +// Gradients +// ------------------------------------------------------------------ +.o-linear-gradient(@deg: 45deg, @start-color: #555, @end-color: #333) { + background-repeat: repeat-x; + background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); + background-image: -o-linear-gradient(@deg, @start-color, @end-color); + background-image: linear-gradient(@deg, @start-color, @end-color); +} +.o-vertical-gradient(@startColor: #555, @endColor: #333) { + background-color: mix(@startColor, @endColor); + background-image: -webkit-gradient(linear, left top, left bottom, from(@startColor), to(@endColor)); + background-image: -webkit-linear-gradient(top, @startColor, @endColor); + background-image: -moz-linear-gradient(top, @startColor, @endColor); + background-image: -ms-linear-gradient(top, @startColor, @endColor); + background-image: -o-linear-gradient(top, @startColor, @endColor); + background-image: linear-gradient(to bottom, @startColor, @endColor); +} +.o-skew-gradient(@startColor: #555, @endColor: #333) { + background-color: @endColor; + background: -moz-linear-gradient(135deg, @endColor, @startColor); + background: -o-linear-gradient(135deg, @startColor, @endColor); + background: -webkit-gradient(linear, left top, right bottom, from(@startColor), to(@endColor)); + background: -ms-linear-gradient(top, @startColor, @endColor); +} + +// ------------------------------------------------------------------ +// Transform +// ------------------------------------------------------------------ +.o-transform(@transform) { + -webkit-transform: @transform; + -moz-transform: @transform; + -ms-transform: @transform; + -o-transform: @transform; + transform: @transform; +} diff --git a/addons/web/static/src/less/variables.less b/addons/web/static/src/less/variables.less index 4bea799e5ec..5a8120d0d97 100644 --- a/addons/web/static/src/less/variables.less +++ b/addons/web/static/src/less/variables.less @@ -1,28 +1,50 @@ @odoo-font-size-base: 13px; -@odoo-font-size-base-touch: 15px; // Colors -@odoo-brand-primary: #337ab7; -@odoo-brand-optional: @odoo-brand-primary; -@odoo-brand-secondary: #8f8f8f; +@odoo-brand-primary: #7c7bad; +@odoo-brand-optional: #7c7bad; +@odoo-brand-secondary: #f0eeee; @odoo-brand-lightsecondary: #e2e2e0; +@odoo-color-pink: #a24689; @odoo-main-color-muted: #a8a8a8; -@odoo-main-text-color: #555555; +@odoo-main-text-color: #4c4c4c; @odoo-view-background-color: white; @odoo-shadow-color: #303030; -@odoo-webclient-background-color: #F0EEEE; + +@odoo-webclient-background-color: @odoo-brand-secondary; +@odoo-control-panel-background-color: @odoo-webclient-background-color; +@odoo-form-lightsecondary: #ccc; @odoo-list-grey: @odoo-brand-lightsecondary; +@odoo-list-footer-color: @odoo-main-text-color; +@odoo-list-footer-bg-color: #eee; +@odoo-list-footer-font-weight: bold; + +@odoo-tooltip-background-color: black; +@odoo-tooltip-color: white; +@odoo-tooltip-arrow-color: black; + // Layout @screen-xs-min: 475px; @screen-xxs-max: (@screen-xs-min - 1); @odoo-form-group-cols: 12; +@odoo-form-spacing-unit: 5px; @odoo-horizontal-padding: 16px; +@odoo-innergroup-rpadding: 45px; +@odoo-dropdown-hpadding: 25px; + +@border-radius-base: 3px; -// Compatibility -@odoo-navbar-height: 46px; +@odoo-sheet-vpadding: 24px; + +@odoo-avatar-size: 90px; + +@odoo-statusbar-height: 36px; +@odoo-statusbar-arrow-width: @odoo-statusbar-height/3; + +@odoo-label-font-size-factor: 0.8; +@odoo-navbar-height: 34px; @odoo-navbar-inverse-link-hover-bg: darken(@odoo-brand-primary, 10%); -@odoo-dropdown-hpadding: 25px; diff --git a/addons/web/static/src/less/views.less b/addons/web/static/src/less/views.less new file mode 100644 index 00000000000..7a4deca60ff --- /dev/null +++ b/addons/web/static/src/less/views.less @@ -0,0 +1,72 @@ +// -------------------------------------------------------------------------- +// This file contains rules applying on elements shared between views +// -------------------------------------------------------------------------- + +// Many2many tags +.o_form_field_many2manytags { + .o_tag_color_0 { + background-color: #B6BBBF; + } + .o_tag_color_1 { + background-color: #61BD4F; + } + .o_tag_color_2 { + background-color: #F2D600; + } + .o_tag_color_3 { + background-color: #FFAB4A; + } + .o_tag_color_4 { + background-color: #EB5A46; + } + .o_tag_color_5 { + background-color: #A24689; + } + .o_tag_color_6 { + background-color: #0079BF; + } + .o_tag_color_7 { + background-color: #00C2E0; + } + .o_tag_color_8 { + background-color: #4CD98E; + } + .o_tag_color_9 { + background-color: #FF80CE; + } + .o_tag_color_10 { + background-color: #4D4D4D; + } +} + +// Stars +.o_priority { + display: inline-block; + padding: 0; + margin: 0; + vertical-align: middle; + > .o_priority_star { + display: inline-block; + font-size: 1.35em; + &:hover, &:focus { + text-decoration: none; + outline: none; + } + + + .o_priority_star { + padding-left: 5px; + } + + &.fa-star-o { + color: @odoo-main-color-muted; + } + &.fa-star { + color: gold; + } + } +} + +// No content helper +.oe_view.oe_cannot_create .oe_view_nocontent_create { + display: none; +} diff --git a/addons/web/static/src/less/views_extra.less b/addons/web/static/src/less/views_extra.less new file mode 100644 index 00000000000..ad6f4c83017 --- /dev/null +++ b/addons/web/static/src/less/views_extra.less @@ -0,0 +1,33 @@ +// No content helper +.oe_view_nocontent { + padding: 15px; + margin-top: 0; + color: #777777; + font-size: 125%; + max-width: 700px; + .oe_view_nocontent_create { + margin-top: 0; + padding-top: 35px; + color: #4c4c4c; + &:before { + content: ""; + display: inline-block; + position: absolute; + width: 70px; + height: 80px; + margin-left: -70px; + margin-top: -50px; + background: transparent url(/web/static/src/img/view_empty_arrow.png) no-repeat 0px 0px; + } + } + > p { + padding-left: 78px; + } + .oe_empty_custom_dashboard { + background: transparent url(/web/static/src/img/graph_background.png) no-repeat 0 0; + margin-top: -15px; + padding: 100px 0 0 137px; + min-height: 327px; + margin-left: -15px; + } +} diff --git a/addons/web/static/src/less/notification.less b/addons/web/static/src/less/webclient.less index 0c910dbc5c5..2f9878fbdc3 100644 --- a/addons/web/static/src/less/notification.less +++ b/addons/web/static/src/less/webclient.less @@ -1,3 +1,29 @@ +// ------------------------------------------------------------------ +// Typography +// ------------------------------------------------------------------ +a { + color: @odoo-brand-optional; + &:hover { + color: darken(@odoo-brand-optional, 10%); + } +} + +// ------------------------------------------------------------------ +// General +// ------------------------------------------------------------------ +.o_web_client { + background-color: @odoo-webclient-background-color; +} +.o_text_overflow { + .o-text-overflow(); +} +.o_hidden { + display: none!important; +} + +// ------------------------------------------------------------------ +// Misc. widgets +// ------------------------------------------------------------------ @notification-error-bg-color: #F16567; @notification-info-bg-color: #FCFBEA; .o_notification_manager { @@ -5,7 +31,7 @@ max-width: 100%; .o-position-absolute(@navbar-height, 0px); - z-index: 50; // same as debug manager. Fixme: expose a variable + z-index: 1100; // Bootstrap modal z-index is 1050 .o_notification { padding: 0; @@ -49,3 +75,15 @@ } } } + +// Buttons +.o_icon_button { + background-color: transparent; + border: 0; + padding: 0; + outline: none; +} + +.oe_highlight { + .btn-primary; +} diff --git a/addons/web/static/src/less/webclient_extra.less b/addons/web/static/src/less/webclient_extra.less new file mode 100644 index 00000000000..094670bf0ca --- /dev/null +++ b/addons/web/static/src/less/webclient_extra.less @@ -0,0 +1,65 @@ +// ------------------------------------------------------------------ +// Checkboxes +// ------------------------------------------------------------------ +div.o_checkbox { + display: inline-block; + vertical-align: sub; + > input + span { + display: none; + } +} + +// ------------------------------------------------------------------ +// Inputs and selects +// ------------------------------------------------------------------ +.o-input() { + border: 1px solid @odoo-form-lightsecondary; + border-radius: @border-radius-base; + padding: 2px 4px; + color: #1f1f1f; +} +input[type="text"], input[type="password"], input[type="number"], textarea, select { + .o-input(); + + width: 100%; + outline: none; + display: block; +} +select { + background: white; +} + +// ------------------------------------------------------------------ +// Loading +// ------------------------------------------------------------------ +.o_loading { + position: fixed; + top: 0; + right: 50%; + padding: 4px 12px; + background-color: @odoo-color-pink; + color: white; + text-align: center; + border-top: none; + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; +} + +// ------------------------------------------------------------------ +// Planner +// ------------------------------------------------------------------ +// Because the `web_planner` module has to work with Odoo community and enterprise without +// code duplication, fine-tune the display of it in the community webclient here +#PlannerDialog { + top: 35px; + box-shadow: 0 3px 9px #666666; + padding-bottom: 44px; +} + +// ------------------------------------------------------------------ +// User menu in navbar +// ------------------------------------------------------------------ +#oe_main_menu_navbar .oe_user_menu_placeholder .o_user_menu .oe_topbar_name { + .o-text-overflow(); + max-width: 150px; +} diff --git a/addons/web/static/src/less/webclient_layout.less b/addons/web/static/src/less/webclient_layout.less new file mode 100644 index 00000000000..2dab3042aba --- /dev/null +++ b/addons/web/static/src/less/webclient_layout.less @@ -0,0 +1,156 @@ +@odoo-submenu-width: 220px; + +.o_web_client { + .o-flex-display(); + .o-flex-flow(column, nowrap); + overflow: hidden; + + .navbar { + .o-flex(0, 0, auto); + + li > a { + padding: 4px 8px; + margin-top: 2px; + font-size: 13px; + } + + .oe_topbar_avatar { + max-height: 18px; + width: 18px; + } + } + + > .o_main { + .o-flex(1, 1, auto); + .o-flex-display(); + + .o_sub_menu { + .o-flex(0, 0, @odoo-submenu-width); + .o-flex-display(); + .o-flex-flow(column, nowrap); + border-right: 1px solid #afafb6; + + .o_sub_menu_logo { + .o-flex(0, 0, auto); + text-align: center; + position: relative; + + > img { + margin: 14px 0; + } + + .oe_logo_edit { + position: absolute; + top: 1px; + width: 100%; + padding: 4px; + margin: 14px 0; + display: none; + color: #eee; + background: rgba(37,37,37,0.9); + } + &:hover .oe_logo_edit_admin { + display: block; + } + } + + .o_sub_menu_content { + .o-flex(1, 1, auto); + height:100%; + overflow-y: auto; + + .oe_secondary_menu_section { + font-weight: bold; + margin-left: 8px; + color: @odoo-brand-optional; + } + .oe_secondary_submenu { + margin: 3px 0px 10px; + line-height: 1.1em; + >li:not(.active) > a { + color: @odoo-main-text-color; + } + > li > a { + padding: 4px 4px 4px 18px; + > .badge { + margin-top: -1px; + } + } + .oe_secondary_submenu { + > li > a { + padding-left: 30px; + } + .oe_secondary_submenu > li > a { + padding-left: 42px; + } + } + .oe_menu_text { + .o-text-overflow(); + max-width: 85%; + margin-top: 1px; + } + .oe_menu_toggler:before { + display: inline-block; + width: 0; + height: 0; + content: "&darr"; + opacity: 0.5; + text-indent: -99999px; + vertical-align: top; + margin-left: -12px; + margin-top: 4px; + margin-right: 4px; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 4px solid #4c4c4c; + } + .oe_menu_opened:before { + margin-top: 6px; + margin-left: -16px; + margin-right: 4px; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #4c4c4c; + } + } + } + + .o_sub_menu_footer { + .o-flex(0, 0, auto); + text-align: center; + + > a { + font-weight: bold; + > span { + color: @odoo-color-pink; + } + } + } + } + + .o_main_content { + .o-flex(1, 1, auto); + .o-flex-display(); + .o-flex-flow(column, nowrap); + + .o_control_panel { + .o-flex(0, 0, auto); + } + .o_content { + .o-flex(1, 1, auto); + height: 100%; + overflow: auto; + position: relative; + + .o_view_manager_content { + .o-position-absolute(0, 0, 0, 0); // Get the 100% height of its flex parent + height: 100%; + + .o_view_manager_content { + position: static; // Reset position for nested view managers + } + } + } + } + } +} diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index a36b9b9685e..a551c7ee1f2 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -2,7 +2,7 @@ <templates id="template" xml:space="preserve"> <div t-name="EmptyComponent"/> -<div t-name="Loading" class="oe_loading">Loading...</div> +<div t-name="Loading" class="o_loading"/> <t t-name="Notification"> <div class="o_notification" t-translation="off"> <a t-if="widget.sticky" class="fa fa-times o_close" href="#"/> @@ -35,262 +35,275 @@ </div> </t> -<t t-name="ChangePassword"> - <form name="change_password_form" method="POST"> - <div class="oe_form"> - <table align="center"> +<form t-name="ChangePassword" name="change_password_form" method="POST"> + <div class="o_form_view"> + <table class="o_group o_inner_group o_label_nowrap"> <tr> - <td class="oe_form_group_cell oe_form_group_cell_label"><label for="old_pwd" class="oe_form_label">Old Password:</label></td> - <td class="oe_form_group_cell"><input type="password" name="old_pwd" - minlength="1" autofocus="autofocus"/></td> + <td class="o_td_label"><label for="old_pwd" class="o_form_label">Old Password</label></td> + <td width="100%"><input type="password" class="o_form_field o_form_input" name="old_pwd" minlength="1" autofocus="autofocus"/></td> </tr> <tr> - <td class="oe_form_group_cell oe_form_group_cell_label"><label for="new_password" class="oe_form_label">New Password:</label></td> - <td class="oe_form_group_cell"><input type="password" name="new_password" - minlength="1"/></td> + <td class="o_td_label"><label for="new_password" class="o_form_label">New Password</label></td> + <td width="100%"><input type="password" class="o_form_field o_form_input" name="new_password" minlength="1"/></td> </tr> <tr> - <td class="oe_form_group_cell oe_form_group_cell_label"><label for="confirm_pwd" class="oe_form_label">Confirm New Password:</label></td> - <td class="oe_form_group_cell"><input type="password" name="confirm_pwd" - minlength="1"/></td> - </tr> - <tr> - <td colspan="2" align="right"> - <button type="button" class='oe_button oe_form_button'>Change Password</button> - <button type="button" class="oe_button oe_form_button_cancel oe_form_button oe_link" href="javascript:void(0)"><span>Cancel</span></button> - </td> + <td class="o_td_label"><label for="confirm_pwd" class="o_form_label">Confirm New Password</label></td> + <td width="100%"><input type="password" class="o_form_field o_form_input" name="confirm_pwd" minlength="1"/></td> </tr> </table> - </div> - </form> -</t> -<t t-name="Menu.needaction_counter"> - <div id="menu_counter" class="badge pull-right"> - <t t-if="widget.needaction_counter > 99"> 99+ </t> - <t t-if="widget.needaction_counter <= 99"> <t t-esc="widget.needaction_counter"/> </t> + <button type="button" class="btn btn-sm btn-primary oe_form_button">Change Password</button> + <button type="button" class="btn btn-sm btn-default oe_form_button oe_form_button_cancel" href="javascript:void(0)">Cancel</button> </div> -</t> - -<t t-name="UserMenu"> - <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown"> - <img class="oe_topbar_avatar" t-att-data-default-src="_s + '/web/static/src/img/user_menu_avatar.png'"/> - <span class="oe_topbar_name"/> <b class="caret"></b> - </a> - <ul class="dropdown-menu"> - <li><a href="#" data-menu="documentation">Documentation</a></li> - <li><a href="#" data-menu="support">Support</a></li> - <li><a href="#" data-menu="about">About</a></li> - <li class="divider"/> - <li><a href="#" data-menu="settings">Preferences</a></li> - <li><a href="#" data-menu="account">My Odoo.com account</a></li> - <li><a href="#" data-menu="logout">Log out</a></li> - </ul> - </li> -</t> +</form> -<div t-name="UserMenu.about" class="oe_about"> - <div class="container-fluid"> - <div class="row"> - <div class="col-sm-offset-2 col-md-offset-2 col-sm-8 col-md-8 user-details"> - <div class="user-image"> - <img src="/web/static/src/img/logo2.png" alt="Odoo (Formerly OpenERP)" title="Odoo (Formerly OpenERP)" class="img-circle"/> - </div> - <div class="user-info-block"> - <div class="user-heading"> - <h3> - Version <t t-esc="db_info.server_version"/> - (Community Edition) - </h3> - <t t-if="db_info.expiration_date"> - <h5>Database expiration: <t t-esc="new moment(db_info.expiration_date).format('LL')"/></h5> +<div t-name="WebClient.database_expiration_panel"> + <div class="database_expiration_panel" id="announcement_bar_table"> + <div t-attf-class="alert alert-#{diffDays<6?'danger':(diffDays<16?'warning':'info')}" role="alert"> + <a href="#" class="oe_instance_hide_panel pull-right" t-translation="off">&times;</a> + <span> + <span class="oe_instance_expires"></span> + <span class="oe_instance_register"> + <t t-if="!has_mail"> + You will be able to register your database once you have installed your first app. + </t> + <t t-if="has_mail"> + <t t-if="diffDays == 30"> + <t t-set="delay">1 month</t> </t> - </div> - <div class="user-body"> - <div class="tab-content"> - <div id="settings" class="tab-pane active"> - <h4>Copyright © 2004-2015 Odoo S.A.</h4> - <p>Odoo is a trademark of <a target="_blank" href="https://www.odoo.com">Odoo S.A.</a></p> - <p>Licenced under the terms of <a target="_blank" href="http://www.gnu.org/licenses/lgpl.html">GNU Lesser General Public License</a></p> - <p>For more information visit <a target="_blank" href="https://www.odoo.com">Odoo.com</a></p> - <p> - <a href="https://www.odoo.com/web/about/facebook"><i class="fa fa-facebook-square fa-3x social-fb"></i></a> - <a href="https://www.odoo.com/web/about/twitter"><i class="fa fa-twitter-square fa-3x social-tw"></i></a> - <a href="https://www.odoo.com/web/about/googleplus"><i class="fa fa-google-plus-square fa-3x social-gp"></i></a> - <a href="https://www.odoo.com/web/about/linkedin"><i class="fa fa-linkedin-square fa-3x social-lk"></i></a> - </p> - </div> - </div> - </div> + <t t-if="diffDays != 30"> + <t t-set="delay"><t t-esc="diffDays"/> days</t> + </t> + <t t-if="diffDays > 0"> + <t t-if="dbexpiration_reason == 'demo'"> + This demo database will expire in <span t-esc="delay"/>. + </t> + <t t-if="dbexpiration_reason != 'demo'"> + This database will expire in <span t-esc="delay"/>. + </t> + </t> + <t t-if="diffDays < 1"> + This database has expired. + </t> + <!-- only show corrective actions to an admin (group_system) --> + <t t-if="warning=='admin'"> + <t t-if="dbexpiration_reason == 'trial' or dbexpiration_reason == 'demo' or dbexpiration_reason === false"> + <a class="oe_instance_register_show" href="#">Register your subscription</a> or <a class="oe_instance_buy" href="#">buy a subscription</a>. + </t> + <t t-if="dbexpiration_reason == 'renewal'"> + <a class="oe_instance_renew" href="#">Renew your subscription</a>. + </t> + <t t-if="dbexpiration_reason == 'upsell'"> + You have more users than your subscription allows.<br/> + <a class="oe_instance_upsell" href="#">Add more users</a> + </t> + </t> + <t t-if="warning=='user'"> + Log in as an administrator to correct the issue. + </t> + </t> + </span> + </span> + <span class="oe_instance_register oe_instance_success"> + Thank you, your registration was successful! Your database is valid until <span class="valid_date"/>. + </span> + <span class="oe_instance_register oe_instance_error"> + Something went wrong while registering your database. You can try again or contact our support at <a href='mailto:help@odoo.com' t-translation="off">help@odoo.com</a>. + </span> + <form class="form-inline oe_instance_register oe_instance_register_form"> + <br/> + <div class="form-group"> + <label for="enterprise_code">Subscription Code: </label> + <input type="text" class="form-control" id="enterprise_code" + placeholder="Paste code here" + title="Your subscription code"/> </div> - </div> - </div> - <div class="row" t-if="!debug"> - <div class="col-sm-12 col-md-12"> - <a class="pull-right oe_activate_debug_mode" href="?debug" >Activate the developer mode</a> - </div> + <button class="btn btn-sm btn-primary" id="confirm_enterprise_code">Register</button> + </form> </div> </div> </div> -<t t-name="WebClient.timezone_warning"> - <i class="fa fa-exclamation-triangle oe_tz_warning"></i> -</t> - -<t t-name="EmbedClient"> - <div class="openerp"> - <div class="oe_application"></div> - </div> -</t> - -<div t-name="ActionManager" class="oe_application" /> +<div t-name="ActionManager" class="o_content"/> <t t-name="ControlPanel"> - <div class="oe-control-panel"> - <div class="container-fluid"> - <div class="row"> - <div class="col-md-6 oe-cp-title"> - <ol class="oe-view-title breadcrumb"> - </ol> - </div> - <div class="oe-cp-search-view col-md-6" /> - </div> - <div class="row"> - <div class="col-md-6 oe-button-column"> - <div class="oe-cp-buttons"></div> - <div class="oe-cp-sidebar"></div> - </div> - <div class="col-md-6"> - <div class="oe-search-options btn-group"/> - <div class="oe-right-toolbar"> - <div class="oe-cp-pager"></div> - <div class="oe-cp-switch-buttons btn-group btn-group-sm"></div> - </div> - </div> - </div> + <div class="o_control_panel"> + <ol class="breadcrumb"/> + <div class="o_cp_searchview"/> + <div class="o_cp_left"> + <div class="o_cp_buttons"/> + <div class="o_cp_sidebar"/> + </div> + <div class="o_cp_right"> + <div class="btn-group o_search_options"/> + <div class="o_cp_pager"/> + <div class="btn-group btn-group-sm o_cp_switch_buttons"/> </div> </div> </t> <t t-name="X2ManyControlPanel"> - <div class="oe-x2m-control-panel container-fluid"> - <div class="oe-cp-buttons pull-left"/> - <div class="oe-cp-pager pull-right"/> + <div class="o_x2m_control_panel"> + <div class="o_cp_buttons"/> + <div class="o_cp_pager"/> </div> </t> -<div t-name="ViewManager" class="oe-view-manager"> - <div class="oe-view-manager-content"> - <t t-foreach="widget.views" t-as="view"> - <div t-attf-class="oe-view-manager-view-#{view}"/> - </t> - </div> -</div> <t t-name="ViewManager.switch-buttons"> <t t-foreach="views" t-as="view"> <button type="button" t-att-accesskey="view.accesskey" - t-attf-class="btn btn-default fa #{view.icon} oe-cp-switch-#{view.type}" + t-attf-class="btn btn-icon fa fa-lg #{view.icon} o_cp_switch_#{view.type}" t-att-data-view-type="view.type" t-att-title="view.label"/> </t> </t> <t t-name="WebClient.DebugManager"> - <li class="oe_debug_manager"> - <a href="#" title="Open Debug Menu" class="dropdown-toggle oe_debug_button" data-toggle="dropdown"> + <li class="o_debug_manager"> + <a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown"> <span class="fa fa-bug"/> <b class="caret"/> </a> - <ul class="dropdown-menu js_debug_dropdown" role="menu"/> + <ul class="dropdown-menu o_debug_dropdown" role="menu"/> </li> </t> -<t t-name="WebClient.DebugDropdown"> - <t t-if="active_view.type === 'form'"> - <li class="oe_metadata"><a href="#" data-action="get_metadata">View Metadata</a></li> - <li class="oe_layout"><a href="#" data-action="toggle_layout_outline">Toggle Form Layout Outline</a></li> - <li class="oe_defaults"><a href="#" data-action="set_defaults">Set Defaults</a></li> - </t> - <li class="oe_defaults"><a href="#" data-action="perform_js_tests">JS Tests</a></li> - <li class="oe_fields"><a href="#" data-action="get_view_fields">View Fields</a></li> - <li class="oe_fvg"><a href="#" data-action="fvg">Fields View Get</a></li> - <li class="oe_filters"><a href="#" data-action="manage_filters">Manage Filters</a></li> - <t t-if="is_admin"> - <li class="oe_translate"><a href="#" data-action="translate">Technical Translation</a></li> - <li class="oe_edit_view"><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="active_view.controller.fields_view.view_id">Edit <t t-esc="_.str.capitalize(view.fields_view.type)"/> View</a></li> - <li class="oe_edit_searchview" t-if="searchview"><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="searchview.view_id">Edit SearchView</a></li> - <li class="oe_edit_action" t-if="action"><a href="#" data-action="edit" t-att-data-model="action.type" t-att-data-id="action.id">Edit Action</a></li> - <li class="oe_edit_workflow"><a href="#" data-action="edit_workflow">Edit Workflow</a></li> - <li class="oe_print_workflow"><a href="#" data-action="print_workflow">Print Workflow</a></li> - <li class="divider"/> - <li class="oe_split_assets"><a href="#" data-action="split_assets">Activate Assets Debugging</a></li> + +<t t-name="WebClient.DebugManager.Global"> + <li t-if="manager._features_group"><a href="#" data-action="toggle_technical_features"> + <t t-if="manager._has_features"> + Disable Technical Features + </t> + <t t-if="!manager._has_features"> + Enable Technical Features + </t> + </a></li> + <li><a href="#" data-action="split_assets">Activate Assets Debugging</a></li> + <li class="divider"/> + <li><a href="#" data-action="perform_js_tests">Run JS Tests</a></li> + <li><a href="#" data-action="select_view">Open View</a></li> + <li><a href="#" data-action="start_tour">Start tour</a></li> + <t t-if="manager._events"> <li class="divider"/> - <li class="oe_leave"><a href="#" data-action="leave_debug">Leave Debug Mode</a></li> + <li><a href="#" data-action="show_timelines">Toggle Timelines</a></li> + <li><a href="#" data-action="requests_clear">Clear Events</a></li> </t> + <li class="divider o_debug_leave_section"/> + <li><a href="#" data-action="leave_debug_mode">Leave the Developer Tools</a></li> </t> -<t t-name="WebClient.DebugViewLog"> - <div class="oe_debug_menu_log"> - <table class="table table-condensed table-striped"> - <tr> - <th>ID:</th> - <td><t t-esc="perm.id"/></td> - </tr> - <tr> - <th>XML ID:</th> - <td><t t-esc="perm.xmlid or '/'"/></td> - </tr> - <tr> - <th>No Update:</th> - <td><t t-esc="perm.noupdate"/></td> - </tr> - <tr> - <th>Creation User:</th> - <td><t t-esc="format(perm.create_uid, { 'type' : 'many2one' }, '/')"/></td> - </tr> - <tr> - <th>Creation Date:</th> - <td><t t-esc="format(perm.create_date, { 'type' : 'datetime' }, '/')"/></td> - </tr> + +<t t-name="WebClient.DebugManager.ToursDialog"> + <div> + <table class="table table-condensed table-striped table-responsive"> <tr> - <th>Latest Modification by:</th> - <td><t t-esc="format(perm.write_uid, { 'type' : 'many2one' }, '/')"/></td> + <th>ID</th> + <th>Name</th> + <th>Path</th> + <th/> </tr> - <tr> - <th>Latest Modification Date:</th> - <td><t t-esc="format(perm.write_date, { 'type' : 'datetime' }, '/')"/></td> + + <tr t-foreach="tours" t-as="tour"> + <td><t t-esc="tours[tour].id"/></td> + <td><t t-esc="tours[tour].name"/></td> + <td><t t-esc="tours[tour].path"/></td> + <td><button type="button" class="btn btn-sm btn-primary fa fa-play o_start_tour" t-att-data-id="tours[tour].id"/></td> </tr> </table> </div> </t> -<span t-name="ViewPager"> - <div class="oe_pager_value" style="display: inline-block"> - <t t-raw="0"/> - </div> - <div class="btn-group btn-group-sm oe-pager-buttons" style="display: inline-block"> - <!-- accesskeys not wanted in X2Many widgets --> - <t t-if="!widget.x2m"> - <t t-set="att_prev" t-value="{'accesskey': 'O'}" /> - <t t-set="att_next" t-value="{'accesskey': 'P'}" /> +<t t-name="WebClient.DebugManager.Action"> + <t t-if="action"> + <li class="divider"/> + <li><a href="#" data-action="edit" t-att-data-model="action.type" t-att-data-id="action.id">Edit Action</a></li> + <t t-if="action.res_model"> + <li><a href="#" data-action="get_view_fields">View Fields</a></li> + <li><a href="#" data-action="manage_filters">Manage Filters</a></li> + <li t-if="manager._is_admin"><a href="#" data-action="edit_workflow">Edit Workflow</a></li> + <li><a href="#" data-action="translate">Technical Translation</a></li> </t> - <a class="fa fa-chevron-left btn btn-default oe-pager-button" t-att="att_prev" type="button" data-pager-action="previous"></a> - <a class="fa fa-chevron-right btn btn-default oe-pager-button" t-att="att_next" type="button" data-pager-action="next"></a> - </div> -</span> + </t> +</t> +<t t-name="WebClient.DebugManager.Action.Fields"> + <dl><t t-foreach="fields" t-as="field"> + <dt><h4><t t-esc="field"/></h4></dt> + <dd> + <dl><ul><li t-foreach="field_value" t-as="attribute"> + <strong><t t-esc="attribute"/></strong>: + <t t-esc="attribute_value"/> + </li></ul></dl> + </dd> + </t></dl> +</t> +<t t-name="WebClient.DebugManager.View"> + <t t-if="view"> + <li class="divider"/> + <t t-if="view.type === 'form'"> + <li><a href="#" data-action="set_defaults">Set Defaults</a></li> + <t t-if="view.controller.get_selected_ids().length === 1"> + <li><a href="#" data-action="get_metadata">View Metadata</a></li> + <li><a href="#" data-action="print_workflow">Print Workflow</a></li> + </t> + </t> + <li><a href="#" data-action="fvg">Fields View Get</a></li> + <t t-if="can_edit"> + <li><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="view.controller.fields_view.view_id">Edit <t t-esc="_.str.capitalize(view.controller.fields_view.type)"/>View</a></li> + <li t-if="searchview and searchview.$el.is(':visible')"><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="searchview.fields_view.view_id">Edit SearchView</a></li> + </t> + </t> +</t> +<t t-name="WebClient.DebugViewLog"> + <table class="table table-condensed table-striped"> + <tr> + <th>ID:</th> + <td><t t-esc="perm.id"/></td> + </tr> + <tr> + <th>XML ID:</th> + <td><t t-esc="perm.xmlid or '/'"/></td> + </tr> + <tr> + <th>No Update:</th> + <td><t t-esc="perm.noupdate"/></td> + </tr> + <tr> + <th>Creation User:</th> + <td><t t-esc="format(perm.create_uid, { 'type' : 'many2one' }, '/')"/></td> + </tr> + <tr> + <th>Creation Date:</th> + <td><t t-esc="format(perm.create_date, { 'type' : 'datetime' }, '/')"/></td> + </tr> + <tr> + <th>Latest Modification by:</th> + <td><t t-esc="format(perm.write_uid, { 'type' : 'many2one' }, '/')"/></td> + </tr> + <tr> + <th>Latest Modification Date:</th> + <td><t t-esc="format(perm.write_date, { 'type' : 'datetime' }, '/')"/></td> + </tr> + </table> +</t> +<div t-name="WebClient.DebugManager.RequestsOverlay" class="o_debug_manager_overlay"> + <header> + <!-- enough height to display all tracks + 1px spacing between tracks --> + <t t-set="canvas_height" t-value="widget.TRACKS * (widget.TRACK_WIDTH + 1)"/> + <canvas t-att-height="canvas_height" id="o_debug_requests_summary"/> + <!-- transparent overlay to display selected range --> + <canvas t-att-height="canvas_height" id="o_debug_requests_selector"/> + </header> + <div class="o_debug_requests"/> + <div class="o_debug_tooltip"/> +</div> -<div t-name="Sidebar" class="oe_sidebar btn-group"> +<t t-name="Sidebar"> <t t-foreach="widget.sections" t-as="section"> - <div class="oe_form_dropdown_section btn-group btn-group-sm"> - <button type="button" class="btn btn-default dropdown-toggle" - t-if="section.name != 'buttons'" - data-toggle="dropdown"> + <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"> <t t-if="section.name == 'files'" t-raw="widget.items[section.name].length || ''"/> - <t t-esc="section.label"/> - <span class="caret"></span> + <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"> - <button type="button" t-att-title="item.title or ''" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url" - target="_blank" class="oe_sidebar_button oe_highlight"> + <a t-att-title="item.title or ''" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url" target="_blank"> <t t-raw="item.label"/> - </button> + </a> </t> <ul class="dropdown-menu" role="menu"> <li t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname"> @@ -310,14 +323,12 @@ <t t-raw="item.write_uid[1] + ' ' + item.write_date"/> </t> </t> - <a class="oe_file_attachment" t-att-title="item.title or ''" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url"> + <a t-att-title="item.title or ''" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url"> <t t-raw="item.label"/> - <span t-if="section.name == 'files' and widget.options.editable and !item.callback" class="oe_sidebar_delete_item" t-att-data-id="item.id" title="Delete this attachment"> - <i class="fa fa-trash-o"/> - </span> + <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> </li> - <li t-if="section.name == 'files' and widget.options.editable" class="oe_sidebar_add_attachment"> + <li t-if="section.name == 'files' and widget.options.editable" class="o_sidebar_add_attachment"> <t t-call="HiddenInputFile"> <t t-set="fileupload_id" t-value="widget.fileupload_id"/> <t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t> @@ -330,12 +341,11 @@ </ul> </div> </t> -</div> +</t> <t t-name="TreeView"> - <select t-if="toolbar" style="width: 30%"> - </select> - <table class="oe_tree_table oe-treeview-table"> + <select t-if="toolbar" style="width: 30%"/> + <table class="o_treeview_table"> <thead> <tr> <th t-foreach="fields_view" t-as="field" @@ -356,7 +366,7 @@ t-att-data-row-parent-id="row_parent_id"> <t t-set="children" t-value="record[children_field]"/> <t t-set="class" t-value="children and children.length ? 'treeview-tr' : 'treeview-td'"/> - <t t-set="rank" t-value="'oe-treeview-first'"/> + <t t-set="rank" t-value="'o_treeview_first'"/> <t t-set="style" t-value="'background-position: ' + 19*(level) + 'px; padding-left: ' + (4 + 19*(level)) + 'px;'"/> <td t-foreach="fields_view" t-as="field" @@ -375,49 +385,47 @@ </td> </tr> -<table t-name="ListView" class="oe_list_content"> - <t t-set="columns_count" t-value="visible_columns.length + (options.selectable ? 1 : 0) + (options.deletable ? 1 : 0)"/> - <thead> - <tr t-if="(!!options.action_buttons and !options.$buttons) or (!!options.pager and !options.$pager)"> - <th t-att-colspan="columns_count"> - <div class="oe_list_buttons"/> - <div class="oe_list_sidebar"/> - <div class="oe_list_pager"/> - </th> - </tr> - <tr t-if="options.header" class="oe_list_header_columns"> - <t t-foreach="columns" t-as="column"> - <th t-if="column.meta"> - <t t-esc="column.string"/> +<div t-name="ListView" class="table-responsive"> + <table class="o_list_view table table-condensed table-striped"> + <t t-set="columns_count" t-value="visible_columns.length + (options.selectable ? 1 : 0) + (options.deletable ? 1 : 0)"/> + <thead> + <tr t-if="options.header"> + <t t-foreach="columns" t-as="column"> + <th t-if="column.meta"> + <t t-esc="column.string"/> + </th> + </t> + <th t-if="options.selectable" class="o_list_record_selector" width="1"> + <div class="o_checkbox"> + <input type="checkbox"/><span/> + </div> </th> - </t> - <th t-if="options.selectable" width="1" > - <input type="checkbox" class="oe_list_record_selector"/> - </th> - <t t-foreach="columns" t-as="column"> - <th t-if="!column.meta and column.invisible !== '1'" t-att-data-id="column.id" - t-attf-class="oe_list_header_#{column.widget or column.type} #{((options.sortable and column.tag !== 'button') ? 'oe_sortable' : null)}"><div> - <t t-if="column.tag !== 'button'"><t t-esc="column.string"/></t> - </div></th> - </t> - <th t-if="options.deletable" class="oe_list_record_delete" width="13px"/> - </tr> - </thead> - <tfoot> - <tr> - <td t-if="options.selectable"/> - <td t-foreach="aggregate_columns" t-as="column" class="oe_list_footer oe_number" - t-att-data-field="column.id" t-att-title="column.label"> - </td> - <td t-if="options.deletable" class="oe_list_record_delete"/> - </tr> - </tfoot> -</table> + <t t-foreach="columns" t-as="column"> + <th t-if="!column.meta and column.invisible !== '1'" t-att-data-id="column.id" + t-attf-class="#{((options.sortable and column.sortable and column.tag !== 'button') ? 'o_column_sortable' : '')}" + t-att-width="column.width()"> + <t t-if="column.tag !== 'button'"><t t-raw="column.heading()"/></t> + </th> + </t> + <th t-if="options.deletable" class="o_list_record_delete"/> + </tr> + </thead> + <tfoot> + <tr> + <td t-if="options.selectable"/> + <td t-foreach="aggregate_columns" t-as="column" t-att-data-field="column.id" t-att-title="column.label"> + </td> + <td t-if="options.deletable" class="o_list_record_delete"/> + </tr> + </tfoot> + </table> +</div> + <t t-name="ListView.buttons"> <t t-if="!widget.no_leaf and widget.options.action_buttons !== false"> - <div class="oe_list_buttons"> + <div class="o_list_buttons"> <t t-if="widget.options.addable and widget.is_action_enabled('create')"> - <button type="button" class="btn btn-sm btn-primary o_list_button_add" accesskey="C"> + <button type="button" class="btn btn-primary btn-sm o_list_button_add" accesskey="C"> <t t-esc="widget.options.addable"/> </button> </t> @@ -430,14 +438,13 @@ </div> </t> </t> -<t t-name="ListView.pager" t-if="!widget.no_leaf and widget.options.pager !== false" t-call="ViewPager"> - <span class="oe_list_pager_state"></span> -</t> + <t t-name="ListView.rows" t-foreach="records.length" t-as="index"> <t t-call="ListView.row"> <t t-set="record" t-value="records.at(index)"/> </t> </t> + <tr t-name="ListView.row" t-att-data-id="record.get('id')" t-attf-style="#{(view.fonts || view.colors) ? view.style_for(record) : ''}" @@ -446,30 +453,34 @@ <t t-foreach="columns" t-as="column"> <td t-if="column.meta"> </td> </t> - <th t-if="options.selectable" class="oe_list_record_selector" width="1"> + <td t-if="options.selectable" class="o_list_record_selector"> <t t-set="checked" t-value="options.select_view_id == record.get('id') ? 'checked' : null"/> <input t-if="options.radio" type="radio" name="radiogroup" t-att-checked="checked"/> - <input t-if="!options.radio" type="checkbox" name="radiogroup" t-att-checked="checked"/> - </th> + <div t-if="!options.radio" class="o_checkbox"> + <input type="checkbox" name="radiogroup" t-att-checked="checked"/><span/> + </div> + </td> <t t-foreach="columns" t-as="column"> <t t-set="number" t-value="column.type === 'integer' or column.type == 'float' or column.type == 'monetary'"/> + <t t-set="text" t-value="column.type === 'text'"/> <t t-set="modifiers" t-value="column.modifiers_for(asData)"/> <td t-if="!column.meta and column.invisible !== '1'" t-att-title="column.help" - t-attf-class="oe_list_field_cell oe_list_field_#{column.widget or column.type} #{number ? 'oe_number' : ''} #{column.tag === 'button' ? 'oe-button' : ''} #{modifiers.readonly ? 'oe_readonly' : ''} #{modifiers.required ? 'oe_required' : ''}" + t-attf-class="#{modifiers.readonly ? 'o_readonly' : ''} #{number ? 'o_list_number' : ''} #{text ? 'o_list_text' : ''} #{column.id === 'sequence' ? 'o_handle_cell' : ''} #{column.tag === 'button' ? 'o_list_button' : ''}" t-att-data-field="column.id" ><t t-raw="render_cell(record, column)"/></td> </t> - <td t-if="options.deletable" class='oe_list_record_delete' width="13px"> + <td t-if="options.deletable" class='o_list_record_delete'> <span name="delete" class="fa fa-trash-o"/> </td> </tr> + <button t-name="ListView.row.text_button" type="button" t-att-title="widget.string" t-att-disabled="disabled || undefined" t-att-class="disabled ? 'oe_list_button_disabled btn_txt oe_link' : 'btn_txt oe_link'" ><t t-esc="widget.string"/></button> <button t-name="ListView.row.button" type="button" t-att-title="widget.string" t-att-disabled="disabled || undefined" - t-att-class="disabled ? 'oe_list_button_disabled' : ''" + t-attf-class="o_icon_button #{disabled ? 'oe_list_button_disabled' : ''}" ><img t-if="!widget.icon.indexOf('fa-') == 0" t-attf-src="#{prefix}/web/static/src/img/icons/#{widget.icon}.png" t-att-alt="widget.string"/><i t-if="widget.icon.indexOf('fa-') == 0" t-attf-class="fa #{widget.icon}" t-att-title="widget.string"/> </button> @@ -483,34 +494,27 @@ </t> </t> -<t t-name="FormView"> - <div class="oe_formview"> - <div class="oe_form_buttons"/> - <div class="oe_form_sidebar"/> - <div class="oe_form_pager"/> - <div class="oe_form_container"/> - </div> -</t> <t t-name="FormView.buttons"> <div class="o_form_buttons_view"> - <button t-if="widget.is_action_enabled('edit')" - type="button" - class="oe_form_button_edit btn btn-default btn-sm" accesskey="E"> + <button t-if="widget.is_action_enabled('edit')" type="button" + class="btn btn-primary btn-sm o_form_button_edit" accesskey="E"> Edit </button> - <button t-if="widget.is_action_enabled('create')" - type="button" class="oe_form_button_create btn btn-default btn-sm" - accesskey="C"> + <button t-if="widget.is_action_enabled('create')" type="button" + class="btn btn-default btn-sm o_form_button_create" accesskey="C"> Create </button> </div> - <span class="o_form_buttons_edit"> - <button type="button" class="oe_form_button_save btn btn-primary btn-sm" accesskey="S">Save</button> - <button type="button" class="oe_form_button_cancel btn btn-sm btn-default" accesskey="D">Discard</button> - </span> -</t> -<t t-name="FormView.pager" t-if="widget.options.pager !== false" t-call="ViewPager"> - <span class="oe_form_pager_state"></span> + <div class="o_form_buttons_edit"> + <button type="button" + class="btn btn-primary btn-sm o_form_button_save" accesskey="S"> + Save + </button> + <button type="button" + class="btn btn-default btn-sm o_form_button_cancel" accesskey="D"> + Discard + </button> + </div> </t> <form t-name="FormView.set_default"> <t t-set="args" t-value="widget.args"/> @@ -574,20 +578,23 @@ </div> </t> <t t-name="FormRenderingSheet"> - <div class="oe_form_sheetbg"> - <div t-attf-class="oe_form_sheet oe_form_sheet_width #{classnames}"> - </div> + <div class="o_form_sheet_bg"> + <div t-attf-class="o_form_sheet #{classnames}"/> </div> </t> -<t t-name="FormRenderingGroup"> +<div t-name="FormRenderingOuterGroup" class="o_group"> <t t-if="string" t-call="FormRenderingSeparator"/> - <table border="0" cellpadding="0" cellspacing="0" t-attf-class="oe_form_group #{classnames}"/> -</t> +</div> +<table t-name="FormRenderingInnerGroup" class="o_group o_inner_group"> + <t t-if="string"> + <tr><td><t t-call="FormRenderingSeparator"/></td></tr> + </t> +</table> <t t-name="FormRenderingNotebook"> - <div class="oe_clear"> + <div class="o_notebook"> <ul t-attf-class="nav nav-tabs #{classnames}" role="tablist"> <li t-foreach="pages" t-as="page" t-att-modifiers="page.modifiers"> - <a role="tab" t-attf-href="##{page.id}" t-att-accesskey="page.accesskey" data-toggle="tab"> + <a role="tab" t-attf-href="##{page.id}" t-att-accesskey="page.accesskey" data-toggle="tab" disable_anchor="true"> <t t-esc="page.string"/> </a> </li> @@ -600,101 +607,96 @@ </div> </t> <t t-name="FormRenderingSeparator"> - <div t-attf-class="oe_horizontal_separator oe_clear #{classnames}"> + <div t-attf-class="o_horizontal_separator"> <t t-esc="string"/> </div> </t> <t t-name="FormRenderingLabel"> - <label t-att-for="_for" - t-attf-class="#{classnames} oe_form_label#{help ? '_help' : ''} oe_align_#{align}"> + <label t-att-for="_for" + t-attf-class="#{classnames} o_form_label #{help ? 'o_form_label_help ' : ''}"> <t t-esc="string"/> </label> </t> <t t-name="GraphView.buttons"> - <div class="btn-group btn-group-sm"> - <button class="btn btn-default fa fa-bar-chart-o oe-bar-mode" title="Bar Chart"></button> - <button class="btn btn-default fa fa-line-chart oe-line-mode" title="Line Chart"></button> - <button class="btn btn-default fa fa-pie-chart oe-pie-mode" title="Pie Chart"></button> - </div> - <div class="btn-group btn-group-sm"> - <button class="btn btn-default dropdown-toggle" data-toggle="dropdown"> - Measure <span class="caret"/> + <div class="btn-group btn-group-sm" role="group"> + <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown"> + Measures <span class="caret"/> </button> - <ul class="oe-measure-list dropdown-menu"> + <ul class="dropdown-menu o_graph_measures_list"> <li t-foreach="measures" t-as="measure" t-att-data-field="measure[0]"> - <a><t t-esc="measure[1].string"/></a> + <a href="#"><t t-esc="measure[1].string"/></a> </li> <li class="divider"></li> - <li data-field="__count__"><a>Quantity</a></li> + <li data-field="__count__"><a href="#">Quantity</a></li> </ul> </div> + <div class="btn-group btn-group-sm"> + <button class="btn btn-default fa fa-bar-chart-o o_graph_button" title="Bar Chart" data-mode="bar"/> + <button class="btn btn-default fa fa-line-chart o_graph_button" title="Line Chart" data-mode="line"/> + <button class="btn btn-default fa fa-pie-chart o_graph_button" title="Pie Chart" data-mode="pie"/> + </div> </t> <div t-name="GraphView.error" class="oe_view_nocontent" > - <div class="oe_empty_custom_dashboard"> - <p><b><t t-esc="title"/></b></p> - <p><t t-esc="description"/></p> - </div> + <p><strong><t t-esc="title"/></strong></p> + <p><t t-esc="description"/></p> </div> - -<div t-name="PivotView" class="oe_pivot"> - <div class="o-field-selection"/> - <div class="o-pivot-table"/> +<div t-name="PivotView" class="o_pivot"> + <div class="o_field_selection"/> </div> <t t-name="PivotView.buttons"> - <div class="btn-group btn-group-sm"> - <button class="btn btn-default fa fa-expand oe-pivot-flip" title="Flip axis"></button> - <button class="btn btn-default fa fa-arrows-alt oe-pivot-expand-all" title="Expand all"></button> - </div> - <div class="btn-group btn-group-sm"> - <button class="btn btn-default dropdown-toggle" data-toggle="dropdown"> + <div class="btn-group btn-group-sm" role="group"> + <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown"> Measures <span class="caret"/> </button> - <ul class="oe-measure-list dropdown-menu"> + <ul class="dropdown-menu o_pivot_measures_list"> <li t-foreach="measures" t-as="measure" t-att-data-field="measure[0]"> - <a><t t-esc="measure[1].string"/></a> + <a href="#"><t t-esc="measure[1].string"/></a> </li> <li class="divider"></li> - <li data-field="__count__"><a>Quantity</a></li> + <li data-field="__count__"><a href="#">Quantity</a></li> </ul> </div> + <div class="btn-group btn-group-sm"> + <button class="btn btn-default fa fa-expand o_pivot_flip_button" title="Flip axis"/> + <button class="btn btn-default fa fa-arrows-alt o_pivot_expand_button" title="Expand all"/> + <button class="btn btn-default fa fa-download o_pivot_download" title="Download xls"/> + </div> </t> <t t-name="PivotView.FieldSelection"> - <ul class="dropdown-menu multi-level o-field-menu" role="menu"> + <ul class="dropdown-menu o_pivot_field_menu" role="menu"> <t t-foreach="fields" t-as="field"> <t t-if="(field[1].type === 'date') || (field[1].type === 'datetime')"> - <li class="pivot-dropdown-submenu" t-att-data-field="field[0]"> - <a class="field-selection"> + <li class="o_inline_dropdown" t-att-data-field="field[0]"> + <a href="#" class="o_pivot_field_selection"> <t t-esc="field[1].string"/> </a> <ul class="dropdown-menu"> - <li t-att-data-field="field[0]"><a t-att-data-interval="'day'">Day</a></li> - <li t-att-data-field="field[0]"><a t-att-data-interval="'week'">Week</a></li> - <li t-att-data-field="field[0]"><a t-att-data-interval="'month'">Month</a></li> - <li t-att-data-field="field[0]"><a t-att-data-interval="'quarter'">Quarter</a></li> - <li t-att-data-field="field[0]"><a t-att-data-interval="'year'">Year</a></li> + <li t-att-data-field="field[0]"><a href="#" t-att-data-interval="'day'">Day</a></li> + <li t-att-data-field="field[0]"><a href="#" t-att-data-interval="'week'">Week</a></li> + <li t-att-data-field="field[0]"><a href="#" t-att-data-interval="'month'">Month</a></li> + <li t-att-data-field="field[0]"><a href="#" t-att-data-interval="'quarter'">Quarter</a></li> + <li t-att-data-field="field[0]"><a href="#" t-att-data-interval="'year'">Year</a></li> </ul> </li> </t> <t t-if="(field[1].type !== 'date') && (field[1].type !== 'datetime')"> - <li t-att-data-field="field[0]"><a><t t-esc="field[1].string"/></a></li> + <li t-att-data-field="field[0]"><a href="#"><t t-esc="field[1].string"/></a></li> </t> </t> </ul> </t> <div t-name="PivotView.nodata" class="oe_view_nocontent" > - <div class="oe_empty_custom_dashboard"> - <p><b>No data to display.</b></p> - <p> - No data available for this pivot table. Try to add some records, or make sure - that there is at least one measure and no active filter in the search bar. - </p> - </div> + <p><strong>No data to display.</strong></p> + <p> + No data available for this pivot table. Try to add some records, or make sure + that there is at least one measure and no active filter in the search bar. + </p> </div> @@ -703,21 +705,16 @@ <t t-js="dict">console.warn('Unhandled widget', dict.widget);</t> </t> <t t-name="FieldChar"> - <span t-att-class="'oe_form_field '+widget.widget_class" t-att-style="widget.node.attrs.style"> - <t t-if="!widget.get('effective_readonly')"> - <input t-att-type="widget.password ? 'password' : 'text'" - t-att-barcode_events="widget.options.barcode_events" - t-att-id="widget.id_for_label" - t-att-tabindex="widget.node.attrs.tabindex" - t-att-autofocus="widget.node.attrs.autofocus" - t-att-placeholder="widget.node.attrs.placeholder" - t-att-maxlength="widget.field.size" - /><img class="oe_field_translate oe_input_icon" t-if="_t.database.multi_lang and widget.field.translate" t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0"/> - </t> - <t t-if="widget.get('effective_readonly')"> - <span class="oe_form_char_content"></span> - </t> - </span> + <span t-if="widget.get('effective_readonly')"/> + + <input t-if="!widget.get('effective_readonly')" class="o_form_input" + t-att-barcode_events="widget.options.barcode_events" + t-att-type="widget.password ? 'password' : 'text'" + t-att-id="widget.id_for_label" + t-att-tabindex="widget.node.attrs.tabindex" + t-att-autofocus="widget.node.attrs.autofocus" + t-att-placeholder="widget.node.attrs.placeholder" + t-att-maxlength="widget.field.size"/> </t> <t t-name="FormSelection"> <div class="btn-group o_kanban_selection"> @@ -738,59 +735,30 @@ <a t-if="!widget.readonly" t-foreach="widget.values.slice(1)" t-as="v" href="#" t-att-data-value="v[0]" t-att-title="v[1]" class="o_priority_star fa"/> <span t-if="widget.readonly" t-foreach="widget.values.slice(1)" t-as="v" t-att-data-value="v[0]" t-att-title="v[1]" class="o_priority_star fa"/> </div> -<t t-name="FieldEmail"> - <span class="oe_form_field oe_form_field_email" t-att-style="widget.node.attrs.style"> - <a t-if="widget.get('effective_readonly')" href="#" class="oe_form_uri" target="_blank"/> - <t t-if="!widget.get('effective_readonly')"> - <div> - <input type="text" - t-att-id="widget.id_for_label" - t-att-tabindex="widget.node.attrs.tabindex" - t-att-autofocus="widget.node.attrs.autofocus" - t-att-placeholder="widget.node.attrs.placeholder" - t-att-maxlength="widget.field.size" - /> - </div> +<t t-name="FieldEmail" t-extend="FieldChar"> + <t t-jquery="span" t-operation="replace"> + <t t-if="widget.get('effective_readonly')"> + <a t-if="widget.clickable" class="o_form_uri o_text_overflow" href="#" target="_blank"/> + <span t-if="!widget.clickable" class="o_text_overflow"/> </t> - </span> -</t> -<t t-name="FieldUrl" t-extend="FieldEmail"> - <t t-jquery="span:first"> - this.removeClass('oe_form_field_email').addClass('oe_form_field_url'); </t> </t> <t t-name="FieldText"> - <div class="oe_form_field oe_form_field_text" t-att-style="widget.node.attrs.style"> - <t t-if="!widget.get('effective_readonly')"> - <textarea rows="6" - t-att-name="widget.name" - class="field_text" - t-att-tabindex="widget.node.attrs.tabindex" - t-att-autofocus="widget.node.attrs.autofocus" - t-att-placeholder="! widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''" - t-att-maxlength="widget.field.size" - ></textarea><img class="oe_field_translate oe_input_icon" - t-if="_t.database.multi_lang and widget.field.translate and !widget.get('effective_readonly')" - t-att-src='_s + "/web/static/src/img/icons/terp-translate.png"' width="16" height="16" border="0" - /> - </t> - <t t-if="widget.get('effective_readonly')"> - <span class="oe_form_text_content"></span> - </t> - </div> -</t> -<t t-name="FieldTextHtml"> - <div t-att-class="'oe_form_field oe_form_field_html' + (widget.get('effective_readonly') ? ' oe_form_embedded_html' : '')" - t-att-style="widget.node.attrs.style"> - <t t-if="! widget.get('effective_readonly')"> - <textarea/> - </t> - </div> + <span t-if="widget.get('effective_readonly')" class="o_form_textarea"/> + + <textarea t-if="!widget.get('effective_readonly')" + class="o_form_textarea" + rows="6" + t-att-name="widget.name" + t-att-tabindex="widget.node.attrs.tabindex" + t-att-autofocus="widget.node.attrs.autofocus" + t-att-placeholder="!widget.get('effective_readonly') ? widget.node.attrs.placeholder : ''" + t-att-maxlength="widget.field.size"/> </t> <t t-name="FieldCharDomain"> - <div class="oe_form_field"> + <div> <span class="o_count"/> - <button class="btn btn-link" type="button" + <button class="btn btn-default" type="button" t-att-accesskey="widget.node.attrs.accesskey"/> <input t-if="widget.debug" type="text" class="o_form_input o_debug_input"/> </div> @@ -802,76 +770,54 @@ class="o_datepicker_input" t-att-name="widget.name" t-att-placeholder="placeholder"/> - <span class="fa fa-calendar o_datepicker_button"/> + <span class="o_datepicker_button"/> </div> </t> -<t t-name="FieldDate"> - <span class="oe_form_field oe_datepicker_root oe_form_field_date" t-att-style="widget.node.attrs.style"/> -</t> -<t t-name="FieldDatetime"> - <span class="oe_form_field oe_datepicker_root oe_form_field_datetime" t-att-style="widget.node.attrs.style"/> -</t> <t t-name="FieldSelection"> - <span class="oe_form_field oe_form_field_selection" t-att-style="widget.node.attrs.style"> - </span> -</t> -<t t-name="FieldSelectionSelect"> + <span t-if="widget.get('effective_readonly')"/> + <select t-if="!widget.get('effective_readonly')" + class="o_form_input" t-att-name="widget.name" t-att-tabindex="widget.node.attrs.tabindex" t-att-autofocus="widget.node.attrs.autofocus" - t-att-id="widget.id_for_label"> - <t t-foreach="values" t-as="option"> - <option t-att-value="JSON.stringify(option[0])"> - <t t-esc="option[1]"/> - </option> - </t> - </select> + t-att-id="widget.id_for_label"/> </t> <t t-name="FieldRadio"> - <span t-attf-class="oe_form_field oe_form_field_radio #{widget.options.horizontal ? 'oe_horizontal' : 'oe_vertical'}" t-att-style="widget.node.attrs.style"> - <span t-if="!widget.get('effective_readonly')"> - <t t-foreach="widget.selection" t-as="selection"> - <div> - <span class="oe_radio_input"><input type="radio" t-att-id="widget.uniqueId + '_' + selection[0]" t-att-name="widget.uniqueId" t-att-value="selection[0]"/></span><label t-if="!widget.options.no_radiolabel" t-att-for="widget.uniqueId + '_' + selection[0]"><t t-esc="selection[1]"/></label> - </div> - </t> - </span> - <span t-if="widget.get('effective_readonly')" class="oe_radio_readonly"/> - </span> + <span t-if="widget.get('effective_readonly')"/> + + <div t-if="!widget.get('effective_readonly')" t-attf-class="o_form_field_radio #{widget.options.horizontal ? 'o_horizontal' : 'o_vertical'}"> + <div t-foreach="widget.selection" t-as="selection" class="o_radio_item"> + <input type="radio" class="o_form_radio" t-att-id="widget.uniqueId + '_' + selection[0]" t-att-name="widget.uniqueId" t-att-value="selection[0]"/> + <label t-if="!widget.options.no_radiolabel" class="o_form_label" t-att-for="widget.uniqueId + '_' + selection[0]"><t t-esc="selection[1]"/></label> + </div> + </div> </t> <t t-name="FieldMany2One"> - <span class="oe_form_field oe_form_field_many2one oe_form_field_with_button" t-att-style="widget.node.attrs.style"> - <t t-if="widget.get('effective_readonly')"> - <a t-if="! widget.options.no_open" href="#" class="oe_form_uri"/> - <span t-if="widget.options.no_open" href="#" class="oe_form_uri"/> - <span class="oe_form_m2o_follow"/> - </t> - <t t-if="!widget.get('effective_readonly')"> - <i t-if="! widget.options.no_open" href="#" tabindex="-1" - class="oe_m2o_cm_button fa fa-external-link" draggable="false"/> - <div> - <input type="text" - t-att-barcode_events="widget.options.barcode_events" - t-att-id="widget.id_for_label" - t-att-tabindex="widget.node.attrs.tabindex" - t-att-autofocus="widget.node.attrs.autofocus" - t-att-placeholder="widget.node.attrs.placeholder" - /> - <span class="oe_m2o_drop_down_button"> - <img t-att-src='_s + "/web/static/src/img/down-arrow.png"' draggable="false"/> - </span> - </div> - </t> - </span> + <t t-if="widget.get('effective_readonly')"> + <a t-if="!widget.options.no_open" class="o_form_uri" href="#"/> + <span t-if="widget.options.no_open"/> + </t> + <div t-if="!widget.get('effective_readonly')" t-att-class="'o_form_field_many2one' + ((widget.options.no_open)? '' : ' o_with_button')"> + <div class="o_form_input_dropdown"> + <input type="text" class="o_form_input" + t-att-barcode_events="widget.options.barcode_events" + t-att-id="widget.id_for_label" + t-att-tabindex="widget.node.attrs.tabindex" + t-att-autofocus="widget.node.attrs.autofocus" + t-att-placeholder="widget.node.attrs.placeholder"/> + <span class="o_dropdown_button" draggable="false"/> + </div> + <button type="button" t-if="!widget.options.no_open" class="fa fa-external-link btn btn-default o_external_button" tabindex="-1" draggable="false"/> + </div> </t> -<t t-name="Many2OneButton.cell" - ><img t-attf-src="#{prefix}/web/static/src/img/icons/#{widget.icon}.png" t-att-alt="widget.string" width="16" height="16"/> +<t t-name="Many2OneButton.cell"> + <img t-attf-src="#{prefix}/web/static/src/img/icons/#{widget.icon}.png" t-att-alt="widget.string" width="16" height="16"/> </t> <t t-name="FieldMany2ManyTag"> <t t-foreach="elements" t-as="el"> - <span t-attf-class="badge dropdown #{'o_tag_color_' + el.color}" t-att-data-color="el.color" t-att-data-index="el_index" t-att-data-id="el.id"> - <t t-esc="el['display_name']"/> + <span t-attf-class="badge dropdown #{'o_tag_color_' + (el['color'] || 0)}" t-att-data-color="(el['color'] || 0)" t-att-data-index="el_index" t-att-data-id="el['id']"> + <span class="o_badge_text"><t t-esc="el['display_name']"/></span> <span t-if="!readonly" class="fa fa-times o_delete"/> </span> </t> @@ -879,57 +825,43 @@ <t t-name="FieldMany2ManyTag.colorpicker"> <ul class="o_colorpicker dropdown-menu" role="menu"> <li t-foreach="10" t-as="color"> - <span href="#" t-att-data-id="tag_id" t-att-data-color="color_index" t-attf-class="o_tag_color_#{color}"/> + <span t-att-data-id="tag_id" t-att-data-color="color_index + 1" t-attf-class="o_tag_color_#{color + 1}"/> </li> <li> - <span t-att-data-id="tag_id" t-att-data-color="10" class="o_tag_color_10"/> + <span t-att-data-id="tag_id" t-att-data-color="0" class="o_tag_color_0"/> </li> <li> Not shown in kanban </li> </ul> </t> -<t t-name="FieldReference"> - <table class="oe_form_field oe_form_field_reference" border="0" cellpadding="0" cellspacing="0" t-att-style="widget.node.attrs.style"> - <tr> - <td t-attf-class="oe_form_frame_cell oe_form_selection"> - <span t-attf-class="oe_form_view_reference_selection"/> - </td> - <td t-attf-class="oe_form_frame_cell oe_form_many2one" nowrap="true"> - <span t-attf-class="oe_form_view_reference_m2o"/> - </td> - </tr> - </table> -</t> <t t-name="FieldBoolean"> - <span class="oe_form_field oe_form_field_boolean" t-att-style="widget.node.attrs.style"> + <div class="o_checkbox o_form_field_boolean"> <input type="checkbox" t-att-id="widget.id_for_label" t-att-name="widget.name" t-att-tabindex="widget.node.attrs.tabindex" - t-att-autofocus="widget.node.attrs.autofocus" - class="field_boolean"/> - </span> + t-att-autofocus="widget.node.attrs.autofocus"/> + <span/> + </div> </t> -<div t-name="FieldUpgradeBoolean" class="oe_form_field oe_form_field_boolean"> - <t t-call="FieldBoolean"/> +<div t-name="FieldUpgradeBoolean" class="o_form_field o_form_field_boolean"> + <t t-call="FieldBoolean"/> </div> <t t-name="ProgressBar"> <div class="o_progressbar"> <div t-if="widget.title" class="o_progressbar_title"><t t-esc="widget.title"/></div><div class="o_progress"> <div class="o_progressbar_complete"/> - </div><div t-if="widget.readonly" class="o_progressbar_value"/><input t-if="!widget.readonly" class="o_progressbar_value"/> + </div><div t-if="widget.readonly" class="o_progressbar_value"/><input t-if="!widget.readonly" class="o_progressbar_value" type="text"/> </div> </t> <t t-name="FieldPercentPie"> - <div class="oe_form_field oe_form_field_percent_pie" t-att-style="widget.node.attrs.style"> - <svg></svg> - <span t-if="widget.string"><t t-esc="widget.string"/></span> - </div> -</t> -<t t-name="FieldBarChart"> - <div class="oe_form_field oe_form_field_bar_chart" t-att-style="widget.node.attrs.style"> - <svg></svg> + <div class="o_form_field_percent_pie"> + <div class="o_pie"> + <div class="o_mask"/> + <div class="o_mask"/> + <div class="o_pie_value"/> + </div> <span t-if="widget.string"><t t-esc="widget.string"/></span> </div> </t> @@ -957,197 +889,149 @@ </li> </t> </t> +<t t-name="FieldStatus.content.button"> + <button type="button" t-attf-class="btn btn-sm o_arrow_button #{i[0] === widget.get('value') ? 'btn-primary' : 'btn-default'} #{!widget.options.clickable || i[0] === widget.get('value') ? 'disabled' : ''}" t-att-data-id="i[0]"> + <t t-esc="i[1]"/> + </button> +</t> <t t-name="FieldBinaryImage"> - <span class="oe_form_field oe_form_field_image" t-att-style="widget.node.attrs.style"> + <div class="o_form_field_image"> <t t-if="!widget.get('effective_readonly')"> - <div class="oe_form_field_image_controls oe_edit_only"> - <i class="fa fa-pencil fa-1g pull-left col-md-offset-1 oe_form_binary_file_edit" title="Edit"/> - <i class="fa fa-trash-o fa-1g col-md-offset-5 oe_form_binary_file_clear" title="Clear"/> - <div class="oe_form_binary_progress" style="display: none"> - <img t-att-src='_s + "/web/static/src/img/throbber.gif"' width="16" height="16"/> - <b>Uploading ...</b> - </div> + <div class="o_form_image_controls"> + <span class="fa fa-pencil fa-lg pull-left o_select_file_button" title="Edit"/> + <span class="fa fa-trash-o fa-lg pull-right o_clear_file_button" title="Clear"/> + + <span class="o_form_binary_progress">Uploading...</span> <t t-call="HiddenInputFile"> <t t-set="fileupload_id" t-value="widget.fileupload_id"/> </t> </div> </t> - </span> + </div> </t> <t t-name="FieldBinaryImage-img"> <img t-att-src='url' t-att-border="widget.readonly ? 0 : 1" t-att-name="widget.name" t-att-width="widget.node.attrs.img_width || widget.node.attrs.width" - t-att-height="widget.node.attrs.img_height || widget.node.attrs.height" - /> + t-att-height="widget.node.attrs.img_height || widget.node.attrs.height"/> </t> <t t-name="FieldBinaryFile"> - <t t-if="!widget.get('effective_readonly')"> - <table class="oe_form_field oe_form_field_binary" cellpadding="0" cellspacing="0" border="0" t-att-style="widget.node.attrs.style"> - <tr> - <td> - <input type="text" size="1" readonly="readonly" - t-att-name="widget.name" - t-att-tabindex="widget.node.attrs.tabindex" - t-att-autofocus="widget.node.attrs.autofocus" - class="field_binary" - /> - </td> - <td class="oe_form_binary" nowrap="true"> - <table cellspacing="0" cellpadding="0" border="0"> - <tr> - <td> - <t t-call="HiddenInputFile"> - <t t-set="fileupload_id" t-value="widget.fileupload_id"/> - <t t-set="fileupload_style" t-translation="off">overflow-x: hidden</t> - <button class="oe_button oe_field_button" type="button"> - <img t-att-src='_s + "/web/static/src/img/icons/STOCK_DIRECTORY.png"'/> - <span>Select</span> - </button> - </t> - </td> - <td> - <a class="oe_form_binary_file_save_data"> - <button class="oe_button oe_form_binary_file_save" type="button" title="Save As"> - <img t-att-src='_s + "/web/static/src/img/icons/gtk-save.png"'/> - <span>Save As</span> - </button> - </a> - </td> - <td> - <button class="oe_button oe_form_binary_file_clear" type="button" title="Clear"> - <img t-att-src='_s + "/web/static/src/img/icons/STOCK_MISSING_IMAGE.png"'/> - <span>Clear</span> - </button> - </td> - </tr> - </table> - </td> - <td class="oe_form_binary_progress" style="display: none" nowrap="true"> - <img t-att-src='_s + "/web/static/src/img/throbber.gif"' width="16" height="16"/> - <b>Uploading ...</b> - </td> - </tr> - </table> - </t> - <t t-if="widget.get('effective_readonly')"> - <span t-att-style="widget.node.attrs.style"> - <a href="javascript:void(0)" class="oe_form_uri"/> - </span> - </t> + <a t-if="widget.get('effective_readonly')" href="javascript:void(0)" class="o_form_uri"/> + + <div t-if="!widget.get('effective_readonly')" class="o_form_field_binary_file"> + <input type="text" class="o_form_input" + readonly="readonly" + t-att-name="widget.name" + t-att-tabindex="widget.node.attrs.tabindex" + t-att-autofocus="widget.node.attrs.autofocus"/> + + <button type="button" class="btn btn-sm btn-primary o_select_file_button" title="Select">Upload your file</button> + <button type="button" class="btn btn-sm btn-default fa fa-pencil o_select_file_button" title="Select"/> + <button type="button" class="btn btn-sm btn-default fa fa-download o_save_file_button" title="Save As..."/> + <button type="button" class="btn btn-sm btn-default fa fa-trash-o o_clear_file_button" title="Clear"/> + + <span class="o_form_binary_progress">Uploading...</span> + <t t-call="HiddenInputFile"> + <t t-set="fileupload_id" t-value="widget.fileupload_id"/> + <t t-set="fileupload_style" t-translation="off">overflow-x: hidden</t> + </t> + </div> </t> <t t-name="HiddenInputFile"> - <div t-attf-class="oe_hidden_input_file #{fileupload_class or ''}" t-att-style="fileupload_style"> + <div t-attf-class="o_hidden_input_file #{fileupload_class or ''}" t-att-style="fileupload_style"> <form class="o_form_binary_form" t-att-target="fileupload_id" method="post" enctype="multipart/form-data" t-att-action="fileupload_action || '/web/binary/upload'"> <input type="hidden" name="csrf_token" t-att-value="csrf_token"/> <input type="hidden" name="session_id" value="" t-if="widget.session.override_session"/> <input type="hidden" name="callback" t-att-value="fileupload_id"/> + <input t-if="widget.widget!='image'" type="file" class="o_form_input_file" name="ufile"/> + <input t-if="widget.widget=='image'" type="file" class="o_form_input_file" name="ufile" accept="image/*"/> <t t-raw="0"/> - <input type="file" class="o_form_input_file" name="ufile" t-if="widget.widget!='image'"/> - <input type="file" class="o_form_input_file" name="ufile" accept="image/*" t-if="widget.widget=='image'"/> </form> <iframe t-att-id="fileupload_id" t-att-name="fileupload_id" style="display: none"/> </div> </t> -<t t-name="FieldBinaryFileUploader.files"> - <div class="oe_attachments"> - <t t-if="!widget.get('effective_readonly')"> - <t t-foreach="values" t-as="id"> - <t t-set="file" t-value="widget.data[id]"/> - <div class="oe_attachment"> - <span t-if="(file.upload or file.percent_loaded<100)" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="#{file.name || file.filename}"> - <span class="oe_fileuploader_in_process">...Upload in progress...</span> - <t t-raw="file.name || file.filename"/> - </span> - <t t-if="(!file.upload or file.percent_loaded>=100)"> - <a class="o_image" t-att-href="file.url" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-att-data-mimetype="file.mimetype" t-attf-data-src="/web/image/#{file.id}/100x80"> - <t t-raw="file.name || file.filename"/> - </a> - <span class="fa fa-stack oe_delete" title="Delete this file" t-attf-data-id="#{file.id}"> - <i class="fa fa-circle fa-stack-1x fa-inverse"></i> - <i class="fa fa-times-circle fa-stack-1x"></i> - </span> - </t> - </div> - </t> +<div t-name="FieldBinaryFileUploader" t-attf-class="oe_fileupload #{widget.node.attrs.class ? widget.node.attrs.class :''}"> + <div class="oe_placeholder_files"/> + <div t-if="!widget.get('effective_readonly')" class="oe_add"> + <button class="btn btn-default o_attach"><span class="fa fa-paperclip"/> <t t-esc="widget.string"/></button> + <t t-call="HiddenInputFile"> + <t t-set="fileupload_id" t-value="widget.fileupload_id"/> + <t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t> + <input type="hidden" name="model" t-att-value="widget.view.model"/> + <input type="hidden" name="id" value="0"/> + <input t-if="widget.session.override_session" type="hidden" name="session_id" t-att-value="widget.session.session_id"/> </t> - <t t-if="widget.get('effective_readonly')"> - <t t-foreach="widget.get('value')" t-as="id"> - <t t-set="file" t-value="widget.data[id]"/> + </div> +</div> +<div t-name="FieldBinaryFileUploader.files" class="oe_attachments"> + <t t-if="widget.get('effective_readonly')"> + <div t-foreach="widget.get('value')" t-as="id"> + <t t-set="file" t-value="widget.data[id]"/> + <a t-att-href="file.url" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"><t t-raw="file.name || file.filename"/></a> + </div> + </t> + <t t-if="!widget.get('effective_readonly')"> + <div t-foreach="values" t-as="id" class="oe_attachment"> + <t t-set="file" t-value="widget.data[id]"/> + <span t-if="file.upload or file.percent_loaded<100" + t-att-title="(file.name || file.filename) + ((file.date)? (' \n(' + file.date + ')') : '')" + t-att-name="file.name || file.filename"> + <span class="oe_fileuploader_in_process">...Upload in progress...</span> + <t t-raw="file.name || file.filename"/> + </span> + <t t-if="!file.upload or file.percent_loaded>=100"> <div> - <a t-att-href="file.url" t-attf-title="#{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}"> - <t t-raw="file.name || file.filename"/> - </a> + <a href="#" class="fa fa-times pull-right oe_delete" title="Delete this file" t-att-data-id="file.id"/> + <t t-raw="file.name || file.filename"/> </div> - </t> - </t> - </div> -</t> -<t t-name="FieldBinaryFileUploader"> - <div t-att-style="widget.node.attrs.style" t-attf-class="oe_fileupload #{widget.node.attrs.class ? widget.node.attrs.class :''}"> - <div class="oe_placeholder_files"/> - <div class="oe_add" t-if="!widget.get('effective_readonly')"> - <!-- uploader of file --> - <button class="oe_attach"><i class="fa fa-paperclip"/></button> - <span class='oe_attach_label'><t t-esc="widget.string"/></span> - <t t-call="HiddenInputFile"> - <t t-set="fileupload_id" t-value="widget.fileupload_id"/> - <t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t> - <input type="hidden" name="model" t-att-value="widget.view.model"/> - <input type="hidden" name="id" value="0"/> - <input type="hidden" name="session_id" t-att-value="widget.session.session_id" t-if="widget.session.override_session"/> + <a class="o_image" t-att-href="file.url" + t-att-title="(file.name || file.filename) + ((file.date)? (' \n(' + file.date + ')') : '')" + t-att-data-mimetype="file.mimetype" + t-attf-data-src="/web/image/#{file.id}/100x80"/> </t> </div> - </div> -</t> - -<t t-name="One2Many.formview" t-extend="FormView"> - <t t-jquery=".oe_form_buttons" t-operation="inner"> - <button type="button" class="oe_button oe_form_button_create">Add</button> </t> -</t> - -<t t-name="One2Many.listview" t-extend="ListView"> - <t t-jquery="thead > tr:first"> - this.removeAttr('t-if'); - </t> -</t> - -<div t-name="SearchView" class="oe_searchview form-control input-sm"> - <div class="oe_searchview_search fa fa-lg fa-search" title="Search Again"/> - <div class="oe_searchview_facets"/> - <div class="oe_searchview_unfold_drawer fa fa-lg fa-fw fa-chevron-right" title="Advanced Search..."/> </div> - -<div t-name="SearchView.InputView" - class="oe_searchview_input" - contenteditable="true" - tabindex="0"/> -<!-- tabindex: makes div focusable --> -<div t-name="SearchView.FacetView" - class="oe_tag oe_searchview_facet" - tabindex="0"> - <span class="oe_facet_remove">x</span> - <span t-if="widget.model.has('icon')" - t-att-class="'label label-default fa ' + widget.model.get('icon')"> - </span> - <span class="label label-default" t-if="!widget.model.has('icon')"> +<div t-name="SearchView" class="o_searchview"> + <span class="o_searchview_more fa" title="Advanced Search..."/> +</div> +<input t-name="SearchView.InputView" type="text" class="o_searchview_input" placeholder="Search..."/> +<div t-name="SearchView.FacetView" tabindex="0" class="o_searchview_facet"> + <span t-if="widget.model.has('icon')" t-att-class="'fa ' + widget.model.get('icon') + ' o_searchview_facet_label'"/> + <span t-if="!widget.model.has('icon')" class="o_searchview_facet_label"> <t t-esc="widget.model.get('category')"/> - </span > - <span class="oe_facet_values"/> + </span> + <div class="o_facet_values"/> + <div class="fa fa-sm fa-remove o_facet_remove"/> </div> -<span t-name="SearchView.FacetView.Value" class="oe_facet_value"> +<span t-name="SearchView.FacetView.Value"> <t t-esc="widget.model.get('label')"/> </span> +<t t-name="SearchView.autocomplete"> + <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"> + <span class="fa fa-filter"/> Filters <span class="caret"/> + </button> + <ul class="dropdown-menu o_filters_menu" role="menu"> + <li class="o_add_filter o_closed_menu"> + <a href="#">Add Custom Filter</a> + </li> + <li class="o_add_filter_menu"> + <button class="btn btn-primary btn-sm o_apply_filter" type="button">Apply</button> + <button class="btn btn-default btn-sm o_add_condition" type="button"><span class="fa fa-plus-circle"/> Add a condition</button> + </li> + </ul> +</div> <t t-name="SearchView.filters"> <li t-foreach="widget.filters" t-as="filter" t-if="!filter.visible || filter.visible()" t-att-title="filter.attrs.string ? filter.attrs.help : undefined" t-att-data-index="filter_index"> - <a> - <t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/> - </a> + <a href="#"><t t-esc="filter.attrs.string or filter.attrs.help or filter.attrs.name or 'Ω'"/></a> </li> </t> <t t-name="SearchView.field"> @@ -1206,56 +1090,11 @@ <t t-if="filters.length" t-raw="filters.render(defaults)"/> </div> </t> -<div t-name="SearchView.FilterMenu" class="btn-group btn-group-sm"> - <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"> - <span class="fa fa-filter"/> Filters <span class="caret"/> - </button> - <ul class="dropdown-menu filters-menu" role="menu"> - <li class="oe-add-filter o_closed_menu"><a>Add Custom Filter</a></li> - <li class="oe-add-filter-menu"><button type="button" class="btn btn-default oe-apply-filter">Apply</button><a class="oe-add-condition"><span class="fa fa-plus-circle"/> Add a condition</a></li> - </ul> -</div> - -<div t-name="SearchView.GroupByMenu" class="btn-group btn-group-sm oe-groupby-menu"> - <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"> - <span class="fa fa-bars"/> Group By <span class="caret"/> - </button> - <ul class="dropdown-menu group-by-menu" role="menu"> - <li class="divider"/> - <li class="add-custom-group o_closed_menu"><a>Add custom group</a></li> - </ul> -</div> - -<t t-name="GroupByMenuSelector"> - <li><select class="form-control oe-add-group oe-group-selector"> - <t t-foreach="groupable_fields" t-as="field"> - <option t-att-data-name="field.name"><t t-esc="field.string"/></option> - </t> - </select></li> - <li><button type="button" class="btn btn-default oe-add-group oe-select-group">Apply</button></li> -</t> -<div t-name="SearchView.FavoriteMenu" class="btn-group btn-group-sm"> - <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"> - <span class="fa fa-star"/> Favorites <span class="caret"/> - </button> - <ul class="dropdown-menu o_favorites_menu" role="menu"> - <li class="divider"/> - <li class="oe-save-search o_closed_menu"><a>Save current search</a></li> - <li class="oe-save-name"><input type="text" class="form-control"></input></li> - <li class="oe-save-name"> - <span><input type="checkbox"/>Use by default</span> - </li> - <li class="oe-save-name"> - <span class="oe-share-filter"><input type="checkbox"/>Share with all users</span> - </li> - <li class="oe-save-name"><button type="button" class="btn btn-default">Save</button></li> - </ul> -</div> <t t-name="SearchView.extended_search.proposition"> - <li class="oe-filter-condition"> - <span class="o-or-filter">or</span> + <li class="o_filter_condition"> + <span class="o_or_filter">or</span> <span> - <select class="searchview_extended_prop_field form-control"> + <select class="o_searchview_extended_prop_field"> <t t-foreach="widget.attrs.fields" t-as="field"> <option t-att="{'selected': field === widget.attrs.selected ? 'selected' : null}" t-att-value="field.name"> @@ -1263,151 +1102,140 @@ </option> </t> </select> - <span class="searchview_extended_delete_prop fa fa-trash-o"/> + <span class="o_searchview_extended_delete_prop fa fa-trash-o"/> </span> - <select class="searchview_extended_prop_op form-control"/> - <span class="searchview_extended_prop_value"/> + <select class="o_searchview_extended_prop_op"/> + <span class="o_searchview_extended_prop_value"/> </li> </t> -<t t-name="SearchView.extended_search.proposition.char"> - <input class="field_char form-control"/> -</t> -<t t-name="SearchView.extended_search.proposition.empty"> - <span/> -</t> -<t t-name="SearchView.extended_search.proposition.integer"> - <input type="number" class="field_integer form-control" value = "0" step="1"/> -</t> <t t-name="SearchView.extended_search.proposition.float"> - <input class="field_float form-control" - t-att-type="widget.decimal_point === '.' ? 'number' : 'text'" + <input t-att-type="widget.decimal_point === '.' ? 'number' : 'text'" t-attf-title="Number using #{widget.decimal_point || '.' } as decimal separator." t-attf-pattern="[0-9]+([\\#{widget.decimal_point || '.' }][0-9]+)?" t-attf-value="0#{widget.decimal_point || '.' }0" step="0.01"/> </t> <t t-name="SearchView.extended_search.proposition.selection"> - <select class="form-control"> - <t t-foreach="widget.field.selection" t-as="element"> - <option t-att-value="element[0]"><t t-esc="element[1]"/></option> - </t> + <select> + <option t-foreach="widget.field.selection" t-as="element" t-att-value="element[0]"> + <t t-esc="element[1]"/> + </option> </select> </t> -<div t-name="SearchView.autocomplete" class="oe-autocomplete"> - <ul> +<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"> + <span class="fa fa-bars"/> Group By <span class="caret"/> + </button> + <ul class="dropdown-menu o_group_by_menu" role="menu"> + <li class="divider"/> + <li class="o_add_custom_group o_closed_menu"> + <a href="#">Add custom group</a> + </li> </ul> </div> -<t t-name="ExportView"> - <a id="exportview" href="javascript: void(0)" style="text-decoration: none;color: #3D3D3D;">Export</a> -</t> -<table t-name="ExportTreeView" class="oe_export" - style="background-color: #F3F3F3;"> - <tr> - <td colspan="3"> - This wizard will export all data that matches the current search criteria to a CSV file. - You can export all data or only the fields that can be reimported after modification. - </td> - </tr> - <tr> - <td colspan="3"> - <label for="import_compat">Export Type:</label> - <select id="import_compat" name="import_compat"> - <option value="yes">Import-Compatible Export</option> - <option value="">Export all Data</option> - </select> +<t t-name="GroupByMenuSelector"> + <li><select class="o_add_group o_group_selector"> + <t t-foreach="groupable_fields" t-as="field"> + <option t-att-data-name="field.name"><t t-esc="field.string"/></option> + </t> + </select></li> + <li> + <button type="button" class="btn btn-primary btn-sm o_add_group o_select_group">Apply</button> + </li> +</t> - <label for="export_format">Export Formats</label> - <select id="export_format" name="export_format"></select> - </td> - </tr> +<div t-name="SearchView.FavoriteMenu" class="btn-group o_dropdown"> + <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown"> + <span class="fa fa-star"/> Favorites <span class="caret"/> + </button> + <ul class="dropdown-menu o_favorites_menu" role="menu"> + <li class="divider"/> + <li class="o_save_search o_closed_menu"> + <a href="#">Save current search</a> + </li> + <li class="o_save_name"> + <input type="text"></input> + </li> + <li class="o_save_name"> + <span><div class="o_checkbox"><input type="checkbox"/><span/></div> Use by default</span> + </li> + <li class="o_save_name"> + <span><div class="o_checkbox"><input type="checkbox"/><span/></div> Share with all users </span><span class="fa fa-users"/> + </li> + <li class="o_save_name"> + <button type="button" class="btn btn-primary btn-sm">Save</button> + </li> + </ul> +</div> - <tr> - <th>Available fields</th> - <th/> - <th> - Fields to export - <a style="color: blue; text-decoration: none;" href="#" id="export_new_list">Save fields list</a> - <div id="savenewlist"></div> - <div id="ExistsExportList"></div> - </th> - </tr> - <tr style="height: 400px;"> - <td class="oe_export_fields_selector_left"> - <div id="left_field_panel"> +<div t-name="ExportDialog" class="o_export"> + <p> + This wizard will export all data that matches the current search criteria to a CSV file. + You can export all data or only the fields that can be reimported after modification. + </p> + + <div class="row"> + <div class="col-md-6"> + <label>Export Type :</label> + <div class="o_import_compat"> + <div><input type="radio" name="o_import_compat_name" value="yes" checked="checked"/><label>Import-Compatible Export</label></div> + <div><input type="radio" name="o_import_compat_name" value=""/><label>Export all Data</label></div> </div> - </td> - <td class="oe_export_fields_selector_center"> - <!-- TODO: replace ids by 'oe_*' classes --> - <button class="oe_button" id="add_field">Add</button> - <button class="oe_button" id="remove_field">Remove</button> - <button class="oe_button" id="remove_all_field">Remove All</button> - <button class="oe_button" id="move_up">Move Up</button> - <button class="oe_button" id="move_down">Move Down</button> - </td> - <td class="oe_export_fields_selector_right"> - <select name="fields_list" id="fields_list" - multiple="multiple"></select> - </td> - </tr> -</table> -<t t-name="ExportTreeView-Domain"> - <t t-if="! record.ids_to_export"> - <p><strong>Please pay attention that all records matching your search filter will be exported. Not only the selected ids.</strong></p> - </t> - <t t-if="record.ids_to_export"> - <p><strong>Please note that only the selected ids will be exported.</strong></p> - </t> -</t> -<table t-name="ExportTreeView-Secondary" - id="field-tree-structure" class="oe_export_fields_selector_export" - cellspacing="0" cellpadding="0"> - <tr><th class="oe_export_tree_header"> Name </th></tr> - <t t-call="ExportTreeView-Secondary.children"/> -</table> -<tr t-name="ExportTreeView-Secondary.children" + </div> + <div class="col-md-6"> + <label>Export Formats :</label> + <div class="o_export_format"/> + </div> + </div> + + <div class="o_export_panel"> + <div class="o_left_panel"> + <h4>Available fields</h4> + <div class="o_left_field_panel"/> + </div> + <div class="o_center_panel"> + <button type="button" class="btn btn-sm btn-default o_add_field">Add</button> + <button type="button" class="btn btn-sm btn-default o_remove_field">Remove</button> + <button type="button" class="btn btn-sm btn-default o_remove_all_field">Remove All</button> + <button type="button" class="btn btn-sm btn-default o_move_up">Move Up</button> + <button type="button" class="btn btn-sm btn-default o_move_down">Move Down</button> + </div> + <div class="o_right_panel"> + <h4> + <a href="#" class="pull-right o_toggle_save_list">Save fields list</a> + Fields to export + </h4> + <div class="o_save_list"/> + <div class="o_exported_lists"/> + <select class="o_fields_list" multiple="multiple"></select> + </div> + </div> +</div> +<p t-name="Export.DomainMessage"> + <strong t-if="!record.ids_to_export">Please pay attention that all records matching your search filter will be exported. Not only the selected ids.</strong> + <strong t-if="record.ids_to_export">Please note that only the selected ids will be exported.</strong> +</p> +<div t-name="Export.TreeItems" t-foreach="fields" t-as="field" - t-att-id="'treerow-' + field.id" class="oe_export_row"> - <td> - <table class="tree_grid" border="0"> - <tr class="oe_export_row"> - <t t-foreach="(field.id).split('/')" t-as="level" > - <t t-if="(field.id).split('/')[0] != level"> - <td width="18" t-translation="off">&nbsp;</td> - </t> - </t> - <td valign="top" align="left" style="cursor: pointer;" width="18"> - <t t-if="field.children"> - <t t-if="(field.id).split('/').length != 3"> - <img t-att-id="'parentimg-' + field.id" t-att-src='_s + "/web/static/src/img/expand.gif"' width="16" height="16" border="0"/> - </t> - </t> - </td> - <td id="tree-column" valign="middle" align="left" style="cursor: pointer;"> - <a t-att-id="'export-' + field.id" t-att-string="field.string" href="javascript: void(0);" style="text-decoration: none;"> - <t t-esc="field.string"/> - </a> - </td> - </tr> - </table> - </td> -</tr> -<t t-name="ExportNewList"> - <tr> - <th><label>Save as:</label></th> - <td><input size="10" type="text" id="savelist_name"/></td> - <td><button class="oe_button oe_export_button_export" id="add_export_list">Ok</button></td> - </tr> -</t> -<t t-name="Exists.ExportList"> - <label for="saved_export_list">Saved exports:</label> - - <select id="saved_export_list"> + t-att-data-id="field.id" + class="o_export_tree_item" + tabindex="-1"> <!-- tabindex make the div focusable --> + <span t-if="field.children && (field.id).split('/').length != 3" + class="o_expand_parent fa fa-plus"/> + <span class="o_tree_column"><t t-esc="field.string"/></span> +</div> +<t t-name="Export.SaveList"> + <label>Save as:</label> <input type="text"/><button type="button" class="btn btn-sm btn-default">Ok</button> +</t> +<t t-name="Export.SavedList"> + <label>Saved exports: </label> + <select class="o_exported_lists_select"> <option></option> <t t-foreach="existing_exports" t-as="export"> <option t-att-value="export.id"><t t-esc="export.name"/></option> </t> </select> - <button class="oe_button oe_export_button_export" id="delete_export_list" type="button">Delete</button> + <button type="button" class="btn btn-sm btn-default o_delete_exported_list">Delete</button> </t> <t t-name="Throbber"> @@ -1421,32 +1249,26 @@ </t> <t t-name="M2ODialog"> <div> - <p class="oe_grey"/> + <p/> Name: <input class="oe_form_m2o_input_name" type="text"/> </div> </t> <t t-name="FieldMonetary" t-extend="FieldChar"> - <t t-jquery="t:first" t-operation="before"> - <t t-if="widget.get('currency_info') and widget.get('currency_info').position === 'before'"> - <t t-esc="widget.get('currency_info').symbol"/> - </t> - </t> - <t t-jquery="t:last" t-operation="after"> - <t t-if="widget.get('currency_info') and widget.get('currency_info').position === 'after'"> - <t t-esc="widget.get('currency_info').symbol"/> - </t> + <t t-jquery="input"> + var $div = $(document.createElement('div')).attr('t-if', this.attr('t-if')).addClass('o_form_field_monetary'); + this.clone(true).appendTo($div); + this.replaceWith($div); </t> </t> <t t-name="FieldMany2ManyCheckBoxes"> - <t t-foreach="widget.get('records')" t-as="record"> - <div> - <label style="font-weight: normal"> - <input type="checkbox" t-att-data-record-id="JSON.stringify(record[0])" - t-att-checked="(widget.get('value').indexOf(record[0]) !== -1) or null"/> - <t t-esc="record[1]"/> - </label> + <div t-foreach="widget.get('records')" t-as="record"> + <div class="o_checkbox"> + <input t-if="widget.get('value').indexOf(record[0]) !== -1" type="checkbox" t-att-data-record-id="JSON.stringify(record[0])" checked="checked"/> + <input t-if="widget.get('value').indexOf(record[0]) === -1" type="checkbox" t-att-data-record-id="JSON.stringify(record[0])"/> + <span/> </div> - </t> + <label class="o_form_label"><t t-esc="record[1]"/></label> + </div> </t> <t t-name="X2ManyCounter"> <a href="javascript:void(0)"><t t-esc="text"/></a> @@ -1456,12 +1278,22 @@ <span class="o_stat_text"><t t-esc="text"/></span> </t> -<button t-name="toggle_button" type="button" - t-att-title="widget.string" - style="box-shadow: none; white-space:nowrap;"> - <img t-attf-src="#{prefix}/web/static/src/img/icons/#{widget.icon}.png" - t-att-alt="widget.string"/> -</button> +<t t-name="toggle_button"> + <button type="button" class="o_icon_button" t-att-title="widget.string"> + <img t-attf-src="#{prefix || _s}/web/static/src/img/icons/#{widget.icon}.png" t-att-alt="widget.string"/> + </button> +</t> + +<t t-name="BooleanButton"> + <t t-if="widget.get_value()"> + <span class="o_stat_text o_not_hover text-success"><t t-esc="widget.string_true"/></span> + <span class="o_stat_text o_hover text-danger"><t t-esc="widget.hover_true"/></span> + </t> + <t t-if="!widget.get_value()"> + <span class="o_stat_text o_not_hover text-danger"><t t-esc="widget.string_false"/></span> + <span class="o_stat_text o_hover text-success"><t t-esc="widget.hover_false"/></span> + </t> +</t> <div t-name="Pager"> <span class="o_pager_value"></span> / <span class="o_pager_limit"></span> @@ -1476,20 +1308,11 @@ </span> </div> -<button t-name="FieldToggleBoolean" type="button" - t-att-title="widget.string" class="oe_toggle_button"> - <img t-attf-src="#{_s}/web/static/src/img/icons/#{widget.icon}" t-att-alt="widget.string"/> -</button> - -<t t-name="BooleanButton"> - <t t-if="widget.get_value()"> - <span class="o_stat_text o_not_hover text-success"><t t-esc="widget.string_true"/></span> - <span class="o_stat_text o_hover text-danger"><t t-esc="widget.hover_true"/></span> - </t> - <t t-if="!widget.get_value()"> - <span class="o_stat_text o_not_hover text-danger"><t t-esc="widget.string_false"/></span> - <span class="o_stat_text o_hover text-success"><t t-esc="widget.hover_false"/></span> - </t> +<t t-name="Menu.needaction_counter"> + <div id="menu_counter" class="badge pull-right"> + <t t-if="widget.needaction_counter > 99"> 99+ </t> + <t t-if="widget.needaction_counter <= 99"> <t t-esc="widget.needaction_counter"/> </t> + </div> </t> <t t-name="notification-box"> @@ -1509,6 +1332,68 @@ </t> </t> +<t t-name="UserMenu"> + <li class="o_user_menu"> + <a class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" href="#"> + <img class="img-circle oe_topbar_avatar" t-att-src="_s + '/web/static/src/img/user_menu_avatar.png'"/> + <span class="oe_topbar_name"/> <span class="caret"></span> + </a> + <ul class="dropdown-menu" role="menu"> + <li><a href="#" data-menu="documentation">Documentation</a></li> + <li><a href="#" data-menu="support">Support</a></li> + <li><a href="#" data-menu="about">About</a></li> + <li class="divider"/> + <li><a href="#" data-menu="settings">Preferences</a></li> + <li><a href="#" data-menu="account">My Odoo.com account</a></li> + <li><a href="#" data-menu="logout">Log out</a></li> + </ul> + </li> +</t> + +<div t-name="UserMenu.about" class="oe_about"> + <div class="container-fluid"> + <div class="row"> + <div class="col-sm-offset-2 col-md-offset-2 col-sm-8 col-md-8 user-details"> + <div class="user-image text-center"> + <img src="/web/static/src/img/logo2.png" alt="Odoo (Formerly OpenERP)" title="Odoo (Formerly OpenERP)" class="img-circle"/> + </div> + <div class="user-info-block text-center"> + <div class="user-heading"> + <h3> + Version <t t-esc="db_info.server_version"/> + (Enterprise Edition) + </h3> + <t t-if="db_info.expiration_date"> + <h5>Database expiration: <t t-esc="new moment(db_info.expiration_date).format('LL')"/></h5> + </t> + </div> + <div class="user-body"> + <div class="tab-content"> + <div id="settings" class="tab-pane active"> + <h4>Copyright © 2004-2015 Odoo S.A.</h4> + <p>Odoo is a trademark of <a target="_blank" href="https://www.odoo.com" style="text-decoration: underline;">Odoo S.A.</a></p> + <p>Licenced under the terms of <a target="_blank" href="http://www.gnu.org/licenses/lgpl.html" style="text-decoration: underline;">GNU Lesser General Public License</a></p> + <p>For more information visit <a target="_blank" href="https://www.odoo.com" style="text-decoration: underline;">Odoo.com</a></p> + <p> + <a href="https://www.odoo.com/web/about/facebook"><i class="fa fa-facebook-square fa-3x social-fb"></i></a> + <a href="https://www.odoo.com/web/about/twitter"><i class="fa fa-twitter-square fa-3x social-tw"></i></a> + <a href="https://www.odoo.com/web/about/googleplus"><i class="fa fa-google-plus-square fa-3x social-gp"></i></a> + <a href="https://www.odoo.com/web/about/linkedin"><i class="fa fa-linkedin-square fa-3x social-lk"></i></a> + </p> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="row" t-if="!debug"> + <div class="col-md-12"> + <a class="oe_activate_debug_mode pull-right" href="?debug" >Activate the developer mode</a> + </div> + </div> + </div> +</div> + <t t-name="EnterpriseUpgrade"> <div class="row"> <div class="col-xs-6"> diff --git a/addons/web/static/src/xml/base_common.xml b/addons/web/static/src/xml/base_common.xml index 2e73f9a153e..1570b5a8717 100644 --- a/addons/web/static/src/xml/base_common.xml +++ b/addons/web/static/src/xml/base_common.xml @@ -5,10 +5,10 @@ <div t-name="Dialog" class="modal" tabindex="-1" data-backdrop="static" role="dialog" aria-hidden="true"> <div class="modal-dialog"> - <div class="modal-content openerp"> + <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h4 class="modal-title"><t t-raw="title"/><span class="o_subtitle text-muted"><t t-esc="subtitle"/></span></h4> + <h4 class="modal-title"><t t-raw="title"/><span class="o_subtitle text-muted small"><t t-esc="subtitle"/></span></h4> </div> <div class="modal-body"/> <div class="modal-footer"/> @@ -17,7 +17,7 @@ </div> <t t-name="WidgetButton"> - <button type="button" t-attf-class="btn btn-sm oe_button #{widget.node.attrs.class or ''}" + <button type="button" t-attf-class="btn btn-sm #{widget.node.attrs.class or ''}" t-att-style="widget.node.attrs.style" t-att-tabindex="widget.node.attrs.tabindex" t-att-autofocus="widget.node.attrs.autofocus" |
