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);

});
Exemple #2
0
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;
}

});
Exemple #3
0
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;

});
Exemple #5
0
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,
};
});
Exemple #6
0
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;

});
Exemple #7
0
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);
});
Exemple #8
0
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,
};

});
Exemple #12
0
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);

});
Exemple #13
0
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;

});
Exemple #16
0
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;

});
Exemple #17
0
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,
};

});
Exemple #18
0
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;
});