odoo.define('web.SwitchCompanyMenu', function(require) { "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 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); });
odoo.define('web.DebugManager', function (require) { "use strict"; var core = require('web.core'); var Dialog = require('web.Dialog'); var formats = require('web.formats'); var framework = require('web.framework'); var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); var utils = require('web.utils'); var ViewManager = require('web.ViewManager'); 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; }, /** * Renders the DebugManager dropdown */ render_dropdown: function() { var self = this; // Empty the previously rendered dropdown this.$dropdown.empty(); // 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; } 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); } }, get_metadata: function() { var self = this; var ids = this.view.get_selected_ids(); if (ids.length === 1) { self.dataset.call('get_metadata', [ids]).done(function(result) { new Dialog(this, { title: _.str.sprintf(_t("Metadata (%s)"), self.dataset.model), size: 'medium', buttons: { Ok: function() { this.parents('.modal').modal('hide');} }, }, 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=*' }); }, 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)); }); }); 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() { this.do_action({ name: _t("Technical Translation"), res_model : 'ir.translation', domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']], views: [[false, 'list'], [false, 'form']], type : 'ir.actions.act_window', view_type : "list", view_mode : "list" }); }, 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, } }); }, 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 }); } else { this.do_warn("Warning", "No record selected."); } }, leave_debug: function() { window.location.search="?"; }, }); SystrayMenu.Items.push(DebugManager); return DebugManager; } });
Users.call('has_group', ['base.group_user']).done(function(is_employee) { if (is_employee) { SystrayMenu.Items.push(ImTopButton); } });
odoo.define('mail.systray.ActivityMenu', function (require) { "use strict"; var core = require('web.core'); var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); var QWeb = core.qweb; /** * Menu item appended in the systray part of the navbar, redirects to the next * activities of all app */ var ActivityMenu = Widget.extend({ name: 'activity_menu', template:'mail.systray.ActivityMenu', events: { 'click .o_mail_preview': '_onActivityFilterClick', 'show.bs.dropdown': '_onActivityMenuShow', }, willStart: function () { return $.when(this.call('mail_service', 'isReady')); }, start: function () { this._$activitiesPreview = this.$('.o_mail_systray_dropdown_items'); this.call('mail_service', 'getMailBus').on('activity_updated', this, this._updateCounter); this._updateCounter(); this._updateActivityPreview(); return this._super(); }, //-------------------------------------------------- // Private //-------------------------------------------------- /** * Make RPC and get current user's activity details * @private */ _getActivityData: function () { var self = this; return self._rpc({ model: 'res.users', method: 'systray_get_activities', args: [], kwargs: {context: session.user_context}, }).then(function (data) { self._activities = data; self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count; }, 0); self.$('.o_notification_counter').text(self.activityCounter); self.$el.toggleClass('o_no_notification', !self.activityCounter); }); }, /** * Get particular model view to redirect on click of activity scheduled on that model. * @private * @param {string} model */ _getActivityModelViewID: function (model) { return this._rpc({ model: model, method: 'get_activity_view_id' }); }, /** * Update(render) activity system tray view on activity updation. * @private */ _updateActivityPreview: function () { var self = this; self._getActivityData().then(function (){ self._$activitiesPreview.html(QWeb.render('mail.systray.ActivityMenu.Previews', { activities : self._activities })); }); }, /** * update counter based on activity status(created or Done) * @private * @param {Object} [data] key, value to decide activity created or deleted * @param {String} [data.type] notification type * @param {Boolean} [data.activity_deleted] when activity deleted * @param {Boolean} [data.activity_created] when activity created */ _updateCounter: function (data) { if (data) { if (data.activity_created) { this.activityCounter ++; } if (data.activity_deleted && this.activityCounter > 0) { this.activityCounter --; } this.$('.o_notification_counter').text(this.activityCounter); this.$el.toggleClass('o_no_notification', !this.activityCounter); } }, //------------------------------------------------------------ // Handlers //------------------------------------------------------------ /** * Redirect to particular model view * @private * @param {MouseEvent} event */ _onActivityFilterClick: function (event) { // fetch the data from the button otherwise fetch the ones from the parent (.o_mail_preview). var data = _.extend({}, $(event.currentTarget).data(), $(event.target).data()); var context = {}; if (data.filter === 'my') { context['search_default_activities_overdue'] = 1; context['search_default_activities_today'] = 1; } else { context['search_default_activities_' + data.filter] = 1; } this.do_action({ type: 'ir.actions.act_window', name: data.model_name, res_model: data.res_model, views: [[false, 'kanban'], [false, 'form']], search_view_id: [false], domain: [['activity_user_id', '=', session.uid]], context:context, }); }, /** * @private */ _onActivityMenuShow: function () { this._updateActivityPreview(); }, }); SystrayMenu.Items.push(ActivityMenu); return ActivityMenu; });
odoo.define('mail.systray', function (require) { "use strict"; var config = require('web.config'); var core = require('web.core'); var framework = require('web.framework'); var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); var chat_manager = require('mail.chat_manager'); var QWeb = core.qweb; /** * Menu item appended in the systray part of the navbar * * The menu item indicates the counter of needactions + unread messages in chat channels. When * clicking on it, it toggles a dropdown containing a preview of each pinned channels (except * static and mass mailing channels) with a quick link to open them in chat windows. It also * contains a direct link to the Inbox in Discuss. **/ var MessagingMenu = Widget.extend({ template:'mail.chat.MessagingMenu', events: { "click": "on_click", "click .o_filter_button": "on_click_filter_button", "click .o_new_message": "on_click_new_message", "click .o_mail_channel_preview": "_onClickChannel", }, init: function () { this._super.apply(this, arguments); this.isMobile = config.device.isMobile; // used by the template }, start: function () { this.$filter_buttons = this.$('.o_filter_button'); this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels'); this.filter = false; chat_manager.bus.on("update_needaction", this, this.update_counter); chat_manager.bus.on("update_channel_unread_counter", this, this.update_counter); chat_manager.is_ready.then(this.update_counter.bind(this)); return this._super(); }, is_open: function () { return this.$el.hasClass('open'); }, update_counter: function () { var counter = chat_manager.get_needaction_counter() + chat_manager.get_unread_conversation_counter(); this.$('.o_notification_counter').text(counter); this.$el.toggleClass('o_no_notification', !counter); if (this.is_open()) { this.update_channels_preview(); } }, update_channels_preview: function () { var self = this; // Display spinner while waiting for channels preview this.$channels_preview.html(QWeb.render('Spinner')); chat_manager.is_ready.then(function () { var channels = _.filter(chat_manager.get_channels(), function (channel) { if (self.filter === 'chat') { return channel.is_chat; } else if (self.filter === 'channels') { return !channel.is_chat && channel.type !== 'static'; } else { return channel.type !== 'static'; } }); chat_manager.get_messages({channel_id: 'channel_inbox'}).then(function(result) { var res = []; _.each(result, function (message) { message.unread_counter = 1; var duplicatedMessage = _.findWhere(res, {model: message.model, 'res_id': message.res_id}); if (message.model && message.res_id && duplicatedMessage) { message.unread_counter = duplicatedMessage.unread_counter + 1; res[_.findIndex(res, duplicatedMessage)] = message; } else { res.push(message); } }); if (self.filter === 'channel_inbox' || !self.filter) { channels = _.union(channels, res); } chat_manager.get_channels_preview(channels).then(self._render_channels_preview.bind(self)); }); }); }, _render_channels_preview: function (channels_preview) { this.$channels_preview.html(QWeb.render('mail.chat.ChannelsPreview', { channels: channels_preview, })); }, on_click: function () { if (!this.is_open()) { this.update_channels_preview(); // we are opening the dropdown so update its content } }, on_click_filter_button: function (event) { event.stopPropagation(); this.$filter_buttons.removeClass('active'); var $target = $(event.currentTarget); $target.addClass('active'); this.filter = $target.data('filter'); this.update_channels_preview(); }, on_click_new_message: function () { chat_manager.bus.trigger('open_chat'); }, // Handlers /** * When a channel is clicked on, we want to open chat/channel window * * @private * @param {MouseEvent} event */ _onClickChannel: function (event) { var self = this; var channelID = $(event.currentTarget).data('channel_id'); if (channelID === 'channel_inbox') { var resID = $(event.currentTarget).data('res_id'); var resModel = $(event.currentTarget).data('res_model'); if (resModel && resID) { this.do_action({ type: 'ir.actions.act_window', res_model: resModel, views: [[false, 'form'], [false, 'kanban']], res_id: resID }); } else { this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true}) .then(function () { self.trigger_up('hide_home_menu'); // we cannot 'go back to previous page' otherwise core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id()); }); } } else { var channel = chat_manager.get_channel(channelID); if (channel) { chat_manager.open_channel(channel); } } }, }); /** * Menu item appended in the systray part of the navbar, redirects to the next activities of all app */ var ActivityMenu = Widget.extend({ template:'mail.chat.ActivityMenu', events: { "click": "_onActivityMenuClick", "click .o_mail_channel_preview": "_onActivityFilterClick", }, start: function () { this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels'); chat_manager.bus.on("activity_updated", this, this._updateCounter); chat_manager.is_ready.then(this._updateCounter.bind(this)); this._updateActivityPreview(); return this._super(); }, // Private /** * Make RPC and get current user's activity details * @private */ _getActivityData: function(){ var self = this; return self._rpc({ model: 'res.users', method: 'activity_user_count', }).then(function (data) { self.activities = data; self.activityCounter = _.reduce(data, function(total_count, p_data){ return total_count + p_data.total_count; }, 0); self.$('.o_notification_counter').text(self.activityCounter); self.$el.toggleClass('o_no_notification', !self.activityCounter); }); }, /** * Get particular model view to redirect on click of activity scheduled on that model. * @private * @param {string} model */ _getActivityModelViewID: function (model) { return this._rpc({ model: model, method: 'get_activity_view_id' }); }, /** * Check wether activity systray dropdown is open or not * @private * @returns {boolean} */ _isOpen: function () { return this.$el.hasClass('open'); }, /** * Update(render) activity system tray view on activity updation. * @private */ _updateActivityPreview: function () { var self = this; self._getActivityData().then(function (){ self.$activities_preview.html(QWeb.render('mail.chat.ActivityMenuPreview', { activities : self.activities })); }); }, /** * update counter based on activity status(created or Done) * @private * @param {Object} [data] key, value to decide activity created or deleted * @param {String} [data.type] notification type * @param {Boolean} [data.activity_deleted] when activity deleted * @param {Boolean} [data.activity_created] when activity created */ _updateCounter: function (data) { if (data) { if (data.activity_created) { this.activityCounter ++; } if (data.activity_deleted && this.activityCounter > 0) { this.activityCounter --; } this.$('.o_notification_counter').text(this.activityCounter); this.$el.toggleClass('o_no_notification', !this.activityCounter); } }, // Handlers /** * Redirect to particular model view * @private * @param {MouseEvent} event */ _onActivityFilterClick: function (event) { // fetch the data from the button otherwise fetch the ones from the parent (.o_mail_channel_preview). var data = _.extend({}, $(event.currentTarget).data(), $(event.target).data()); var context = {}; if (data.filter === 'my') { context['search_default_activities_overdue'] = 1; context['search_default_activities_today'] = 1; } else { context['search_default_activities_' + data.filter] = 1; } this.do_action({ type: 'ir.actions.act_window', name: data.model_name, res_model: data.res_model, views: [[false, 'kanban'], [false, 'form']], search_view_id: [false], domain: [['activity_user_id', '=', session.uid]], context:context, }); }, /** * When menu clicked update activity preview if counter updated * @private * @param {MouseEvent} event */ _onActivityMenuClick: function () { if (!this._isOpen()) { this._updateActivityPreview(); } }, }); SystrayMenu.Items.push(MessagingMenu); SystrayMenu.Items.push(ActivityMenu); // to test activity menu in qunit test cases we need it return { ActivityMenu: ActivityMenu, }; });
odoo.define('web.DebugManager', function (require) { "use strict"; var ActionManager = require('web.ActionManager'); var dialogs = require('web.view_dialogs'); var startClickEverywhere = require('web.clickEverywhere'); var config = require('web.config'); var core = require('web.core'); var Dialog = require('web.Dialog'); var field_utils = require('web.field_utils'); var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); var utils = require('web.utils'); var WebClient = require('web.WebClient'); var Widget = require('web.Widget'); var QWeb = core.qweb; var _t = core._t; /** * 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(.show)": function(e) { // Open other dropdowns on mouseover var $opened = this.$('.o_debug_dropdowns > li.show'); if($opened.length) { $opened.removeClass('show'); $(e.currentTarget).addClass('show').find('> a').focus(); } }, }, 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('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'))); } 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 = session.is_system; return $.when( this._rpc({ model: 'res.users', method: 'check_access_rights', kwargs: {operation: 'write', raise_exception: false}, }), session.user_has_group('base.group_no_one'), this._rpc({ model: 'ir.model.data', method: 'xmlid_to_res_id', kwargs: {xmlid: 'base.group_no_one'}, }), this._super() ).then(function (can_write_user, has_group_no_one, group_no_one_id) { this._features_group = can_write_user && group_no_one_id; this._has_features = has_group_no_one; 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; 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); }, /** * Update the debug manager: reinserts all "universal" controls */ update: function () { this.$dropdown .empty() .append(QWeb.render('WebClient.DebugManager.Global', { manager: this, })); return $.when(); }, select_view: function () { var self = this; new dialogs.SelectCreateDialog(this, { res_model: 'ir.ui.view', title: _t('Select a view'), disable_multiple_selection: true, domain: [['type', '!=', 'qweb'], ['type', '!=', 'search']], on_selected: function (records) { self._rpc({ model: 'ir.ui.view', method: 'search_read', domain: [['id', '=', records[0].id]], fields: ['name', 'model', 'type'], limit: 1, }) .then(function (views) { var view = views[0]; view.type = view.type === 'tree' ? 'list' : view.type; // ignore tree view self.do_action({ type: 'ir.actions.act_window', name: view.name, res_model: view.model, views: [[view.id, view.type]] }); }); } }).open(); }, /** * Runs the JS (desktop) tests */ perform_js_tests: function () { this.do_action({ name: _t("JS Tests"), target: 'new', type: 'ir.actions.act_url', url: '/web/tests?mod=*' }); }, /** * Runs the JS mobile tests */ perform_js_mobile_tests: function () { this.do_action({ name: _t("JS Mobile Tests"), target: 'new', type: 'ir.actions.act_url', url: '/web/tests/mobile?mod=*' }); }, perform_click_everywhere_test: function () { startClickEverywhere(); }, split_assets: function() { window.location = $.param.querystring(window.location.href, 'debug=assets'); }, /** * Delete assets bundles to force their regeneration * * @returns {void} */ regenerateAssets: function () { var self = this; var domain = [ ['res_model', '=', 'ir.ui.view'], ['name', 'like', 'assets_'] ]; this._rpc({ model: 'ir.attachment', method: 'search', args: [domain], }).then(function (ids) { self._rpc({ model: 'ir.attachment', method: 'unlink', args: [ids], }).then(self.do_action('reload')); }); } }); /** * 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 model = this._action.res_model, self = this; this._rpc({ model: 'ir.model', method: 'search', args: [[['model', '=', model]]] }).done(function (ids) { self.do_action({ res_model: 'ir.model.fields', name: _t('View Fields'), views: [[false, 'list'], [false, 'form']], domain: [['model_id', '=', model]], type: 'ir.actions.act_window', context: { 'default_model_id': ids[0] } }); }); }, 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 } }); }, translate: function() { this._rpc({ model: 'ir.translation', method: 'get_technical_translations', args: [this._action.res_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(), this._rpc({ model: 'ir.ui.view', method: 'check_access_rights', kwargs: {operation: 'write', raise_exception: false}, }) .then(function (ar) { this._can_edit_views = ar; }.bind(this)) ); }, update: function (tag, descriptor, widget) { if (tag === 'action' || tag === 'view') { this._controller = widget; } return this._super(tag, descriptor).then(function () { this.$dropdown.find(".o_debug_leave_section").before(QWeb.render('WebClient.DebugManager.View', { action: this._action, can_edit: this._can_edit_views, controller: this._controller, controlPanelView: this._controller && this._controller._controlPanel, manager: this, view: this._controller && _.findWhere(this._action.views, { type: this._controller.viewType, }), })); }.bind(this)); }, get_attachments: function() { var selectedIDs = this._controller.getSelectedIds(); if (!selectedIDs.length) { console.warn(_t("No attachment available")); return; } this.do_action({ res_model: 'ir.attachment', name: _t('Manage Attachments'), views: [[false, 'list'], [false, 'form']], type: 'ir.actions.act_window', domain: [['res_model', '=', this._action.res_model], ['res_id', '=', selectedIDs[0]]], context: { default_res_model: this._action.res_model, default_res_id: selectedIDs[0], }, }); }, get_metadata: function() { var self = this; var selectedIDs = this._controller.getSelectedIds(); if (!selectedIDs.length) { console.warn(_t("No metadata available")); return; } this._rpc({ model: this._action.res_model, method: 'get_metadata', args: [selectedIDs], }).done(function(result) { var metadata = result[0]; metadata.creator = field_utils.format.many2one(metadata.create_uid); metadata.lastModifiedBy = field_utils.format.many2one(metadata.write_uid); var createDate = field_utils.parse.datetime(metadata.create_date); metadata.create_date = field_utils.format.datetime(createDate); var modificationDate = field_utils.parse.datetime(metadata.write_date); metadata.write_date = field_utils.format.datetime(modificationDate); var dialog = new Dialog(this, { title: _.str.sprintf(_t("Metadata (%s)"), self._action.res_model), size: 'medium', $content: QWeb.render('WebClient.DebugViewLog', { perm : metadata, }) }); dialog.open().opened(function () { dialog.$el.on('click', 'a[data-action="toggle_noupdate"]', function (ev) { ev.preventDefault(); self._rpc({ model: 'ir.model.data', method: 'toggle_noupdate', args: [self._action.res_model, metadata.id] }).then(function (res) { dialog.close(); self.get_metadata(); }) }); }) }); }, set_defaults: function() { var self = this; var display = function (fieldInfo, value) { var displayed = value; if (value && fieldInfo.type === 'many2one') { displayed = value.data.display_name; value = value.data.id; } else if (value && fieldInfo.type === 'selection') { displayed = _.find(fieldInfo.selection, function (option) { return option[0] === value; })[1]; } return [value, displayed]; }; var renderer = this._controller.renderer; var state = renderer.state; var fields = state.fields; var fieldsInfo = state.fieldsInfo.form; var fieldNamesInView = state.getFieldNames(); var fieldsValues = state.data; var modifierDatas = {}; _.each(fieldNamesInView, function (fieldName) { modifierDatas[fieldName] = _.find(renderer.allModifiersData, function (modifierdata) { return modifierdata.node.attrs.name === fieldName; }); }); this.fields = _.chain(fieldNamesInView) .map(function (fieldName) { var modifierData = modifierDatas[fieldName]; var invisibleOrReadOnly; if (modifierData) { var evaluatedModifiers = modifierData.evaluatedModifiers[state.id]; invisibleOrReadOnly = evaluatedModifiers.invisible || evaluatedModifiers.readonly; } var fieldInfo = fields[fieldName]; var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]); var value = valueDisplayed[0]; var displayed = valueDisplayed[1]; // ignore fields which are empty, invisible, readonly, o2m // or m2m if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' || fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' || fieldsInfo[fieldName].options.isPassword || !_.isEmpty(fieldInfo.depends)) { return false; } return { name: fieldName, string: fieldInfo.string, value: value, displayed: displayed, }; }) .compact() .sortBy(function (field) { return field.string; }) .value(); var conditions = _.chain(fieldNamesInView) .filter(function (fieldName) { var fieldInfo = fields[fieldName]; return fieldInfo.change_default; }) .map(function (fieldName) { var fieldInfo = fields[fieldName]; var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]); var value = valueDisplayed[0]; var displayed = valueDisplayed[1]; return { name: fieldName, string: fieldInfo.string, value: value, displayed: displayed, }; }) .value(); var d = new Dialog(this, { title: _t("Set Default"), buttons: [ {text: _t("Close"), close: true}, {text: _t("Save default"), click: function () { var $defaults = d.$el.find('#formview_default_fields'); var fieldToSet = $defaults.val(); if (!fieldToSet) { $defaults.parent().addClass('o_form_invalid'); return; } var selfUser = d.$el.find('#formview_default_self').is(':checked'); var condition = d.$el.find('#formview_default_conditions').val(); var value = _.find(self.fields, function (field) { return field.name === fieldToSet; }).value; self._rpc({ model: 'ir.default', method: 'set', args: [ self._action.res_model, fieldToSet, value, selfUser, true, condition || false, ], }).done(function () { d.close(); }); }} ] }); d.args = { fields: this.fields, conditions: conditions, }; d.template = 'FormView.set_default'; d.open(); }, fvg: function() { var self = this; var dialog = new Dialog(this, { title: _t("Fields View Get") }); dialog.opened().then(function () { $('<pre>').text(utils.json_node_to_xml( self._controller.renderer.arch, true) ).appendTo(dialog.$el); }); dialog.open(); }, }); 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++ }); } 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(); }, '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 (config.debug) { SystrayMenu.Items.push(DebugManager); WebClient.include({ //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- /** * @override */ current_action_updated: function (action, controller) { this._super.apply(this, arguments); var debugManager = _.find(this.menu.systray_menu.widgets, function(item) { return item instanceof DebugManager; }); debugManager.update('action', action, controller && controller.widget); }, }); ActionManager.include({ //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- /** * Returns the action of the controller currently opened in a dialog, * i.e. a target='new' action, if any. * * @returns {Object|null} */ getCurrentActionInDialog: function () { if (this.currentDialogController) { return this.actions[this.currentDialogController.actionID]; } return null; }, /** * Returns the controller currently opened in a dialog, if any. * * @returns {Object|null} */ getCurrentControllerInDialog: function () { return this.currentDialogController; }, }); Dialog.include({ //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * @override */ open: function() { var self = this; // if the dialog is opened by the ActionManager, instantiate a // DebugManager and insert it into the DOM once the dialog is opened // (delay this with a setTimeout(0) to ensure that the internal // state, i.e. the current action and controller, of the // ActionManager is set to properly update the DebugManager) this.opened(function() { setTimeout(function () { var parent = self.getParent(); if (parent instanceof ActionManager) { var action = parent.getCurrentActionInDialog(); if (action) { var controller = parent.getCurrentControllerInDialog(); self.debugManager = new DebugManager(self); var $header = self.$modal.find('.modal-header:first'); return self.debugManager.prependTo($header).then(function () { self.debugManager.update('action', action, controller.widget); }); } } }, 0); }); return this._super.apply(this, arguments); }, }); } return DebugManager; });
odoo.define('pop-up_reminders.reminder_topbar', function (require) { "use strict"; var core = require('web.core'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); var QWeb = core.qweb; var ajax = require('web.ajax'); var reminder_menu = Widget.extend({ template:'pop-up_reminders.reminder_menu', events: { "click .dropdown-toggle": "on_click_reminder", "click .detail-client-address-country": "reminder_active", }, init:function(parent, name){ this.reminder = null; this._super(parent); }, on_click_reminder: function (event) { var self = this ajax.jsonRpc("/pop-up_reminders/all_reminder", 'call',{} ).then(function(all_reminder){ self.all_reminder = all_reminder /*console.log(self.all_reminder);*/ self.$('.o_mail_navbar_dropdown_top').html(QWeb.render('pop-up_reminders.reminder_menu',{ values: self.all_reminder })); }) }, reminder_active: function(){ var self = this; var value =$("#reminder_select").val(); ajax.jsonRpc("/pop-up_reminders/reminder_active", 'call',{'reminder_name':value} ).then(function(reminder){ self.reminder = reminder for (var i=0;i<1;i++){ var model = self.reminder[i] var date = self.reminder[i+1] if (self.reminder[i+2] == 'today'){ return self.do_action({ type: 'ir.actions.act_window', res_model: model, view_mode: 'tree', view_type: 'tree', domain: [[date, '=', new Date()]], views: [[false, 'list']], target: 'new',}) } else if (self.reminder[i+2] == 'set_date'){ return self.do_action({ type: 'ir.actions.act_window', res_model: model, view_mode: 'tree', view_type: 'tree', domain: [[date, '=', self.reminder[i+3]]], views: [[false, 'list']], target: 'new',}) } else if (self.reminder[i+2] == 'set_period'){ return self.do_action({ type: 'ir.actions.act_window', res_model: model, view_mode: 'tree', view_type: 'tree', domain: [[date, '<', self.reminder[i+5]],[date, '>', self.reminder[i+4]]], views: [[false, 'list']], target: 'new',}) } } }); }, }); SystrayMenu.Items.push(reminder_menu); });
Users.call('has_group', ['base.group_user']).done(function(is_employee) { if (is_employee) { SystrayMenu.Items.push(AttendanceSlider); } });
odoo.define('mail.systray', function (require) { "use strict"; var core = require('web.core'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); var chat_manager = require('mail.chat_manager'); var QWeb = core.qweb; /** * Menu item appended in the systray part of the navbar, redirects to the Inbox in Discuss * Also displays the needaction counter (= Inbox counter) */ var InboxItem = Widget.extend({ template:'mail.chat.InboxItem', events: { "click": "on_click", }, start: function () { this.$needaction_counter = this.$('.o_notification_counter'); chat_manager.bus.on("update_needaction", this, this.update_counter); chat_manager.is_ready.then(this.update_counter.bind(this)); return this._super(); }, update_counter: function () { var counter = chat_manager.get_needaction_counter(); this.$needaction_counter.text(counter); this.$el.toggleClass('o_no_notification', !counter); }, on_click: function (event) { event.preventDefault(); chat_manager.is_ready.then(this.discuss_redirect.bind(this)); }, discuss_redirect: _.debounce(function () { var self = this; var discuss_ids = chat_manager.get_discuss_ids(); this.do_action(discuss_ids.action_id, {clear_breadcrumbs: true}).then(function () { self.trigger_up('hide_app_switcher'); core.bus.trigger('change_menu_section', discuss_ids.menu_id); }); }, 1000, true), }); SystrayMenu.Items.push(InboxItem); /** * Menu item appended in the systray part of the navbar * * The menu item indicates the counter of needactions + unread messages in chat channels. When * clicking on it, it toggles a dropdown containing a preview of each pinned channels (except * static and mass mailing channels) with a quick link to open them in chat windows. It also * contains a direct link to the Inbox in Discuss. **/ var MessagingMenu = Widget.extend({ template:'mail.chat.MessagingMenu', events: { "click": "on_click", "click .o_filter_button": "on_click_filter_button", "click .o_new_message": "on_click_new_message", "click .o_mail_channel_preview": "on_click_channel", }, start: function () { this.$filter_buttons = this.$('.o_filter_button'); this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels'); this.filter = false; chat_manager.bus.on("update_channel_unread_counter", this, this.update_counter); chat_manager.is_ready.then(this.update_counter.bind(this)); return this._super(); }, is_open: function () { return this.$el.hasClass('open'); }, update_counter: function () { var counter = chat_manager.get_unread_conversation_counter(); this.$('.o_notification_counter').text(counter); this.$el.toggleClass('o_no_notification', !counter); this.$el.toggleClass('o_unread_chat', !!chat_manager.get_chat_unread_counter()); if (this.is_open()) { this.update_channels_preview(); } }, update_channels_preview: function () { var self = this; // Display spinner while waiting for channels preview this.$channels_preview.html(QWeb.render('mail.chat.Spinner')); chat_manager.is_ready.then(function () { var channels = _.filter(chat_manager.get_channels(), function (channel) { if (self.filter === 'chat') { return channel.is_chat; } else if (self.filter === 'channels') { return !channel.is_chat && channel.type !== 'static'; } else { return channel.type !== 'static'; } }); chat_manager.get_channels_preview(channels).then(self._render_channels_preview.bind(self)); }); }, _render_channels_preview: function (channels_preview) { // Sort channels: 1. channels with unread messages, 2. chat, 3. by date of last msg channels_preview.sort(function (c1, c2) { return Math.min(1, c2.unread_counter) - Math.min(1, c1.unread_counter) || c2.is_chat - c1.is_chat || c2.last_message.date.diff(c1.last_message.date); }); // Generate last message preview (inline message body and compute date to display) _.each(channels_preview, function (channel) { channel.last_message_preview = chat_manager.get_message_body_preview(channel.last_message.body); if (channel.last_message.date.isSame(new Date(), 'd')) { // today channel.last_message_date = channel.last_message.date.format('LT'); } else { channel.last_message_date = channel.last_message.date.format('lll'); } }); this.$channels_preview.html(QWeb.render('mail.chat.ChannelsPreview', { channels: channels_preview, })); }, on_click: function () { if (!this.is_open()) { this.update_channels_preview(); // we are opening the dropdown so update its content } }, on_click_filter_button: function (event) { event.stopPropagation(); this.$filter_buttons.removeClass('o_selected'); var $target = $(event.currentTarget); $target.addClass('o_selected'); this.filter = $target.data('filter'); this.update_channels_preview(); }, on_click_new_message: function () { chat_manager.bus.trigger('open_chat'); }, on_click_channel: function (event) { var channel_id = $(event.currentTarget).data('channel_id'); var channel = chat_manager.get_channel(channel_id); if (channel) { chat_manager.open_channel(channel); } }, }); SystrayMenu.Items.push(MessagingMenu); });
odoo.define('mail.systray.MessagingMenu', function (require) { "use strict"; var config = require('web.config'); var core = require('web.core'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); var QWeb = core.qweb; /** * Menu item appended in the systray part of the navbar * * The menu item indicates the counter of needactions + unread messages in chat * channels. When clicking on it, it toggles a dropdown containing a preview of * each pinned channels (except mailbox and mass mailing channels) with a quick * link to open them in chat windows. It also contains a direct link to the * Inbox in Discuss. **/ var MessagingMenu = Widget.extend({ name: 'messaging_menu', template:'mail.systray.MessagingMenu', events: { 'click': '_onClick', 'click .o_mail_preview': '_onClickPreview', 'click .o_filter_button': '_onClickFilterButton', 'click .o_new_message': '_onClickNewMessage', 'click .o_mail_preview_mark_as_read': '_onClickPreviewMarkAsRead', }, /** * @override */ willStart: function () { return $.when(this._super.apply(this, arguments), this.call('mail_service', 'isReady')); }, /** * @override */ start: function () { this._$filterButtons = this.$('.o_filter_button'); this._$previews = this.$('.o_mail_systray_dropdown_items'); this._filter = false; this._updateCounter(); var mailBus = this.call('mail_service', 'getMailBus'); mailBus.on('update_needaction', this, this._updateCounter); mailBus.on('new_channel', this, this._updateCounter); mailBus.on('update_thread_unread_counter', this, this._updateCounter); return this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- /** * States whether the widget is in mobile mode or not. * This is used by the template. * * @returns {boolean} */ isMobile: function () { return config.device.isMobile; }, //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- /** * Called when clicking on a preview related to a mail failure * * @private * @param {$.Element} $target DOM of preview element clicked */ _clickMailFailurePreview: function ($target) { var documentID = $target.data('document-id'); var documentModel = $target.data('document-model'); if (documentModel && documentID) { this._openDocument(documentModel, documentID); } else if (documentModel !== 'mail.channel') { // preview of mail failures grouped to different document of same model this.do_action({ name: "Mail failures", type: 'ir.actions.act_window', view_mode: 'kanban,list,form', views: [[false, 'kanban'], [false, 'list'], [false, 'form']], target: 'current', res_model: documentModel, domain: [['message_has_error', '=', true]], }); } }, /** * @private * @return {boolean} whether the messaging menu is open or not. */ _isOpen: function () { return this.$el.hasClass('open'); }, /** * Open discuss * * @private * @param {integer} [channelID] if set, auto-select this channel when * opening the discuss app. */ _openDiscuss: function (channelID) { var self = this; var discussOptions = { clear_breadcrumbs: true }; if (channelID) { discussOptions.active_id = channelID; } this.do_action('mail.mail_channel_action_client_chat', discussOptions) .then(function () { // we cannot 'go back to previous page' otherwise self.trigger_up('hide_home_menu'); core.bus.trigger('change_menu_section', self.call('mail_service', 'getDiscussMenuID')); }); }, /** * Open the document * * @private * @param {string} documentModel the model of the document * @param {integer} documentID */ _openDocument: function (documentModel, documentID) { if (documentModel === 'mail.channel') { this._openDiscuss(documentID); } else { this.do_action({ type: 'ir.actions.act_window', res_model: documentModel, views: [[false, 'form']], res_id: documentID }); } }, /** * Render the list of conversation previews * * @private * @param {Object} previews list of valid objects for preview rendering * (see mail.Preview template) */ _renderPreviews: function (previews) { this._$previews.html(QWeb.render('mail.systray.MessagingMenu.Previews', { previews: previews, })); }, /** * Get and render list of previews, based on the selected filter * * preview shows the last message of a channel with inline format. * There is a hack where filter "All" also shows preview of chatter * messages (which are not channels). * * List of filters: * * 1. All * - filter: undefined * - previews: last messages of all non-mailbox channels, in addition * to last messages of chatter (get from inbox) * * 2. Channel * - filter: "Channels" * - previews: last messages of all non-mailbox and non-DM channels * * 3. Chat * - filter: "Chat" * - previews: last messages of all DM channels * * @private */ _updatePreviews: function () { // Display spinner while waiting for conversations preview this._$previews.html(QWeb.render('Spinner')); this.call('mail_service', 'getSystrayPreviews', this._filter) .then(this._renderPreviews.bind(this)); }, /** * Update the counter on the systray messaging menu icon. * The counter display the number of unread messages in channels (DM included), the number of * messages in Inbox mailbox, and the number of mail failures. * Also updates the previews if the messaging menu is open. * * Note that the number of unread messages in document thread are ignored, because they are * already considered in the number of messages in Inbox with the current design. * Also, some unread messages in channel can also be in inbox, so they are considered twice in * the counter. This is intended, as the number of needaction messages in a channel are * separately considered in the messaging menu from the unread messages, even though a message * can be both unread and needaction (such a message increments the counter twice). * * The global counter of the messaging menu should match the counter next to each of the preview * item when the messaging menu is open. * * @private */ _updateCounter: function () { var counter; var channels = this.call('mail_service', 'getChannels'); var channelUnreadCounters = _.map(channels, function (channel) { return channel.getUnreadCounter(); }); var unreadChannelCounter = _.reduce(channelUnreadCounters, function (c1, c2) { return c1 + c2; }, 0); var inboxCounter = this.call('mail_service', 'getMailbox', 'inbox').getMailboxCounter(); var mailFailureCounter = this.call('mail_service', 'getMailFailures').length; counter = unreadChannelCounter + inboxCounter + mailFailureCounter; this.$('.o_notification_counter').text(counter); this.$el.toggleClass('o_no_notification', !counter); if (this._isOpen()) { this._updatePreviews(); } }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * @private */ _onClick: function () { if (!this._isOpen()) { // we are opening the dropdown so update its content this._updatePreviews(); } }, /** * @private * @param {MouseEvent} ev */ _onClickFilterButton: function (ev) { ev.stopPropagation(); this._$filterButtons.removeClass('active'); var $target = $(ev.currentTarget); $target.addClass('active'); this._filter = $target.data('filter'); this._updatePreviews(); }, /** * @private */ _onClickNewMessage: function () { this.call('mail_service', 'openBlankThreadWindow'); }, /** * When a preview is clicked on, we want to open the related object * (thread, mail failure, etc.) * * @private * @param {MouseEvent} ev */ _onClickPreview: function (ev) { var $target = $(ev.currentTarget); var previewID = $target.data('preview-id'); if (previewID === 'mail_failure') { this._clickMailFailurePreview($target); } else if (previewID === 'mailbox_inbox') { // inbox preview for non-document thread, // e.g. needaction message of channel var documentID = $target.data('document-id'); var documentModel = $target.data('document-model'); this._openDocument(documentModel, documentID); } else { // preview of thread this.call('mail_service', 'openThread', previewID); } }, /** * When a preview "Mark as read" button is clicked on, we want mark message * as read * * @private * @param {MouseEvent} event */ _onClickPreviewMarkAsRead: function (ev) { ev.stopPropagation(); var thread; var $preview = $(ev.currentTarget).closest('.o_mail_preview'); var previewID = $preview.data('preview-id'); var documentModel = $preview.data('document-model'); if (previewID === 'mailbox_inbox') { var documentID = $preview.data('document-id'); var inbox = this.call('mail_service', 'getMailbox', 'inbox'); var messages = inbox.getLocalMessages({ documentModel: documentModel, documentID: documentID, }); var messageIDs = _.map(messages, function (message) { return message.getID(); }); this.call('mail_service', 'markMessagesAsRead', messageIDs); } else if (previewID === 'mail_failure') { var unreadCounter = $preview.data('unread-counter'); this.do_action('mail.mail_resend_cancel_action', { additional_context: { default_model: documentModel, unread_counter: unreadCounter } }); } else { // this is mark as read on a thread thread = this.call('mail_service', 'getThread', previewID); if (thread) { thread.markAsRead(); } } }, }); // Systray menu items display order matches order in the list // lower index comes first, and display is from right to left. // For messagin menu, it should come before activity menu, if any // otherwise, it is the next systray item. var activityMenuIndex = _.findIndex(SystrayMenu.Items, function (SystrayMenuItem) { return SystrayMenuItem.prototype.name === 'activity_menu'; }); if (activityMenuIndex > 0) { SystrayMenu.Items.splice(activityMenuIndex, 0, MessagingMenu); } else { SystrayMenu.Items.push(MessagingMenu); } return MessagingMenu; });
odoo.define('web.planner', function (require) { "use strict"; var Model = require('web.Model'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); var planner = require('web.planner.common'); var PlannerDialog = planner.PlannerDialog; var PlannerLauncher = Widget.extend({ template: "PlannerLauncher", events: { 'click .o_planner_progress': 'toggle_dialog' }, init: function(parent) { this._super(parent); this.planner_by_menu = {}; this.webclient = parent.getParent(); this.need_reflow = false; }, start: function() { var self = this; self._super(); self.webclient.menu.on("open_menu", self, self.on_menu_clicked); self.$el.hide(); // hidden by default return self.fetch_application_planner().done(function(apps) { self.planner_apps = apps; return apps; }); }, fetch_application_planner: function() { var self = this; var def = $.Deferred(); if (!_.isEmpty(this.planner_by_menu)) { def.resolve(self.planner_by_menu); }else{ (new Model('web.planner')).query().all().then(function(res) { _.each(res, function(planner){ self.planner_by_menu[planner.menu_id[0]] = planner; self.planner_by_menu[planner.menu_id[0]].data = $.parseJSON(self.planner_by_menu[planner.menu_id[0]].data) || {}; }); def.resolve(self.planner_by_menu); }).fail(function() {def.reject();}); } return def; }, on_menu_clicked: function(id, $clicked_menu) { var menu_id = $clicked_menu.parents('.oe_secondary_menu').data('menu-parent') || 0; // find top menu id if (_.contains(_.keys(this.planner_apps), menu_id.toString())) { this.$el.show(); this.setup(this.planner_apps[menu_id]); this.need_reflow = true; } else { if (this.$el.is(":visible")) { this.$el.hide(); this.need_reflow = true; } } if (this.need_reflow) { this.webclient.menu.reflow(); this.need_reflow = false; } }, setup: function(planner){ var self = this; this.planner = planner; this.dialog && this.dialog.destroy(); this.dialog = new PlannerDialog(this, planner); this.$(".o_planner_progress").tooltip({html: true, title: this.planner.tooltip_planner, placement: 'bottom', delay: {'show': 500}}); this.dialog.on("planner_progress_changed", this, function(percent){ self.update_parent_progress_bar(percent); }); this.dialog.appendTo(document.body); }, // event update_parent_progress_bar: function(percent) { this.$(".progress-bar").css('width', percent+"%"); }, toggle_dialog: function() { this.dialog.$('#PlannerModal').modal('toggle'); } }); // add planner launcher to the systray // if it is empty, it won't be display. Then, each time a top menu is clicked // a planner will be given to the launcher. The launcher will appears if the // given planner is not null. SystrayMenu.Items.push(PlannerLauncher); return { PlannerLauncher: PlannerLauncher, }; });
odoo.define('mail.systray', function (require) { "use strict"; var core = require('web.core'); var SystrayMenu = require('web.SystrayMenu'); var web_client = require('web.web_client'); var Widget = require('web.Widget'); var chat_manager = require('mail.chat_manager'); /** * Widget Top Menu Notification Counter * * Counter of notification in the Systray Menu. Need to know if InstantMessagingView is displayed to * increment (or not) the counter. On click, should redirect to the client action. **/ var NotificationTopButton = Widget.extend({ template:'mail.chat.NotificationTopButton', events: { "click": "on_click", }, start: function () { chat_manager.bus.on("update_needaction", this, this.update_counter); this.update_counter(chat_manager.get_needaction_counter()); return this._super(); }, update_counter: function (counter) { this.$('.o_notification_counter').html(counter); }, on_click: function (event) { event.preventDefault(); this.discuss_redirect(); }, discuss_redirect: _.debounce(function () { var discuss_ids = chat_manager.get_discuss_ids(); this.do_action(discuss_ids.action_id, {clear_breadcrumbs: true}).then(function () { core.bus.trigger('change_menu_section', discuss_ids.menu_id); }); }, 1000, true), }); SystrayMenu.Items.push(NotificationTopButton); /** * * Global ComposeMessage Top Button * * * * Add a link on the top user bar to write a full mail. It opens the form view * * of the mail.compose.message (in a modal). * */ var ComposeMessageTopButton = Widget.extend({ template:'mail.ComposeMessageTopButton', events: { "click": "on_compose_message", }, on_compose_message: function (ev) { ev.preventDefault(); web_client.action_manager.do_action({ type: 'ir.actions.act_window', res_model: 'mail.compose.message', view_mode: 'form', view_type: 'form', views: [[false, 'form']], target: 'new', }); }, }); // Put the ComposeMessageTopButton widget in the systray menu SystrayMenu.Items.push(ComposeMessageTopButton); });
odoo.define('mail.systray', function (require) { "use strict"; var config = require('web.config'); var core = require('web.core'); var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); var time = require('web.time'); var Widget = require('web.Widget'); var QWeb = core.qweb; /** * Menu item appended in the systray part of the navbar * * The menu item indicates the counter of needactions + unread messages in chat channels. When * clicking on it, it toggles a dropdown containing a preview of each pinned channels (except * static and mass mailing channels) with a quick link to open them in chat windows. It also * contains a direct link to the Inbox in Discuss. **/ var MessagingMenu = Widget.extend({ template:'mail.chat.MessagingMenu', events: { "click": "on_click", "click .o_filter_button": "on_click_filter_button", "click .o_new_message": "on_click_new_message", "click .o_mail_channel_preview": "_onClickChannel", "click .o_mail_channel_mark_read":"_onClickMarkRead", }, init: function () { this._super.apply(this, arguments); this.isMobile = config.device.isMobile; // used by the template }, start: function () { this.$filter_buttons = this.$('.o_filter_button'); this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels'); this.filter = false; var self = this; this.call('chat_manager', 'isReady').then(function () { self.update_counter(); var chatBus = self.call('chat_manager', 'getChatBus'); chatBus.on('update_needaction', self, self.update_counter); chatBus.on('update_channel_unread_counter', self, self.update_counter); }); return this._super(); }, is_open: function () { return this.$el.hasClass('open'); }, update_counter: function () { var messageFailures = this.call('chat_manager', 'getMessageFailures'); var needactionCounter = this.call('chat_manager', 'getNeedactionCounter'); var unreadConversationCounter = this.call('chat_manager', 'getUnreadConversationCounter'); var counter = needactionCounter + unreadConversationCounter + messageFailures.length; this.$('.o_notification_counter').text(counter); this.$el.toggleClass('o_no_notification', !counter); if (this.is_open()) { this.update_channels_preview(); } }, update_channels_preview: function () { var self = this; // Display spinner while waiting for channels preview this.$channels_preview.html(QWeb.render('Spinner')); this.call('chat_manager', 'isReady').then(function () { var allChannels = self.call('chat_manager', 'getChannels'); var channels = _.filter(allChannels, function (channel) { if (self.filter === 'chat') { return channel.is_chat; } else if (self.filter === 'channels') { return !channel.is_chat && channel.type !== 'static'; } else { return channel.type !== 'static'; } }); var formatedFailures = []; var messageFailures = self.call('chat_manager', 'getMessageFailures'); _.each(messageFailures, function (messageFailure) { var model = messageFailure.model; var existingFailure = _.findWhere(formatedFailures, { model: model }); if (existingFailure) { if (existingFailure.res_id !== messageFailure.res_id) { existingFailure.res_id = null; existingFailure.record_name = messageFailure.model_name;//for display, will be used as subject } existingFailure.unread_counter ++; } else { messageFailure = _.clone(messageFailure); messageFailure.unread_counter = 1; messageFailure.id = "mail_failure"; messageFailure.body = "An error occured sending an email"; messageFailure.date = moment(time.str_to_datetime(messageFailure.last_message_date)); messageFailure.displayed_author = ""; formatedFailures.push(messageFailure); } }); channels = _.union(channels, formatedFailures); self.call('chat_manager', 'getMessages', {channelID: 'channel_inbox'}) .then(function (messages) { var res = []; _.each(messages, function (message) { message.unread_counter = 1; var duplicatedMessage = _.findWhere(res, { model: message.model, 'res_id': message.res_id }); if (message.model && message.res_id && duplicatedMessage) { message.unread_counter = duplicatedMessage.unread_counter + 1; res[_.findIndex(res, duplicatedMessage)] = message; } else { res.push(message); } }); if (self.filter === 'channel_inbox' || !self.filter) { channels = _.union(channels, res); } self.call('chat_manager', 'getChannelsPreview', channels) .then(self._render_channels_preview.bind(self)); }); }); }, _render_channels_preview: function (channels_preview) { this.$channels_preview.html(QWeb.render('mail.chat.ChannelsPreview', { channels: channels_preview, })); }, on_click: function () { if (!this.is_open()) { this.update_channels_preview(); // we are opening the dropdown so update its content } }, on_click_filter_button: function (event) { event.stopPropagation(); this.$filter_buttons.removeClass('active'); var $target = $(event.currentTarget); $target.addClass('active'); this.filter = $target.data('filter'); this.update_channels_preview(); }, on_click_new_message: function () { this.call('chat_window_manager', 'openChat'); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * When a channel is clicked on, we want to open chat/channel window * * @private * @param {MouseEvent} event */ _onClickChannel: function (event) { var self = this; var channelID = $(event.currentTarget).data('channel_id'); if (channelID === 'channel_inbox' || channelID === 'mail_failure') { var resID = $(event.currentTarget).data('res_id'); var resModel = $(event.currentTarget).data('res_model'); if (resModel && resModel !== 'mail.channel' && resID) { this.do_action({ type: 'ir.actions.act_window', res_model: resModel, views: [[false, 'form']], res_id: resID }); } else if (resModel) { this.do_action({ name: "Mail failures", type: 'ir.actions.act_window', view_mode: 'kanban,list,form', views: [[false, 'kanban'], [false, 'list'], [false, 'form']], target: 'current', res_model: resModel, domain: [['message_has_error', '=', true]], }); } else { var clientChatOptions = {clear_breadcrumbs: true}; if (resModel && resModel === 'mail.channel' && resID) { clientChatOptions.active_id = resID; } this.do_action('mail.mail_channel_action_client_chat', clientChatOptions) .then(function () { self.trigger_up('hide_home_menu'); // we cannot 'go back to previous page' otherwise core.bus.trigger('change_menu_section', self.call('chat_manager', 'getDiscussMenuID')); }); } } else { var channel = this.call('chat_manager', 'getChannel', channelID); if (channel) { this.call('chat_manager', 'openChannel', channel); } } }, /** * When a channel Mark As Read button is clicked on, we want mark message as read * * @private * @param {MouseEvent} event */ _onClickMarkRead: function (ev) { ev.stopPropagation(); var channelID = $(ev.currentTarget).data('channel_id'), channel = this.call('chat_manager', 'getChannel', channelID); //Mark only static channel's messages as read and clear notification if (channelID === 'channel_inbox') { var resID = $(ev.currentTarget).data('res_id'); var model = $(ev.currentTarget).data('model'); var domain = [['model', '=', model], ['res_id', '=', resID]]; this.call('chat_manager', 'markAllAsRead', channel, domain); } else { this.call('chat_manager', 'markChannelAsSeen', channel); } }, }); /** * Menu item appended in the systray part of the navbar, redirects to the next activities of all app */ var ActivityMenu = Widget.extend({ template:'mail.chat.ActivityMenu', events: { "click": "_onActivityMenuClick", "click .o_mail_channel_preview": "_onActivityFilterClick", }, start: function () { this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels'); this.call('chat_manager', 'getChatBus').on("activity_updated", this, this._updateCounter); this.call('chat_manager', 'isReady').then(this._updateCounter.bind(this)); this._updateActivityPreview(); return this._super(); }, //-------------------------------------------------- // Private //-------------------------------------------------- /** * Make RPC and get current user's activity details * @private */ _getActivityData: function () { var self = this; return self._rpc({ model: 'res.users', method: 'systray_get_activities', args: [], kwargs: {context: session.user_context}, }).then(function (data) { self.activities = data; self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count; }, 0); self.$('.o_notification_counter').text(self.activityCounter); self.$el.toggleClass('o_no_notification', !self.activityCounter); }); }, /** * Get particular model view to redirect on click of activity scheduled on that model. * @private * @param {string} model */ _getActivityModelViewID: function (model) { return this._rpc({ model: model, method: 'get_activity_view_id' }); }, /** * Check wether activity systray dropdown is open or not * @private * @returns {boolean} */ _isOpen: function () { return this.$el.hasClass('open'); }, /** * Update(render) activity system tray view on activity updation. * @private */ _updateActivityPreview: function () { var self = this; self._getActivityData().then(function (){ self.$activities_preview.html(QWeb.render('mail.chat.ActivityMenuPreview', { activities : self.activities })); }); }, /** * update counter based on activity status(created or Done) * @private * @param {Object} [data] key, value to decide activity created or deleted * @param {String} [data.type] notification type * @param {Boolean} [data.activity_deleted] when activity deleted * @param {Boolean} [data.activity_created] when activity created */ _updateCounter: function (data) { if (data) { if (data.activity_created) { this.activityCounter ++; } if (data.activity_deleted && this.activityCounter > 0) { this.activityCounter --; } this.$('.o_notification_counter').text(this.activityCounter); this.$el.toggleClass('o_no_notification', !this.activityCounter); } }, //------------------------------------------------------------ // Handlers //----------------------------------------------------------- /** * Redirect to particular model view * @private * @param {MouseEvent} event */ _onActivityFilterClick: function (event) { // fetch the data from the button otherwise fetch the ones from the parent (.o_mail_channel_preview). var data = _.extend({}, $(event.currentTarget).data(), $(event.target).data()); var context = {}; if (data.filter === 'my') { context['search_default_activities_overdue'] = 1; context['search_default_activities_today'] = 1; } else { context['search_default_activities_' + data.filter] = 1; } this.do_action({ type: 'ir.actions.act_window', name: data.model_name, res_model: data.res_model, views: [[false, 'kanban'], [false, 'form']], search_view_id: [false], domain: [['activity_user_id', '=', session.uid]], context:context, }); }, /** * When menu clicked update activity preview if counter updated * @private * @param {MouseEvent} event */ _onActivityMenuClick: function () { if (!this._isOpen()) { this._updateActivityPreview(); } }, }); SystrayMenu.Items.push(MessagingMenu); SystrayMenu.Items.push(ActivityMenu); // to test activity and messaging menus in qunit test cases we need it return { ActivityMenu: ActivityMenu, MessagingMenu: MessagingMenu, }; });
odoo.define('web.planner', function (require) { "use strict"; var core = require('web.core'); var Model = require('web.Model'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); var planner = require('web.planner.common'); var webclient = require('web.web_client'); var PlannerDialog = planner.PlannerDialog; var PlannerLauncher = Widget.extend({ template: "PlannerLauncher", init: function(parent) { this._super(parent); this.planner_by_menu = {}; this.need_reflow = false; }, start: function() { var self = this; core.bus.on("change_menu_section", self, self.on_menu_clicked); var res = self._super.apply(this, arguments).then(function() { self.$el.filter('.o_planner_systray').on('click', self, self.show_dialog.bind(self)); return self.fetch_application_planner(); }).then(function(apps) { self.do_hide(); // hidden by default self.planner_apps = apps; return apps; }); return res; }, fetch_application_planner: function() { var self = this; var def = $.Deferred(); if (!_.isEmpty(this.planner_by_menu)) { def.resolve(self.planner_by_menu); }else{ (new Model('web.planner')).query().all().then(function(res) { _.each(res, function(planner){ self.planner_by_menu[planner.menu_id[0]] = planner; self.planner_by_menu[planner.menu_id[0]].data = $.parseJSON(self.planner_by_menu[planner.menu_id[0]].data) || {}; }); def.resolve(self.planner_by_menu); }).fail(function() {def.reject();}); } return def; }, on_menu_clicked: function(menu_id) { if (_.contains(_.keys(this.planner_apps), menu_id.toString())) { this.setup(this.planner_apps[menu_id]); this.need_reflow = true; } else { this.do_hide(); this.need_reflow = true; } if (this.need_reflow) { core.bus.trigger('resize'); this.need_reflow = false; } if (this.dialog) { this.dialog.$el.modal('hide'); this.dialog.$el.detach(); } }, setup: function(planner) { var self = this; this.planner = planner; if (this.dialog) { this.dialog.$el.modal('hide'); this.dialog.destroy(); } this.dialog = new PlannerDialog(this, planner); this.dialog.appendTo($('<div>')); this.$(".progress").tooltip({html: true, title: this.planner.tooltip_planner, placement: 'bottom', delay: {'show': 500}}); this.dialog.on("planner_progress_changed", this, function(percent){ self.update_parent_progress_bar(percent); }); }, update_parent_progress_bar: function(percent) { if (percent == 100) { this.$(".progress").hide(); } else { this.$(".progress").show(); } this.do_show(); this.$(".progress-bar").css('width', percent+"%"); }, show_dialog: function() { this.dialog.$el.appendTo(webclient.$el); this.dialog.$el.modal('show'); } }); SystrayMenu.Items.push(PlannerLauncher); return { PlannerLauncher: PlannerLauncher, }; });
odoo.define('web.SwitchCompanyMenu', function(require) { "use strict"; /** * When Odoo is configured in multi-company mode, users should obviously be able * to switch their interface from one company to the other. This is the purpose * of this widget, by displaying a dropdown menu in the systray. */ var config = require('web.config'); var core = require('web.core'); var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); var _t = core._t; var SwitchCompanyMenu = Widget.extend({ template: 'SwitchCompanyMenu', events: { 'click .dropdown-item[data-menu]': '_onClick', }, /** * @override */ init: function () { this._super.apply(this, arguments); this.isMobile = config.device.isMobile; this._onClick = _.debounce(this._onClick, 1500, true); }, /** * @override */ willStart: function () { return session.user_companies ? this._super() : $.Deferred().reject(); }, /** * @override */ start: function () { var companiesList = ''; if (this.isMobile) { companiesList = '<li class="bg-info">' + _t('Tap on the list to change company') + '</li>'; } else { this.$('.oe_topbar_name').text(session.user_companies.current_company[1]); } _.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 mr8"></i>'; } else { a = '<span style="margin-right: 24px;"/>'; } companiesList += '<a href="#" class="dropdown-item" data-menu="company" data-company-id="' + company[0] + '">' + a + company[1] + '</a>'; }); this.$('.dropdown-menu').html(companiesList); return this._super(); }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * @private * @param {MouseEvent} ev */ _onClick: function (ev) { ev.preventDefault(); var companyID = $(ev.currentTarget).data('company-id'); this._rpc({ model: 'res.users', method: 'write', args: [[session.uid], {'company_id': companyID}], }) .then(function() { location.reload(); }); }, }); SystrayMenu.Items.push(SwitchCompanyMenu); return SwitchCompanyMenu; });
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'); 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; 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(); } }, }, 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'))); } 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; 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); }, /** * 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]] }); }); } }).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); }); 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; if (!this.view.get_selected_ids().length) { console.warn(_t("No metadata available")); return } 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++ }); } 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(); }, '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); }); }; }); }, }); 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); }); }); } return this._super.apply(this, arguments); }, }); } return DebugManager; });
odoo.define('mail.chat_backend', function (require) { "use strict"; var core = require('web.core'); var data = require('web.data'); var framework = require('web.framework'); var Model = require('web.Model'); var pyeval = require('web.pyeval'); var SystrayMenu = require('web.SystrayMenu'); var Widget = require('web.Widget'); var Dialog = require('web.Dialog'); var ControlPanelMixin = require('web.ControlPanelMixin'); var SearchView = require('web.SearchView'); var Sidebar = require('web.Sidebar'); var WebClient = require('web.WebClient'); var session = require('web.session'); var web_client = require('web.web_client'); var mail_chat_common = require('mail.chat_common'); var mail_thread = require('mail.thread'); var _t = core._t; var QWeb = core.qweb; var internal_bus = core.bus; /** * Widget handeling the channels, in the backend * * Responsible to listen the bus and apply action with the received message. Add layer to coordinate the * folded conversation and trigger event for the InstantMessagingView client action (using internal * comminication bus). It is a component of the WebClient. **/ var ConversationManagerBackend = mail_chat_common.ConversationManager.extend({ _setup: function(init_data){ var self = this; this._super.apply(this, arguments); _.each(init_data.notifications, function(n){ self.on_notification(n); }); }, // window title window_title_change: function() { this._super.apply(this, arguments); var title; if (this.get("waiting_messages") !== 0) { title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages")); } web_client.set_title_part("im_messages", title); }, // sessions and messages session_apply: function(active_session){ // for chat windows if(active_session.is_minimized || (!active_session.is_minimized && active_session.state === 'closed')){ this._super.apply(this, arguments); } // for client action if(!active_session.is_minimized){ // if not minimized, internal_bus.trigger('mail_session_receive', active_session); } }, message_receive: function(message) { var actual_channel_ids = _.map(_.keys(this.sessions), function(item){ return parseInt(item); }); var message_channel_ids = message.channel_ids; if(_.intersection(actual_channel_ids, message_channel_ids).length){ this._super.apply(this, arguments); } // broadcast the message to the NotificationButton and the InstantMessagingView internal_bus.trigger('mail_message_receive', message); // increment the needaction top counter if(message.needaction_partner_ids && _.contains(message.needaction_partner_ids, session.partner_id)){ internal_bus.trigger('mail_needaction_new', 1); } }, }); /** * Widget Minimized Conversation * * Add layer of WebClient integration, and user fold state handling (comminication with server) **/ mail_chat_common.Conversation.include({ session_fold: function(state){ var self = this; var args = arguments; var super_call = this._super; // broadcast the state changing return new Model("mail.channel").call("channel_fold", [], {"uuid" : this.get("session").uuid, "state" : state}).then(function(){ super_call.apply(self, args); }); }, }); /** * Widget : Patch for WebClient * * Create the conversation manager, and attach it to the web_client. **/ WebClient.include({ show_application: function(){ var self = this; var args = arguments; var super_call = this._super; this.mail_conversation_manager = new ConversationManagerBackend(this); this.mail_conversation_manager.start().then(function(){ super_call.apply(self, args); self.mail_conversation_manager.bus.start_polling(); }); }, }); /** * Widget Top Menu Notification Counter * * Counter of notification in the Systray Menu. Need to know if InstantMessagingView is displayed to * increment (or not) the counter. On click, should redirect to the client action. **/ var NotificationTopButton = Widget.extend({ template:'mail.chat.NotificationTopButton', events: { "click": "on_click", }, init: function(){ this._super.apply(this, arguments); this.set('counter', 0); }, willStart: function(){ var self = this; return $.when(session.rpc('/mail/needaction'), this._super()).then(function(needaction_count){ self.set('counter', needaction_count); }); }, start: function() { this.on("change:counter", this, this.on_change_counter); // events internal_bus.on('mail_needaction_new', this, this.counter_increment); internal_bus.on('mail_needaction_done', this, this.counter_decrement); return this._super(); }, counter_increment: function(inc){ this.set('counter', this.get('counter')+inc); }, counter_decrement: function(dec){ this.set('counter', this.get('counter')-dec); }, on_change_counter: function() { this.$('.fa-comment').html(this.get('counter') || ''); }, on_click: function(e){ e.preventDefault(); this.do_action({ type: 'ir.actions.client', tag: 'mail.chat.instant_messaging', params: { 'default_active_id': 'channel_inbox', }, }, { clear_breadcrumbs: true, }); }, }); SystrayMenu.Items.push(NotificationTopButton); /** * Abstract Class to 'Add More/Search' Widget * * Inputbox using jQueryUI autocomplete to fetch selection, like a Many2One field (on form view) * Used to create or pin a mail.channel or a res.partner on the InstantMessagingView **/ var AbstractAddMoreSearch = Widget.extend({ template: 'mail.chat.AbstractAddMoreSearch', events: { "click .o_mail_chat_add_more_text": "on_click_text", "blur .o_mail_chat_search_input": "_toggle_elements", }, init: function(parent, options){ this._super.apply(this, arguments); options = _.defaults(options || {}, { 'can_create': false, 'label': _t('+ Add More'), }); this.limit = 10; this.can_create = options.can_create; this.label = options.label; }, start: function(){ this.last_search_val = false; this.$input = this.$('.o_mail_chat_search_input'); this.$add_more_text = this.$('.o_mail_chat_add_more_text'); this.$add_more_search_bar = this.$('.o_mail_chat_add_more_search_bar'); this._bind_events(); return this._super(); }, _bind_events: function(){ // autocomplete var self = this; this.$input.autocomplete({ source: function(request, response) { self.last_search_val = request.term; self.do_search(request.term).done(function(result){ if(self.can_create){ result.push({ 'label': _.str.sprintf('<strong>'+_t("Create %s")+'</strong>', '<em>"'+self.last_search_val+'"</em>'), 'value': '_create', }); } response(result); }); }, select: function(event, ui) { self.on_click_item(ui.item); }, focus: function(event) { event.preventDefault(); }, html: true, }); }, // ui _toggle_elements: function(){ this.$add_more_text.toggle(); this.$add_more_search_bar.toggle(); this.$input.val(''); this.$input.focus(); }, on_click_text: function(event){ event.preventDefault(); this._toggle_elements(); }, // to be redefined do_search: function(){ return $.when(); }, on_click_item: function(item){ if(item.value === '_create'){ if(this.last_search_val){ this.trigger('item_create', this.last_search_val); } }else{ this.trigger('item_clicked', item); } }, }); var PartnerAddMoreSearch = AbstractAddMoreSearch.extend({ /** * Do the search call * @override */ do_search: function(search_val){ var Partner = new Model("res.partner"); return Partner.call('im_search', [search_val, this.limit]).then(function(result){ var values = []; _.each(result, function(user){ values.push(_.extend(user, { 'value': user.name, 'label': user.name, })); }); return values; }); }, }); var ChannelAddMoreSearch = AbstractAddMoreSearch.extend({ /** * Do the search call * @override */ do_search: function(search_val){ var Channel = new Model("mail.channel"); return Channel.call('channel_search_to_join', [search_val]).then(function(result){ var values = []; _.each(result, function(channel){ values.push(_.extend(channel, { 'value': channel.name, 'label': channel.name, })); }); return values; }); }, }); var PrivateGroupAddMoreSearch = AbstractAddMoreSearch.extend({ _bind_events: function(){ // don't call the super to avoid autocomplete this.$input.on('keyup', this, this.on_keydown); }, on_keydown: function(event){ if(event.which === $.ui.keyCode.ENTER && this.$input.val()){ this.trigger('item_create', this.$input.val()); } }, }); /** * Widget : Invite People to Channel Dialog * * Popup containing a 'many2many_tags' custom input to select multiple partners. * Search user according to the input, and trigger event when selection is validated. **/ var PartnerInviteDialog = Dialog.extend({ dialog_title: _t('Invite people'), template: "mail.chat.PartnerInviteDialog", init: function(parent, options){ options = _.defaults(options || {}, { buttons: [{ text: _t("Invite"), close: true, classes: "btn-primary", click: _.bind(this.on_click_add, this), }], channel: undefined, }); this._super.apply(this, arguments); this.set("partners", []); this.channel = options.channel; this.PartnersModel = new Model('res.partner'); this.ChannelModel = new Model('mail.channel'); this.limit = 20; }, start: function(){ var self = this; this.$partner_invite_input = this.$('.o_mail_chat_partner_invite_input'); this.$partner_invite_input.select2({ width: '100%', allowClear: true, multiple: true, formatResult: function(item){ if(item.im_status === 'online'){ return '<span class="fa fa-circle"> ' + item.text + '</span>'; } return '<span class="fa fa-circle-o"> ' + item.text + '</span>'; }, query: function (query) { self.PartnersModel.call('im_search', [query.term, self.limit]).then(function(result){ var data = []; _.each(result, function(partner){ partner.text = partner.name; data.push(partner); }); query.callback({results: data}); }); } }); return this._super.apply(this, arguments); }, on_click_add: function(){ var self = this; var data = this.$partner_invite_input.select2('data'); if(data.length >= 1){ return this.ChannelModel.call('channel_invite', [], {"ids" : [this.channel.id], 'partner_ids': _.pluck(data, 'id')}).then(function(){ var names = _.pluck(data, 'text').join(', '); self.do_notify(_t('New people'), _.str.sprintf(_t('You added <b>%s</b> to the conversation.'), names)); }); } }, }); /** * Client Action : Instant Messaging View, inspired by Slack.com * * Action replacing the Inbox, and the list of group (mailing list, multiple conversation, rooms, ...) * Includes real time messages (received and sent), creating group, channel, chat conversation, ... **/ var ChatMailThread = Widget.extend(mail_thread.MailThreadMixin, ControlPanelMixin, { template: 'mail.chat.ChatMailThread', events: { "click .o_mail_thread_show_more > button": "message_load_history", // events from MailThreadMixin "click .o_mail_redirect": "on_click_redirect", "click .o_mail_thread_message_star": "on_message_star", // events specific for ChatMailThread "click .o_mail_chat_channel_item": "on_click_channel", "click .o_mail_chat_partner_item": "on_click_partner", "click .o_mail_chat_partner_unpin": "on_click_partner_unpin", "click .o_mail_thread_message_needaction": "on_message_needaction", }, init: function (parent, action) { this._super.apply(this, arguments); // attributes this.action_manager = parent; this.help_message = action.help || ''; this.context = action.context; this.action = action; this.chatter_needaction_auto = false; this.options = this.action.params; // mail thread mixin mail_thread.MailThreadMixin.init.call(this); // components : conversation manager and search widget (channel_type + '_search_widget') this.conv_manager = web_client.mail_conversation_manager; this.channel_search_widget = new ChannelAddMoreSearch(this, {'label': _t('+ Subscribe'), 'can_create': true}); this.group_search_widget = new PrivateGroupAddMoreSearch(this, {'label': _t('+ New private group'), 'can_create': true}); this.partner_search_widget = new PartnerAddMoreSearch(this); // options (update the default of MailThreadMixin) this.options = _.extend(this.options, { 'display_document_link': true, 'display_needaction_button': true, 'emoji_list': this.conv_manager.emoji_list, 'default_username': _t('Anonymous'), }); this.emoji_set_substitution(this.conv_manager.emoji_list); // channel business this.channels = {}; this.mapping = {}; // mapping partner_id/channel_id for 'direct message' channel this.set('current_channel_id', false); this.set('needaction_inbox_counter', 0); this.search_domain = []; // channel slots this.set('channel_channel', []); this.set('channel_direct_message', []); this.set('channel_private_group', []); this.set('partners', []); // models this.ChannelModel = new Model('mail.channel', this.context); // control panel items this.control_elements = {}; }, willStart: function(){ var self = this; return session.rpc('/mail/client_action').then(function(result){ self.chatter_needaction_auto = result.chatter_needaction_auto; self.set('needaction_inbox_counter', result.needaction_inbox_counter); self.set('partners', result.channel_slots.partners); self.mapping = result.channel_slots.mapping; self._channel_slot(_.omit(result.channel_slots, 'partners', 'mapping')); }); }, start: function(){ var self = this; this._super.apply(this, arguments); this.$messages = this.$('.o_mail_chat_messages'); mail_thread.MailThreadMixin.start.call(this); // channel business events this.on("change:current_channel_id", this, this.channel_change); this.on("change:channel_channel", this, function(){ self.channel_render('channel_channel'); }); this.on("change:channel_private_group", this, function(){ self.channel_render('channel_private_group'); }); this.on("change:partners", this, this.partner_render); // search widget for channel this.channel_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_channel_channel')); this.channel_search_widget.on('item_create', this, function(name){ self.channel_create(name, 'public'); }); this.channel_search_widget.on('item_clicked', this, function(item){ self.channel_join_and_get_info(item.id).then(function(channel){ self.channel_apply(channel); }); }); // search widget for direct message this.partner_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_partners')); this.partner_search_widget.on('item_clicked', this, function(item){ self.channel_get([item.id]); self.partner_add(item); }); // search widget for private group this.group_search_widget.insertAfter(this.$('.o_mail_chat_channel_slot_channel_private_group')); this.group_search_widget.on('item_create', this, function(name){ self.channel_create(name, 'private'); }); // needaction inbox counter this.on('change:needaction_inbox_counter', this, this.needaction_inbox_change); return $.when(this._super.apply(this, arguments), this.cp_render_searchview()).then(function(){ // render control panel's elements self.cp_render_elements(); // apply default channel var channel_id = self.context.active_id || self.action.params.default_active_id || 'channel_inbox'; if(!_.isString(channel_id)){ if(_.contains(_.keys(self.channels), channel_id)){ self.set('current_channel_id', channel_id); }else{ self.channel_info(channel_id).then(function(channel){ self.channel_apply(channel); }); } }else{ self.set('current_channel_id', channel_id); } // internal communication (bind here to avoid receving message from bus when client action still not totally ready) internal_bus.on('mail_message_receive', self, self.message_receive); internal_bus.on('mail_session_receive', self, self.channel_receive); // needaction : the inbox has the same counter as the needaction top counter internal_bus.on('mail_needaction_new', this, function(inc){ self.set('needaction_inbox_counter' , self.get('needaction_inbox_counter') + inc); }); internal_bus.on('mail_needaction_done', this, function(dec){ self.set('needaction_inbox_counter' , self.get('needaction_inbox_counter') - dec); }); }); }, // event actions on_click_channel: function(event){ event.preventDefault(); var channel_id = this.$(event.currentTarget).data('channel-id'); this.set('current_channel_id', channel_id); }, on_click_partner: function(event){ if(!this.$(event.target).hasClass('o_mail_chat_channel_unpin')){ event.preventDefault(); var partner_id = this.$(event.currentTarget).data('partner-id'); if(this.mapping[partner_id]){ // don't fetch if channel already in local this.set('current_channel_id', this.mapping[partner_id]); }else{ this.channel_get([partner_id]); } } }, on_click_partner_unpin: function(event){ event.preventDefault(); var self = this; var $source = this.$(event.currentTarget); var partner_id = $source.data('partner-id'); var channel_id = this.mapping[partner_id]; var channel = this.channels[channel_id]; this.channel_pin(channel.uuid, false).then(function(){ self.set('partners', _.filter(self.get('partners'), function(p){ return p.id !== partner_id; })); self.channel_remove(channel_id); delete self.mapping[partner_id]; // if unpin current channel, switch to inbox if(self.get('current_channel_id') === channel_id){ self.set('current_channel_id', 'channel_inbox'); } }); }, on_click_button_minimize: function(event){ event.preventDefault(); var current_channel = this.channels[this.get('current_channel_id')]; if(current_channel){ var channel_uuid = current_channel.uuid; return this.ChannelModel.call("channel_minimize", [channel_uuid, true]); } return $.Deferred().reject(); }, on_click_button_invite: function(){ new PartnerInviteDialog(this, { title: _.str.sprintf(_t('Invite people to %s'), this.get_current_channel_name()), channel: this.channels[this.get('current_channel_id')], }).open(); }, on_click_button_unsubscribe: function(){ var self = this; this.ChannelModel.call('action_unfollow', [[this.get('current_channel_id')]]).then(function(){ var channel = self.channels[self.get('current_channel_id')]; var slot = self.get_channel_slot(channel); self.set(slot, _.filter(self.get(slot), function(c){ return c.id !== channel.id; })); self.do_notify(_t('Unsubscribe'), _.str.sprintf(_t('You unsubscribe from <b>%s</b>.'), self.get_current_channel_name())); self.set('current_channel_id', 'channel_inbox'); // jump to inbox }); }, on_click_button_settings: function(){ this.do_action({ type: 'ir.actions.act_window', res_model: "mail.channel", res_id: this.get('current_channel_id'), views: [[false, 'form']], target: 'current' }, { 'on_reverse_breadcrumb': this.on_reverse_breadcrumb, }); }, on_message_needaction: function(event){ var self = this; mail_thread.MailThreadMixin.on_message_needaction.call(this, event).then(function(message_id){ // decrement the needaction top counter internal_bus.trigger('mail_needaction_done', 1); // decrement the channel labels var treated_messages = _.filter(self.get('messages'), function(m){ return m.id == message_id; }); if(treated_messages.length){ self.needaction_decrement(treated_messages[0].channel_ids || []); } }); }, // control panel cp_update: function(){ // toggle cp elements var current_channel_id = this.get('current_channel_id'); if(_.isString(current_channel_id)){ this.control_elements.$buttons.hide(); this.control_elements.$sidebar.hide(); }else{ this.control_elements.$buttons.show(); if(this.channels[current_channel_id].channel_type === 'chat'){ this.control_elements.$buttons.find('.o_mail_chat_button_invite').hide(); this.control_elements.$sidebar.hide(); } else { this.control_elements.$buttons.find('.o_mail_chat_button_invite').show(); this.control_elements.$sidebar.show(); } } // update control panel var status = { breadcrumbs: this.action_manager.get_breadcrumbs(), cp_content: { $buttons: this.control_elements.$buttons, $searchview: this.control_elements.$searchview, $searchview_buttons: this.control_elements.$searchview_buttons, $sidebar: this.control_elements.$sidebar, }, searchview: this.searchview, }; this.update_control_panel(status); }, cp_render_elements: function() { this.control_elements.$buttons = $(QWeb.render("mail.chat.ControlButtons", {})); this.control_elements.$buttons.on('click', '.o_mail_chat_button_invite', _.bind(this.on_click_button_invite, this)); this.control_elements.$buttons.on('click', '.o_mail_chat_button_minimize', _.bind(this.on_click_button_minimize, this)); var $sidebar_container = $('<div>'); new Sidebar(this, { sections: [ {name: 'more', label: _t('More')}, ], items: { more: [ {label: _t('Unsubscribe'), classname: 'o_mail_chat_button_unsubscribe', callback: this.on_click_button_unsubscribe}, {label: _t('Settings'), classname: 'o_mail_chat_button_settings', callback: this.on_click_button_settings}, ], }, }).appendTo($sidebar_container); this.control_elements.$sidebar = $sidebar_container.contents(); }, cp_render_searchview: function(){ var self = this; var options = { $buttons: $("<div>"), action: this.action, disable_groupby: true, }; var view_id = (this.action && this.action.search_view_id && this.action.search_view_id[0]) || false; this.searchview = new SearchView(this, this.MessageDatasetSearch, view_id, {}, options); this.searchview.on('search_data', this, this.on_search); return $.when(this.searchview.appendTo($("<div>"))).done(function() { self.control_elements.$searchview = self.searchview.$el; self.control_elements.$searchview_buttons = self.searchview.$buttons.contents(); }); }, /** * Method call when action if done after redirecting (using 'on_click_redirect') * @override */ on_reverse_breadcrumb: function(){ var current_channel_id = this.get('current_channel_id'); this.cp_update(); // do not reload the client action, just display it, but a refresh of the control panel is needed. // push state this.action_manager.do_push_state({ action: this.action.id, active_id: current_channel_id, }); }, // messages search methods on_search: function(domains, contexts, groupbys){ var self = this; return pyeval.eval_domains_and_contexts({ domains: [[]].concat(domains || []), contexts: [this.context].concat(contexts || []), group_by_seq: groupbys || [] }).done(function(results){ if (results.error) { throw new Error(_.str.sprintf(_t("Failed to evaluate search criterions")+": \n%s", JSON.stringify(results.error))); } // modify the search domain and do search self.search_domain = results.domain; return self.message_load_new(); }); }, // channels /** * Set the channel slot * @param {Object[]} fetch_result : should contains only the slot name (as key) and the list of channel header as value. */ _channel_slot: function(fetch_result){ var self = this; var channel_slots = _.keys(fetch_result); _.each(channel_slots, function(slot){ // update the channel slot self.set(slot, fetch_result[slot]); // flatten the result : update the complete channel list _.each(fetch_result[slot], function(channel){ self.channels[channel.id] = channel; }); }); }, /** * Apply a channel means adding it, and swith to it * @param {Object} channel : channel header */ channel_apply: function(channel){ this.channel_add(channel); this.set('current_channel_id', channel.id); }, /** * Add the given channel, or update it if already exists and loaded * @param {Object} channel : channel header to add */ channel_add: function(channel){ var channel_slot = this.get_channel_slot(channel); var existing = this.get(channel_slot); if(_.contains(_.pluck(existing, 'id'), channel.id)){ // update the old channel var filtered_channels = _.filter(existing, function(item){ return item.id != channel.id; }); this.set(channel_slot, filtered_channels.concat([channel])); }else{ // simply add the reveiced channel this.set(channel_slot, existing.concat([channel])); } // also update the flatten list this.channels[channel.id] = channel; // update the mapping for 'direct message' channel, and the partner list if(channel_slot === 'channel_direct_message'){ var partner = channel.direct_partner[0]; this.mapping[partner.id] = channel.id; this.partner_add(partner); } }, channel_remove: function(channel_id){ var channel = this.channels[channel_id]; var slot = this.get_channel_slot(channel); this.set(slot, _.filter(this.get(slot), function(c){ return c.id !== channel_id; })); delete this.channels[channel_id]; }, /** * Get the channel the current user has with the given partner, and get the channel header * @param {Number[]} partner_ids : list of res.partner identifier */ channel_get: function(partner_ids){ var self = this; return this.ChannelModel.call('channel_get', [partner_ids]).then(function(channel){ self.channel_apply(channel); }); }, /** * Create a channel with the given name and type, and apply it * @param {String} channel_name : the name of the channel * @param {String} privacy : the privacy of the channel (groups, public, ...) */ channel_create: function(channel_name, privacy){ var self = this; return this.ChannelModel.call('channel_create', [channel_name, privacy]).then(function(channel){ self.channel_apply(channel); }); }, channel_info: function(channel_id){ return this.ChannelModel.call('channel_info', [[channel_id]]).then(function(channels){ return channels[0]; }); }, channel_pin: function(uuid, pinned){ return this.ChannelModel.call('channel_pin', [uuid, pinned]); }, channel_join_and_get_info: function(channel_id){ return this.ChannelModel.call('channel_join_and_get_info', [[channel_id]]); }, channel_change: function(){ var self = this; var current_channel_id = this.get('current_channel_id'); // mail chat compose message (destroy and replace it) if (this.mail_chat_compose_message) { this.mail_chat_compose_message.destroy(); } this.mail_chat_compose_message = new mail_thread.MailComposeMessage(this, new data.DataSetSearch(this, 'mail.channel', this.context), { 'emoji_list': this.options.emoji_list, 'context': _.extend(this.context, { 'default_res_id': current_channel_id, }), 'display_mode': 'chat', }); if(_.isString(current_channel_id)){ this.$('.o_mail_chat_composer_wrapper').hide(); }else{ this.$('.o_mail_chat_composer_wrapper').show(); } this.mail_chat_compose_message.appendTo(this.$('.o_mail_chat_composer_wrapper')); this.mail_chat_compose_message.focus(); // push state (the action is referred by action_manager, and no reloaded when jumping // channel, so update context is required) this.action.context.active_id = current_channel_id; this.action.context.active_ids = [current_channel_id]; web_client.action_manager.do_push_state({ action: this.action.id, active_id: current_channel_id, }); // update title (to display in the breadcrumbs) according to current channel this.set('title', this.get_current_channel_name()); // update control panel this.cp_update(); // fetch the messages return this.message_load_new().then(function(){ // if normal channel, update last message id, and unbold var def = $.Deferred().resolve(); if(_.isNumber(current_channel_id)){ def = self.ChannelModel.call('channel_seen', [[current_channel_id]]); }else{ // auto treat needaction from inbox if(current_channel_id === 'channel_inbox' && self.chatter_needaction_auto){ def = self.MessageDatasetSearch._model.query(['id', 'channel_ids']).filter([['needaction', '=', true]]).all().then(function(messages){ // get needaction message ids var message_ids = _.pluck(messages, 'id'); if(message_ids){ // call to unlink self.MessageDatasetSearch._model.call('set_message_done',[message_ids]).then(function(){ // decrement channel items in batch var flatten_channel_ids = _.flatten(_.pluck(messages, 'channel_ids')); var channel_count = _.countBy(flatten_channel_ids, function(cid) { return cid; }); _.each(_.keys(channel_count), function(cid){ self.needaction_decrement([cid], channel_count[cid]); }); // decrement the needaction top counter internal_bus.trigger('mail_needaction_done', message_ids.length); }); } }); } } def.then(function(){ self._toggle_unread_message(current_channel_id, false, true); self._scroll(); }); }); }, channel_render: function(channel_slot){ this.$('.o_mail_chat_channel_slot_' + channel_slot).replaceWith(QWeb.render("mail.chat.ChatMailThread.channels", {'widget': this, 'channel_slot': channel_slot})); }, // partners partner_add: function(partner){ var partners = _.filter(this.get('partners'), function(p){ return p.id != partner.id; }); this.set('partners', partners.concat([partner])); }, partner_render: function(){ this.$('.o_mail_chat_channel_slot_partners').replaceWith(QWeb.render("mail.chat.ChatMailThread.partners", {'widget': this})); }, // needaction needaction_increment: function(channel_ids, inc){ var self = this; _.each(channel_ids, function(channel_id){ if(self.channels[channel_id]){ var count = (self.channels[channel_id].message_needaction_counter || 0) + (inc || 1); self.channels[channel_id].message_needaction_counter = count; self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').html(count); self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').show(); } }); }, needaction_decrement: function(channel_ids, dec){ var self = this; _.each(channel_ids, function(channel_id){ if(self.channels[channel_id]){ var count = (self.channels[channel_id].message_needaction_counter || 0) - (dec || 1); self.channels[channel_id].message_needaction_counter = count; self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').html(count); if(count <= 0){ self.$('.o_mail_chat_needaction[data-channel-id='+channel_id+']').show(); } } }); }, needaction_inbox_change: function(){ var count = this.get('needaction_inbox_counter'); var $elem = this.$('.o_mail_chat_needaction[data-channel-id="channel_inbox"]'); $elem.html(count); if(count > 0){ $elem.show(); }else{ $elem.hide(); } }, // from bus channel_receive: function(channel){ this.channel_add(channel); }, message_receive: function(message){ var self = this; var in_current_channel = _.contains(message.channel_ids, this.get('current_channel_id')); // if current channel should reveice message, give it to it if(in_current_channel){ this.message_insert([message]); } // for other message channel, get the channel if not loaded yet, and bolded them var other_message_channel_ids = _.without(message.channel_ids, this.get('current_channel_id')); var active_channel_ids = _.map(_.keys(this.channels), function(cid){ return parseInt(cid); }); // integer as key of a dict is cast as string in javascript var channel_to_fetch = _.difference(other_message_channel_ids, active_channel_ids); // fetch unloaded channels and add it var def = $.Deferred(); if(channel_to_fetch.length >= 1){ def = this.ChannelModel.call("channel_info", [], {"ids" : channel_to_fetch}).then(function(channels){ _.each(channels, function(channel){ self.channel_add(channel); }); }); }else{ def.resolve([]); } // bold the channel to indicate unread messages def.then(function(){ // bold channel having unread messages _.each(other_message_channel_ids, function(channel_id){ self._toggle_unread_message(channel_id, true); }); // auto scroll to bottom if the message arrived in the current channel if(in_current_channel){ self._scroll(); } }); // if needaction, then increment the label if(message.needaction_partner_ids && _.contains(message.needaction_partner_ids, session.partner_id)){ this.needaction_increment(message.channel_ids || []); } }, // utils function get_channel_slot: function(channel){ if(channel.channel_type === 'channel'){ if(channel.public === 'private'){ return 'channel_private_group'; } return 'channel_channel'; } if(channel.channel_type === 'chat'){ return 'channel_direct_message'; } }, get_current_channel_name: function(){ var current_channel_id = this.get('current_channel_id'); var current_channel = this.channels[current_channel_id]; var current_channel_name = current_channel && current_channel.name || _t('Unknown'); // virtual channel id (for inbox, or starred channel) if(_.isString(current_channel_id)){ if(current_channel_id == 'channel_inbox'){ current_channel_name = _t('Inbox'); } if(current_channel_id == 'channel_starred'){ current_channel_name = _t('Starred'); } } return current_channel_name; }, _toggle_unread_message: function(channel_id, add_or_remove, set_as_active){ var partner_id = false; var inverse_mapping = _.invert(this.mapping); // toggle bold channel/partner item this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item[data-channel-id="'+channel_id+'"]').toggleClass('o_mail_chat_channel_unread', add_or_remove); if(_.contains(_.keys(inverse_mapping), channel_id.toString())){ partner_id = parseInt(inverse_mapping[channel_id]); this.$('.o_mail_chat_sidebar .o_mail_chat_partner_item[data-partner-id="'+partner_id+'"]').toggleClass('o_mail_chat_channel_unread', add_or_remove); } // set the channel/partner item as the active one if(set_as_active){ this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item, .o_mail_chat_sidebar .o_mail_chat_partner_item').find('a').removeClass('active'); this.$('.o_mail_chat_sidebar .o_mail_chat_channel_item[data-channel-id="'+channel_id+'"]').find('a').addClass('active'); if(partner_id){ this.$('.o_mail_chat_sidebar .o_mail_chat_partner_item[data-partner-id="'+partner_id+'"]').find('a').addClass('active'); } } }, _scroll: function(){ var current_channel_id = this.get('current_channel_id'); var current_channel = this.channels[current_channel_id]; var last_seen_message; if(_.isNumber(current_channel_id)){ if(current_channel.seen_message_id){ last_seen_message = this.$(".o_mail_thread_message[data-message-id="+current_channel.seen_message_id+"]"); if(last_seen_message.length){ this.$messages.scrollTop(last_seen_message.offset().top); }else{ this.$messages.scrollTop(0); } }else{ this.$messages.scrollTop(this.$messages[0].scrollHeight); } current_channel.seen_message_id = false; }else{ this.$messages.scrollTop(this.$messages[0].scrollHeight); } }, /** * Render the messages * @override */ message_render: function(){ this.$messages.html(QWeb.render('mail.chat.ChatMailThread.content', {'widget': this})); }, /** * Return the message domain for the current channel * @override */ get_message_domain: function(){ // default channel domain var current_channel_id = this.get('current_channel_id'); var domain = [['channel_ids', 'in', current_channel_id]]; // virtual channel id (for inbox, or starred channel) if(_.isString(current_channel_id)){ if(current_channel_id == 'channel_inbox'){ domain = [['needaction', '=', true]]; } if(current_channel_id == 'channel_starred'){ domain = [['starred', '=', true]]; } } // add search domain domain = domain.concat(this.search_domain); return domain; }, /** * Extend message_load_history to keep scroll position at last seen message */ message_load_history: function() { var self = this; var oldest_msg_selector = '.o_mail_thread_message[data-message-id="' + this.get('messages')[0].id + '"]'; var offset = -framework.getPosition(document.querySelector(oldest_msg_selector)).top; mail_thread.MailThreadMixin.message_load_history.call(this).then(function() { offset += framework.getPosition(document.querySelector(oldest_msg_selector)).top; self.$messages.scrollTop(offset); }); }, }); core.action_registry.add('mail.chat.instant_messaging', ChatMailThread); return { ChatMailThread: ChatMailThread, }; });
ecore.define('web.Menu', function (require) { "use strict"; var core = require('web.core'); var Widget = require('web.Widget'); var SystrayMenu = require('web.SystrayMenu'); var UserMenu = require('web.UserMenu'); SystrayMenu.Items.push(UserMenu); var QWeb = core.qweb; var Menu = Widget.extend({ template: 'Menu', events: { 'click .o_menu_toggle': function (ev) { ev.preventDefault(); this.trigger_up((this.appswitcher_displayed)? 'hide_app_switcher' : 'show_app_switcher'); }, 'mouseover .o_menu_sections > li:not(.open)': function(e) { var $opened = this.$('.o_menu_sections > li.open'); if($opened.length) { $opened.removeClass('open'); $(e.currentTarget).addClass('open').find('> a').focus(); } }, }, init: function (parent, menu_data) { var self = this; this._super.apply(this, arguments); this.appswitcher_displayed = true; this.backbutton_displayed = false; this.$menu_sections = {}; this.menu_data = menu_data; // Prepare navbar's menus var $menu_sections = $(QWeb.render('Menu.sections', {'menu_data': this.menu_data})); $menu_sections.filter('section').each(function () { self.$menu_sections[parseInt(this.className, 10)] = $(this).children('li'); }); // Bus event core.bus.on('change_menu_section', this, this.change_menu_section); }, start: function () { var self = this; this.$menu_toggle = this.$('.o_menu_toggle'); this.$menu_brand_placeholder = this.$('.o_menu_brand'); this.$section_placeholder = this.$('.o_menu_sections'); core.bus.on('keyup', this, this._hide_app_switcher); // Navbar's menus event handlers var menu_ids = _.keys(this.$menu_sections); var primary_menu_id, $section; for(var i = 0; i < menu_ids.length; i++) { primary_menu_id = menu_ids[i]; $section = this.$menu_sections[primary_menu_id]; $section.on('click', 'a[data-menu]', self, function (ev) { ev.preventDefault(); var menu_id = $(ev.currentTarget).data('menu'); var action_id = $(ev.currentTarget).data('action-id'); self._on_secondary_menu_click(menu_id, action_id); }); }; // Systray Menu this.systray_menu = new SystrayMenu(this); this.systray_menu.attachTo(this.$('.oe_systray')); return this._super.apply(this, arguments); }, destroy: function () { this._super.apply(this, arguments); core.bus.off('keyup', this, this._hide_app_switcher); }, _hide_app_switcher: function (ev) { if (ev.keyCode === $.ui.keyCode.ESCAPE && this.backbutton_displayed) { this.trigger_up('hide_app_switcher'); } }, toggle_mode: function (appswitcher, overapp) { this.appswitcher_displayed = !!appswitcher; this.backbutton_displayed = this.appswitcher_displayed && !!overapp; this.$menu_toggle.find('i').toggleClass('fa-chevron-left', this.appswitcher_displayed) .toggleClass('fa-th', !this.appswitcher_displayed); this.$menu_toggle.toggleClass('hidden', this.appswitcher_displayed && !this.backbutton_displayed); this.$menu_brand_placeholder.toggleClass('hidden', this.appswitcher_displayed); this.$section_placeholder.toggleClass('hidden', this.appswitcher_displayed); }, change_menu_section: function (primary_menu_id) { if (!this.$menu_sections[primary_menu_id]) { return; // unknown menu_id } if (this.current_primary_menu) { this.$menu_sections[this.current_primary_menu].detach(); } // Get back the application name for (var i = 0; i < this.menu_data.children.length; i++) { if (this.menu_data.children[i].id === primary_menu_id) { this.$menu_brand_placeholder.text(this.menu_data.children[i].name); break; } } this.$menu_sections[primary_menu_id].appendTo(this.$section_placeholder); this.current_primary_menu = primary_menu_id; }, _trigger_menu_clicked: function(menu_id, action_id) { this.trigger_up('menu_clicked', { id: menu_id, action_id: action_id, previous_menu_id: this.current_secondary_menu || this.current_primary_menu, }); }, _on_secondary_menu_click: function(menu_id, action_id) { var self = this; // It is still possible that we don't have an action_id (for example, menu toggler) if (action_id) { self._trigger_menu_clicked(menu_id, action_id); this.current_secondary_menu = menu_id; } }, /** * Helpers used by web_client in order to restore the state from * an url (by restore, read re-synchronize menu and action manager) */ action_id_to_primary_menu_id: function (action_id) { var primary_menu_id, found; for (var i = 0; i < this.menu_data.children.length && !primary_menu_id; i++) { found = this._action_id_in_subtree(this.menu_data.children[i], action_id); if (found) { primary_menu_id = this.menu_data.children[i].id; } } return primary_menu_id; }, _action_id_in_subtree: function (root, action_id) { if (root.action && root.action.split(',')[1] == action_id) { return true; } var found; for (var i = 0; i < root.children.length && !found; i++) { found = this._action_id_in_subtree(root.children[i], action_id); } return found; }, menu_id_to_action_id: function (menu_id, root) { if (!root) {root = $.extend(true, {}, this.menu_data)} if (root.id == menu_id) { return root.action.split(',')[1] ; } for (var i = 0; i < root.children.length; i++) { var action_id = this.menu_id_to_action_id(menu_id, root.children[i]); if (action_id !== undefined) { return action_id; } } return undefined; }, }); return Menu; });