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/js/widgets | |
| 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/js/widgets')
| -rw-r--r-- | addons/web/static/src/js/widgets/auto_complete.js | 220 | ||||
| -rw-r--r-- | addons/web/static/src/js/widgets/change_password.js | 2 | ||||
| -rw-r--r-- | addons/web/static/src/js/widgets/data_export.js | 608 | ||||
| -rw-r--r-- | addons/web/static/src/js/widgets/debug_manager.js | 845 | ||||
| -rw-r--r-- | addons/web/static/src/js/widgets/loading.js | 3 | ||||
| -rw-r--r-- | addons/web/static/src/js/widgets/pager.js | 76 | ||||
| -rw-r--r-- | addons/web/static/src/js/widgets/sidebar.js | 37 | ||||
| -rw-r--r-- | addons/web/static/src/js/widgets/switch_company_menu.js | 79 | ||||
| -rw-r--r-- | addons/web/static/src/js/widgets/user_menu.js | 100 |
9 files changed, 1345 insertions, 625 deletions
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'), + }); }, }); |
