From c24332e6da68dbed28ea8ab4702a97c49860d374 Mon Sep 17 00:00:00 2001 From: Valerie Pirenne Date: Wed, 29 Apr 2015 17:32:49 +0200 Subject: [PATCH] [IMP][ADD] mail, various: timeline view. This commit introduces a new view type, the timeline view. This view is intended to display messages like the previous chatter. This is now a view like the form or list view. Like other views it will be possible to have custom templates for some specific needs, allowing customization. [IMP] mail: the value tracking is modified. Previously messages were created containing the modified values. Those values are now stored, using a new model mail.tracking.value. The message body is dynamically build based on the values. Tests have been updated. This version is temporary. Indeed two main modifications will come in a short future : - the new design will improve the display - the slack mode will change the way the Inbox and notifications are managed All glory to the Hypnotoad. Special thanks to Valerie Pirenne (vpi), Jerome Maes (jem) and Richat Mathot (rim) that did not code but said a lot of things. Martin Trigaux (mat) did nothing, a bit like for the slides modules, but he is busy sending emails. --- .../static/src/css/gamification.css | 295 +- .../static/src/css/gamification.sass | 49 +- .../static/src/js/gamification.js | 4 +- .../static/src/xml/gamification.xml | 4 +- addons/hr/mail_hr_view.xml | 31 +- addons/hr/static/src/js/suggestions.js | 2 +- addons/mail/__openerp__.py | 1 + addons/mail/models/__init__.py | 1 + addons/mail/models/mail_group.py | 46 +- addons/mail/models/mail_message.py | 263 +- addons/mail/models/mail_thread.py | 54 +- addons/mail/models/mail_tracking_value.py | 103 + addons/mail/security/ir.model.access.csv | 4 + addons/mail/static/src/css/mail.css | 1845 ++++++---- addons/mail/static/src/js/mail.js | 3042 +++++++---------- addons/mail/static/src/js/mail_followers.js | 247 +- addons/mail/static/src/js/mail_utils.js | 17 + addons/mail/static/src/js/suggestions.js | 6 +- addons/mail/static/src/xml/mail.xml | 571 ++-- addons/mail/static/src/xml/mail_followers.xml | 133 +- addons/mail/static/src/xml/suggestions.xml | 16 +- addons/mail/tests/test_mail_features.py | 18 +- addons/mail/tests/test_message_read.py | 136 +- addons/mail/tests/test_message_track.py | 139 +- addons/mail/views/mail_group_views.xml | 15 +- addons/mail/views/mail_message_views.xml | 284 ++ addons/mail/views/mail_thread_views.xml | 199 +- addons/mail/views/mail_tracking_views.xml | 67 + addons/mail_tip/static/src/js/mail_tip.js | 2 +- addons/portal/portal_view.xml | 104 +- addons/portal/tests/test_portal.py | 6 +- addons/share/static/src/js/share.js | 2 +- addons/web/static/src/css/base.css | 1 + addons/web/static/src/css/base.sass | 1 + openerp/addons/base/ir/ir_ui_view.py | 1 + openerp/addons/base/rng/view.rng | 15 + 36 files changed, 3900 insertions(+), 3824 deletions(-) create mode 100644 addons/mail/models/mail_tracking_value.py create mode 100644 addons/mail/views/mail_tracking_views.xml diff --git a/addons/gamification/static/src/css/gamification.css b/addons/gamification/static/src/css/gamification.css index b21e4e4e010..a2c416ffc2d 100644 --- a/addons/gamification/static/src/css/gamification.css +++ b/addons/gamification/static/src/css/gamification.css @@ -1,185 +1,146 @@ @charset "UTF-8"; .openerp .oe_kanban_view .oe_kanban_card.oe_kanban_goal { width: 230px; - min-height: 200px; -} + min-height: 200px; } +.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_badge { + width: 250px; + min-height: 150px; } +.openerp .oe_kanban_badge_avatars { + margin-top: 8px; } + .openerp .oe_kanban_badge_avatars img { + width: 30px; + height: 30px; + padding-left: 0; + margin-top: 3px; + border-radius: 2px; + -box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } +.openerp .oe_margin_top_8 { + margin-top: 8px; } +.openerp .oe_margin_top_16 { + margin-top: 16px; } .openerp .oe_kanban_goal .oe_goal_state_block { width: 200px; height: 130px; margin: auto; - margin-bottom: -20px; -} -.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state { - font-size: 2.5em; - font-weight: bold; - padding-top: 30px; -} -.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state .fa { - font-size: 3em; - margin-top: -17px; - margin-left: 17px; -} + margin-bottom: -20px; } + .openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state { + font-size: 2.5em; + font-weight: bold; + padding-top: 30px; } + .openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state .fa { + font-size: 3em; + margin-top: -17px; + margin-left: 17px; } .openerp .oe_kanban_goal .oe_goal_state_block, .openerp .oe_kanban_goal p { - text-align: center; -} - -.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_badge { - width: 250px; - min-height: 150px; -} -.openerp .oe_kanban_badge_avatars { - width: 30px; -} -.openerp .oe_margin_top_8 { - margin-top: 8px; -} -.openerp .oe_margin_top_16 { - margin-top: 16px; -} + text-align: center; } +.openerp .oe_kanban_content .oe_goal_gauge:first-child { + margin: auto; } +.openerp .oe_kanban_content .oe_goal_gauge svg { + margin-top: -20px; } .openerp .oe_no_overflow { - overflow: hidden; -} + overflow: hidden; } .openerp .oe_red { - color: red; -} + color: red; } .openerp .oe_green { - color: green; -} + color: green; } .openerp .oe_orange { - color: orange; -} + color: orange; } .openerp .oe_form td .oe_no_padding { - margin-left: -6px; -} -.openerp .oe_mail_wall .oe_mail_wall_aside { - margin-top: 15px; - position: relative; - display: inline-block; - vertical-align: top; - width: 280px; - border-radius: 2px; -} -.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_challenge_list { - background-color: #ededf6; -} -.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_suggestion { - background-color: #d3def1; -} -.openerp .oe_mail_wall .oe_mail_wall_aside .oe_gamification_suggestion ul { - padding-left: 15px; -} -.openerp .oe_mail_wall .oe_mail_wall_aside h4, .openerp .oe_mail_wall .oe_mail_wall_aside .oe_goals_list .oe_thead { - text-align: center; - padding-bottom: 15px; -} -.openerp .oe_mail_wall .oe_mail_wall_aside > div { - border-bottom: solid 5px white; - border-radius: 2px; -} -.openerp .oe_mail_wall .oe_goal { - border-bottom: solid 3px white; - padding: 5px 10px; -} -.openerp .oe_mail_wall .oe_goal .oe_update_challenge.fa, .openerp .oe_mail_wall .oe_goal .oe_goal_action.fa { - visibility: hidden; - font-size: 16px; - float: right; - position: relative; -} -.openerp .oe_mail_wall .oe_goal div:hover > .oe_update_challenge, .openerp .oe_mail_wall .oe_goal div:hover > .oe_goal_action, .openerp .oe_mail_wall .oe_goal th:hover > .oe_goal_action { - visibility: visible; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list { - padding-left: 0; - margin-top: 5px; - margin-bottom: 10px; - width: 100%; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress div { - display: inline-block; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress::before { - content: "•"; - padding-right: 4px; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached::before { - content: "✓"; - padding-right: 0; - margin-left: -2px; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached .oe_cell { - text-decoration: line-through; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_cell.oe_goal_current { - font-size: 150%; - font-weight: bold; - min-width: 50px; - padding: 0 5px; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_outer_box { - display: inline-block; - position: relative; - z-index: 0; - vertical-align: middle; - width: 100%; - border: solid 1px rgba(0, 0, 0, 0.03); - border-radius: 2px; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_outer_box.oe_no_progress { - border: none; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_progress_background { - background-color: white; - position: absolute; - height: 100%; - width: 100%; - z-index: -2; - top: 0; - left: 0; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_goal_progress { - background-color: #d4e9de; - position: absolute; - height: 100%; - width: 0; - z-index: -1; - top: 0; - left: 0; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_thead { - font-weight: normal; - padding: 5px; - text-align: center; -} -.openerp .oe_mail_wall .oe_goal .oe_goals_list .oe_cell { - padding: 3px 0; -} -.openerp .oe_mail_wall .oe_table.oe_goals_list .col0 { + margin-left: -6px; } +.openerp .oe_timeline .oe_tl_inbox_aside .oe_gamification_suggestion { + background-color: #d3def1; } + .openerp .oe_timeline .oe_tl_inbox_aside .oe_gamification_suggestion ul { + padding-left: 15px; } + .openerp .oe_timeline .oe_tl_inbox_aside .oe_gamification_suggestion span { + background-color: #f0eeee; + display: inline-block; + height: 25px; + vertical-align: middle; + font-size: 14px; + font-weight: bold; + margin-top: -20px; + padding-left: 0 5px 0px 5px; } +.openerp .oe_timeline .oe_goal { + background-color: #f0eeee; + border: solid 1px #cacaca; + margin-top: 20px; + padding-left: 10px; } + .openerp .oe_timeline .oe_goal .oe_update_challenge.fa, .openerp .oe_timeline .oe_goal .oe_goal_action.fa { + visibility: hidden; + font-size: 16px; + float: right; + position: relative; } + .openerp .oe_timeline .oe_goal div:hover > .oe_update_challenge, .openerp .oe_timeline .oe_goal div:hover > .oe_goal_action, .openerp .oe_timeline .oe_goal th:hover > .oe_goal_action { + visibility: visible; } + .openerp .oe_timeline .oe_goal .oe_goals_list { + padding-left: 0; + margin-top: 5px; + margin-bottom: 10px; + width: 100%; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress div { + display: inline-block; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress::before { + content: "•"; + padding-right: 4px; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached::before { + content: "✓"; + padding-right: 0; + margin-left: -2px; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached .oe_cell { + text-decoration: line-through; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_cell.oe_goal_current { + font-size: 150%; + font-weight: bold; + min-width: 50px; + padding: 0 5px; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_outer_box { + display: inline-block; + position: relative; + z-index: 0; + vertical-align: middle; + width: 100%; + border: solid 1px rgba(0, 0, 0, 0.03); + border-radius: 2px; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_outer_box.oe_no_progress { + border: none; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_progress_background { + background-color: white; + position: absolute; + height: 100%; + width: 100%; + z-index: -2; + top: 0; + left: 0; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_progress { + background-color: #d4e9de; + position: absolute; + height: 100%; + width: 0; + z-index: -1; + top: 0; + left: 0; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_thead { + font-weight: normal; + padding: 5px; + text-align: center; } + .openerp .oe_timeline .oe_goal .oe_goals_list .oe_cell { + padding: 3px 0; } +.openerp .oe_timeline .oe_table.oe_goals_list .col0 { font-size: 200%; font-weight: bold; width: 25px; - text-align: center; -} -.openerp .oe_mail_wall .oe_table.oe_goals_list .col1 { - padding: 0 5px; -} -.openerp .oe_mail_wall .oe_table.oe_goals_list .col2 { - width: auto; -} -.openerp .oe_mail_wall .oe_user_avatar { + text-align: center; } +.openerp .oe_timeline .oe_table.oe_goals_list .col1 { + padding: 0 5px; } +.openerp .oe_timeline .oe_table.oe_goals_list .col2 { + width: auto; } +.openerp .oe_timeline .oe_user_avatar { width: 24px; - padding-right: 5px; -} -.openerp .oe_mail_wall .oe_mail { - display: inline-block; -} -.openerp .oe_mail_wall .oe_table { - display: table; -} -.openerp .oe_mail_wall .oe_row { - display: table-row; -} -.openerp .oe_mail_wall .oe_cell { + padding-right: 5px; } +.openerp .oe_timeline .oe_table { + display: table; } +.openerp .oe_timeline .oe_row { + display: table-row; } +.openerp .oe_timeline .oe_cell { display: table-cell; - vertical-align: middle; -} \ No newline at end of file + vertical-align: middle; } diff --git a/addons/gamification/static/src/css/gamification.sass b/addons/gamification/static/src/css/gamification.sass index fa5e4f86dcf..b524c9486b8 100644 --- a/addons/gamification/static/src/css/gamification.sass +++ b/addons/gamification/static/src/css/gamification.sass @@ -62,34 +62,29 @@ .oe_form td .oe_no_padding margin-left: -6px - - .oe_mail_wall - .oe_mail_wall_aside - margin-top: 15px - position: relative - display: inline-block - vertical-align: top - width: 280px - border-radius: 2px - - .oe_gamification_challenge_list - background-color: #EDEDF6 + .oe_timeline + .oe_tl_inbox_aside .oe_gamification_suggestion - background-color: rgb(211, 222, 241) - ul + background-color: #d3def1 + + ul padding-left: 15px - h4, .oe_goals_list .oe_thead - text-align: center - padding-bottom: 15px - - .oe_mail_wall_aside > div - border-bottom: solid 5px white - border-radius: 2px + span + background-color: #f0eeee + display: inline-block + height: 25px + vertical-align: middle + font-size: 14px + font-weight: bold + margin-top: -20px + padding-left: 0 5px 0px 5px .oe_goal - border-bottom: solid 3px white - padding: 5px 10px + background-color: #f0eeee + border: solid 1px #CACACA + margin-top: 20px + padding-left: 10px .oe_update_challenge.fa, .oe_goal_action.fa visibility: hidden font-size: 16px @@ -97,7 +92,6 @@ position: relative div:hover > .oe_update_challenge, div:hover > .oe_goal_action, th:hover > .oe_goal_action visibility: visible - .oe_goals_list padding-left: 0 margin-top: 5px @@ -170,18 +164,15 @@ padding: 0 5px .col2 width: auto - + .oe_user_avatar width: 24px padding-right: 5px - .oe_mail - display: inline-block - .oe_table display: table .oe_row display: table-row .oe_cell display: table-cell - vertical-align: middle + vertical-align: middle \ No newline at end of file diff --git a/addons/gamification/static/src/js/gamification.js b/addons/gamification/static/src/js/gamification.js index fb70b7e3d61..37a4f334e78 100644 --- a/addons/gamification/static/src/js/gamification.js +++ b/addons/gamification/static/src/js/gamification.js @@ -119,11 +119,11 @@ var Sidebar = Widget.extend({ } }); -mail.Wall.include({ +mail.TimelineView.include({ start: function() { this._super(); var sidebar = new Sidebar(this); - sidebar.appendTo($('.oe_mail_wall_aside')); + sidebar.appendTo($('.o_timeline__inbox_aside')); }, }); diff --git a/addons/gamification/static/src/xml/gamification.xml b/addons/gamification/static/src/xml/gamification.xml index d20bf3a722b..570cfe936d1 100644 --- a/addons/gamification/static/src/xml/gamification.xml +++ b/addons/gamification/static/src/xml/gamification.xml @@ -7,9 +7,9 @@
-
+
-

+
diff --git a/addons/hr/mail_hr_view.xml b/addons/hr/mail_hr_view.xml index 203878c7a77..0a5ea43d43a 100644 --- a/addons/hr/mail_hr_view.xml +++ b/addons/hr/mail_hr_view.xml @@ -1,19 +1,24 @@ - - - + + Inbox + mail.message + form + timeline + { + 'default_model': 'res.users', + 'default_res_id': uid, + 'thread_model': 'res.partner', + 'options' : {'view_mailbox': True, + 'view_inbox': True, + 'show_write_to_followers': True} + } + [ + '|', + ('notification_ids.partner_id.user_ids', 'in', [uid]), + ('author_id.user_ids', 'in', [uid]), + ] - \ No newline at end of file diff --git a/addons/hr/static/src/js/suggestions.js b/addons/hr/static/src/js/suggestions.js index 45d42db313a..14a8a72867e 100644 --- a/addons/hr/static/src/js/suggestions.js +++ b/addons/hr/static/src/js/suggestions.js @@ -72,7 +72,7 @@ var Employees = Widget.extend({ } }); -mail.WallSidebar.include({ +mail.Sidebar.include({ start: function () { this._super.apply(this, arguments); var sug_employees = new Employees(this); diff --git a/addons/mail/__openerp__.py b/addons/mail/__openerp__.py index ba1a7bc7a51..62a84ed26eb 100644 --- a/addons/mail/__openerp__.py +++ b/addons/mail/__openerp__.py @@ -31,6 +31,7 @@ Main Features 'wizard/invite_view.xml', 'wizard/mail_compose_message_view.xml', 'views/mail_message_subtype_views.xml', + 'views/mail_tracking_views.xml', 'views/mail_message_views.xml', 'views/mail_mail_views.xml', 'views/mail_followers_views.xml', diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index 92048d992ed..3c3bfdd28d3 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -2,6 +2,7 @@ import ir_attachment import mail_message_subtype +import mail_tracking_value import mail_alias import mail_followers import mail_message diff --git a/addons/mail/models/mail_group.py b/addons/mail/models/mail_group.py index f337aa97c37..44e92584025 100644 --- a/addons/mail/models/mail_group.py +++ b/addons/mail/models/mail_group.py @@ -76,27 +76,28 @@ class MailGroup(models.Model): ).create(vals) group.alias_id.write({"alias_force_thread_id": group.id, 'alias_parent_thread_id': group.id}) - # Create client action for this group and link the menu to it + # Create action window for this group and link the menu to it inbox_ref = self.env.ref('mail.action_mail_group_feeds') search_ref = self.env.ref('mail.view_message_search') - params = { - 'search_view_id': search_ref.id, - 'domain': [ - ('model', '=', 'mail.group'), - ('res_id', '=', group.id), - ], - 'context': { - 'default_model': 'mail.group', - 'default_res_id': group.id, - }, - 'res_model': 'mail.message', - 'thread_level': 1, - 'header_description': group._get_header(), - 'view_mailbox': True, - 'compose_placeholder': 'Send a message to the group', - } - new_action = inbox_ref.sudo().copy(default={'params': str(params), 'name': vals['name']}) - menu.write({'action': 'ir.actions.client,%d' % new_action.id, 'mail_group_id': group.id}) + act_domain = [('model', '=', 'mail.group'), ('res_id', '=', group.id)] + act_context = {'default_model': 'mail.group', + 'default_res_id': group.id, + 'options': {'view_mailbox': False, + 'view_inbox': True, + 'read_action': 'read', + 'compose_placeholder': 'Send a message to the group'}, + 'params': {'header_description': group._get_header(), + 'name': vals['name'],} + } + act_res_model = 'mail.message' + act_search_view_id = search_ref.id + + new_action = inbox_ref.sudo().copy(default={'domain': act_domain, + 'context': act_context, + 'res_model': act_res_model, + 'search_view_id': act_search_view_id, + 'name': vals['name']}) + menu.write({'action': 'ir.actions.act_window,%d' % new_action.id, 'mail_group_id': group.id}) if vals.get('group_ids'): group._subscribe_users() @@ -126,12 +127,11 @@ class MailGroup(models.Model): result = super(MailGroup, self).write(vals) if vals.get('group_ids'): self._subscribe_users() - # if description, name or alias is changed: update client action + # if description, name or alias is changed: update action window if vals.get('description') or vals.get('name') or vals.get('alias_id') or vals.get('alias_name'): for group in self: - new_params = group.menu_id.action.params - new_params['header_description'] = group._generate_header_description() - group.menu_id.action.sudo().write({'params': str(new_params)}) + if (vals.get('name')): + group.menu_id.action.sudo().write({'name': vals.get('name')}) # if name is changed: update menu if vals.get('name'): self.sudo().mapped('menu_id').write({'name': vals.get('name')}) diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 14c23e78862..bb010372025 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -31,7 +31,7 @@ class Message(models.Model): _message_read_limit = 30 _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from', - 'message_type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name'] + 'message_type', 'vote_user_ids', 'attachment_ids', 'tracking_value_ids' ,'author_id', 'partner_ids', 'record_name'] _message_record_name_length = 18 _message_read_more_limit = 1024 @@ -98,6 +98,7 @@ class Message(models.Model): 'res.users', 'mail_vote', 'message_id', 'user_id', string='Votes', help='Users that voted for this message') mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server', readonly=1) + tracking_value_ids = fields.One2many('mail.tracking.value', 'mail_message_id', string='Tracking values') @api.depends('notification_ids') def _get_to_read(self): @@ -236,32 +237,43 @@ class Message(models.Model): """ pid = self.env.user.partner_id.id - # 1. Aggregate partners (author_id and partner_ids) and attachments - partner_ids = set() - attachment_ids = set() + # 1. Aggregate partners (author_id and partner_ids), attachments and tracking values + partners = self.env['res.partner'] + attachments = self.env['ir.attachment'] + trackings = self.env['mail.tracking.value'] for key, message in message_tree.iteritems(): if message.author_id: - partner_ids |= set([message.author_id.id]) + partners |= message.author_id if message.subtype_id and message.notified_partner_ids: # take notified people of message with a subtype - partner_ids |= set([partner.id for partner in message.notified_partner_ids]) + partners |= message.notified_partner_ids elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log) - partner_ids |= set([partner.id for partner in message.partner_ids]) + partners |= message.partner_ids if message.attachment_ids: - attachment_ids |= set([attachment.id for attachment in message.attachment_ids]) + attachments |= message.attachment_ids + if message.tracking_value_ids: + trackings |= message.tracking_value_ids # Read partners as SUPERUSER -> display the names like classic m2o even if no access - partners = self.env['res.partner'].sudo().browse(partner_ids).name_get() - partner_tree = dict((partner[0], partner) for partner in partners) + partners_names = partners.sudo().name_get() + partner_tree = dict((partner[0], partner) for partner in partners_names) # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see - attachments = self.env['ir.attachment'].sudo().browse(attachment_ids).read(['id', 'datas_fname', 'name', 'file_type_icon']) + attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'file_type_icon']) attachments_tree = dict((attachment['id'], { 'id': attachment['id'], 'filename': attachment['datas_fname'], 'name': attachment['name'], 'file_type_icon': attachment['file_type_icon'], - }) for attachment in attachments) + }) for attachment in attachments_data) - # 3. Update message dictionaries + # 3. Tracking values + tracking_tree = dict((tracking.id, { + 'id': tracking.id, + 'changed_field': tracking.field_desc, + 'old_value': tracking.get_old_display_value()[0], + 'new_value': tracking.get_new_display_value()[0], + }) for tracking in trackings) + + # 4. Update message dictionaries for message_dict in messages: message_id = message_dict.get('id') message = message_tree[message_id] @@ -280,13 +292,20 @@ class Message(models.Model): for attachment in message.attachment_ids: if attachment.id in attachments_tree: attachment_ids.append(attachments_tree[attachment.id]) + tracking_value_ids = [] + for tracking_value in message.tracking_value_ids: + if tracking_value.id in tracking_tree: + tracking_value_ids.append(tracking_tree[tracking_value.id]) + message_dict.update({ 'is_author': pid == author[0], 'author_id': author, 'partner_ids': partner_ids, 'attachment_ids': attachment_ids, + 'tracking_value_ids': tracking_value_ids, 'user_pid': pid }) + return True @api.multi @@ -306,22 +325,10 @@ class Message(models.Model): vote_nb = len(self.vote_user_ids) has_voted = self._uid in [user.id for user in self.vote_user_ids] - try: - if parent_id: - max_length = 300 - else: - max_length = 100 - body_short = tools.html_email_clean(self.body, remove=False, shorten=True, max_length=max_length) - - except Exception: - body_short = '

Encoding Error :
Unable to convert this message (id: %s).

' % self.id - _logger.exception(Exception) - return {'id': self.id, - 'type': self.message_type, + 'message_type': self.message_type, 'subtype': self.subtype_id.name if self.subtype_id else False, 'body': self.body, - 'body_short': body_short, 'model': self.model, 'res_id': self.res_id, 'record_name': self.record_name, @@ -338,108 +345,17 @@ class Message(models.Model): 'has_voted': has_voted, 'is_favorite': self.starred, 'attachment_ids': [], + 'tracking_value_ids': [], } - @api.model - def _message_read_add_expandables(self, messages, message_tree, parent_tree, - message_unload_ids=[], thread_level=0, domain=[], parent_id=False): - """ Create expandables for message_read, to load new messages. - 1. get the expandable for new threads - if display is flat (thread_level == 0): - fetch message_ids < min(already displayed ids), because we - want a flat display, ordered by id - else: - fetch message_ids that are not childs of already displayed - messages - 2. get the expandables for new messages inside threads if display - is not flat - for each thread header, search for its childs - for each hole in the child list based on message displayed, - create an expandable - - :param list messages: list of message structure for the Chatter - widget to which expandables are added - :param dict message_tree: dict [id]: browse record of this message - :param dict parent_tree: dict [parent_id]: [child_ids] - :param list message_unload_ids: list of message_ids we do not want - to load - :return bool: True - """ - def _get_expandable(domain, message_nb, parent_id, max_limit): - return { - 'domain': domain, - 'nb_messages': message_nb, - 'type': 'expandable', - 'parent_id': parent_id, - 'max_limit': max_limit, - } - - if not messages: - return True - message_ids = sorted(message_tree.keys()) - - # 1. get the expandable for new threads - if thread_level == 0: - exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))] - else: - exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())] - more_count = self.search_count(exp_domain) - if more_count: - # inside a thread: prepend - if parent_id: - messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True)) - # new threads: append - else: - messages.append(_get_expandable(exp_domain, -1, parent_id, True)) - - # 2. get the expandables for new messages inside threads if display is not flat - if thread_level == 0: - return True - for message_id in message_ids: - message = message_tree[message_id] - - # generate only for thread header messages (TDE note: parent_id may be False is uid cannot see parent_id, seems ok) - if message.parent_id: - continue - - # check there are message for expandable - child_ids = set([child.id for child in message.child_ids]) - set(message_unload_ids) - child_ids = sorted(list(child_ids), reverse=True) - if not child_ids: - continue - - # make groups of unread messages - id_min, id_max, nb = max(child_ids), 0, 0 - for child_id in child_ids: - if not child_id in message_ids: - nb += 1 - if id_min > child_id: - id_min = child_id - if id_max < child_id: - id_max = child_id - elif nb > 0: - exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)] - idx = [msg.get('id') for msg in messages].index(child_id) + 1 - # messages.append(_get_expandable(exp_domain, nb, message_id, False)) - messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False)) - id_min, id_max, nb = max(child_ids), 0, 0 - else: - id_min, id_max, nb = max(child_ids), 0, 0 - if nb > 0: - exp_domain = [('id', '>=', id_min), ('id', '<=', id_max), ('id', 'child_of', message_id)] - idx = [msg.get('id') for msg in messages].index(message_id) + 1 - # messages.append(_get_expandable(exp_domain, nb, message_id, id_min)) - messages.insert(idx, _get_expandable(exp_domain, nb, message_id, False)) - - return True - @api.cr_uid_context - def message_read_wrapper(self, cr, uid, ids=None, domain=None, message_unload_ids=None, - thread_level=0, context=None, parent_id=False, limit=None): - return self.message_read(cr, uid, ids, domain=domain, message_unload_ids=message_unload_ids, thread_level=thread_level, parent_id=parent_id, limit=limit) + def message_read_wrapper(self, cr, uid, ids=None, domain=None, context=None, + thread_level=0, parent_id=False, limit=None, child_limit=None): + return self.message_read(cr, uid, ids, domain=domain, thread_level=thread_level, context=context, + parent_id=parent_id, limit=limit, child_limit=child_limit) @api.multi - def message_read(self, domain=None, message_unload_ids=None, thread_level=0, parent_id=False, limit=None): + def message_read(self, domain=None, thread_level=0, context= None, parent_id=False, limit=None, child_limit=None): """ Read messages from mail.message, and get back a list of structured messages to be displayed as discussion threads. If IDs is set, fetch these records. Otherwise use the domain to fetch messages. @@ -447,31 +363,33 @@ class Message(models.Model): well formed threads, if uid has access to them. After reading the messages, expandable messages are added in the - message list (see ``_message_read_add_expandables``). It consists - in messages holding the 'read more' data: number of messages to - read, domain to apply. + message list. It consists in messages holding the 'read more' data: + number of messages to read, domain to apply. :param list ids: optional IDs to fetch :param list domain: optional domain for searching ids if ids not set - :param list message_unload_ids: optional ids we do not want to fetch, - because i.e. they are already displayed somewhere :param int parent_id: context of parent_id - if parent_id reached when adding ancestors, stop going further in the ancestor search - if set in flat mode, ancestor_id is set to parent_id :param int limit: number of messages to fetch, before adding the ancestors and expandables - :return list: list of message structure for the Chatter widget + :param int child_limit: number of child messages to fetch + :return dict: + - int: number of messages read (status 'unread' to 'read') + - list: list of threads [[messages_of_thread1], [messages_of_thread2]] """ assert thread_level in [0, 1], 'message_read() thread_level should be 0 (flat) or 1 (1 level of thread); given %s.' % thread_level + domain = domain if domain is not None else [] - message_unload_ids = message_unload_ids if message_unload_ids is not None else [] - if message_unload_ids: - domain += [('id', 'not in', message_unload_ids)] limit = limit or self._message_read_limit + child_limit = child_limit or self._message_read_limit + message_tree = {} - message_list = [] parent_tree = {} + child_ids = [] + parent_ids = [] + exp_domain = [] # no specific IDS given: fetch messages according to the domain, add their parents if uid has access to if not self.ids and domain: @@ -497,24 +415,73 @@ class Message(models.Model): message_tree[parent.id] = parent # newest messages first parent_tree.setdefault(tree_parent_id, []) - if tree_parent_id != message_id: - parent_tree[tree_parent_id].append(message_tree[message_id]._message_read_dict(parent_id=tree_parent_id)) - if thread_level: - for key, message_id_list in parent_tree.iteritems(): - message_id_list.sort(key=lambda item: item['id']) - message_id_list.insert(0, message_tree[key]._message_read_dict()) + # build thread structure + # for each parent_id: get child messages, add message expandable and parent message if needed [child1, child2, expandable, parent_message] + # add thread expandable if it remains some uncaught parent_id + if self.ids and len(self.ids) > 0: + for parent in parent_tree: + parent_ids.append(parent) - # create final ordered message_list based on parent_tree - parent_list = parent_tree.items() - parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item[1]]) if item[1] else item[0], reverse=True) - message_list = [message for (key, msg_list) in parent_list for message in msg_list] + if not thread_level: + child_ids = self.ids; + exp_domain = domain + [('id', '<', min(child_ids))] + else: + child_ids = [msg.id for msg in self.browse(parent).child_ids][0:child_limit] + exp_domain = [('parent_id', '=', parent), ('id', '>', parent)] + if len(child_ids): + exp_domain += [('id', '<', min(child_ids))] - # get the child expandable messages for the tree - self._message_read_dict_postprocess(message_list, message_tree) - self._message_read_add_expandables(message_list, message_tree, parent_tree, - thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id) - return message_list + for cid in child_ids: + if cid not in message_tree: + message_tree[cid] = self.browse(cid) + parent_tree[parent].append(message_tree[cid]._message_read_dict(parent_id=parent)) + + if parent and thread_level: + parent_tree[parent].sort(key=lambda item: item['id']) + parent_tree[parent].reverse(); + parent_tree[parent].append(message_tree[parent]._message_read_dict()) + + self._message_read_dict_postprocess(parent_tree[parent], message_tree) + + # add 'message' expandable (inside a thread) + more_count = self.search_count(exp_domain) + if more_count: + exp = {'message_type':'expandable', + 'domain': exp_domain, + 'nb_messages': more_count, + 'parent_id': parent} + + if parent and thread_level: + #insert expandable before parent message + parent_tree[parent].insert(len(parent_tree[parent])-1, exp) + else: + #insert expandable at the end of the message list + parent_tree[parent].append(exp) + + # create final ordered parent_list based on parent_tree + parent_list = parent_tree.values() + parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item]), reverse=True) + + #add 'thread' expandable + if thread_level: + exp_domain = domain + [('id', '<', min(self.ids)), ('id', 'not in', parent_ids), ('parent_id', 'not in', parent_ids)] + more_count = self.search_count(exp_domain) + if more_count: + parent_list.append([{'message_type':'expandable', + 'domain': exp_domain, + 'nb_messages': more_count, + 'parent_id': parent_id}]) + + nb_read = 0 + if context and 'mail_read_set_read' in context and context['mail_read_set_read']: + nb_read = self.set_message_read(True, create_missing=False) + + else: + nb_read = 0 + parent_list = [] + + return {'nb_read': nb_read, 'threads': parent_list} @api.multi def get_like_names(self, limit=10): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index f3ca4ba4ad7..d4cc073790e 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -426,57 +426,27 @@ class MailThread(models.AbstractModel): @api.multi def message_track(self, tracked_fields, initial_values): - - def convert_for_display(value, col_info): - if not value and col_info['type'] == 'boolean': - return 'False' - if not value: - return '' - if col_info['type'] == 'many2one': - return value.name_get()[0][1] - if col_info['type'] == 'selection': - return dict(col_info['selection'])[value] - return value - - def format_message(message_description, tracked_values): - message = '' - if message_description: - message = '%s' % message_description - for name, change in tracked_values.items(): - message += '
    • %s: ' % change.get('col_info') - if change.get('old_value'): - message += '%s → ' % change.get('old_value') - message += '%s
' % change.get('new_value') - return message - if not tracked_fields: return True for record in self: initial = initial_values[record.id] changes = set() - tracked_values = {} + tracking_value_ids = [] # generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}} for col_name, col_info in tracked_fields.items(): - field = self._fields[col_name] initial_value = initial[col_name] - record_value = getattr(record, col_name) + new_value = getattr(record, col_name) + + if new_value != initial_value and (new_value or initial_value): # because browse null != False + tracking = self.env['mail.tracking.value'].create_tracking_values(initial_value, new_value, col_name, col_info) + if tracking: + tracking_value_ids.append([0, 0, tracking]) - if record_value == initial_value and getattr(field, 'track_visibility', None) == 'always': - tracked_values[col_name] = dict( - col_info=col_info['string'], - new_value=convert_for_display(record_value, col_info), - ) - elif record_value != initial_value and (record_value or initial_value): # because browse null != False - if getattr(field, 'track_visibility', None) in ['always', 'onchange']: - tracked_values[col_name] = dict( - col_info=col_info['string'], - old_value=convert_for_display(initial_value, col_info), - new_value=convert_for_display(record_value, col_info), - ) if col_name in tracked_fields: changes.add(col_name) + if not changes: continue @@ -500,10 +470,10 @@ class MailThread(models.AbstractModel): if not (subtype_rec and subtype_rec.exists()): _logger.debug('subtype %s not found' % subtype_xmlid) continue - message = format_message(subtype_rec.description if subtype_rec.description else subtype_rec.name, tracked_values) + record.message_post(subtype=subtype_xmlid, tracking_value_ids=tracking_value_ids) else: - message = format_message('', tracked_values) - record.message_post(body=message, subtype=subtype_xmlid) + record.message_post(tracking_value_ids=tracking_value_ids) + return True #------------------------------------------------------ @@ -1509,7 +1479,6 @@ class MailThread(models.AbstractModel): content_subtype='html', **kwargs): """ Post a new message in an existing thread, returning the new mail.message ID. - :param int thread_id: thread ID to post into, or list with one ID; if False/0, mail.message model will also be set as False :param str body: body of the message, usually raw HTML that will @@ -1520,7 +1489,6 @@ class MailThread(models.AbstractModel): parent partners to the message in case of private discussion :param tuple(str,str) attachments or list id: list of attachment tuples in the form ``(name,content)``, where content is NOT base64 encoded - Extra keyword arguments will be used as default column values for the new mail.message record. Special cases: - attachment_ids: supposed not attached to any document; attach them diff --git a/addons/mail/models/mail_tracking_value.py b/addons/mail/models/mail_tracking_value.py new file mode 100644 index 00000000000..c8d9a590ea4 --- /dev/null +++ b/addons/mail/models/mail_tracking_value.py @@ -0,0 +1,103 @@ +from datetime import datetime + +from openerp import api, fields, models, tools + + +class MailTracking(models.Model): + _name = 'mail.tracking.value' + _description = 'Mail Tracking Value' + + field = fields.Char('Changed Field', required=True, readonly=1) + field_desc = fields.Char('Field Description', required=True, readonly=1) + field_type = fields.Char('Field Type') + + old_value_integer = fields.Integer('Old Value Integer', readonly=1) + old_value_float = fields.Float('Old Value Float', readonly=1) + old_value_char = fields.Char('Old Value Char', readonly=1) + old_value_text = fields.Text('Old Value Text', readonly=1) + old_value_datetime = fields.Datetime('Old Value DateTime', readonly=1) + + new_value_integer = fields.Integer('New Value Integer', readonly=1) + new_value_float = fields.Float('New Value Float', readonly=1) + new_value_char = fields.Char('New Value Char', readonly=1) + new_value_text = fields.Text('New Value Text', readonly=1) + new_value_datetime = fields.Datetime('New Value Datetime', readonly=1) + + mail_message_id = fields.Many2one('mail.message', 'Message ID', required=True, select=True, ondelete='cascade') + + @api.model + def create_tracking_values(self, initial_value, new_value, col_name, col_info): + tracked = True + values = {'field': col_name, 'field_desc': col_info['string'], 'field_type': col_info['type']} + + if col_info['type'] in ['integer', 'float', 'char', 'text', 'datetime']: + values.update({ + 'old_value_%s' % col_info['type']: initial_value, + 'new_value_%s' % col_info['type']: new_value + }) + elif col_info['type'] == 'date': + values.update({ + 'old_value_datetime': initial_value and datetime.strftime(datetime.combine(datetime.strptime(initial_value, tools.DEFAULT_SERVER_DATE_FORMAT), datetime.min.time()), tools.DEFAULT_SERVER_DATETIME_FORMAT) or False, + 'new_value_datetime': new_value and datetime.strftime(datetime.combine(datetime.strptime(new_value, tools.DEFAULT_SERVER_DATE_FORMAT), datetime.min.time()), tools.DEFAULT_SERVER_DATETIME_FORMAT) or False, + }) + elif col_info['type'] == 'boolean': + values.update({ + 'old_value_integer': initial_value, + 'new_value_integer': new_value + }) + elif col_info['type'] == 'selection': + values.update({ + 'old_value_char': initial_value and dict(col_info['selection'])[initial_value] or '', + 'new_value_char': new_value and dict(col_info['selection'])[new_value] or '' + }) + elif col_info['type'] == 'many2one': + values.update({ + 'old_value_integer': initial_value and initial_value.id or 0, + 'new_value_integer': new_value and new_value.id or 0, + 'old_value_char': initial_value and initial_value.name_get()[0][1] or '', + 'new_value_char': new_value and new_value.name_get()[0][1] or '' + }) + else: + tracked = False + + if tracked: + return values + return {} + + @api.multi + def get_old_display_value(self): + result = [] + for record in self: + if record.field_type in ['integer', 'float', 'char', 'text', 'datetime']: + result.append(getattr(record, 'old_value_%s' % record.field_type)) + elif record.field_type == 'date': + if record.old_value_datetime: + result.append(datetime.strptime(record.old_value_integer, tools.DEFAULT_SERVER_DATETIME_FORMAT).date()) + else: + result.append(record.old_value_datetime) + elif record.field_type == 'boolean': + result.append(bool(record.old_value_integer)) + elif record.field_type in ['many2one', 'selection']: + result.append(record.old_value_char) + else: + result.append(record.old_value_char) + return result + + @api.multi + def get_new_display_value(self): + result = [] + for record in self: + if record.field_type in ['integer', 'float', 'char', 'text', 'datetime']: + result.append(getattr(record, 'new_value_%s' % record.field_type)) + elif record.field_type == 'date': + if record.new_value_datetime: + result.append(datetime.strptime(record.old_value_integer, tools.DEFAULT_SERVER_DATETIME_FORMAT).date()) + else: + result.append(record.new_value_datetime) + elif record.field_type == 'boolean': + result.append(bool(record.new_value_integer)) + elif record.field_type in ['many2one', 'selection']: + result.append(record.new_value_char) + else: + result.append(record.new_value_char) + return result diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index 6b48001866f..c8f90dcaeac 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -21,6 +21,10 @@ access_mail_alias_user,mail.alias.user,model_mail_alias,base.group_user,1,1,1,0 access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1,1,1,1 access_mail_message_subtype_all,mail.message.subtype.all,model_mail_message_subtype,,1,0,0,0 access_mail_message_subtype_system,mail.message.subtype.system,model_mail_message_subtype,base.group_system,1,1,1,1 +access_mail_tracking_value_all,mail.tracking.value.all,model_mail_tracking_value,,1,0,0,0 +access_mail_tracking_value_portal,mail.tracking.value.portal,model_mail_tracking_value,base.group_portal,1,1,1,1 +access_mail_tracking_value_user,mail.tracking.value.user,model_mail_tracking_value,base.group_user,1,1,1,1 +access_mail_tracking_value_system,mail.tracking.value.system,model_mail_tracking_value,base.group_system,1,1,1,1 access_mail_thread_all,mail.thread.all,model_mail_thread,,1,1,1,1 access_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,,1,1,1,1 access_mail_template,mail.template,model_mail_template,base.group_user,1,1,1,0 diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index cd6da8a4dc3..5f550ed0d9c 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -1,761 +1,1112 @@ /* ------------ TOPBAR MAIL BUTTON --------------- */ -.oe_topbar_item.oe_topbar_compose_full_email *{ - position: relative; - color: white; -} -/* ------------ MAIL WIDGET --------------- */ -.openerp .oe_mail, .openerp .oe_mail *{ - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -.openerp .oe_mail { - display: block; - position: relative; - margin: 0px; -} -.openerp .oe_mail .oe_thread{ - margin-left: 32px; -} -.openerp .oe_mail > .oe_thread{ - margin-left: 0px; -} -.openerp .oe_inline.oe_compose_recipients { - margin-top: -2px; -} - -/* -------- Mail Wall + Control Panel --------- */ -.openerp .oe_mail_wall{ - display: -webkit-box; - display: -moz-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; - -ms-flex-direction: column; - -webkit-flex-direction: column; - flex-direction: column; -} -.openerp .oe_mail_wall .oe-view-manager-content{ - -webkit-box-flex: 1; - -webkit-flex: 1 1 auto; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - overflow: auto; - position: static; -} - -/* ---------------- MESSAGES ------------------ */ - -.openerp .oe_mail .oe_msg{ - position: relative; - background: #EDEDF6; - border-radius: 2px; - margin-bottom: 2px; - min-height: 42px; - border: solid 1px rgba(0,0,0,0.03); -} -.openerp .oe_mail .oe_msg.oe_msg_nobody{ - background: #E4E4E4; -} -.openerp .oe_mail .oe_msg.oe_msg_notification{ - background: #F8F8F8; -} -.openerp .oe_mail .oe_msg .oe_msg_left{ - position: absolute; - left:0; top: 0; bottom: 0; width: 40px; - overflow: hidden; -} -.openerp .oe_mail .oe_msg .oe_msg_left a, -.openerp .oe_mail .oe_msg .oe_msg_left img{ - border: 0; -} -.openerp .oe_mail .oe_msg .oe_msg_icon{ - width: 32px; - margin: 4px; - border-radius: 2px; -} -.openerp .oe_mail .oe_msg .oe_msg_center{ - position: relative; - display: block; - margin-left: 40px; -} -.openerp .oe_mail .oe_msg .oe_msg_footer{ - margin-left: 4px; - overflow: hidden; - font-size: 11px; -} -.openerp .oe_mail .oe_msg .oe_msg_content{ - display: block; - overflow: hidden; - padding: 4px; - padding-bottom:1px; -} -.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_title{ - font-size: 13px; - margin-bottom: 0px; - margin-top: 2px; -} -.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body p:first-of-type { - margin-top: 0px; -} - -.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body p { - margin-bottom: 0px; -} -.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body pre { - white-space: pre-wrap; -} -.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body blockquote { - font-size: 1em; -} -.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body * { - text-overflow:ellipsis; - word-wrap: break-word; -} -.openerp .oe_mail .oe_msg .oe_msg_content .oe_msg_body .oe_mail_cleaned { - display: none; -} -/* a) Indented Messages */ - -.openerp .oe_mail .oe_msg_indented{ - background: #FFF; - border: none; - margin-bottom:0px; - min-height:38px; -} -.openerp .oe_mail .oe_msg.oe_msg_indented .oe_msg_icon{ - width:32px; - margin:2px; - border-radius:2px; -} -.openerp .oe_mail .oe_msg .oe_subtle{ - color: #B7B7D5; -} -.openerp .oe_mail .oe_msg_indented .oe_msg_center{ - margin-left:34px; -} -.openerp .oe_mail .oe_msg.oe_msg_indented .oe_msg_content{ - padding-top:2px; -} -.openerp .oe_mail .oe_msg.oe_msg_indented .oe_msg_footer{ - margin-bottom: 5px; -} - -/* b) Votes (likes) */ - -.openerp .oe_mail .oe_mail_vote_count{ - display: inline; - position: relative; - background: white; - box-shadow: 0px 0px 0px 1px rgba(124, 123, 173, 0.36) inset; - color: #7c7bad; - text-shadow: none; - border-radius: 3px; - margin: 0px; - padding-left: 3px; - padding-right: 5px; - margin-right: 5px; - padding-top: 2px; -} - -/* c) Message action icons */ - -.openerp .oe_mail .oe_msg .oe_msg_icons{ - float: right; - margin-top: 4px; - margin-right: 8px; - margin-left: 8px; - height: 24px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -.openerp .oe_mail .oe_msg .oe_msg_icons span{ - float:right; - width:24px; - height:24px; - line-height:24px; - text-align: center; -} -.openerp .oe_mail .oe_msg .oe_msg_icons i{ - text-decoration: none; - color: #FFF; - text-shadow: 0px 1px #AAA,0px -1px #AAA, -1px 0px #AAA, 1px 0px #AAA, 0px 3px 3px rgba(0,0,0,0.1); - font-size: 1.3em; - margin-top: 8px; - -webkit-transition: all 0.2s linear; - -moz-transition: all 0.2s linear; - -o-transition: all 0.2s linear; - transition: all 0.2s linear; -} -.openerp .oe_mail .oe_msg:hover .oe_msg_icons i{ - opacity: 1; - -webkit-transition: all 0.1s linear; - -moz-transition: all 0.1s linear; - -o-transition: all 0.1s linear; - transition: all 0.1s linear; -} -.openerp .oe_mail .oe_msg .oe_msg_icons .oe_star:hover i{ - color: #FFF670; - text-shadow: 0px 1px #FFA162,0px -1px #FFA162, -1px 0px #FFA162, 1px 0px #FFA162, 0px 3px 3px rgba(0,0,0,0.1); -} -.openerp .oe_mail .oe_msg .oe_msg_icons .oe_star.oe_starred i{ - color: #FFE41F; - text-shadow: 0px 1px #DF6200,0px -1px #DF6200, -1px 0px #DF6200, 1px 0px #DF6200, 0px 3px 3px rgba(0,0,0,0.1); -} -.openerp .oe_mail .oe_msg .oe_msg_icons .oe_reply:hover i{ - color: #1fc0ff; - text-shadow: 0px 1px #184fc5,0px -1px #184fc5, -1px 0px #184fc5, 1px 0px #184fc5, 0px 3px 3px rgba(0,0,0,0.1); -} -.openerp .oe_mail .oe_msg .oe_msg_icons .oe_read:hover i{ - color: #bbbaff; - text-shadow: 0px 1px #7c7bad,0px -1px #7c7bad, -1px 0px #7c7bad, 1px 0px #7c7bad, 0px 3px 3px rgba(0,0,0,0.1); -} -.openerp .oe_mail .oe_msg .oe_msg_icons .oe_unread:hover i{ - color: #c2ff00; - text-shadow: 0px 1px #009441,0px -1px #009441, -1px 0px #009441, 1px 0px #009441, 0px 3px 3px rgba(0,0,0,0.1); -} -.openerp .oe_mail .oe_msg .oe_msg_content textarea{ - width: 100%; - height: 64px; - margin: 0px; - padding: 0px; - resize: vertical; + .oe_topbar_item.oe_topbar_compose_full_email *{ + position: relative; + color: white; + } - padding: 4px; -} -.openerp .oe_mail .oe_msg.oe_msg_composer_compact, .openerp .oe_mail .oe_msg.oe_msg_expandable{ - padding:4px; - min-height:0px; -} -.openerp .oe_mail .oe_msg.oe_msg_composer_compact textarea{ - height: 24px; - width: 100%; -} -.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_sep_word{ - margin-right: 8px; - margin-left: 8px; -} -.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact{ - height: 24px; - width: 100%; - padding: 2px 4px; - color: #AAA; - cursor: text; -} -.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact_record { - font-size: 13px; - font-style: bold; - text-align: center; -} -.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact_inbox { - border: 1px solid #CCC; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - background: white; - font-size: 14px; - font-style: italic; -} -/* d) I.E. tweaks for Message action icons */ +/* ----------------------- TIMELINE ----------------------- */ -.openerp.openerp_ie .oe_mail .oe_msg .oe_msg_icons i { - color: #C8BFDA; - text-shadow: none; -} -.openerp.openerp_ie .oe_mail .oe_msg .oe_msg_icons .oe_star:hover i{ - color: #FFB700; - text-shadow: none; -} -.openerp.openerp_ie .oe_mail .oe_msg .oe_msg_icons .oe_starred i{ - color: #FFB700; - text-shadow:none; -} -.openerp.openerp_ie .oe_mail .oe_msg .oe_msg_icons .oe_read:hover i{ - color: #7C7BAD; - text-shadow: none; -} -.openerp.openerp_ie .oe_mail .oe_msg .oe_msg_icons .oe_unread:hover i{ - color: #7C7BAD; - text-shadow: none; -} + .openerp .o_timeline, .openerp .o_timeline *{ + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } -/* --------------------- ATTACHMENTS --------------------- */ + .openerp .o_timeline { + display: block; + position: relative; + margin: 0px; + background: #f0eeee; + } -.openerp .oe_mail .oe_msg_attachment_list{ - margin-top: 4px; - margin-bottom: 4px; -} -.openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list { - display: block; -} -.openerp .oe_mail .oe_msg_composer .oe_recipients { - font-size: 12px; - margin-left: 20px; - margin-bottom: 2px; -} -.openerp .oe_mail .oe_msg_composer .oe_recipients label{ - vertical-align: middle; - display: block; - line-height: 14px; -} -.openerp .oe_mail .oe_msg_composer .oe_recipients input{ - vertical-align: middle; -} -.oe_attachment{ - display: inline-block; - width: 100px; - margin: 4px 2px; - min-height: 80px; - position: relative; - border-radius: 3px; - text-align: center; - vertical-align: top; -} -.oe_attachment .oe_name{ - display: inline-block; - max-width: 100%; - padding: 1px 3px; - margin-top: 50px; - margin-bottom: 5px; - background: #F4F5FA; - overflow: hidden; - color: #4c4c4c; - text-shadow: none; - border-radius: 3px; -} + .openerp .o_timeline.o_timeline_inbox { + padding-top: 15px; + padding-bottom: 15px; + } -.oe_attachment.oe_preview{ - background: url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJ0lEQVQYV2MsLS39z4AGLCws0IUYGIeCwrVr12J45sSJE5ieGQIKAbuZKf/EMCs7AAAAAElFTkSuQmCC ); -} -.oe_attachment .oe_progress_bar{ - display: none; - position: absolute; - top: 18px; - left: 16px; - right: 16px; - height: 17px; - line-height: 13px; - padding: 0px; - background: #4BBD00; - color: white; - text-align: center; - border-radius: 3px; - border: solid 1px rgba(0,0,0,0.2); - box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.34); - -webkit-animation: oe_mail_attach_loading_anim 0.75s infinite linear; - -moz-animation: oe_mail_attach_loading_anim 0.75s infinite linear; - -o-animation: oe_mail_attach_loading_anim 0.75s infinite linear; - animation: oe_mail_attach_loading_anim 0.75s infinite linear; -} -.oe_attachment.oe_uploading .oe_progress_bar{ - display: block; -} -@-webkit-keyframes oe_mail_attach_loading_anim{ - 0% { background: #4BBD00 } - 50% { background: #009123 } - 100% { background: #4BBD00 } -} -@-moz-keyframes oe_mail_attach_loading_anim{ - 0% { background: #4BBD00 } - 50% { background: #009123 } - 100% { background: #4BBD00 } -} -@-o-keyframes oe_mail_attach_loading_anim{ - 0% { background: #4BBD00 } - 50% { background: #009123 } - 100% { background: #4BBD00 } -} -@keyframes oe_mail_attach_loading_anim{ - 0% { background: #4BBD00 } - 50% { background: #009123 } - 100% { background: #4BBD00 } -} -.oe_attachment.oe_preview .oe_name{ - position: absolute; - bottom: 0px; - margin: 3px; - left: 0px; - right: 0px; - max-height: 64px; - background: rgba(0,0,0,0.8); - color: white; - border-radius: 1px; - border-top-left-radius: 0px; - border-top-right-radius: 0px; - opacity: 0; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; -} -.oe_attachment.oe_preview:hover .oe_name{ - opacity: 1; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; -} -.oe_attachment img{ - position: absolute; - width: 48px; - height: 48px; - top: 0px; - left: 50%; - margin-left: -24px; -} -.oe_attachment.oe_preview img{ - display: block; - position: relative; - margin:0px; - width: 100px; - height: 80px; - border-radius: 1px; - border: solid 3px #FFF; - margin-left: -50px; - box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.19); -} -.openerp .oe_mail .oe_attachment .oe_delete .fa{ - display: none; -} -.openerp .oe_mail .oe_msg_composer .oe_attachment .oe_delete .fa{ - display: block; - position: absolute; - top: -7px; - right: 5px; - color: black; - text-shadow: 1px 0px white, -1px 0px white, 0px 1px white, 0px -1px white; - cursor: pointer; - opacity: 0; - font-size: 20px; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; -} -.openerp .oe_mail .oe_msg_composer .oe_attachment:hover .oe_delete .fa{ - opacity: 1; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; -} -/* ---------------- MESSAGE QUICK COMPOSER --------------- */ + .openerp .o_timeline.o_timeline_inbox .oe_thread { + width: 855px; + display: inline-block; + } -.openerp .oe_mail .oe_msg_composer .oe_msg_footer{ - padding-right:4px; - padding-top: 2px; - padding-bottom:6px; -} -.openerp .oe_mail .oe_msg_images { - display: block; -} -.openerp .oe_mail .oe_msg_footer button{ - display: inline; - height: 24px; - font-size: 12px; - line-height: 12px; - vertical-align: middle; -} -.openerp .oe_mail .oe_msg_footer button.oe_post, -.openerp .oe_mail .oe_msg_footer button.oe_log{ - position: relative; - z-index: 2; -} -.openerp .oe_mail .oe_msg_footer button.oe_attach{ - width: 24px; - margin-bottom: 5px; /* improved margin of file attach button according bootstrap3 */ - filter:none; -} -.openerp .oe_mail .oe_msg_footer button.oe_attach .fa{ - position: relative; - top: 2px; - margin-left: 3px; - vertical-align: middle; - filter:none; - font-size: 1.4em; -} -.openerp .oe_mail .oe_hidden_input_file, .openerp .oe_mail .oe_hidden_input_file form{ - display:inline; -} -.openerp .oe_mail .oe_msg_center button.oe_full{ - width: 24px; - height: 22px; - overflow: hidden; - float: right; - filter: none; -} -.openerp .oe_mail .oe_msg_center button.oe_full .fa{ - position: relative; - top: 0px; - margin-left: -5px; - vertical-align: top; - filter: none; - height: 16px; - font-size: 22px -} -.openerp .oe_mail button.oe_attach, .openerp .oe_mail button.oe_full{ - background: transparent; - color: #7C7BAD; - box-shadow: none; - border: none; - text-shadow: none; - filter:none; -} -.openerp .oe_mail .oe_attach_label{ - color: #7C7BAD; - margin-left: -3px; -} -.openerp .oe_mail .oe_msg_footer .oe_attachment_file{ - cursor: pointer; -} -.openerp .oe_mail .oe_msg_footer .oe_attachment_file .oe_form_binary_file{ - display: inline-block; - margin-left: -122px; - height: 22px; - width: 128px; - margin-top: -4px; - filter:none; - cursor: pointer; -} -.openerp .oe_mail .oe_msg_content .oe_mail_list_recipients{ - font-size: 12px; - margin: 4px 0 4px 20px; - text-indent: -20px; -} - -/* ---------------- HIDDEN MESSAGES ------------------ */ - -.openerp .oe_mail .oe_msg_content.oe_msg_more_message{ - text-align: right; - cursor: pointer; -} -.openerp .oe_mail .oe_msg_content.oe_msg_more_message .oe_separator{ - height: 0; - border-bottom: dashed 1px #e6e6e6; - margin-left: -4px; - margin-right: 8px; - margin-top: 6px; - margin-bottom: -9px; -} -.openerp .oe_mail .oe_msg_more_message .oe_msg_fetch_more { - background: white; - margin-right: 210px; - padding-left: 8px; - padding-right: 8px; - text-decoration: none; - color: #b4b4b4; -} -.openerp .oe_mail .oe_msg_more_message .oe_msg_fetch_more:hover{ - text-decoration: none; -} - -/* ---------------- FOLLOWERS ------------------ */ - -.openerp .oe_followers{ - position: relative; - display: inline-block; - padding-top: 5px; - width: 160px; - float: right; - margin-right: 16px; -} - -/* a) THE FOLLOW BUTTON */ - -.openerp .oe_followers button.oe_follower{ - display: block; - text-align: center; - width:135px; -} -.openerp .oe_followers button.oe_follower.oe_following{ - color: white; - background-color: #3465A4; - background-image: -webkit-linear-gradient(top, #729FCF, #3465A4); - background-image: -moz-linear-gradient(top, #729FCF, #3465A4); - background-image: -ms-linear-gradient(top, #729FCF, #3465A4); - background-image: -o-linear-gradient(top, #729FCF, #3465A4); - background-image: linear-gradient(to bottom, #729FCF, #3465A4); -} -.openerp .oe_followers button.oe_follower.oe_following:hover{ - color: white; - filter:none; - background-color: #A21A1A; - background-image: -webkit-linear-gradient(top, #DF3F3F, #A21A1A); - background-image: -moz-linear-gradient(top, #DF3F3F, #A21A1A); - background-image: -ms-linear-gradient(top, #DF3F3F, #A21A1A); - background-image: -o-linear-gradient(top, #DF3F3F, #A21A1A); - background-image: linear-gradient(to bottom, #DF3F3F, #A21A1A); -} - -.openerp .oe_followers button.oe_follower .oe_follow, -.openerp .oe_followers button.oe_follower .oe_unfollow, -.openerp .oe_followers button.oe_follower .oe_following{ - display: none; -} -/* a.1) when following, show 'following' */ -.openerp .oe_followers button.oe_follower.oe_following .oe_following{ - display: inline; -} -/* a.2) when following and hovering, show 'unfollow' */ -.openerp .oe_followers button.oe_follower.oe_following:hover .oe_following{ - display: none; -} -.openerp .oe_followers button.oe_follower.oe_following:hover .oe_unfollow{ - display: inline; -} -/* a.3) when not following show 'follow' */ -.openerp .oe_followers button.oe_follower.oe_notfollow .oe_follow{ - display: inline; -} - -.openerp .oe_followers .oe_subtype_list{ - margin-top: 4px; -} - -/* b) THE FOLLOWERS */ -.openerp .oe_followers .oe_follower_title{ - display: inline; -} -.openerp .oe_followers .oe_follower_title_box{ - margin-top: 12px; - margin-bottom: 4px; -} -.openerp .oe_followers .oe_invite{ - padding-left: 5px; -} -.openerp .oe_followers .oe_partner { - height: 32px; - margin-right: 24px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} -.openerp .oe_followers .oe_partner img{ - width: 32px; - margin-right:4px; - border-radius: 2px; -} -.openerp .oe_followers .oe_remove_follower{ - cursor: pointer; - position: absolute; - right: 0px; - line-height: 30px; -} -.openerp .oe_followers .oe_edit_subtype{ - cursor: pointer; - position: absolute; - right: 12px; - line-height: 30px; -} -.openerp .oe_followers .oe_partner .oe_hidden{ - display: none; -} -.openerp .oe_followers .oe_show_more{ - cursor: pointer; -} -/* ---------------- MESSAGES BODY ------------------ */ -.openerp .oe_mail .oe_msg_content .oe_blockquote, -.openerp .oe_mail .oe_msg_content blockquote { - padding: 4px; - border-radius: 2px; - border: solid 1px rgba(124,123,173,0.14); -} - -/* ----------- FORM INTEGRATION ------------ */ - -.openerp .oe_record_thread{ - display: block; - margin-left: 16px; - margin-right: 212px; -} - -/* ----------- INBOX INTEGRATION ----------- */ - -.openerp .oe_mail_wall .oe_mail{ - margin: 16px; - width: 600px; - display: inline-block; -} - -.openerp .oe_mail .oe_view_nocontent > p { - padding-left: 15px; -} + .oe-view-manager-view-timeline { + background-color: #f0eeee; + } -/* ------------- WALL SIDEBAR ------------- */ +/* ------------------- FORM INTEGRATION ------------------- */ -.openerp .oe_mail_wall .oe_mail_wall_aside { - margin: 16px; - position: relative; - display: inline-block; - vertical-align: top; - width: 260px; -} -.openerp .oe_mail_wall_aside .oe_sidebar_suggestion { - background-color: #EDEDF6; - border-radius: 2px; - padding-top: 1px; -} -.openerp .oe_sidebar_suggestion .oe_suggest_title h2 { - font-size: 14px; - font-weight: bold; - margin-left: 10px; - padding: 0px; -} -.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item { - border-radius: 2px; - width: 100%; - margin-left: 10px; - min-height: 67px; /* image_small 66x66px */ -} -.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_image { - float: left; - padding-right: 10px; -} -.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_image img { - border-radius: 2px; - border: solid 1px rgba(0,0,0,0.03); -} -.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_content button { - margin-top: 10px; -} -.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_content a.oe_suggestion_item_name { - text-overflow: ellipsis; - overflow: hidden; - width: 90%; -} -.openerp .oe_sidebar_suggestion .oe_suggest_title .oe_suggestion_remove { - line-height: 15px; - margin-top: -2px; - float: right; - visibility: hidden; - margin-right: 7px; -} -.openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_content .oe_suggestion_remove_item { - line-height: 21px; - margin-top: -2px; - float: right; - visibility: hidden; - margin-right: 16px; -} -.openerp .oe_sidebar_suggestion .oe_suggest_title:hover .oe_suggestion_remove, -.openerp .oe_sidebar_suggestion .oe_suggest_items:hover .oe_suggestion_remove_item { - visibility: visible; -} -.subtype-border { - border-bottom: 1px solid #dddddd; - margin-bottom: 10px; -} + .openerp .o_timeline_record_thread { + display: block; + z-index: 10; + } -.oe-view-manager-content .oe_mail { - display: inline-block !important; - position: relative !important; -} + .openerp .o_timeline_record_thread .o_timeline { + width: 100%; + } -.oe_record_thread > .oe_mail { - display: block !important; -} \ No newline at end of file + .o_timeline_record_thread > .o_timeline { + display: block !important; + } + + .oe-view-manager-view-form, + .openerp .oe_formview { + background: #f0eeee; + } + + +/* ----------------------- MESSAGE ----------------------- */ + + .openerp .o_timeline .o_timeline_msg { + position: relative; + background: #f0eeee; + min-height: 42px; + margin: 12px 0px 12px 0px; + padding: 0px 5px 0px 5px; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_msg { + padding-left : 15px; + } + + .openerp .o_timeline .oe_view_nocontent { + display: inline-block; + border-left: dashed 2px #A4A4A4; + width: 83%; + margin-left: 17%; + } + + /* -------------- parent message -------------- */ + + .openerp .o_timeline .o_timeline_thread_parent { + width: 850px; + margin-top: 2px; + vertical-align: middle; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message { + background-color: white; + border: solid 1px #CACACA; + margin-left: 16.2%; + padding: 3px; + white-space: nowrap; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_subject { + display: inline-block; + width: 442px; + cursor: pointer; + vertical-align: middle; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_subject.o_timeline_small_subject { + width: 411px; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_subject .o_timeline_subject { + display: inline-block; + max-width: 415px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_subject.o_timeline_small_subject .o_timeline_subject { + max-width: 405px; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_subject .o_timeline_nb_unread_msg { + font-weight: bold; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_author { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + width: 122px; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_action_author { + color: #337ab7; + } + + /* date */ + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_msg_date { + line-height: 15px; + } + + /* avatar */ + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_avatar { + left:0; + top: 0; + bottom: 0; + width: 30px; + overflow: hidden; + padding-right: 3px; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_avatar img { + border: solid 1px #CACACA; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_parent_avatar .o_timeline_msg_icon { + width: 25px; + border-radius: 2px; + } + + /* go to doc */ + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_go_to_doc { + display: inline-block; + width: 20px; + text-align: left; + margin-left: 10px; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_go_to_doc i { + text-decoration: none; + color: #AAA; + font-size: 1.2em; + padding-top: 1px; + cursor: pointer; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_go_to_doc i:hover { + font-size: 1.4em; + } + + /* followers */ + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_followers { + float: right; + margin-right: 15px; + margin-top: 3px; + color: #337ab7; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_followers i { + text-decoration: none; + color: #FFF; + font-size: 1.1em; + padding-left: 5px; + cursor: pointer; + text-shadow: 0px 1px #AAA,0px -1px #AAA, -1px 0px #AAA, 1px 0px #AAA, 0px 3px 3px rgba(0,0,0,0.1); + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; + } + + /* icons */ + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_msg_icons { + margin-top: -7px; + height: 8px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_msg_icons .oe_read, + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_msg_icons .oe_unread { + margin-top: 7px; + margin-right: 5px; + float: left; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_msg_icons .oe_reply { + margin-top: 3px; + float: right; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_msg_icons i { + width: 20px; + height: 20px; + line-height: 20px; + text-align: center; + text-decoration: none; + color: #FFF; + text-shadow: 0px 1px #AAA,0px -1px #AAA, -1px 0px #AAA, 1px 0px #AAA, 0px 3px 3px rgba(0,0,0,0.1); + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_msg_icons .oe_reply:hover { + color: #1fc0ff; + text-shadow: 0px 1px #184fc5,0px -1px #184fc5, -1px 0px #184fc5, 1px 0px #184fc5, 0px 3px 3px rgba(0,0,0,0.1); + } + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_msg_icons .oe_read:hover { + color: #bbbaff; + text-shadow: 0px 1px #7c7bad,0px -1px #7c7bad, -1px 0px #7c7bad, 1px 0px #7c7bad, 0px 3px 3px rgba(0,0,0,0.1); + } + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_parent_message .o_timeline_msg_icons .oe_unread:hover { + color: #c2ff00; + text-shadow: 0px 1px #009441,0px -1px #009441, -1px 0px #009441, 1px 0px #009441, 0px 3px 3px rgba(0,0,0,0.1); + } + + /* ------------ message composer ------------ */ + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_composer_compact { + display: inline-block; + position: relative; + background: #f0eeee; + width: 83%; + margin: 0px 0px 0px 0px; + margin-left: 17%; + border-bottom: solid 1px #C4C4C4; + border-left: dashed 2px #A4A4A4; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_composer.o_timeline_write_to_followers { + width: 700px; + margin: 0px; + } + + .openerp .o_timeline .o_timeline_thread_parent .o_timeline_msg.o_timeline_msg_composer_compact { + margin: 10px 0px 5px 0px; + margin-left: 18%; + padding: 0px; + border-bottom: none; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_composer .o_timeline_is_log { + width: 55%; + margin-bottom: 10px; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_composer_compact .o_timeline_compact { + width: 100%; + padding: 2px 4px; + color: #AAA; + cursor: text; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_composer_compact .o_timeline_compact_record { + text-align: left; + padding-bottom: 11px; + padding-left: 10px; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_composer_compact .o_timeline_compact_record .o_timeline_compose_log { + padding-left: 0px; + padding-right: 0px; + font-weight: bold; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_composer_compact .o_timeline_sep_word { + margin-left: 10px; + margin-right: 8px; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content textarea { + width: 95%; + height: 64px; + margin: 0px; + margin-bottom: 3px; + padding: 0px; + resize: vertical; + padding: 4px; + } + + .openerp .o_timeline .o_timeline_msg_footer .oe_post, + .openerp .o_timeline .o_timeline_msg_footer .oe_log { + margin: 0px 15px 0px 5px; + } + + .openerp .o_timeline .o_timeline_msg_center button.oe_full { + position: absolute; + width: 20px; + height: 22px; + overflow: hidden; + bottom: 0; + right: 0; + filter: none; + margin: 0px 3px 8px 0px; + } + + .openerp .o_timeline .o_timeline_msg_center button.oe_full .fa { + position: relative; + top: 2px; + margin-left: -5px; + vertical-align: top; + filter: none; + height: 14px; + font-size: 20px; + } + + .openerp .o_timeline button.oe_full, + .openerp .o_timeline button.oe_attach { + background: transparent; + color: #337ab7; + box-shadow: none; + border: none; + text-shadow: none; + filter:none; + } + + /* ------- message composer recipient ------- */ + + .openerp .o_timeline .o_timeline_list_recipients { + width: 50%; + font-size: 12px; + margin: 0px 0 10px 20px; + text-indent: -20px; + } + + .openerp .o_timeline .o_timeline_list_recipients a { + color: #337ab7; + } + + .openerp .o_timeline .o_timeline_msg_composer .o_timeline_recipients { + font-size: 12px; + margin: 2px 0px 0px 20px; + } + + .openerp .o_timeline .o_timeline_msg_composer .o_timeline_recipients label { + vertical-align: middle; + display: block; + line-height: 14px; + } + + .openerp .o_timeline .o_timeline_msg_composer .o_timeline_recipients input { + margin: 0px 0px 0px 0px; + vertical-align: top; + } + + /* ------ message composer attachments ------ */ + + .openerp .o_timeline .o_timeline_msg_composer .o_timeline_msg_attachment_list { + display: block; + } + + .oe_attachment { + display: inline-block; + width: 100px; + margin: 4px 2px; + min-height: 80px; + position: relative; + border-radius: 3px; + text-align: center; + vertical-align: top; + } + + .oe_attachment img { + position: absolute; + width: 48px; + height: 48px; + top: 0px; + left: 50%; + margin-left: -24px; + } + + .oe_attachment .oe_name { + display: inline-block; + max-width: 100%; + padding: 1px 3px; + margin-top: 50px; + margin-bottom: 5px; + background: #F2F7FF; + overflow: hidden; + color: #4c4c4c; + text-shadow: none; + border-radius: 3px; + } + + .oe_attachment.oe_preview { + background: url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAJ0lEQVQYV2MsLS39z4AGLCws0IUYGIeCwrVr12J45sSJE5ieGQIKAbuZKf/EMCs7AAAAAElFTkSuQmCC ); + } + + .oe_attachment.oe_preview img { + display: block; + position: relative; + margin:0px; + width: 100px; + height: 80px; + border-radius: 1px; + border: solid 3px #FFF; + margin-left: -50px; + box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.19); + } + + .oe_attachment .oe_progress_bar { + display: none; + position: absolute; + top: 18px; + left: 16px; + right: 16px; + height: 17px; + line-height: 13px; + padding: 0px; + background: #4BBD00; + color: white; + text-align: center; + border-radius: 3px; + border: solid 1px rgba(0,0,0,0.2); + box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.34); + -webkit-animation: oe_mail_attach_loading_anim 0.75s infinite linear; + -moz-animation: oe_mail_attach_loading_anim 0.75s infinite linear; + -o-animation: oe_mail_attach_loading_anim 0.75s infinite linear; + animation: oe_mail_attach_loading_anim 0.75s infinite linear; + } + + .oe_attachment.oe_uploading .oe_progress_bar { + display: block; + } + + @-webkit-keyframes oe_mail_attach_loading_anim { + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } + } + @-moz-keyframes oe_mail_attach_loading_anim { + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } + } + @-o-keyframes oe_mail_attach_loading_anim { + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } + } + @keyframes oe_mail_attach_loading_anim { + 0% { background: #4BBD00 } + 50% { background: #009123 } + 100% { background: #4BBD00 } + } + + .oe_attachment.oe_preview .oe_name { + position: absolute; + bottom: 0px; + margin: 3px; + left: 0px; + right: 0px; + max-height: 64px; + background: rgba(0,0,0,0.8); + color: white; + border-radius: 1px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } + + .oe_attachment.oe_preview:hover .oe_name { + opacity: 1; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } + + .openerp .o_timeline .o_timeline_msg_footer button.oe_attach { + width: 24px; + margin-bottom: 5px; + filter: none; + } + + .openerp .o_timeline .o_timeline_msg_footer button.oe_attach .fa { + position: relative; + top: 2px; + margin-left: -10px; + font-size: 1.4em; + vertical-align: middle; + filter:none; + } + + .openerp .o_timeline .o_timeline_msg_footer .oe_attachment_file { + cursor: pointer; + } + + .openerp .o_timeline .o_timeline_msg_footer .oe_attachment_file .oe_form_binary_file { + display: inline; + position: relative; + margin-left: -100px; + height: 30px; + width: 100px; + filter: none; + cursor: pointer; + } + + .openerp .o_timeline .oe_attach_label { + color: #337ab7; + margin-left: -3px; + height: 30px; + vertical-align: middle; + } + + .openerp .o_timeline .oe_hidden_input_file, + .openerp .o_timeline .oe_hidden_input_file form { + display: inline; + cursor: pointer; + } + + .openerp .o_timeline .oe_attachment .oe_delete .fa { + display: none; + } + + .openerp .o_timeline .o_timeline_msg_composer .oe_attachment .oe_delete .fa { + display: block; + position: absolute; + top: -7px; + right: 0px; + color: black; + text-shadow: 1px 0px white, -1px 0px white, 0px 1px white, 0px -1px white; + cursor: pointer; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } + + .openerp .o_timeline .o_timeline_msg_composer .oe_attachment:hover .oe_delete .fa { + opacity: 1; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } + + /* ------------ expandable message ------------ */ + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_expandable, + .openerp .o_timeline .o_timeline_parent_expandable { + color: #A4A4A4; + cursor: pointer; + margin-bottom: 0px; + min-height: 30px; + height: 30px; + } + + .openerp .o_timeline .o_timeline_msg_expandable span, + .openerp .o_timeline .o_timeline_parent_expandable span { + display: inline-block; + text-align: right; + padding-left: 24%; + } + + .openerp .o_timeline .o_timeline_msg_expandable span { + border-left: dashed 2px #A4A4A4; + } + + .openerp .o_timeline .o_timeline_parent_expandable { + padding-top: 10px; + } + + /* ----------------- message ----------------- */ + + .openerp .o_timeline .o_timeline_msg_message { + border-left: dashed 2px #A4A4A4; + } + + /* 1. date */ + + .openerp .o_timeline .o_timeline_msg_date { + text-align: center; + font-size: 0.9em; + } + + .openerp .o_timeline .o_timeline_msg_date span { + display: block; + background-color: white; + padding: 1px 10px 2px 10px; + -webkit-border-radius: 12px; + -moz-border-radius: 12px; + -ms-border-radius: 12px; + -o-border-radius: 12px; + border-radius: 12px; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_date { + margin-top: 0px; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_notification .o_timeline_msg_date span{ + background-color: #f0eeee; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_comment.o_timeline_msg_nobody .o_timeline_msg_date span, + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_email.o_timeline_msg_nobody .o_timeline_msg_date span { + background-color: #DADADA; + } + + /* 2. avatar (message_left) */ + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_left { + float: left; + left:0; + top: 0; + bottom: 0; + width: 45px; + overflow: hidden; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_left a, + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_left img { + border: 0; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_icon { + width: 40px; + border-radius: 2px; + } + + /* 3. message content */ + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content { + position: relative; + display: block; + overflow: hidden; + padding-left: 8px; + min-height: 42px; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_comment .o_timeline_msg_content, + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_email .o_timeline_msg_content { + -webkit-border-top-right-radius: 4px; + -moz-border-top-right-radius: 4px; + -ms-border-top-right-radius: 4px; + -o-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + -moz-border-bottom-right-radius: 4px; + -ms-border-bottom-right-radius: 4px; + -o-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + position: relative; + background: white; + margin-left: 53px; + padding: 5px 5px 11px 7px; + box-shadow: 0px 1px #CACACA; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_comment .o_timeline_msg_center:after, /*********/ + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_email .o_timeline_msg_center:after { /**arrow**/ + left: 48px; /*********/ + top: 17px; /*********/ + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-right-color: white; + border-width: 10px; + margin-top: -10px; + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_comment.o_timeline_msg_nobody .o_timeline_msg_content, + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_email.o_timeline_msg_nobody .o_timeline_msg_content { + background-color: #DADADA; + } + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_comment.o_timeline_msg_nobody .o_timeline_msg_center:after, + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_email.o_timeline_msg_nobody .o_timeline_msg_center:after { + border-right-color: #DADADA; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_action_author { + font-size: 1em; + color: #337ab7; + } + + /* message header */ + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header { + font-weight: normal; + margin-bottom: 11px; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header a { + color: #337ab7; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header .o_timeline_msg_header { + font-size: 0.8em; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header .o_timeline_msg_header .oe_sep { + margin-left: 5px; + margin-right: 5px; + } + + /* message header - votes */ + + .openerp .o_timeline .o_timeline_vote { + display: inline-block; + position: relative; + color: #337ab7; + cursor: pointer; + } + + .openerp .o_timeline .o_timeline_vote_count { + display: inline-block; + position: relative; + background: white; + box-shadow: 0px 0px 0px 1px rgba(124, 123, 173, 0.36) inset; + text-shadow: none; + border-radius: 3px; + margin: 0px; + padding: 2px 5px 0px 5px; + margin-right: 5px; + cursor: default; + } + + /* message header - icons */ + + .openerp .o_timeline .o_timeline_msg .o_timeline_header .o_timeline_msg_icons { + float: right; + margin-top: 1px; + margin-right: 8px; + margin-left: 8px; + height: 21px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_header .o_timeline_msg_icons span { + float:right; + width:21px; + height:21px; + line-height:21px; + text-align: center; + display: inline-block; + margin: 0px 5px 0px 5px; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_header .o_timeline_msg_icons i { + text-decoration: none; + color: #FFF; + text-shadow: 0px 1px #AAA,0px -1px #AAA, -1px 0px #AAA, 1px 0px #AAA, 0px 3px 3px rgba(0,0,0,0.1); + font-size: 1.15em; + margin-top: 2px; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; + } + + .openerp .o_timeline .o_timeline_msg:hover .o_timeline_header .o_timeline_msg_icons i{ + opacity: 1; + -webkit-transition: all 0.1s linear; + -moz-transition: all 0.1s linear; + -o-transition: all 0.1s linear; + transition: all 0.1s linear; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_header .o_timeline_msg_icons .oe_star:hover { + color: #FFF670; + text-shadow: 0px 1px #FFA162,0px -1px #FFA162, -1px 0px #FFA162, 1px 0px #FFA162, 0px 3px 3px rgba(0,0,0,0.1); + } + .openerp .o_timeline .o_timeline_msg .o_timeline_header .o_timeline_msg_icons .oe_star.oe_starred { + color: #FFE41F; + text-shadow: 0px 1px #DF6200,0px -1px #DF6200, -1px 0px #DF6200, 1px 0px #DF6200, 0px 3px 3px rgba(0,0,0,0.1); + } + .openerp .o_timeline .o_timeline_msg .o_timeline_header .o_timeline_msg_icons .oe_read:hover { + color: #bbbaff; + text-shadow: 0px 1px #7c7bad,0px -1px #7c7bad, -1px 0px #7c7bad, 1px 0px #7c7bad, 0px 3px 3px rgba(0,0,0,0.1); + } + .openerp .o_timeline .o_timeline_msg .o_timeline_header .o_timeline_msg_icons .oe_unread:hover { + color: #c2ff00; + text-shadow: 0px 1px #009441,0px -1px #009441, -1px 0px #009441, 1px 0px #009441, 0px 3px 3px rgba(0,0,0,0.1); + } + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_notification.o_timeline_msg_nobody .o_timeline_header .o_timeline_msg_icons { + padding-right: 5px; + } + + /* message body */ + + .openerp .o_timeline .o_timeline_msg.o_timeline_msg_notification .o_timeline_msg_body { + margin-left: 7px; + padding-left: 5px; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_msg_body p:first-of-type { + margin-top: 0px; + } + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_msg_body p { + margin-bottom: 0px; + } + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_msg_body pre { + white-space: pre-wrap; + } + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_msg_body blockquote { + font-size: 1em; + } + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_msg_body * { + text-overflow: ellipsis; + word-wrap: break-word; + } + + /* message footer - attachments */ + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_msg_footer .o_timeline_msg_attachment_list { + margin-top: 8px; + } + + /* message footer - recipients */ + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_msg_footer .o_timeline_partners_followers { + font-size: 0.8em; + margin-top: 15px; + } + + +/* ------------------- TRACKING VALUES ------------------- */ + + /* -------- tracking values content -------- */ + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_msg_body .o_timeline_tracking_value_list li { + line-height: 22px; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_msg_body .o_timeline_tracking_value_list .o_timeline_tracking_value { + font-weight: bold; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_msg_body .o_timeline_tracking_value_list i { + padding-left: 5px; + padding-right: 5px; + } + + /* --------- tracking value header --------- */ + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header .o_timeline_tracking_value_header { + margin-top: 10px; + padding-left: 12px; + margin-bottom: -5px; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header .o_timeline_msg_icons.o_timeline_msg_icons_track { + padding-right: 5px; + margin-top: -14px; + } + + .openerp .o_timeline .o_timeline_msg .o_timeline_msg_content .o_timeline_header .o_timeline_tracking_value_header .o_timeline_no_subtype { + font-size: 0.8em; + } + + +/* ---------------------- FOLLOWERS ---------------------- */ + + .openerp .o_timeline_followers { + position: relative; + display: inline-block; + width: 270px; + height: 0px; + float: right; + margin-right: 20px; + z-index: 1; + } + + .openerp .o_timeline_followers .oe_actions { + float: right; + position: relative; + } + + .openerp .o_timeline_followers button.o_timeline_follower { + background-color: white; + text-align: center; + width: 110px; + } + + .openerp .o_timeline_followers button.o_timeline_follower.o_timeline_following { + width: 110px; + color: #337ab7; + } + + .openerp .o_timeline_followers button.o_timeline_follower.o_timeline_following:hover { + width: 110px; + color: #D43F3A; + text-overflow: ellipsis; + } + + .openerp .o_timeline_followers button.o_timeline_follower .o_timeline_follow, + .openerp .o_timeline_followers button.o_timeline_follower .o_timeline_unfollow, + .openerp .o_timeline_followers button.o_timeline_follower .o_timeline_following { + display: none; + } + /* a.1) when following, show 'following' */ + .openerp .o_timeline_followers button.o_timeline_follower.o_timeline_following .o_timeline_following { + display: inline; + } + /* a.2) when following and hovering, show 'unfollow' */ + .openerp .o_timeline_followers button.o_timeline_follower.o_timeline_following:hover .o_timeline_following { + display: none; + } + .openerp .o_timeline_followers button.o_timeline_follower.o_timeline_following:hover .o_timeline_unfollow { + display: inline; + } + /* a.3) when not following show 'follow' */ + .openerp .o_timeline_followers button.o_timeline_follower.o_timeline_notfollow .o_timeline_follow { + display: inline; + } + + .openerp .o_timeline_followers .o_timeline_follower_title { + display: inline; + padding-right: 10px; + } + + .openerp .o_timeline_followers .o_timeline_follower_title_box { + position: relative; + float: right; + margin-left: 10px; + } + + .openerp .o_timeline_followers button.o_timeline_partner_list { + width: 110px; + } + + .openerp .o_timeline_followers .o_timeline_follower_list { + height: auto; + max-height: 350px; + overflow-x: hidden; + } + + .openerp .o_timeline_followers .oe_partner { + height: 32px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 120px; + } + + .openerp .o_timeline_followers .oe_partner img { + width: 32px; + margin-right: 4px; + border-radius: 2px; + } + + .openerp .o_timeline_followers .oe_partner a, + .openerp .o_timeline_followers .oe_invite a { + font-size: 0.95em; + color: #337ab7; + } + + .openerp .o_timeline_followers .oe_remove_follower { + cursor: pointer; + position: absolute; + right: 10px; + height: 25px; + vertical-align: middle; + } + + .openerp .o_timeline_followers .oe_remove_follower.fa-remove { + margin-top: 6px; + } + + .openerp .o_timeline_followers .oe_partner .oe_hidden { + display: none; + } + + .openerp .o_timeline_followers .o_timeline_sbtp_list { + margin-left: -5px; + } + + .openerp .o_timeline_followers .oe_subtype { + padding-left: 5px; + } + + +/* ----------------------- SIDEBAR ----------------------- */ + + .openerp .o_timeline .o_timeline_inbox_aside { + margin-left: 10px; + margin-top: 5px; + position: relative; + display: inline-block; + vertical-align: top; + width: 200px; + } + + .openerp .o_timeline .o_timeline_inbox_aside .oe_sidebar_suggestion { + background-color: #f0eeee; + border: solid 1px #CACACA; + border-radius: 3px; + padding-top: 1px; + padding-bottom: 7px; + margin-bottom: 20px + } + + .openerp .o_timeline .o_timeline_inbox_aside .oe_suggest_title span { + background-color: #f0eeee; + display: inline-block; + height: 25px; + vertical-align: middle; + font-size: 14px; + font-weight: bold; + margin-left: 10px; + margin-top: -20px; + padding-left: 5px; + padding-right: 5px; + } + + .openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item { + border-radius: 2px; + margin-left: 10px; + min-height: 67px; + white-space: nowrap; + } + + .openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_image { + float: left; + padding-right: 10px; + } + + .openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_image img { + border-radius: 2px; + border: solid 1px rgba(0,0,0,0.03); + } + + .openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_content button { + margin-top: 10px; + } + + .openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_content .oe_suggestion_item_name { + display: inline-block; + text-overflow: ellipsis; + overflow: hidden; + max-width: 110px; + } + + .openerp .o_timeline .o_timeline_inbox_aside a { + color: #337ab7; + } + + .openerp .oe_sidebar_suggestion .oe_suggest_title .oe_suggestion_remove { + line-height: 15px; + margin-top: -2px; + float: right; + visibility: hidden; + margin-right: 7px; + } + + .openerp .oe_sidebar_suggestion .oe_suggest_items .oe_suggested_item_content .oe_suggestion_remove_item { + line-height: 21px; + margin-top: -2px; + float: right; + visibility: hidden; + margin-right: 16px; + } + + .openerp .oe_sidebar_suggestion .oe_suggest_title:hover .oe_suggestion_remove, + .openerp .oe_sidebar_suggestion .oe_suggest_items:hover .oe_suggestion_remove_item { + visibility: visible; + } \ No newline at end of file diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index fecaa24f984..4872200ef15 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -5,275 +5,1058 @@ var mail_utils = require('mail.utils'); var core = require('web.core'); var data = require('web.data'); var form_common = require('web.form_common'); -var pyeval = require('web.pyeval'); -var SearchView = require('web.SearchView'); -var real_session = require('web.session'); +var session = require('web.session'); var SystrayMenu = require('web.SystrayMenu'); var time = require('web.time'); var utils = require('web.utils'); var web_client = require('web.web_client'); var Widget = require('web.Widget'); +var View = require('web.View'); var _t = core._t; -var qweb = core.qweb; +var QWeb = core.qweb; /** * ------------------------------------------------------------ - * MessageCommon + * TimelineRecordThread (mail_thread Widget) * ------------------------------------------------------------ - * - * Common base for expandables, chatter messages and composer. It manages - * the various variables common to those models. + * + * This widget instantiates the timeline view on a document. Its + * main use is to receive a context and a domain, then display the + * timeline view. */ +var TimelineRecordThread = form_common.AbstractField.extend ({ + className: 'o_timeline_record_thread', -var MessageCommon = Widget.extend({ + init: function (parent, node) { + this._super.apply(this, arguments); + + this.parent = parent; + this.dataset = new data.DataSet(this, 'mail.message', this.view.dataset.context); + this.domain = (this.node.params && this.node.params.domain) || (this.field && this.field.domain) || []; + + this.opts = _.extend({ + 'show_reply_button': false, + 'show_read_unread_button': true, + 'read_action': 'unread', + 'show_record_name': false, + 'show_compact_message': true, + 'compose_as_todo': false, + 'view_inbox': false, + 'view_mailbox': false, + 'emails_from_on_composer': true, + 'fetch_limit': 20, + 'readonly': this.node.attrs.readonly || false, + 'compose_placeholder' : this.node.attrs.placeholder || false, + 'display_log_button' : this.options.display_log_button || true, + 'show_compose_message': this.view.is_action_enabled('edit') || false, + 'show_link': this.parent.is_action_enabled('edit') || true, + }, this.node.params); + }, + + start: function () { + this.view.on("change:actual_mode", this, this._check_visibility); + this._check_visibility(); + + this.timeline_view = new TimelineView(this, this.dataset, false, this.opts); + this._super.apply(this, arguments); + + return this.timeline_view.appendTo(this.$el); + }, + + _check_visibility: function () { + this.$el.toggle(this.view.get("actual_mode") !== "create"); + }, + + render_value: function () { + if (! this.view.datarecord.id || data.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) { + return; + } + + this.timeline_view.set_message_ids(this.get_value()); + + var domain = (this.domain || []).concat([['model', '=', this.view.model], ['res_id', '=', this.view.datarecord.id]]); + var context = { + 'mail_read_set_read': true, + 'default_res_id': this.view.datarecord.id || false, + 'default_model': this.view.model || false, + }; + + return this.timeline_view.do_search(domain, context); + } +}); + +/** + * ------------------------------------------------------------ + * Timeline View + * ------------------------------------------------------------ + * + * This widget handles the display of messages on a document and in + * the inbox. Its main use is to receive a context and a domain, and + * to delegate the message fetching and displaying to the MailThread + * widget. + */ +var TimelineView = View.extend ({ + display_name: _t('Timeline'), + view_type: 'timeline', + template: 'TimelineWall', + + init: function (parent, dataset, view_id, options) { + this._super(parent, dataset, view_id, options); + + this.model = dataset.model; + this.domain = dataset.domain || []; + this.context = dataset.context || {}; + + var opt = _.clone(this.context.options); + this.options = options || {}; + this.options = _.extend(this.options, (opt || {})); + this.options = _.extend({ + 'show_link': true, + 'show_reply_button': true, + 'show_read_unread_button': true, + 'fetch_limit': 15, + 'fetch_child_limit': 5, + }, this.options); + + this.fields_view = {}; + this.fields_keys = []; + + this.qweb = new QWeb2.Engine(); + this.has_been_loaded = $.Deferred(); + }, + + start: function () { + var wall_sidebar = new Sidebar(this); + wall_sidebar.appendTo(this.$('.o_timeline_inbox_aside')); + return this._super.apply(this, arguments); + }, + + view_loading: function (fields_view_get) { + return this.load_timeline(fields_view_get); + }, + + load_timeline: function (fields_view_get) { + var self = this; + + this.fields_view = fields_view_get; + this.fields_keys = _.keys(this.fields_view.fields); + + this.add_qweb_template(); + this.has_been_loaded.resolve(); + }, + + do_show: function () { + this.do_push_state({}); + return this._super(); + }, + + do_search: function (dom, cxt, group_by) { + var self = this; + + var domain = this.domain.concat(dom || []); + var context = _.clone(this.context); + context = _.extend(context, (cxt || {})); + var ds = { + "domain" : domain, + "context" : context, + }; + var opts = _.clone(this.options); + + return this.has_been_loaded.then(function() { + if (self.thread) { + $('').insertAfter(self.thread.$el); + self.thread.destroy(); + } + self.thread = new MailThread(self, ds, opts); + self.thread.message_fetch(null, null, self.options.message_ids, self.options.view_inbox ? 1 : 0); + + return self.thread.replace(self.$('.o_timeline-placeholder')); + }); + }, + + do_reload_menu_emails: function () { + var menu = web_client.menu; + if (!menu || !menu.current_menu) { + return $.when(); + } + return menu.rpc("/web/menu/load_needaction", {'menu_ids': [menu.current_menu]}).done(function(r) { + menu.on_needaction_loaded(r); + }).then(function () { + menu.trigger("need_action_reloaded"); + }); + }, + + render_buttons: function($node) { + var self = this; + + this.$buttons = $(QWeb.render("TimelineView.buttons", {'widget': this})); + this.$buttons + .on('click', 'button.o_timeline_button_new', this.on_compose_message) + .on('click', '.o_timeline_share', function(event) { + self.write_to_followers(event, self); + }); + + $node = $node || this.options.$buttons; + if ($node) { + this.$buttons.appendTo($node); + } + else { + this.$('.o_timeline_buttons').replaceWith(this.$buttons); + } + }, + + add_qweb_template: function () { + for (var i = 0; i < this.fields_view.arch.children.length; i++) { + var child = this.fields_view.arch.children[i]; + if (child.tag === 'templates') { + this.qweb.add_template(utils.json_node_to_xml(child)); + break; + } + } + }, + + on_compose_message: function (event) { + event.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', + context: {}, + }); + }, + + write_to_followers: function (event, view) { + view.thread.instantiate_write_to_followers(event); + }, + + set_message_ids: function (val) { + this.options.message_ids = val; + }, +}); + +/** + * ------------------------------------------------------------ + * Attachment + * ------------------------------------------------------------ + * + * This widget handles some features about the attachments. Those + * features are common to the MailThread widget and ComposeMessage + * widget. + */ +var Attachment = Widget.extend ({ /** - * ------------------------------------------------------------ - * FIXME: this comment was moved as is from the ThreadMessage Init as - * part of a refactoring. Check that it is still correct - * ------------------------------------------------------------ - * This widget handles the display of a messages in a thread. - * Displays a record and performs some formatting on the record : - * - record.date: formatting according to the user timezone - * - record.timerelative: relative time givein by timeago lib - * - record.avatar: image url - * - record.attachment_ids[].url: url of each attachmentThe - * thread view : - * - root thread - * - - sub message (parent_id = root message) - * - - - sub thread - * - - - - sub sub message (parent id = sub thread) - * - - sub message (parent_id = root message) - * - - - sub thread + * Upload the file on the server and add in the attachments list */ - - init: function (parent, datasets, options) { - this._super(parent, options); - - // record options - this.options = datasets.options || options || {}; - // record domain and context - this.domain = datasets.domain || options.domain || []; - this.context = _.extend({ - default_model: false, - default_res_id: 0, - default_parent_id: false }, options.context || {}); - - // data of this message - this.id = datasets.id || false; - this.last_id = this.id; - this.model = datasets.model || this.context.default_model || false; - this.res_id = datasets.res_id || this.context.default_res_id || false; - this.parent_id = datasets.parent_id || false; - this.type = datasets.type || false; - this.subtype = datasets.subtype || false; - this.is_author = datasets.is_author || false; - this.author_avatar = datasets.author_avatar || false; - this.is_private = datasets.is_private || false; - this.subject = datasets.subject || false; - this.name = datasets.name || false; - this.record_name = datasets.record_name || false; - this.body = datasets.body || ''; - this.body_short = datasets.body_short || ''; - this.vote_nb = datasets.vote_nb || 0; - this.has_voted = datasets.has_voted || false; - this.is_favorite = datasets.is_favorite || false; - this.thread_level = datasets.thread_level || 0; - this.to_read = datasets.to_read || false; - this.author_id = datasets.author_id || false; - this.attachment_ids = datasets.attachment_ids || []; - this.partner_ids = datasets.partner_ids || []; - this.date = datasets.date; - this.user_pid = datasets.user_pid || false; - this.format_data(); - // update record_name: Partner profile - if (this.model == 'res.partner') { - this.record_name = 'Partner Profile of ' + this.record_name; - } - else if (this.model == 'hr.employee') { - this.record_name = 'News from ' + this.record_name; - } - // record options and data - this.show_record_name = this.options.show_record_name && this.record_name && !this.thread_level; - this.options.show_read = false; - this.options.show_unread = false; - if (this.options.show_read_unread_button) { - if (this.options.read_action == 'read') this.options.show_read = true; - else if (this.options.read_action == 'unread') this.options.show_unread = true; - else { - this.options.show_read = this.to_read; - this.options.show_unread = !this.to_read; - } - this.options.rerender = true; - this.options.toggle_read = true; - } - this.parent_thread = typeof parent.on_message_detroy == 'function' ? parent : this.options.root_thread; - this.thread = false; - }, - - /* Convert date, timerelative and avatar in displayable data. */ - format_data: function () { - - //formating and add some fields for render - this.date = this.date ? time.str_to_datetime(this.date) : false; - this.display_date = moment(new Date(this.date)).format('ddd MMM DD YYYY LT'); - if (this.date && new Date().getTime()-this.date.getTime() < 7*24*60*60*1000) { - this.timerelative = $.timeago(this.date); - } - if (this.author_avatar) { - this.avatar = "data:image/png;base64," + this.author_avatar; - } else if (this.type == 'email' && (!this.author_id || !this.author_id[0])) { - this.avatar = ('/mail/static/src/img/email_icon.png'); - } else if (this.author_id && this.template != 'mail.compose_message') { - this.avatar = mail_utils.get_image(this.session, 'res.partner', 'image_small', this.author_id[0]); - } else { - this.avatar = mail_utils.get_image(real_session, 'res.users', 'image_small', real_session.uid); - } - if (this.author_id && this.author_id[1]) { - var parsed_email = mail_utils.parse_email(this.author_id[1]); - this.author_id.push(parsed_email[0], parsed_email[1]); - } - if (this.partner_ids && this.partner_ids.length > 3) { - this.extra_partners_nbr = this.partner_ids.length - 3; - this.extra_partners_str = ''; - var extra_partners = this.partner_ids.slice(3); - for (var key in extra_partners) { - this.extra_partners_str += extra_partners[key][1]; - } - } - }, - - - /* upload the file on the server, add in the attachments list and reload display - */ - display_attachments: function () { - for (var l in this.attachment_ids) { - var attach = this.attachment_ids[l]; + format_attachments: function (message) { + for (var l in message.attachment_ids) { + var attach = message.attachment_ids[l]; if (!attach.formating) { - attach.url = mail_utils.get_attachment_url(real_session, this.id, attach.id); + attach.url = mail_utils.get_attachment_url(session, message.id, attach.id); attach.name = mail_utils.breakword(attach.name || attach.filename); attach.formating = true; } } - this.$(".oe_msg_attachment_list").html( qweb.render('mail.thread.message.attachments', {'widget': this}) ); + return message.attachment_ids; }, - /* return the link to resized image - */ - attachments_resize_image: function (id, resize) { - return mail_utils.get_image(real_session, 'ir.attachment', 'datas', id, resize); - }, - - /* get all child message id linked. - * @return array of id - */ - get_child_ids: function () { - return _.map(this.get_childs(), function (val) { return val.id; }); - }, - - /* get all child message linked. - * @return array of message object - */ - get_childs: function (nb_thread_level) { - var res=[]; - if (arguments[1] && this.id) res.push(this); - if ((isNaN(nb_thread_level) || nb_thread_level>0) && this.thread) { - _(this.thread.messages).each(function (val, key) { - res = res.concat( val.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) ); - }); - } - return res; + display_attachments: function (message) { + return QWeb.render('ThreadMessageAttachments', {'record': message, 'widget': this}); }, /** - * search a message in all thread and child thread. - * This method return an object message. - * @param {object}{int} option.id - * @param {object}{string} option.model - * @param {object}{boolean} option._go_thread_wall - * private for check the top thread - * @return thread object + * Return the link to resized image */ - browse_message: function (options) { - var i, res; - // goto the wall thread for launch browse - if (!options._go_thread_wall) { - options._go_thread_wall = true; - for (i in this.options.root_thread.messages) { - res=this.options.root_thread.messages[i].browse_message(options); - if (res) return res; - } + attachments_resize_image: function (id, resize) { + return mail_utils.get_image(session, 'ir.attachment', 'datas', id, resize); + }, +}); + +/** + * ------------------------------------------------------------ + * MailThread Widget + * ------------------------------------------------------------ + * + * This widget handles the display of a thread of messages. By + * default, a thread is folded in the inbox and unfolded on a + * document. In the inbox, a thread can be unfolded by clicking + * on it. + * When unfolded, the display of the threads looks like : + * - root thread + * - - thread 1 + * - - - message (parent_id = thread 1) + * - - thread 2 + * - - - message (parent_id = thread 2) + */ +var MailThread = Attachment.extend ({ + template: 'MailThread', + + events: { + 'click .o_timeline_msg .oe_read':'on_message_read', + 'click .o_timeline_msg .oe_unread':'on_message_unread', + 'click .o_timeline_msg .oe_star':'on_star', + 'click .o_timeline_parent_message .oe_reply':'on_message_reply', + 'click .o_timeline_parent_message .oe_read':'on_thread_read', + 'click .o_timeline_vote':'on_vote', + 'mouseenter .o_timeline_vote_count':'on_vote_count', + 'click .o_timeline_action_author':'on_record_author_clicked', + 'click .o_timeline_msg_expandable':'on_message_expandable', + 'click .o_timeline_parent_expandable':'on_thread_expandable', + 'click .o_timeline_parent_subject':'on_parent_message', + 'click .o_timeline_go_to_doc':'on_record_clicked', + 'mouseenter .oe_follwrs':'on_display_followers', + }, + + /** + * @param {Object} parent parent + * @param {Object} dataset + * @param {Object} root thread with context, domain, etc + * @param {Object} messages : {parent_id, list of messages} + * or {expandable} + * @param {Object} [options] + */ + init: function (parent, dataset, options) { + this._super(parent, dataset, options); + var self = this; + + this.domain = dataset.domain || []; + this.context = _.clone(dataset.context) || {}; + this.options = _.clone(options); + + this.is_ready = $.Deferred(); + this.root = parent instanceof TimelineView ? parent : false; + this.view = parent instanceof TimelineView ? parent : parent.view; + + // data of this thread : + // - list of messages in the thread, + // - parent message = first message of the thread + // - thread type : false (if root thread), parent or expandable + this.messages = dataset.messages ? dataset.messages : []; + this.parent_message = this.messages.length ? this.messages[this.messages.length - 1] : false; + this.thread_type = this.messages.length ? 'parent' : false; + if (this.messages.length && this.messages[0].message_type == 'expandable') { + this.thread_type = 'expandable'; } - if (this.id === options.id) - return this; + // thread represented by parent message data + this.id = this.parent_message.id || false; + this.parent_id = this.parent_message.id || false; + this.author_id = this.parent_message.author_id || false; + this.user_pid = this.parent_message.user_pid || false; + this.is_private = this.parent_message.is_private || false; + this.partner_ids = this.parent_message.partner_ids || []; + if (this.parent_message.author_id && !_.contains(_.flatten(this.parent_message.partner_ids),this.parent_message.author_id[0]) && this.parent_message.author_id[0]) { + this.partner_ids.push(this.parent_message.author_id); + } - for (i in this.thread.messages) { - if (this.thread.messages[i].thread) { - res=this.thread.messages[i].browse_message(options); - if (res) return res; + if (!this.root) { + this.context = _.extend(this.context, { + 'default_model': this.parent_message.model, + 'default_res_id': this.parent_message.res_id, + }); + } + + this.options.root_thread = this.options.root_thread != undefined ? this.options.root_thread : this; + this.options.show_compose_message = this.root ? false : this.options.root_thread.view.options.show_compose_message; + this.options.is_folded = this.options.view_inbox ? true : false; + this.options.toggle_read = true; + + // add some data to messages for the display + _.each(this.messages, function (msg) { + if (msg.message_type != "expandable") { + msg = self.format_message(msg); + } + }); + + this.model = this.parent_message.model || false; + this.res_id = this.parent_message.res_id || false; + this.record_name = this.parent_message.record_name || false; + this.subject = this.parent_message.subject || false; + + // add some data to thread for the display (followers, ...) + if (this.messages.length) { + this.format_thread().then(function () { + self.is_ready.resolve(); + }); + }else { + self.is_ready.resolve(); + } + + this.compose_message = false; + + this.ds_thread = new data.DataSetSearch(this, this.context.default_model || 'mail.thread'); + this.ds_message = new data.DataSetSearch(this, 'mail.message'); + }, + + start: function () { + var self = this; + + return this.is_ready.then(function () { + var $el = $(QWeb.render('MailThread', {'widget': self})); + self.replaceElement($el); + + if (self.options.show_compose_message) { + self.instantiate_compose_message(); + self.compose_message.do_show_compact(); + } + }); + }, + + /** + * instantiate the compose message object and insert this on the DOM + * (its place in the DOM depends on those cases : reply to thread in + * the inbox, write to followers in the inbox, compose new message on + * a document). The compose message is display in compact form. + */ + instantiate_compose_message: function (option) { + if (!this.compose_message) { + this.context = this.options.compose_as_todo ? _.extend({'default_starred': true}, this.context) : this.context; + this.compose_message = new ComposeMessage(this, this, this.options); + + if (this.options.view_inbox && option == 'reply') { + this.compose_message.insertAfter(this.$('.o_timeline_thread_content')); + } else { + if (this.options.view_inbox && option == 'write_to_followers') { + this.compose_message.prependTo(this.$el); + } else { + this.compose_message.insertBefore(this.$el); + } } } + }, + + /** + * instantiate the compose message and call the on_toggle_quick_composer + * method to allow the user to write his message. + */ + on_compose_message: function (event, option) { + if (this.compose_message) { + this.compose_message.destroy(); + } + this.compose_message = false; + + this.instantiate_compose_message(option); + return this.compose_message.on_toggle_quick_composer(event); + }, + + instantiate_write_to_followers: function (event) { + var self = this; + return this.on_compose_message(event, 'write_to_followers').then(function () { + self.$('.o_timeline_msg_composer').addClass('o_timeline_write_to_followers'); + }); + }, + + /** + * make a request to read the messages and set them 'read' if needed (calling + * RPC to "message_read_wrapper"). + * @param {Array} replace_domain: domain + * @param {Object} replace_context: context + * @param {Array} ids read (if the are some ids, the method don't use the domain) + */ + message_fetch: function (replace_domain, replace_context, ids, thread_level, callback) { + var self = this; + return this.ds_message.call('message_read_wrapper', [ + // ids force to read + ids === false ? undefined : ids && ids.slice(0, this.options.fetch_limit), + // domain + additional + (replace_domain ? replace_domain : this.domain), + // context + additional + (replace_context ? replace_context : this.context), + // thread_level + thread_level, + // parent_id + this.context.default_parent_id || undefined, + // limits + this.options.fetch_limit, + this.options.fetch_child_limit + ]).done(callback ? _.bind(callback, this, arguments) : this.proxy('treat_threads') + ).done(function (records) { + // records : { + // nb_reab : number of read messages (status from 'unread' to 'read'), + // threads : list of threads [[messages_thread1], [messages_thread2]] + // } + if (records.nb_read) { + self.options.root_thread.view.do_reload_menu_emails(); + } + }); + }, + + /** + * manage the list of threads recieved from message_fetch method + * and create a MailThread oject for each of them + */ + treat_threads: function (records) { + var self = this; + + _.each(records.threads, function (record) { + self.create_thread(record); + }); + + if (!records.threads.length) { + this.create_thread(); + } + }, + + create_thread: function (record) { + var thread = new MailThread(this, _.extend(_.clone(this), { + 'messages': record, + }), this.options); + + thread.appendTo(this.$el); + return thread; + }, + + /** + * set a message 'read' when click on Read in message + */ + on_message_read: function (event) { + var msg_id = $(event.target).data('id'); + var msg = _.findWhere(this.messages, {id: msg_id}); + event.stopPropagation(); + if (msg_id) { + this.on_message_read_unread([msg], true).then(function() { + $(event.target).replaceWith(QWeb.render("MessageReadUnread", {record: msg})); + }); + + } + }, + + /** + * set a message 'unread' when click on Unread in message + */ + on_message_unread: function (event) { + var msg_id = $(event.target).data('id'); + var msg = _.findWhere(this.messages, {id: msg_id}); + event.stopPropagation(); + if (msg_id) { + this.on_message_read_unread([msg], false).then(function() { + $(event.target).replaceWith(QWeb.render("MessageReadUnread", {record: msg})); + }); + } + }, + + /** + * set all the messages of this thread 'read' when click on + * Read in thread + */ + on_thread_read: function (event) { + var self = this; + + event.stopPropagation(); + this.on_message_read_unread(_.filter(this.messages, function (val) {return val.type != 'expandable'}), true).then(function () { + self.check_for_rerender(); + }); + }, + + /** + * Set the selected message(s) to 'read' or 'unread' + * @param {messages} array of messages + * @param {boolean} read_value + */ + on_message_read_unread: function (messages, read_value) { + var self = this; + + // inside the inbox, when the user mark a message as read/done, don't apply this value + // for the starred/favorite message + if (this.options.view_inbox && read_value) { + messages = _.filter(messages, function (val) {return !val.is_favorite && val.id;}); + if (!messages.length) { + return $.Deferred(); + } + } + var message_ids = _.map(messages, function (val) {return val.id;}); + return this.ds_message.call('set_message_read', [message_ids, read_value, true, this.context]).then(function (nb_read) { + // apply modification + _.each(messages, function (msg) { + msg.to_read = !read_value; + if (self.options.toggle_read) { + msg.options.show_read = msg.to_read; + msg.options.show_unread = !msg.to_read; + } + }); + }); + }, + + /** + * add or remove a favorite (or starred) for a message and change class on the DOM + */ + on_star: function (event) { + var self = this; + var msg_id = $(event.target).data('id'); + var msg = _.findWhere(this.messages, {id: msg_id}); + var button = self.$('.oe_star:first'); + event.stopPropagation(); + + this.ds_message.call('set_message_starred', [[msg_id], !msg.is_favorite, true]).then(function (star) { + msg.is_favorite = star; + if (msg.is_favorite) { + button.addClass('oe_starred'); + }else { + button.removeClass('oe_starred'); + } + + if (self.options.view_inbox && msg.is_favorite) { + self.on_message_read_unread([msg], true); + } + }); + return false; + }, + + /** + * instantiate ComposeMessage when click on reply + */ + on_message_reply:function (event) { + event.stopPropagation(); + this.on_compose_message(event, 'reply'); + return false; + }, + + /** + * Add or remove a vote for a message and display the result + */ + on_vote: function (event) { + var self = this; + var msg_id = $(event.target).data('id'); + var msg = _.findWhere(this.messages, {id: msg_id}); + event.stopPropagation(); + + this.ds_message.call('vote_toggle', [[msg_id]]).then(_.bind(function (vote) { + msg.has_voted = vote; + msg.vote_nb += msg.has_voted ? 1 : -1; + }, self)). then(function () { + $(event.target).html(QWeb.render("MessageVote", {record: msg})); + }); return false; }, /** - * call on_message_delete on his parent thread + * display users who voted for the message (tooltip) + */ + on_vote_count : function (event) { + var voter = ""; + var limit = 10; + var msg_id = $(event.target).data('id'); + event.stopPropagation(); + + var $target = $(event.target).hasClass("fa-thumbs-o-up") ? $(event.target).parent() : $(event.target); + // Note: We can set data-content attr on target element once we fetch data so that + // next time when one moves mouse on element it saves call + // But if there is new like comes then we'll not have new likes in popover in that case + if ($target.data('liker-list')) + { + voter = $target.data('liker-list'); + mail_utils.bindTooltipTo($target, voter, 'top'); + $target.tooltip('hide').tooltip('show'); + $(".tooltip").on("mouseleave", function () { + $(this).remove(); + }); + } else { + this.ds_message.call('get_likers_list', [msg_id, limit]).done(function (data) { + _.each(data, function(people, index) { + voter = voter + people.substring(0,1).toUpperCase() + people.substring(1); + if (index != data.length-1) { + voter = voter + "
"; + } + }); + $target.data('liker-list', voter); + mail_utils.bindTooltipTo($target, voter, 'top'); + $target.tooltip('hide').tooltip('show'); + $(".tooltip").on("mouseleave", function () { + $(this).remove(); + }); + }); + } + + return true; + }, + + /** + * go to form view of the message author + */ + on_record_author_clicked: function (event) { + event.preventDefault(); + var partner_id = $(event.target).data('partner'); + var record_name = $(event.target).data('record'); + var state = { + 'model': 'res.partner', + 'id': partner_id, + 'title': record_name + }; + + web_client.action_manager.do_push_state(state); + this.do_action({ + type:'ir.actions.act_window', + view_type: 'form', + view_mode: 'form', + res_model: 'res.partner', + views: [[false, 'form']], + res_id: partner_id, + }); + }, + + /** + * go to the document + */ + on_record_clicked: function (event) { + event.preventDefault(); + event.stopPropagation(); + + var self = this; + var state = { + 'model': this.context.default_model, + 'id': this.context.default_res_id, + 'title': this.record_name, + }; + + web_client.action_manager.do_push_state(state); + + this.context.params = { + model: this.context.default_model, + res_id: this.context.default_res_id, + }; + + this.ds_thread.call("message_redirect_action", {context: this.context}).then(function(action){ + self.do_action(action); + }); + }, + + /** + * load next messages when click on 'see more messages' (inside a thread) + */ + on_message_expandable: function (event) { + event.stopPropagation(); + + var self = this; + if (this.options.view_inbox) { + var parent_message = this.messages.pop(); + } + var msg = this.messages.pop(); + + this.message_fetch(msg.domain, this.context, false, 0, function (arg, data) { + var messages = data.threads[0]; + _.each(messages, function (message) { + self.add_message(message, true); + }); + if (this.options.view_inbox) { + this.messages.push(parent_message); + } + self.check_for_rerender(); + }); + }, + + /** + * load next threads when click on 'see more messages' + */ + on_thread_expandable: function (event) { + event.stopPropagation(); + var self = this; + var msg = this.messages.pop(); + + this.options.root_thread.message_fetch(msg.domain, this.context, false, 1).then(function () { + self.$el.remove(); + self.destroy(); + }); + }, + + /** + * unfolded the thread by rerender it */ - destroy: function () { + on_parent_message: function (event) { + event.stopPropagation(); + this.options.is_folded = !this.options.is_folded; + this.check_for_rerender(); + }, - this._super(); - this.parent_thread.on_message_detroy(this); + /** + * get and display the users following the document (tooltip) + */ + on_display_followers: function (event) { + var self = this; + var $target = $(event.target); + event.stopPropagation(); - } + if (this.followers_data) { + mail_utils.bindTooltipTo($target, this.followers_data, 'right'); + $target.tooltip('hide').tooltip('show'); + $(".tooltip").on("mouseleave", function () { + $(this).remove(); + }); + } + else { + var ds_model = new data.DataSetSearch(this, this.model, this.context); + ds_model.call('read_followers_data', [this.follower_ids.slice(0, this.options.followers_limit)]).then(function (records) { + self.followers_data = []; + _.each(records, function(f) { + self.followers_data.push(f[1] + '
'); + }); + if (self.nb_followers - self.followers_limit > 0) { + self.followers_data.push('and ' + self.nb_followers - self.followers_limit + 'more ...
'); + } + mail_utils.bindTooltipTo($target, self.followers_data, 'right'); + $target.tooltip('hide').tooltip('show'); + $(".tooltip").on("mouseleave", function () { + $(this).remove(); + }); + }); + } + }, + /** + * rerender this thread after some change + */ + check_for_rerender: function () { + var self = this; + + if (this.messages.length) { + this.is_ready = $.Deferred(); + this.format_thread().then(function () { + self.is_ready.resolve(); + }); + } + + this.is_ready.then(function () { + var $el = $(QWeb.render('MailThread', {'widget': self})); + self.replaceElement($el); + self.options.root_thread.view.do_reload_menu_emails(); + }); + }, + + /** + * add a message to this list of messages of this thread (after reply e.g.) + */ + add_message: function (message, to_end) { + message = this.format_message(message); + if (to_end) { + this.messages.push(message); + }else{ + this.messages.unshift(message); + } + }, + + /** + * insert a message in the DOM + */ + insert_message: function (message) { + message.content = this.view.qweb.render("timeline-message", {record: message}); + this.$('.oe_view_nocontent').remove(); + if (this.options.view_inbox && !this.root) { + $(message.content).insertAfter(this.$('.o_timeline_thread_content')); + }else{ + if (this.options.view_inbox && this.root) { + $(message.content).appendTo(this.$('.o_timeline_thread_parent:first')); + } else { + $(message.content).prependTo(this.$el); + } + } + }, + + /** + * add and convert thread data for display + */ + format_thread: function () { + var self = this; + + this.followers_loaded = $.Deferred(); + + if ((! this.nb_followers || !this.follower_ids) && (this.model && this.res_id)) { + this.nb_followers = 0; + this.follower_ids = []; + + this.ds_res = new data.DataSetSearch(this, this.model, this.context, [['id', '=', this.res_id]]); + this.ds_res.read_slice(['id', 'message_follower_ids']).then(function (data) { + self.follower_ids = data[0].message_follower_ids; + self.nb_followers = data[0].message_follower_ids.length; + self.followers_loaded.resolve(); + }); + } else { + this.followers_loaded.resolve(); + } + + return this.followers_loaded.then(function () { + var qweb_context = { + record: self.messages[0], + widget: self, + options: self.options, + }; + self.content = self.view.qweb.render("timeline-thread", qweb_context); + }); + }, + + /** + * add and convert message data for display + */ + format_message: function (msg) { + msg.tracking_values = msg.tracking_value_ids ? (msg.tracking_value_ids.length > 0) : false; + msg.partners = msg.partner_ids ? (msg.partner_ids.length > 0) : false; + msg.attachments = msg.attachment_ids ? (msg.attachment_ids.length > 0) : false; + msg.parent_id = msg.parent_id || false; + + msg.date = this.format_date(msg.date); + msg.author_avatar = this.format_avatar(msg.author_avatar, msg.author_id, msg.message_type); + msg.author_id = this.format_author(msg.author_id); + + if (msg.partners) { + msg.partners_nb = this.format_partners_nb(msg.partner_ids); + msg.partners_str = this.format_partners_str(msg.partner_ids); + } + + if (msg.attachments) { + msg.attachments_content = this.display_attachments(msg); + } + + if (msg.model == 'res.partner') { + msg.record_name = _.str.sprintf(_t('Partner Profile of ' + msg.record_name)); + }else{ + if (msg.model == 'hr.employee') { + msg.record_name = _.str.sprintf(_t('News from ' + msg.record_name)); + } + } + + msg.parent_thread = this; + + msg.options = _.clone(this.options); + msg.options.show_read = false; + msg.options.show_unread = false; + + if (this.options.show_read_unread_button) { + if (this.options.read_action == 'read') { + msg.options.show_read = true; + }else{ + if (this.options.read_action == 'unread') { + msg.options.show_unread = true; + }else { + msg.options.show_read = msg.to_read; + msg.options.show_unread = !msg.to_read; + } + } + } + return msg; + }, + + /** + * Convert date in displayable data. + */ + format_date: function (date) { + var d = date ? time.str_to_datetime(date) : false; + if (d) { + var display_date = moment(new Date(d).toISOString()).format('ddd MMM DD YYYY LT'); + date = display_date; + if (new Date().getTime()-d.getTime() < 7*24*60*60*1000) { + var timerelative = $.timeago(d); + date = timerelative; + } + } + return date; + }, + + /** + * Convert avatar in displayable data. + */ + format_avatar: function (avatar, author, type) { + var avt = false; + if (avatar) { + avt = "data:image/png;base64," + avatar; + }else if (type == 'email' && (!author || !author[0])) { + avt = ('/mail/static/src/img/email_icon.png'); + } + else if (author) { + avt = mail_utils.get_image( + session, 'res.partner', 'image_small', author[0]); + }else { + avt = mail_utils.get_image( + session, 'res.users', 'image_small', session.uid); + } + return avt; + }, + + format_author: function (author) { + if (author && author[1]) { + var parsed_email = mail_utils.parse_email(author[1]); + author.push(parsed_email[0], parsed_email[1]); + } + return author; + }, + + format_partners_nb: function (partners) { + return partners.length - 3; + }, + + format_partners_str: function (partners) { + if (partners && partners.length > 3) { + var extra_partners_nbr = this.format_partners_nb(partners); + var extra_partners_str = '' + var extra_partners = partners.slice(3); + + for (var key in extra_partners) { + extra_partners_str += extra_partners[key][1]; + } + + return extra_partners_str; + } + }, }); /** * ------------------------------------------------------------ * ComposeMessage widget * ------------------------------------------------------------ - * - * This widget handles the display of a form to compose a new message. - * This form is a mail.compose.message form_view. - * On first time : display a compact textarea that is not the compose form. - * When the user focuses the textarea, the compose message is instantiated. + * + * This widget handles the display to compose a new message. */ +var ComposeMessage = Attachment.extend ({ + template: 'ComposeMessage', -var ThreadComposeMessage = MessageCommon.extend({ - template: 'mail.compose_message', + events: { + 'click .o_timeline_compose_post':'on_toggle_quick_composer', + 'click .o_timeline_compose_log':'on_toggle_quick_composer', + 'click .oe_post':'on_message_post', + 'click .oe_full':'on_full', + 'click .o_timeline_msg_attachment_list .oe_delete': 'on_attachment_delete', + 'change input.oe_form_binary_file': 'on_attachment_change', + 'blur textarea':'on_toggle_quick_composer', + 'mousedown .o_timeline_msg_center':function (event) { + this.stay_open = true; + }, + 'mouseup .o_timeline_msg_content':function (event) { + this.$('textarea').focus(); + }, + 'change .o_timeline_recipients input':'on_checked_recipient', + }, - /** - * @param {Object} parent parent - * @param {Object} [options] - * @param {Object} [context] context passed to the - * mail.compose.message DataSetSearch. Please refer to this model - * for more details about fields and default values. - * @param {Object} recipients = [ - { - 'email_address': [str], - 'partner_id': False/[int], - 'name': [str], - 'full_name': name, - }, - { ... }, - ] - */ + init: function (parent, dataset, options) { + this._super(parent, dataset, options); + + this.domain = dataset.domain || []; + this.context = _.clone(dataset.context) || {}; + this.options = _.clone(options); - init: function (parent, datasets, options) { - this._super(parent, datasets, options); this.show_compact_message = false; this.show_delete_attachment = true; this.is_log = false; + + this.id = dataset.id || false; + this.user_pid = dataset.user_pid || false; + this.parent_id = dataset.parent_id || false; + this.is_private = dataset.is_private || false; + this.partner_ids = dataset.partner_ids || []; this.recipients = []; this.recipient_ids = []; - core.bus.on('clear_uncommitted_changes', this, function(e) { - if (this.show_composer && !e.isDefaultPrevented()){ + this.attachment_ids = []; + + this.parent_thread = parent; + this.view = parent.view; + this.session = session; + + core.bus.on('clear_uncommitted_changes', this, function (e) { + if (this.show_composer && !e.isDefaultPrevented()) { if (!confirm(_t("You are currently composing a message, your message will be discarded.\n\nAre you sure you want to leave this page ?"))) { e.preventDefault(); } - else{ + else { this.on_cancel(); } } @@ -281,207 +1064,183 @@ var ThreadComposeMessage = MessageCommon.extend({ }, start: function () { - this._super.apply(this, arguments); - this.ds_attachment = new data.DataSetSearch(this, 'ir.attachment'); this.fileupload_id = _.uniqueId('oe_fileupload_temp'); $(window).on(this.fileupload_id, this.on_attachment_loaded); - - this.display_attachments(); + this.$(".o_timeline_msg_attachment_list").html(this.display_attachments(this)); this.bind_events(); + return this._super.apply(this, arguments); }, - /* when a user click on the upload button, send file read on_attachment_loaded - */ - on_attachment_change: function (event) { - event.stopPropagation(); - var $target = $(event.target); - if ($target.val() !== '') { - - var filename = $target.val().replace(/.*[\\\/]/,''); - - // if the files exits for this answer, delete the file before upload - var attachments=[]; - for (var i in this.attachment_ids) { - if ((this.attachment_ids[i].filename || this.attachment_ids[i].name) == filename) { - if (this.attachment_ids[i].upload) { - return false; - } - this.ds_attachment.unlink([this.attachment_ids[i].id]); - } else { - attachments.push(this.attachment_ids[i]); - } - } - this.attachment_ids = attachments; - - // submit file - this.$('form.oe_form_binary_form').submit(); - - this.$(".oe_attachment_file").hide(); - - this.attachment_ids.push({ - 'id': 0, - 'name': filename, - 'filename': filename, - 'url': '', - 'upload': true - }); - this.display_attachments(); - } - }, - - /* when the file is uploaded - */ - on_attachment_loaded: function (event, result) { - - if (result.erorr || !result.id ) { - this.do_warn( qweb.render('mail.error_upload'), result.error); - this.attachment_ids = _.filter(this.attachment_ids, function (val) { return !val.upload; }); - } else { - for (var i in this.attachment_ids) { - if (this.attachment_ids[i].filename == result.filename && this.attachment_ids[i].upload) { - this.attachment_ids[i]={ - 'id': result.id, - 'name': result.name, - 'filename': result.filename, - 'url': mail_utils.get_attachment_url(this.session, this.id, result.id) - }; - } - } - } - this.display_attachments(); - var $input = this.$('input.oe_form_binary_file'); - $input.after($input.clone(true)).remove(); - this.$(".oe_attachment_file").show(); - }, - - /* unlink the file on the server and reload display - */ - on_attachment_delete: function (event) { - event.stopPropagation(); - var attachment_id=$(event.target).data("id"); - if (attachment_id) { - var attachments=[]; - for (var i in this.attachment_ids) { - if (attachment_id!=this.attachment_ids[i].id) { - attachments.push(this.attachment_ids[i]); - } - else { - this.ds_attachment.unlink([attachment_id]); - } - } - this.attachment_ids = attachments; - this.display_attachments(); - } + reinit: function () { + var $render = $(QWeb.render('ComposeMessage', {'widget': this})); + this.replaceElement($render); + this.$(".o_timeline_msg_attachment_list").html(this.display_attachments(this)); + this.bind_events(); }, bind_events: function () { var self = this; - this.$('.oe_compact_inbox').on('click', self.on_toggle_quick_composer); - this.$('.oe_compose_post').on('click', self.on_toggle_quick_composer); - this.$('.oe_compose_log').on('click', self.on_toggle_quick_composer); - this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this)); - this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this)); - this.$('.oe_post').on('click', self.on_message_post); - this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment')); - /* stack for don't close the compose form if the user click on a button */ - this.$('.oe_msg_left, .oe_msg_center').on('mousedown', _.bind( function () { this.stay_open = true; }, this)); - this.$('.oe_msg_left, .oe_msg_content').on('mouseup', _.bind( function () { this.$('textarea').focus(); }, this)); var ev_stay = {}; - ev_stay.mouseup = ev_stay.keydown = ev_stay.focus = function () { self.stay_open = false; }; + ev_stay.mouseup = ev_stay.keydown = ev_stay.focus = function () {self.stay_open = false;}; this.$('textarea').on(ev_stay); this.$('textarea').autosize(); - - // auto close - this.$('textarea').on('blur', self.on_toggle_quick_composer); - - // event: delete child attachments off the oe_msg_attachment_list box - this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete); - - this.$(".oe_recipients").on('change', 'input', this.on_checked_recipient); - }, - - on_compose_fullmail: function (default_composition_mode) { - var self = this; - if(!this.do_check_attachment_upload()) { - return false; - } - var recipient_done = $.Deferred(); - if (this.is_log) { - recipient_done.resolve([]); - } - else { - recipient_done = this.check_recipient_partners(); - } - $.when(recipient_done).done(function (partner_ids) { - var context = { - 'default_parent_id': self.id, - 'default_body': mail_utils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''), - 'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}), - 'default_partner_ids': partner_ids, - 'default_is_log': self.is_log, - 'mail_post_autofollow': true, - 'mail_post_autofollow_partner_ids': partner_ids, - 'is_private': self.is_private - }; - if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) { - context.default_model = self.context.default_model; - context.default_res_id = self.context.default_res_id; - } - - var action = { - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - view_mode: 'form', - view_type: 'form', - views: [[false, 'form']], - target: 'new', - context: context, - }; - - self.do_action(action); - self.on_cancel(); - }); - - }, - - reinit: function() { - var $render = $( qweb.render('mail.compose_message', {'widget': this}) ); - - $render.insertAfter(this.$el.last()); - this.$el.remove(); - this.$el = $render; - - this.display_attachments(); - this.bind_events(); }, on_cancel: function (event) { if (event) event.stopPropagation(); - this.attachment_ids=[]; + this.attachment_ids = []; this.stay_open = false; this.show_composer = false; this.reinit(); }, - /* return true if all file are complete else return false and make an alert */ - do_check_attachment_upload: function () { - if (_.find(this.attachment_ids, function (file) {return file.upload;})) { - this.do_warn(qweb.render('mail.error_upload'), qweb.render('mail.error_upload_please_wait')); - return false; + /** + * compute the list of unknown email_from the the given thread + */ + compute_emails_from: function () { + var self = this; + var messages = this.parent_thread.messages; + _.each(messages, function (thread) { + if (thread.author_id && !thread.author_id[0] && + !_.find(self.recipients, function (recipient) {return recipient.email_address == thread.author_id[3];})) { + self.recipients.push({'full_name': thread.author_id[1], + 'name': thread.author_id[2], + 'email_address': thread.author_id[3], + 'partner_id': false, + 'checked': true, + 'reason': 'Incoming email author' + }); + } + }); + return self.recipients; + }, + + /** + * Quick composer: toggle minimal / expanded mode + * - toggle minimal (one-liner) / expanded (textarea, buttons) mode + * - when going into expanded mode: + * - call `message_get_suggested_recipients` to have a list of partners to add + * - compute email_from list (list of unknown email_from to propose to create partners) + */ + on_toggle_quick_composer: function (event) { + var self = this; + var $input = $(event.target); + + this.compute_emails_from(); + var email_addresses = _.pluck(this.recipients, 'email_address'); + + var suggested_partners = $.Deferred(); + + // if clicked: call for suggested recipients + if (event.type == 'click') { + this.is_log = $input.hasClass('o_timeline_compose_log'); + suggested_partners = this.parent_thread.ds_thread.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context]).done(function (additional_recipients) { + var thread_recipients = additional_recipients[self.context.default_res_id]; + + _.each(thread_recipients, function (recipient) { + var parsed_email = mail_utils.parse_email(recipient[1]); + if (_.indexOf(email_addresses, parsed_email[1]) == -1) { + self.recipients.push({ + 'checked': true, + 'partner_id': recipient[0], + 'full_name': recipient[1], + 'name': parsed_email[0], + 'email_address': parsed_email[1], + 'reason': recipient[2], + }) + } + }); + }); } else { - return true; + suggested_partners.resolve({}); } + + // when call for suggested partners finished: re-render the widget + $.when(suggested_partners).pipe(function (additional_recipients) { + if ((!self.stay_open || (event && event.type == 'click')) + && (!self.show_composer || !self.$('textarea:not(.o_timeline_compact)').val().match(/\S+/) + && !self.attachment_ids.length)) { + self.show_composer = !self.show_composer || self.stay_open; + self.reinit(); + } + if (!self.stay_open && self.show_composer && (!event || event.type != 'blur')) { + self.$('textarea:not(.o_timeline_compact):first').focus(); + } + }); + + return suggested_partners; + }, + + on_message_post: function (event) { + if (this.flag_post){ + return; + } + + var self = this; + this.flag_post = true; + if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) { + if (this.is_log) { + this.do_send_message_post([], this.is_log); + } else { + this.check_recipient_partners().done(function (partner_ids) { + self.do_send_message_post(partner_ids, self.is_log); + }); + } + } + }, + + /** + * post a message and fetch the message + */ + do_send_message_post: function (partner_ids, log) { + var self = this; + + var values = { + 'body': this.$('textarea').val(), + 'subject': false, + 'parent_id': this.parent_id || this.id, + 'partner_ids': partner_ids, + 'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}), + 'context': _.extend(this.parent_thread.context, { + 'mail_post_autofollow': true, + 'mail_post_autofollow_partner_ids': partner_ids, + }), + 'message_type': 'comment', + 'content_subtype': 'plaintext', + }; + + if(log){ + values.subtype = false; + }else{ + values.subtype = 'mail.mt_comment'; + } + + this.parent_thread.ds_thread._model.call('message_post', [this.context.default_res_id], values).done(function (message_id) { + var thread = self.parent_thread; + + // attach message to the thread object + thread.message_fetch([["id", "=", message_id]], false, [message_id], 0, function (arg, data) { + var message = data.threads[0][0]; + thread.add_message(message, false); + thread.insert_message(message); + }); + + self.on_cancel(); + self.flag_post = false; + }); }, check_recipient_partners: function () { var self = this; var check_done = $.Deferred(); + var recipients = _.filter(this.recipients, function (recipient) { return recipient.checked; }); var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id); }); var names_to_find = _.pluck(recipients_to_find, 'full_name'); var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); }); var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id'); + var names_to_remove = []; var recipient_ids_to_remove = []; @@ -555,164 +1314,55 @@ var ThreadComposeMessage = MessageCommon.extend({ return check_done; }, - on_message_post: function () { - var self = this; - if (self.flag_post) { - return; - } - if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) { - self.flag_post = true; - if (this.is_log) { - this.do_send_message_post([], this.is_log); - } - else { - this.check_recipient_partners().done(function (partner_ids) { - self.do_send_message_post(partner_ids, self.is_log); - }); - } - } + on_full: function () { + return this.on_compose_fullmail(this.id ? 'reply' : 'comment'); }, - /* do post a message and fetch the message */ - do_send_message_post: function (partner_ids, log) { + on_compose_fullmail: function (default_composition_mode) { + if (!this.do_check_attachment_upload()) + return false; + var self = this; - var values = { - 'body': this.$('textarea').val(), - 'subject': false, - 'parent_id': this.context.default_parent_id, - 'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}), - 'partner_ids': partner_ids, - 'context': _.extend(this.parent_thread.context, { + var recipient_done = $.Deferred(); + + if (this.is_log){ + recipient_done.resolve([]); + }else{ + recipient_done = this.check_recipient_partners(); + } + + $.when(recipient_done).done(function (partner_ids) { + var context = { + 'default_parent_id': self.id, + 'default_body': mail_utils.get_text2html( + self.$el ? (self.$el.find('textarea:not(.o_timeline_compact)').val() || '') : ''), + 'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}), + 'default_partner_ids': partner_ids, + 'default_is_log': self.is_log, 'mail_post_autofollow': true, 'mail_post_autofollow_partner_ids': partner_ids, - }), - 'message_type': 'comment', - 'content_subtype': 'plaintext', - }; - if (log) { - values.subtype = false; - } - else { - values.subtype = 'mail.mt_comment'; - } - this.parent_thread.ds_thread._model.call('message_post', [this.context.default_res_id], values).done(function (message_id) { - var thread = self.parent_thread; - var root = thread == self.options.root_thread; - if (self.options.display_indented_thread < self.thread_level && thread.parent_message) { - thread = thread.parent_message.parent_thread; + 'is_private': self.is_private, + }; + + if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) { + context.default_model = self.context.default_model; + context.default_res_id = self.context.default_res_id; } - // create object and attach to the thread object - thread.message_fetch([["id", "=", message_id]], false, [message_id], function (arg, data) { - var message = thread.create_message_object( data.slice(-1)[0] ); - // insert the message on dom - thread.insert_message( message, root ? undefined : self.$el, root ); - }); + + var action = { + type: 'ir.actions.act_window', + res_model: 'mail.compose.message', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + target: 'new', + context: context, + }; + self.do_action(action); self.on_cancel(); - self.flag_post = false; }); }, - /* Quick composer: toggle minimal / expanded mode - * - toggle minimal (one-liner) / expanded (textarea, buttons) mode - * - when going into expanded mode: - * - call `message_get_suggested_recipients` to have a list of partners to add - * - compute email_from list (list of unknown email_from to propose to create partners) - */ - on_toggle_quick_composer: function (event) { - var self = this; - var $input = $(event.target); - this.compute_emails_from(); - var email_addresses = _.pluck(this.recipients, 'email_address'); - var suggested_partners = $.Deferred(); - - // if clicked: call for suggested recipients - if (event.type == 'click') { - this.is_log = $input.hasClass('oe_compose_log'); - suggested_partners = this.parent_thread.ds_thread.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context]).done(function (additional_recipients) { - var thread_recipients = additional_recipients[self.context.default_res_id]; - _.each(thread_recipients, function (recipient) { - var parsed_email = mail_utils.parse_email(recipient[1]); - if (_.indexOf(email_addresses, parsed_email[1]) == -1) { - self.recipients.push({ - 'checked': true, - 'partner_id': recipient[0], - 'full_name': recipient[1], - 'name': parsed_email[0], - 'email_address': parsed_email[1], - 'reason': recipient[2], - }); - } - }); - }); - } - else { - suggested_partners.resolve({}); - } - - // when call for suggested partners finished: re-render the widget - $.when(suggested_partners).pipe(function () { - if ((!self.stay_open || (event && event.type == 'click')) && (!self.show_composer || !self.$('textarea:not(.oe_compact)').val().match(/\S+/) && !self.attachment_ids.length)) { - self.show_composer = !self.show_composer || self.stay_open; - self.reinit(); - } - if (!self.stay_open && self.show_composer && (!event || event.type != 'blur')) { - self.$('textarea:not(.oe_compact):first').focus(); - } - }); - - return suggested_partners; - - }, - - do_hide_compact: function () { - this.show_compact_message = false; - if (!this.show_composer) { - this.reinit(); - } - }, - - do_show_compact: function () { - this.show_compact_message = true; - if (!this.show_composer) { - this.reinit(); - } - }, - - /** Compute the list of unknown email_from the the given thread - * TDE FIXME: seems odd to delegate to the composer - * TDE TODO: please de-obfuscate and comment your code */ - compute_emails_from: function () { - var self = this; - var messages = []; - - if (this.parent_thread.parent_message) { - // go to the parented message - var message = this.parent_thread.parent_message; - var parent_message = message.parent_id ? message.parent_thread.parent_message : message; - if(parent_message){ - messages = [parent_message].concat(parent_message.get_childs()); - } - } else if (this.options.emails_from_on_composer) { - // get all wall messages if is not a mail.Wall - _.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());}); - } - - _.each(messages, function (thread) { - if (thread.author_id && !thread.author_id[0] && - !_.find(self.recipients, function (recipient) {return recipient.email_address == thread.author_id[3];}) && - _.some([thread.author_id[1], thread.author_id[2], thread.author_id[3]])) { - self.recipients.push({ 'full_name': thread.author_id[1], - 'name': thread.author_id[2], - 'email_address': thread.author_id[3], - 'partner_id': false, - 'checked': true, - 'reason': 'Incoming email author' - }); - } - }); - return self.recipients; - }, - on_checked_recipient: function (event) { var $input = $(event.target); var full_name = $input.attr("data"); @@ -722,1239 +1372,128 @@ var ThreadComposeMessage = MessageCommon.extend({ } }); }, -}); - -/** - * ------------------------------------------------------------ - * Thread Message Expandable Widget - * ------------------------------------------------------------ - * - * This widget handles the display the expandable message in a thread. - * - thread - * - - visible message - * - - expandable - * - - visible message - * - - visible message - * - - expandable - */ -var ThreadExpandable = MessageCommon.extend({ - template: 'mail.thread.expandable', - - init: function (parent, datasets, options) { - this._super(parent, datasets, options); - this.type = 'expandable'; - this.max_limit = datasets.max_limit; - this.nb_messages = datasets.nb_messages; - this.flag_used = false; - }, - - start: function () { - this._super.apply(this, arguments); - this.bind_events(); - }, - - reinit: function () { - var $render = $(qweb.render('mail.thread.expandable', {'widget': this})); - this.$el.replaceWith( $render ); - this.$el = $render; - this.bind_events(); - }, /** - * Bind events in the widget. Each event is slightly described - * in the function. */ - bind_events: function () { - this.$('.oe_msg_more_message').on('click', this.on_expandable); - }, - - animated_destroy: function (fadeTime) { - var self=this; - this.$el.fadeOut(fadeTime, function () { - self.destroy(); - }); - }, - - /*The selected thread and all childs (messages/thread) became read - * @param {object} mouse envent - */ - on_expandable: function (event) { - var self = this; - if (event)event.stopPropagation(); - if (this.flag_used) { - return false; - } - this.flag_used = true; - - // read messages - self.parent_thread.message_fetch(this.domain, this.context, false, function (arg, data) { - self.id = false; - // insert the message on dom after this message - var element = self.$el; - if (self.thread_level === 0){ - element = element.parent(); - } - self.parent_thread.switch_new_message(data, element); - self.animated_destroy(200); - }); - - return false; - }, - -}); - -var ThreadMessage = MessageCommon.extend({ - template: 'mail.thread.message', - - start: function () { - this._super.apply(this, arguments); - this.bind_events(); - if(this.thread_level < this.options.display_indented_thread) { - this.create_thread(); - } - this.display_attachments(); - - this.ds_notification = new data.DataSetSearch(this, 'mail.notification'); - this.ds_message = new data.DataSetSearch(this, 'mail.message'); - }, - - /** - * Bind events in the widget. Each event is slightly described - * in the function. */ - bind_events: function () { - // header icons bindings - this.$('.oe_read').on('click', this.on_message_read); - this.$('.oe_unread').on('click', this.on_message_unread); - this.$('.oe_msg_delete').on('click', this.on_message_delete); - this.$('.oe_reply').on('click', this.on_message_reply); - this.$('.oe_star').on('click', this.on_star); - this.$('.oe_msg_vote').on('click', this.on_vote); - this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover); - this.$('.oe_mail_expand').on('click', this.on_expand); - this.$('.oe_mail_reduce').on('click', this.on_expand); - this.$('.oe_mail_action_model').on('click', this.on_record_clicked); - this.$('.oe_mail_action_author').on('click', this.on_record_author_clicked); - }, - on_hover : function(event){ - var self = this; - var voter = ""; - var limit = 10; + * when a user click on the upload button, send file read on_attachment_loaded + */ + on_attachment_change: function (event) { event.stopPropagation(); - var $target = $(event.target).hasClass("fa-thumbs-o-up") ? $(event.target).parent() : $(event.target); - //Note: We can set data-content attr on target element once we fetch data so that next time when one moves mouse on element it saves call - //But if there is new like comes then we'll not have new likes in popover in that case - if ($target.data('liker-list')) - { - voter = $target.data('liker-list'); - self.bindTooltipTo($target, voter); - $target.tooltip('hide').tooltip('show'); - $(".tooltip").on("mouseleave", function () { - $(this).remove(); - }); - }else{ - this.ds_message.call('get_likers_list', [this.id, limit]) - .done(function (data) { - _.each(data, function(people, index) { - voter = voter + people.substring(0,1).toUpperCase() + people.substring(1); - if(index != data.length-1) { - voter = voter + "
"; + var $target = $(event.target); + + if ($target.val() !== '') { + var filename = $target.val().replace(/.*[\\\/]/,''); + + // if the files exits for this answer, delete the file before upload + var attachments = []; + for (var i in this.attachment_ids) { + if ((this.attachment_ids[i].filename || this.attachment_ids[i].name) == filename) { + if (this.attachment_ids[i].upload) { + return false; } - }); - $target.data('liker-list', voter); - self.bindTooltipTo($target, voter); - $target.tooltip('hide').tooltip('show'); - $(".tooltip").on("mouseleave", function () { - $(this).remove(); - }); + this.ds_attachment.unlink([this.attachment_ids[i].id]); + } else { + attachments.push(this.attachment_ids[i]); + } + } + + this.attachment_ids = attachments; + + // submit file + this.$('form.oe_form_binary_form').submit(); + this.$(".oe_attachment_file").hide(); + + this.attachment_ids.push({ + 'id': 0, + 'name': filename, + 'filename': filename, + 'url': '', + 'upload': true }); + + this.$(".o_timeline_msg_attachment_list").html(this.display_attachments(this)); + } + }, + + /** + * when the file is uploaded + */ + on_attachment_loaded: function (event, result) { + if (result.erorr || !result.id ) { + this.do_warn(QWeb.render('mail.error_upload'), result.error); + this.attachment_ids = _.filter(this.attachment_ids, function (val) { return !val.upload; }); + } + else { + for (var i in this.attachment_ids) { + if (this.attachment_ids[i].filename == result.filename && this.attachment_ids[i].upload) { + this.attachment_ids[i] = { + 'id': result.id, + 'name': result.name, + 'filename': result.filename, + 'url': mail_utils.get_attachment_url(session, this.id, result.id) + }; + } + } + } + + this.$(".o_timeline_msg_attachment_list").html(this.display_attachments(this)); + + var $input = this.$('input.oe_form_binary_file'); + $input.after($input.clone(true)).remove(); + this.$(".oe_attachment_file").show(); + }, + + /** + * Unlink the file on the server and reload display + */ + on_attachment_delete: function (event) { + event.stopPropagation(); + var attachment_id = $(event.target).data("id"); + + if (attachment_id) { + var attachments = []; + for (var i in this.attachment_ids) { + if (attachment_id != this.attachment_ids[i].id) { + attachments.push(this.attachment_ids[i]); + } + else { + this.ds_attachment.unlink([attachment_id]); + } + } + this.attachment_ids = attachments; + this.$(".o_timeline_msg_attachment_list").html(this.display_attachments(this)); + } + }, + + /** + * return true if all file are complete else return false and make an alert + */ + do_check_attachment_upload: function () { + if (_.find(this.attachment_ids, function (file) {return file.upload;})) { + this.do_warn(_t("Uploading error"), _t("Please, wait while the file is uploading.")); + return false; } return true; }, - bindTooltipTo: function($el, value) { - $el.tooltip({ - 'title': value, - 'placement': 'top', - 'container': this.el, - 'html': true, - 'trigger': 'manual', - 'animation': false - }).on("mouseleave", function () { - setTimeout(function () { - if (!$(".tooltip:hover").length) { - $el.tooltip("hide"); - } - },100); - }); - }, - on_record_clicked: function (event) { - event.preventDefault(); - var self = this; - var state = { - 'model': this.model, - 'id': this.res_id, - 'title': this.record_name - }; - web_client.action_manager.do_push_state(state); - this.context.params = { - model: this.model, - res_id: this.res_id, - }; - this.thread.ds_thread.call("message_redirect_action", {context: this.context}).then(function(action){ - self.do_action(action); - }); - }, - on_record_author_clicked: function (event) { - event.preventDefault(); - var partner_id = $(event.target).data('partner'); - var state = { - 'model': 'res.partner', - 'id': partner_id, - 'title': this.record_name - }; - web_client.action_manager.do_push_state(state); - var action = { - type:'ir.actions.act_window', - view_type: 'form', - view_mode: 'form', - res_model: 'res.partner', - views: [[false, 'form']], - res_id: partner_id, - }; - this.do_action(action); - }, - - /* Call the on_compose_message on the thread of this message. */ - on_message_reply:function (event) { - event.stopPropagation(); - this.create_thread(); - this.thread.on_compose_message(event); - return false; - }, - - on_expand: function (event) { - event.stopPropagation(); - this.$('.oe_msg_body:first > .oe_msg_body_short:first').toggle(); - this.$('.oe_msg_body:first > .oe_msg_body_long:first').toggle(); - return false; - }, - - /** - * Instantiate the thread object of this message. - * Each message have only one thread. - */ - create_thread: function () { - if (this.thread) { - return false; + do_show_compact: function () { + this.show_compact_message = true; + if (!this.show_composer) { + this.reinit(); } - /*create thread*/ - this.thread = new Thread(this, this, { - 'domain': this.domain, - 'context':{ - 'default_model': this.model, - 'default_res_id': this.res_id, - 'default_parent_id': this.id - }, - 'options': this.options - } - ); - /*insert thread in parent message*/ - this.thread.insertAfter(this.$el); - }, - - /** - * Fade out the message and his child thread. - * Then this object is destroyed. - */ - animated_destroy: function (fadeTime) { - var self=this; - this.$el.fadeOut(fadeTime, function () { - self.parent_thread.message_to_expandable(self); - }); - if (this.thread) { - this.thread.$el.fadeOut(fadeTime); - } - }, - - /** - * Wait a confirmation for delete the message on the DB. - * Make an animate destroy - */ - on_message_delete: function (event) { - event.stopPropagation(); - if (! confirm(_t("Do you really want to delete this message?"))) { return false; } - - this.animated_destroy(150); - // delete this message and his childs - var ids = [this.id].concat( this.get_child_ids() ); - this.ds_message.unlink(ids); - return false; - }, - - /* Check if the message must be destroy and detroy it or check for re render widget - * @param {callback} apply function - */ - check_for_rerender: function () { - var self = this; - - var messages = [this].concat(this.get_childs()); - var message_ids = _.map(messages, function (msg) { return msg.id;}); - var domain = mail_utils.expand_domain( this.options.root_thread.domain ) - .concat([["id", "in", message_ids ]]); - - return this.parent_thread.ds_message.call('message_read_wrapper', [undefined, domain, [], !!this.parent_thread.options.display_indented_thread, this.context, this.parent_thread.id]) - .then( function (records) { - // remove message not loaded - _.map(messages, function (msg) { - if(!_.find(records, function (record) { return record.id == msg.id; })) { - msg.animated_destroy(150); - } else { - msg.renderElement(); - msg.start(); - } - self.options.root_thread.MailWidget.do_reload_menu_emails(); - }); - - }); - }, - - on_message_read: function (event) { - event.stopPropagation(); - this.on_message_read_unread(true); - return false; - }, - - on_message_unread: function (event) { - event.stopPropagation(); - this.on_message_read_unread(false); - return false; - }, - - /* Set the selected thread and all childs as read or unread, based on - * read parameter. - * @param {boolean} read_value - */ - on_message_read_unread: function (read_value) { - var self = this; - var messages = [this].concat(this.get_childs()); - - // inside the inbox, when the user mark a message as read/done, don't apply this value - // for the stared/favorite message - if (this.options.view_inbox && read_value) { - messages = _.filter(messages, function (val) { return !val.is_favorite && val.id; }); - if (!messages.length) { - this.check_for_rerender(); - return false; - } - } - var message_ids = _.map(messages, function (val) { return val.id; }); - - this.ds_message.call('set_message_read', [message_ids, read_value, true, this.context]) - .then(function () { - // apply modification - _.each(messages, function (msg) { - msg.to_read = !read_value; - if (msg.options.toggle_read) { - msg.options.show_read = msg.to_read; - msg.options.show_unread = !msg.to_read; - } - }); - // check if the message must be display, destroy or rerender - self.check_for_rerender(); - }); - return false; - }, - - /** - * add or remove a vote for a message and display the result - */ - on_vote: function (event) { - event.stopPropagation(); - this.ds_message.call('vote_toggle', [[this.id]]) - .then( - _.bind(function (vote) { - this.has_voted = vote; - this.vote_nb += this.has_voted ? 1 : -1; - this.display_vote(); - }, this)); - return false; - }, - - /** - * Display the render of this message's vote - */ - display_vote: function () { - var vote_element = qweb.render('mail.thread.message.vote', {'widget': this}); - this.$(".oe_msg_footer:first .oe_mail_vote_count").remove(); - this.$(".oe_msg_footer:first .oe_msg_vote").replaceWith(vote_element); - this.$('.oe_msg_vote').on('click', this.on_vote); - this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover); - }, - - /** - * add or remove a favorite (or starred) for a message and change class on the DOM - */ - on_star: function (event) { - event.stopPropagation(); - var self=this; - var button = self.$('.oe_star:first'); - - this.ds_message.call('set_message_starred', [[self.id], !self.is_favorite, true]) - .then(function (star) { - self.is_favorite=star; - if (self.is_favorite) { - button.addClass('oe_starred'); - } else { - button.removeClass('oe_starred'); - } - - if (self.options.view_inbox && self.is_favorite) { - self.on_message_read_unread(true); - } else { - self.check_for_rerender(); - } - }); - return false; - }, - -}); - -/** - * ------------------------------------------------------------ - * Thread Widget - * ------------------------------------------------------------ - * - * This widget handles the display of a thread of messages. The - * thread view: - * - root thread - * - - sub message (parent_id = root message) - * - - - sub thread - * - - - - sub sub message (parent id = sub thread) - * - - sub message (parent_id = root message) - * - - - sub thread - */ -var Thread = Widget.extend({ - template: 'mail.thread', - - /** - * @param {Object} parent parent - * @param {Array} [domain] - * @param {Object} [context] context of the thread. It should - contain at least default_model, default_res_id. Please refer to - the ComposeMessage widget for more information about it. - * @param {Object} [options] - * @param {Object} [message] read about mail.ThreadMessage object - * @param {Object} [thread] - * @param {int} [display_indented_thread] number thread level to indented threads. - * other are on flat mode - * @param {Array} [parents] liked with the parents thread - * use with browse, fetch... [O]= top parent - */ - init: function (parent, datasets, options) { - this._super(parent, options); - this.MailWidget = parent instanceof MailWidget ? parent : false; - this.domain = options.domain || []; - this.context = _.extend(options.context || {}); - - this.options = options.options; - this.options.root_thread = (options.options.root_thread !== undefined ? options.options.root_thread : this); - this.options.show_compose_message = this.options.show_compose_message && !this.thread_level; - - // record options and data - this.parent_message= parent.thread !== undefined ? parent : false ; - - // data of this thread - this.id = datasets.id || false; - this.last_id = datasets.last_id || false; - this.parent_id = datasets.parent_id || false; - this.is_private = datasets.is_private || false; - this.author_id = datasets.author_id || false; - this.thread_level = (datasets.thread_level+1) || 0; - datasets.partner_ids = datasets.partner_ids || []; - if (datasets.author_id && !_.contains(_.flatten(datasets.partner_ids),datasets.author_id[0]) && datasets.author_id[0]) { - datasets.partner_ids.push(datasets.author_id); - } - this.user_pid = datasets.user_pid || false; - this.partner_ids = datasets.partner_ids; - this.messages = []; - this.options.flat_mode = (this.options.display_indented_thread - this.thread_level > 0); - - // object compose message - this.compose_message = false; - - this.ds_thread = new data.DataSetSearch(this, this.context.default_model || 'mail.thread'); - this.ds_message = new data.DataSetSearch(this, 'mail.message'); - this.render_mutex = new utils.Mutex(); - }, - - start: function () { - this._super.apply(this, arguments); - this.bind_events(); - return $.when(); - }, - - /* instantiate the compose message object and insert this on the DOM. - * The compose message is display in compact form. - */ - instantiate_compose_message: function () { - // add message composition form view - if (!this.compose_message) { - this.compose_message = new ThreadComposeMessage(this, this, { - 'context': this.options.compose_as_todo && !this.thread_level ? _.extend(this.context, { 'default_starred': true }) : this.context, - 'options': this.options, - }); - if (!this.thread_level || this.thread_level > this.options.display_indented_thread) { - this.compose_message.insertBefore(this.$el); - } else { - this.compose_message.prependTo(this.$el); - } - } - }, - - /* When the expandable object is visible on screen (with scrolling) - * then the on_expandable function is launch - */ - on_scroll: function () { - _.each( _.filter(this.messages, function (val) {return val.max_limit && !val.parent_id;}), function (val) { - var pos = val.$el.position(); - if (pos.top) { - /* bottom of the screen */ - var bottom = $(window).scrollTop()+$(window).height()+200; - if (bottom > pos.top) { - val.on_expandable(); - } - } - }); - }, - - /** - * Bind events in the widget. Each event is slightly described - * in the function. */ - bind_events: function () { - var self = this; - self.$('.oe_mail_list_recipients .oe_more').on('click', self.on_show_recipients); - self.$('.oe_mail_compose_textarea .oe_more_hidden').on('click', self.on_hide_recipients); - }, - - /** - *show all the partner list of this parent message - */ - on_show_recipients: function () { - var p=$(this).parent(); - p.find('.oe_more_hidden, .oe_hidden').show(); - p.find('.oe_more').hide(); - return false; - }, - - /** - *hide a part of the partner list of this parent message - */ - on_hide_recipients: function () { - var p=$(this).parent(); - p.find('.oe_more_hidden, .oe_hidden').hide(); - p.find('.oe_more').show(); - return false; - }, - - /* get all child message/thread id linked. - * @return array of id - */ - get_child_ids: function () { - return _.map(this.get_childs(), function (val) { return val.id; }); - }, - - /* get all child message/thread linked. - * @param {int} nb_thread_level, number of traversed thread level for this search - * @return array of thread object - */ - get_childs: function (nb_thread_level) { - var res=[]; - if (arguments[1]) res.push(this); - if (isNaN(nb_thread_level) || nb_thread_level>0) { - _(this.messages).each(function (val) { - if (val.thread) { - res = res.concat( val.thread.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) ); - } - }); - } - return res; - }, - - /** - *search a thread in all thread and child thread. - * This method return an object thread. - * @param {object}{int} option.id - * @param {object}{string} option.model - * @param {object}{boolean} option._go_thread_wall - * private for check the top thread - * @param {object}{boolean} option.default_return_top_thread - * return the top thread (wall) if no thread found - * @return thread object - */ - browse_thread: function (options) { - // goto the wall thread for launch browse - if (!options._go_thread_wall) { - options._go_thread_wall = true; - return this.options.root_thread.browse_thread(options); - } - - if (this.id == options.id) { - return this; - } - - if (options.id) { - for (var i in this.messages) { - if (this.messages[i].thread) { - var res = this.messages[i].thread.browse_thread({'id':options.id, '_go_thread_wall':true}); - if (res) return res; - } - } - } - - //if option default_return_top_thread, return the top if no found thread - if (options.default_return_top_thread) { - return this; - } - - return false; - }, - - /** - *search a message in all thread and child thread. - * This method return an object message. - * @param {object}{int} option.id - * @param {object}{string} option.model - * @param {object}{boolean} option._go_thread_wall - * private for check the top thread - * @return message object - */ - browse_message: function (options) { - if (this.options.root_thread.messages[0]) - return this.options.root_thread.messages[0].browse_message(options); - }, - - /** - *If compose_message doesn't exist, instantiate the compose message. - * Call the on_toggle_quick_composer method to allow the user to write his message. - * (Is call when a user click on "Reply" button) - */ - on_compose_message: function (event) { - this.instantiate_compose_message(); - this.compose_message.on_toggle_quick_composer(event); - return false; - }, - - /** - *display the message "there are no message" on the thread - */ - no_message: function () { - var no_message = $(qweb.render('mail.wall_no_message', {})); - if (this.options.help) { - no_message.html(this.options.help); - } - if (!this.$el.find(".oe_view_nocontent").length) - { - no_message.appendTo(this.$el); - } - }, - - /** - *make a request to read the message (calling RPC to "message_read"). - * The result of this method is send to the switch message for sending ach message to - * his parented object thread. - * @param {Array} replace_domain: added to this.domain - * @param {Object} replace_context: added to this.context - * @param {Array} ids read (if the are some ids, the method don't use the domain) - */ - message_fetch: function (replace_domain, replace_context, ids, callback) { - return this.ds_message.call('message_read_wrapper', [ - // ids force to read - ids === false ? undefined : ids && ids.slice(0, this.options.fetch_limit), - // domain + additional - (replace_domain ? replace_domain : this.domain), - // ids allready loaded - (this.id ? [this.id].concat( this.get_child_ids() ) : this.get_child_ids()), - // option for sending in flat mode by server - this.options.flat_mode, - // context + additional - (replace_context ? replace_context : this.context), - // parent_id - this.context.default_parent_id || undefined, - this.options.fetch_limit, - ]).done(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message') - ).done(this.proxy('message_fetch_set_read')); - }, - - message_fetch_set_read: function (message_list) { - if (! this.context.mail_read_set_read) return; - var self = this; - this.render_mutex.exec(function() { - var msg_ids = _.pluck(message_list, 'id'); - return self.ds_message.call('set_message_read', [msg_ids, true, false, self.context]) - .then(function (nb_read) { - if (nb_read) { - self.options.root_thread.MailWidget.do_reload_menu_emails(); - } - }); - }); - }, - - /** - *create the message object and attached on this thread. - * When the message object is create, this method call insert_message for, - * displaying this message on the DOM. - * @param : {object} data from calling RPC to "message_read" - */ - create_message_object: function (data) { - var self = this; - var message; - - data.thread_level = self.thread_level || 0; - data.options = _.extend(data.options || {}, self.options); - - if (data.type=='expandable') { - message = new ThreadExpandable(self, data, {'context':{ - 'default_model': data.model || self.context.default_model, - 'default_res_id': data.res_id || self.context.default_res_id, - 'default_parent_id': self.id, - }}); - } else { - data.record_name= (data.record_name !== '' && data.record_name) || (self.parent_message && self.parent_message.record_name); - message = new ThreadMessage(self, data, {'context':{ - 'default_model': data.model, - 'default_res_id': data.res_id, - 'default_parent_id': data.id, - }}); - } - - // check if the message is already create - for (var i in self.messages) { - if (message.id && self.messages[i] && self.messages[i].id === message.id) { - self.messages[i].destroy(); - } - } - self.messages.push( message ); - - return message; - }, - - /** - *insert the message on the DOM. - * All message (and expandable message) are sorted. The method get the - * older and newer message to insert the message (before, after). - * If there are no older or newer, the message is prepend or append to - * the thread (parent object or on root thread for flat view). - * The sort is define by the thread_level (O for newer on top). - * @param : {object} ThreadMessage object - */ - insert_message: function (message, dom_insert_after, prepend) { - var self=this; - if (this.options.show_compact_message > this.thread_level) { - this.instantiate_compose_message(); - this.compose_message.do_show_compact(); - } - - this.$('.oe_view_nocontent').remove(); - if (dom_insert_after && dom_insert_after.parent()[0] == self.$el[0]) { - message.insertAfter(dom_insert_after); - } else if (prepend) { - message.prependTo(self.$el); - } else { - message.appendTo(self.$el); - } - message.$el.hide().fadeIn(500); - - return message; - }, - - /** - *get the parent thread of the messages. - * Each message is send to his parent object (or parent thread flat mode) for creating the object message. - * @param : {Array} datas from calling RPC to "message_read" - */ - switch_new_message: function (records, dom_insert_after) { - var self = this; - dom_insert_after = typeof dom_insert_after == 'object' ? dom_insert_after : false; - _(records).each(function (record) { - var thread = self.browse_thread({ - 'id': record.parent_id, - 'default_return_top_thread':true - }); - // create object and attach to the thread object - var message = thread.create_message_object( record ); - // insert the message on dom - thread.insert_message( message, dom_insert_after); - }); - if (!records.length && this.options.root_thread == this) { - this.no_message(); - } - }, - - /** - * this method is call when the widget of a message or an expandable message is destroy - * in this thread. The this.messages array is filter to remove this message - */ - on_message_detroy: function () { - - this.messages = _.filter(this.messages, function (val) { return !val.isDestroyed(); }); - if (this.options.root_thread == this && !this.messages.length) { - this.no_message(); - } - return false; - - }, - - /** - * Convert a destroyed message into a expandable message - */ - message_to_expandable: function (message) { - - if (!this.thread_level || message.isDestroyed()) { - message.destroy(); - return false; - } - - var messages = _.sortBy( this.messages, function (val) { return val.id; }); - - var msg_up = message.$el.prev('.oe_msg'); - var msg_down = message.$el.next('.oe_msg'); - msg_up = msg_up.hasClass('oe_msg_expandable') ? _.find( this.messages, function (val) { return val.$el[0] == msg_up[0]; }) : false; - msg_down = msg_down.hasClass('oe_msg_expandable') ? _.find( this.messages, function (val) { return val.$el[0] == msg_down[0]; }) : false; - - var message_dom = [ ["id", "=", message.id] ]; - - if ( msg_up && msg_up.type == "expandable" && msg_down && msg_down.type == "expandable") { - // concat two expandable message and add this message to this dom - msg_up.domain = mail_utils.expand_domain( msg_up.domain ); - msg_down.domain = mail_utils.expand_domain( msg_down.domain ); - - msg_down.domain = ['|','|'].concat( msg_up.domain ).concat( message_dom ).concat( msg_down.domain ); - - if ( !msg_down.max_limit ) { - msg_down.nb_messages += 1 + msg_up.nb_messages; - } - - msg_up.$el.remove(); - msg_up.destroy(); - - msg_down.reinit(); - - } else if ( msg_up && msg_up.type == "expandable") { - // concat preview expandable message and this message to this dom - msg_up.domain = mail_utils.expand_domain( msg_up.domain ); - msg_up.domain = ['|'].concat( msg_up.domain ).concat( message_dom ); - - msg_up.nb_messages++; - - msg_up.reinit(); - - } else if ( msg_down && msg_down.type == "expandable") { - // concat next expandable message and this message to this dom - msg_down.domain = mail_utils.expand_domain( msg_down.domain ); - msg_down.domain = ['|'].concat( msg_down.domain ).concat( message_dom ); - - // it's maybe a message expandable for the max limit read message - if ( !msg_down.max_limit ) { - msg_down.nb_messages++; - } - - msg_down.reinit(); - - } else { - // create a expandable message - var expandable = new ThreadExpandable(this, { - 'model': message.model, - 'parent_id': message.parent_id, - 'nb_messages': 1, - 'thread_level': message.thread_level, - 'domain': message_dom, - 'options': message.options, - }, { - 'context':{ - 'default_model': message.model || this.context.default_model, - 'default_res_id': message.res_id || this.context.default_res_id, - 'default_parent_id': this.id, - } - }); - - // add object on array and DOM - this.messages.push(expandable); - expandable.insertAfter(message.$el); - } - - // destroy message - message.destroy(); - - return true; }, }); -/** - * ------------------------------------------------------------ - * mail : root Widget - * ------------------------------------------------------------ - * - * This widget handles the display of messages with thread options. Its main - * use is to receive a context and a domain, and to delegate the message - * fetching and displaying to the Thread widget. - */ -var MailWidget = Widget.extend({ - template: 'mail.Root', - - /** - * @param {Object} parent parent - * @param {Array} [domain] - * @param {Object} [context] context of the thread. It should - * contain at least default_model, default_res_id. Please refer to - * the compose_message widget for more information about it. - * @param {Object} [options] - *... @param {Number} [truncate_limit=250] number of character to - * display before having a "show more" link; note that the text - * will not be truncated if it does not have 110% of the parameter - *... @param {Boolean} [show_record_name] display the name and link for do action - *... @param {boolean} [show_reply_button] display the reply button - *... @param {boolean} [show_read_unread_button] display the read/unread button - *... @param {int} [display_indented_thread] number thread level to indented threads. - * other are on flat mode - *... @param {Boolean} [show_compose_message] allow to display the composer - *... @param {Boolean} [show_compact_message] display the compact message on the thread - * when the user clic on this compact mode, the composer is open - *... @param {Array} [message_ids] List of ids to fetch by the root thread. - * When you use this option, the domain is not used for the fetch root. - * @param {String} [no_message] Message to display when there are no message - * @param {Boolean} [show_link] Display partner (authors, followers...) on link or not - * @param {Boolean} [compose_as_todo] The root composer mark automatically the message as todo - * @param {Boolean} [readonly] Read only mode, hide all action buttons and composer - */ - init: function (parent, action) { - this._super(parent, action); - this.action = _.clone(action); - this.domain = this.action.domain || this.action.params.domain || []; - this.context = this.action.context || this.action.params.context || {}; - - this.action.params = _.extend({ - 'display_indented_thread' : -1, - 'show_reply_button' : false, - 'show_read_unread_button' : false, - 'truncate_limit' : 250, - 'show_record_name' : false, - 'show_compose_message' : false, - 'show_compact_message' : false, - 'compose_placeholder': false, - 'show_link': true, - 'view_inbox': false, - 'message_ids': undefined, - 'compose_as_todo' : false, - 'readonly' : false, - 'emails_from_on_composer': true, - 'fetch_limit': 30 // limit of chatter messages - }, this.action.params); - - this.action.params.help = this.action.help || false; - }, - - start: function () { - this._super.apply(this, arguments); - this.message_render(); - }, - - /** - * create an object "related_menu" - * contains the menu widget and the sub menu related of this wall - */ - do_reload_menu_emails: function () { - var menu = web_client.menu; - if (!menu || !menu.current_menu) { - return $.when(); - } - return menu.rpc("/web/menu/load_needaction", {'menu_ids': [menu.current_menu]}).done(function(r) { - menu.on_needaction_loaded(r); - }).then(function () { - menu.trigger("need_action_reloaded"); - }); - }, - - /** - *Create the root thread and display this object in the DOM. - * Call the no_message method then c all the message_fetch method - * of this root thread to display the messages. - */ - message_render: function () { - - this.thread = new Thread(this, {}, { - 'domain' : this.domain, - 'context' : this.context, - 'options': this.action.params, - }); - - this.thread.appendTo( this.$el ); - - if (this.action.params.show_compose_message) { - this.thread.instantiate_compose_message(); - this.thread.compose_message.do_show_compact(); - } - - this.thread.message_fetch(null, null, this.action.params.message_ids); - - }, - -}); -core.action_registry.add('mail.Widget', MailWidget); - - -/** - * ------------------------------------------------------------ - * mail_thread Widget - * ------------------------------------------------------------ - * - * This widget handles the display of messages on a document. Its main - * use is to receive a context and a domain, and to delegate the message - * fetching and displaying to the Thread widget. - * Use Help on the field to display a custom "no message loaded" - */ -var RecordThread = form_common.AbstractField.extend({ - template: 'mail.record_thread', - - init: function (parent, node) { - this._super.apply(this, arguments); - this.ParentViewManager = parent; - this.node = _.clone(node); - this.node.params = _.extend({ - 'display_indented_thread': -1, - 'show_reply_button': false, - 'show_read_unread_button': true, - 'read_action': 'unread', - 'show_record_name': false, - 'show_compact_message': 1, - 'display_log_button' : true, - }, this.node.params); - if (this.node.attrs.placeholder) { - this.node.params.compose_placeholder = this.node.attrs.placeholder; - } - if (this.node.attrs.readonly) { - this.node.params.readonly = this.node.attrs.readonly; - } - if ('display_log_button' in this.options) { - this.node.params.display_log_button = this.options.display_log_button; - } - this.domain = (this.node.params && this.node.params.domain) || (this.field && this.field.domain) || []; - - if (!this.ParentViewManager.is_action_enabled('edit')) { - this.node.params.show_link = false; - } - }, - - start: function () { - this._super.apply(this, arguments); - // NB: check the actual_mode property on view to know if the view is in create mode anymore - this.view.on("change:actual_mode", this, this._check_visibility); - this._check_visibility(); - }, - - _check_visibility: function () { - this.$el.toggle(this.view.get("actual_mode") !== "create"); - }, - - render_value: function () { - if (! this.view.datarecord.id || data.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) { - this.$('oe_mail_thread').hide(); - return; - } - - this.node.params = _.extend(this.node.params, { - 'message_ids': this.get_value(), - 'show_compose_message': this.view.is_action_enabled('edit'), - }); - this.node.context = { - 'mail_read_set_read': true, // set messages as read in Chatter - 'default_res_id': this.view.datarecord.id || false, - 'default_model': this.view.model || false, - }; - - if (this.root) { - $('').insertAfter(this.root.$el); - this.root.destroy(); - } - // create and render Thread widget - this.root = new MailWidget(this, _.extend(this.node, { - 'domain' : (this.domain || []).concat([['model', '=', this.view.model], ['res_id', '=', this.view.datarecord.id]]), - })); - - return this.root.replace(this.$('.oe_mail-placeholder')); - }, -}); -core.form_widget_registry.add('mail_thread', RecordThread); /** * ------------------------------------------------------------ * Aside Widget * ------------------------------------------------------------ - * - * This widget handles the display of a sidebar on the Wall. Its main use - * is to display group and employees suggestion (if hr is installed). - */ -var WallSidebar = Widget.extend({ - template: 'mail.wall.sidebar', -}); - - - -/** - * ------------------------------------------------------------ - * Wall Widget - * ------------------------------------------------------------ * - * This widget handles the display of messages on a Wall. Its main - * use is to receive a context and a domain, and to delegate the message - * fetching and displaying to the Thread widget. + * This widget handles the display of a sidebar in the inbox. Its main + * use is to display group and employees suggestion (if hr is installed). */ - - -var MailWall = Widget.extend({ - template: 'mail.wall', - - /** - * @param {Object} parent parent - * @param {Object} [options] - * @param {Array} [options.domain] domain on the Wall - * @param {Object} [options.context] context, is an object. It should - * contain default_model, default_res_id, to give it to the threads. - */ - init: function (parent, action) { - var key; - this._super(parent, action); - this.ActionManager = parent; - - this.action = _.clone(action); - this.domain = this.action.params.domain || this.action.domain || []; - this.context = _.extend(this.action.params.context || {}, this.action.context || {}); - - // filter some parameters that we will propagate as search_default - this.defaults = {}; - for (key in this.action.context.params) { - if (_.indexOf(['model', 'res_id'], key) == -1) { - continue; - } - this.context['search_default_' + key] = this.action.context.params[key]; - } - for (key in this.context) { - if (key.match(/^search_default_/)) { - this.defaults[key.replace(/^search_default_/, '')] = this.context[key]; - } - } - this.action.params = _.extend({ - 'display_indented_thread': 1, - 'show_reply_button': true, - 'show_read_unread_button': true, - 'show_compose_message': true, - 'show_record_name': true, - 'show_compact_message': this.action.params.view_mailbox ? false : 1, - 'view_inbox': false, - 'emails_from_on_composer': false, - 'fetch_limit': 30 // allow inbox to load all children messages - }, this.action.params); - }, - - start: function () { - this._super.apply(this); - this.bind_events(); - this.load_searchview(this.defaults); - if (! this.searchview.has_defaults) { - this.message_render(); - } - // render sidebar - var wall_sidebar = new WallSidebar(this); - wall_sidebar.appendTo(this.$el.find('.oe_mail_wall_aside')); - }, - - /** - * Load the mail.message search view - * @param {Object} defaults ?? - */ - load_searchview: function (defaults) { - var self = this, - ds_msg = new data.DataSetSearch(this, 'mail.message'), - options = { - $buttons: this.$('.oe-search-options'), - action: this.action, - }; - this.searchview = new SearchView(this, ds_msg, false, defaults || {}, options); - this.searchview.on('search_data', this, this.do_searchview_search); - this.searchview.appendTo(this.$('.oe-cp-search-view')).then(function () { - self.searchview.toggle_visibility(true); - }); - if (this.searchview.has_defaults) { - this.searchview.ready.then(this.searchview.do_search); - } - return this.searchview; - }, - - /** - * Get the domains, contexts and groupbys in parameter from search - * view, then render the filtered threads. - * @param {Array} domains - * @param {Array} contexts - * @param {Array} groupbys - */ - do_searchview_search: function (domains, contexts, groupbys) { - var self = this; - pyeval.eval_domains_and_contexts({ - domains: domains || [], - contexts: contexts || [], - group_by_seq: groupbys || [] - }).then(function (results) { - if(self.root) { - $('').insertAfter(self.root.$el); - self.root.destroy(); - } - return self.message_render(results); - }); - }, - - /** - * Create the root thread widget and display this object in the DOM - */ - message_render: function (search) { - var domain = this.domain.concat(search && search.domain ? search.domain : []); - var context = _.extend(this.context, search && search.context ? search.context : {}); - - this.root = new MailWidget(this, _.extend(this.action, { - 'domain' : domain, - 'context' : context, - })); - return this.root.replace(this.$('.oe_mail-placeholder')); - }, - - bind_events: function () { - var self=this; - this.$(".oe_write_full").click(function (event) { - event.stopPropagation(); - var action = { - name: _t('Compose Email'), - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - view_mode: 'form', - view_type: 'form', - action_from: 'mail.ThreadComposeMessage', - views: [[false, 'form']], - target: 'new', - context: { - }, - }; - web_client.action_manager.do_action(action); - }); - this.$(".oe_write_onwall").click(function (event) { self.root.thread.on_compose_message(event); }); - } +var Sidebar = Widget.extend({ + template: 'TimelineSidebar', }); -core.action_registry.add('mail.wall', MailWall); - -/** - * ------------------------------------------------------------ - * Sub-widgets loading - * ------------------------------------------------------------ - * - * Load here widgets that could depend on widgets defined in mail.js - */ - -// openerp.mail.suggestions(session, mail); // import suggestion.js (suggestion widget) /** * ------------------------------------------------------------ @@ -1964,20 +1503,23 @@ core.action_registry.add('mail.wall', MailWall); * Add a link on the top user bar for write a full mail */ var ComposeMessageTopButton = Widget.extend({ - template:'mail.compose_message_top_button', + template:'ComposeMessageTopButton', + events: { "click": "on_compose_message", }, + on_compose_message: function (ev) { ev.preventDefault(); var ctx = {} - if ($('a.oe_compose_post') && $('a.oe_compose_post').is(":visible") == true && (this.getParent()).getParent().action_manager.inner_widget.active_view.type == 'form'){ + if ($('a.oe_compose_post') && $('a.oe_compose_post').is(":visible") == true && + (this.getParent()).getParent().action_manager.inner_widget.active_view.type == 'form'){ ctx = { 'default_res_id': (this.getParent()).getParent().action_manager.inner_widget.active_view.controller.datarecord.id, 'default_model': (this.getParent()).getParent().action_manager.inner_widget.active_view.controller.model, - } + }; } - var action = { + web_client.action_manager.do_action({ type: 'ir.actions.act_window', res_model: 'mail.compose.message', view_mode: 'form', @@ -1985,19 +1527,21 @@ var ComposeMessageTopButton = Widget.extend({ views: [[false, 'form']], target: 'new', context: ctx, - }; - web_client.action_manager.do_action(action); + }); }, }); // Put the ComposeMessageTopButton widget in the systray menu SystrayMenu.Items.push(ComposeMessageTopButton); +core.view_registry.add('timeline', TimelineView); +core.form_widget_registry.add('mail_thread', TimelineRecordThread); + return { - Thread: Thread, - RecordThread: RecordThread, - WallSidebar: WallSidebar, - Wall: MailWall, + TimelineView: TimelineView, + MailThread: MailThread, + TimelineRecordThread: TimelineRecordThread, + Sidebar: Sidebar, }; }); diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js index 1f5c385fc11..b58ba3f45a7 100644 --- a/addons/mail/static/src/js/mail_followers.js +++ b/addons/mail/static/src/js/mail_followers.js @@ -14,7 +14,7 @@ var qweb = core.qweb; /** * ------------------------------------------------------------ - * mail_followers Widget + * timeline_followers Widget * ------------------------------------------------------------ * * This widget handles the display of a list of records as a vertical @@ -26,19 +26,19 @@ var qweb = core.qweb; var Followers = form_common.AbstractField.extend({ - template: 'mail.followers', + template: 'mail_followers', init: function() { this._super.apply(this, arguments); + this.image = this.node.attrs.image || 'image_small'; this.comment = this.node.attrs.help || false; - this.displayed_limit = this.node.attrs.displayed_nb || 10; - this.displayed_nb = this.displayed_limit; this.ds_model = new data.DataSetSearch(this, this.view.model); this.ds_users = new data.DataSetSearch(this, 'res.users'); this.value = []; this.followers = []; + this.data_subtype = {}; this.view_is_editable = this.__parentedParent.is_action_enabled('edit'); }, @@ -49,7 +49,7 @@ var Followers = form_common.AbstractField.extend({ this.on_check_visibility_mode(); this.reinit(); this.bind_events(); - this._super(); + return this._super(); }, on_check_visibility_mode: function () { @@ -62,37 +62,43 @@ var Followers = form_common.AbstractField.extend({ }, reinit: function() { - this.message_is_follower = false; + this.data_subtype = {}; + this.message_is_follower == undefined; this.display_buttons(); }, bind_events: function() { var self = this; + // event: click on '(Un)Follow' button, that toggles the follow for uid - this.$('.oe_follower').on('click', function (event) { - if($(this).hasClass('oe_notfollow')) + this.$('.o_timeline_follower').on('click', function (event) { + if($(this).hasClass('o_timeline_notfollow')) { self.do_follow(); - else + } + else { self.do_unfollow(); + } }); + // event: click on a subtype, that (un)subscribe for this subtype - this.$el.on('click', '.oe_subtype_list input', function(event) { + this.$el.on('click', '.o_timeline_subtype_list input', function(event) { self.do_update_subscription(event); - var $list = self.$('.oe_subtype_list'); + var $list = self.$('.o_timeline_subtype_list'); if(!$list.hasClass('open')) { $list.addClass('open'); } - if(self.$('.oe_subtype_list ul')[0].children.length < 1) { + if(self.$('.o_timeline_subtype_list ul')[0].children.length < 1) { $list.removeClass('open'); } event.stopPropagation(); }); + // event: click on 'invite' button, that opens the invite wizard - this.$('.oe_invite').on('click', self.on_invite_follower); + this.$el.on('click', '.oe_invite', self.on_invite_follower); + // event: click on 'edit_subtype(pencil)' button to edit subscription this.$el.on('click', '.oe_edit_subtype', self.on_edit_subtype); this.$el.on('click', '.oe_remove_follower', self.on_remove_follower); - this.$el.on('click', '.oe_show_more', self.on_show_more_followers); this.$el.on('click', 'a[data-partner]', self.on_follower_clicked); }, @@ -137,19 +143,16 @@ var Followers = form_common.AbstractField.extend({ }); }, - on_show_more_followers: function (event) { - this.displayed_nb += this.displayed_limit; - this.display_followers(false); - }, - on_remove_follower: function (event) { var partner_id = $(event.target).data('id'); - var name = $(event.target).parent().find("a").html(); - if (confirm(_.str.sprintf(_t("Warning! \n %s won't be notified of any email or discussion on this document. Do you really want to remove him from the followers ?"), name))) { - var context = new data.CompoundContext(this.build_context(), {}); - return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context]) - .then(this.proxy('read_value')); - } + var name = $(event.target).parent().find("a").html(); + if (confirm(_.str.sprintf(_t("Warning! \n %s won't be notified of any email or discussion on this document. Do you really want to remove him from the followers ?"), name))) { + var context = new data.CompoundContext(this.build_context(), {}); + return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], + [partner_id], + context]) + .then(this.proxy('read_value')); + } }, on_follower_clicked: function (event) { @@ -174,11 +177,11 @@ var Followers = form_common.AbstractField.extend({ read_value: function () { var self = this; - this.displayed_nb = this.displayed_limit; - return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']).then(function (results) { - self.value = results[0].message_follower_ids; - self.render_value(); - }); + return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']) + .then(function (results) { + self.value = results[0].message_follower_ids; + self.render_value(); + }); }, render_value: function () { @@ -200,10 +203,13 @@ var Followers = form_common.AbstractField.extend({ fetch_generic: function (error, event) { var self = this; event.preventDefault(); - return this.ds_users.call('read', [[session.uid], ['partner_id']]).then(function (results) { - var pid = results[0]['partner_id'][0]; - self.message_is_follower = (_.indexOf(self.value, pid) != -1); - }).then(self.proxy('display_generic')); + + return this.ds_users.call('read', [[session.uid], ['partner_id']]) + .then(function (results) { + var pid = results[0]['partner_id'][0]; + self.message_is_follower = (_.indexOf(self.value, pid) != -1); + }) + .then(self.proxy('display_generic')); }, _format_followers: function(count){ @@ -220,8 +226,8 @@ var Followers = form_common.AbstractField.extend({ /* Display generic info about follower, for people not having access to res_partner */ display_generic: function () { - this.$('.oe_follower_list').empty(); - this.$('.oe_follower_title').html(this._format_followers(this.value.length)); + var node_user_list = this.$('.o_timeline_follower_list').empty(); + this.$('.o_timeline_follower_title').html(this._format_followers(this.value.length)); }, /** Display the followers */ @@ -229,140 +235,169 @@ var Followers = form_common.AbstractField.extend({ var self = this; this.message_is_follower = false; this.followers = records || this.followers; + // clean and display title - var node_user_list = this.$('.oe_follower_list').empty(); - this.$('.oe_follower_title').html(this._format_followers(this.followers.length)); - self.message_is_follower = _.indexOf(this.followers.map(function (rec) { return rec[2]['is_uid'];}), true) != -1; + var node_user_list = this.$('.o_timeline_follower_list').empty(); + this.$('.o_timeline_follower_title').html(this._format_followers(this.followers.length)); + + $(qweb.render('mail_followers_add_more', {'widget': self})).appendTo(node_user_list); + + self.message_is_follower = _.indexOf(this.followers.map(function(rec) {return rec[2]['is_uid']}), true) != -1; // truncate number of displayed followers - var truncated = this.followers.slice(0, this.displayed_nb); - _(truncated).each(function (record) { + _(this.followers).each(function (record) { var partner = { 'id': record[0], 'name': record[1], 'is_uid': record[2]['is_uid'], 'is_editable': record[2]['is_editable'], 'avatar_url': mail_utils.get_image(session, 'res.partner', 'image_small', record[0]), - }; - $(qweb.render('mail.followers.partner', {'record': partner, 'widget': self})).appendTo(node_user_list); + } + + $(qweb.render('mail_followers_partner', {'record': partner, 'widget': self})) + .appendTo(node_user_list); + // On mouse-enter it will show the edit_subtype pencil. if (partner.is_editable) { - self.$('.oe_follower_list').on('mouseenter mouseleave', function(e) { - self.$('.oe_edit_subtype').toggleClass('oe_hidden', e.type == 'mouseleave'); - self.$('.oe_follower_list').find('.oe_partner').toggleClass('oe_partner_name', e.type == 'mouseenter'); + self.$('.o_timeline_follower_list').on('mouseenter mouseleave', function(e) { + self.$('.o_timeline_edit_subtype') + .toggleClass('oe_hidden', e.type == 'mouseleave'); + self.$('.o_timeline_follower_list').find('.oe_partner') + .toggleClass('oe_partner_name', e.type == 'mouseenter'); }); } }); - // FVA note: be sure it is correctly translated - if (truncated.length < this.followers.length) { - $(qweb.render('mail.followers.show_more', {'number': (this.followers.length - truncated.length)} )).appendTo(node_user_list); - } }, display_buttons: function () { if (this.message_is_follower) { - this.$('button.oe_follower').removeClass('oe_notfollow').addClass('oe_following'); + this.$('button.o_timeline_follower').removeClass('o_timeline_notfollow').addClass('o_timeline_following'); } else { - this.$('button.oe_follower').removeClass('oe_following').addClass('oe_notfollow'); + this.$('button.o_timeline_follower').removeClass('o_timeline_following').addClass('o_timeline_notfollow'); } - if (this.view.is_action_enabled('edit')) - this.$('span.oe_mail_invite_wrapper').hide(); - else - this.$('span.oe_mail_invite_wrapper').show(); + if (this.view.is_action_enabled('edit')) { + this.$('span.oe_timeline_invite_wrapper').hide(); + } + else { + this.$('span.oe_timeline_invite_wrapper').show(); + } }, /** Fetch subtypes, only if current user is follower */ fetch_subtypes: function (user_pid) { var self = this; var dialog = false; + if (user_pid) { dialog = true; - } else { - this.$('.oe_subtype_list ul').empty(); + } + else { + var subtype_list_ul = this.$('.o_timeline_subtype_list ul').empty(); if (! this.message_is_follower) { - this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', true); + this.$('.o_timeline_subtype_list > .dropdown-toggle').attr('disabled', true); return; } else { - this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', false); + this.$('.o_timeline_subtype_list > .dropdown-toggle').attr('disabled', false); } } var id = this.view.datarecord.id; - this.ds_model.call('message_get_subscription_data', [[id], user_pid, new data.CompoundContext(this.build_context(), {})]) + this.ds_model.call('message_get_subscription_data', + [[id], user_pid, new data.CompoundContext(this.build_context(), {})]) .then(function (data) {self.display_subtypes(data, id, dialog);}); }, /** Display subtypes: {'name': default, followed} */ display_subtypes:function (data, id, dialog) { var self = this; - var $list; - if (dialog) { - $list = self.$dialog.$el; - } - else { - $list = this.$('.oe_subtype_list ul'); - } - var records = data[id].message_subtype_data; - this.records_length = $.map(records, function(value, index) { return index; }).length; - if (this.records_length > 1) { self.display_followers(); } - var old_model = ''; - _(records).each(function (record, record_name) { - if (old_model != record.parent_model){ - if (old_model !== ''){ - var index = $($list).find('.oe_subtype').length; - $($($list).find('.oe_subtype')[index-1]).addClass('subtype-border'); - } - old_model = record.parent_model; + var $list = this.$('.o_timeline_subtype_list ul'); + + if (_.isEmpty(this.data_subtype)) { + this.data_subtype = data; + + if (dialog) { + $list = this.$dialog.$el; } - record.name = record_name; - record.followed = record.followed || undefined; - $(qweb.render('mail.followers.subtype', {'record': record, 'dialog': dialog})).appendTo($list); - }); + + var old_model = ''; + var records = data[id].message_subtype_data; + this.records_length = $.map(records, function(value, index) { return index; }).length; + + if (this.records_length > 1) { + self.display_followers(); + } + + _.each(records, function (record, record_name) { + if (old_model != record.parent_model) { + if (old_model != '') { + var index = $($list).find('.oe_subtype').length; + $list.find('.oe_subtype')[index-1].addClass('subtype-border'); + } + old_model = record.parent_model; + } + + record.name = record_name; + record.followed = record.followed || undefined; + $(qweb.render('mail_followers_subtype', {'record': record, + 'dialog': dialog})) + .appendTo($list); + }); + } }, do_follow: function () { var context = new data.CompoundContext(this.build_context(), {}); - this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', false); - this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [session.uid], undefined, context]) + this.$('.o_timeline_subtype_list > .dropdown-toggle').attr('disabled', false); + this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], + [session.uid], + undefined, + context]) .then(this.proxy('read_value')); - _.each(this.$('.oe_subtype_list input'), function (record) { + _.each(this.$('.o_timeline_subtype_list input'), function (record) { $(record).attr('checked', 'checked'); }); }, do_unfollow: function (user_pid) { if (confirm(_t("Warning! \nYou won't be notified of any email or discussion on this document. Do you really want to unfollow this document ?"))) { - _(this.$('.oe_msg_subtype_check')).each(function (record) { + _(this.$('.o_timeline_msg_subtype_check')).each(function (record) { $(record).attr('checked',false); }); - var action_unsubscribe = 'message_unsubscribe_users'; - this.$('.oe_subtype_list > .dropdown-toggle').attr('disabled', true); - var follower_ids = [session.uid]; - if (user_pid) { - action_unsubscribe = 'message_unsubscribe'; - follower_ids = [user_pid]; - } + + var action_unsubscribe = 'message_unsubscribe_users'; + this.$('.o_timeline_subtype_list > .dropdown-toggle').attr('disabled', true); + var follower_ids = [session.uid]; + + if (user_pid) { + action_unsubscribe = 'message_unsubscribe'; + follower_ids = [user_pid]; + } + var context = new data.CompoundContext(this.build_context(), {}); - return this.ds_model.call(action_unsubscribe, [[this.view.datarecord.id], follower_ids, context]) - .then(this.proxy('read_value')); + return this.ds_model.call(action_unsubscribe, [[this.view.datarecord.id], + follower_ids, context]) + .then(this.proxy('read_value')); } return false; }, do_update_subscription: function (event, user_pid) { var self = this; + this.data_subtype = {}; var action_subscribe = 'message_subscribe_users'; var follower_ids = [session.uid]; var oe_action = this.$('.oe_actions input[type="checkbox"]'); + if (user_pid) { action_subscribe = 'message_subscribe'; follower_ids = [user_pid]; oe_action = $('.oe_edit_actions input[type="checkbox"]'); } - var checklist = []; + + var checklist = new Array(); _(oe_action).each(function (record) { if ($(record).is(':checked')) { checklist.push(parseInt($(record).data('id'))); @@ -372,17 +407,23 @@ var Followers = form_common.AbstractField.extend({ if (!checklist.length) { if (!this.do_unfollow(user_pid)) { $(event.target).attr("checked", "checked"); - } else { - self.$('.oe_subtype_list ul').empty(); } - } else { + else { + self.$('.o_timeline_subtype_list ul').empty(); + } + } + else { var context = new data.CompoundContext(this.build_context(), {}); - return this.ds_model.call(action_subscribe, [[this.view.datarecord.id], follower_ids, checklist, context]) - .then(this.proxy('read_value')); + return this.ds_model.call(action_subscribe, [[this.view.datarecord.id], + follower_ids, + checklist, + context]) + .then(this.proxy('read_value')); } }, }); + /* Add the widget to registry */ core.form_widget_registry.add('mail_followers', Followers); -}); +}); \ No newline at end of file diff --git a/addons/mail/static/src/js/mail_utils.js b/addons/mail/static/src/js/mail_utils.js index 0634e240340..b30968304aa 100644 --- a/addons/mail/static/src/js/mail_utils.js +++ b/addons/mail/static/src/js/mail_utils.js @@ -107,6 +107,22 @@ function breakword(str){ return out; } +function bindTooltipTo($el, value, position) { + $el.tooltip({ + 'title': value, + 'placement': position, + 'html': true, + 'trigger': 'manual', + 'animation': false, + }).on("mouseleave", function () { + setTimeout(function () { + if (!$(".tooltip:hover").length) { + $el.tooltip("hide"); + } + }, 100); + }); +} + return { parse_email: parse_email, get_image: get_image, @@ -115,6 +131,7 @@ return { get_text2html: get_text2html, expand_domain: expand_domain, breakword: breakword, + bindTooltipTo: bindTooltipTo, }; }); diff --git a/addons/mail/static/src/js/suggestions.js b/addons/mail/static/src/js/suggestions.js index 5a80c0685de..120fe6a22d8 100644 --- a/addons/mail/static/src/js/suggestions.js +++ b/addons/mail/static/src/js/suggestions.js @@ -48,7 +48,7 @@ var Groups = Widget.extend({ if (this.groups.length === 0) { return this.$el.empty(); } - return this.$el.empty().html(QWeb.render('mail.suggestions.groups', {'widget': this})); + return this.$el.empty().html(QWeb.render('TimelineSuggestionGroups', {'widget': this})); }, join_group: function (event) { @@ -72,7 +72,7 @@ var Groups = Widget.extend({ } }); -mail.WallSidebar.include({ +mail.Sidebar.include({ start: function () { this._super.apply(this, arguments); var sug_groups = new Groups(this); @@ -80,4 +80,4 @@ mail.WallSidebar.include({ }, }); -}); +}); \ No newline at end of file diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 295a8a1da10..a0657be3c76 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -1,356 +1,257 @@ - +
+
+ +
+ + + +
+ + + + + +
+ + +
+
+
+ +
+
+ uploading +
+
+
+ +
+ + +
+
+
+ +
+
+ uploading +
+
+
+
+
+ + + + + + + + + + + + Like + Unlike + + + +
No message.
+
+ + +
+
+ + + +
  • + + + +
  • +
    + + \ No newline at end of file diff --git a/addons/mail/static/src/xml/mail_followers.xml b/addons/mail/static/src/xml/mail_followers.xml index 66c411defc5..f7f9c8886c2 100644 --- a/addons/mail/static/src/xml/mail_followers.xml +++ b/addons/mail/static/src/xml/mail_followers.xml @@ -1,65 +1,78 @@ - + +
  • + + + + + + + +
  • + + +
  • + Add followers +
  • +
  • +
    + + + +
  • + + + +
  • +
    + + \ No newline at end of file diff --git a/addons/mail/static/src/xml/suggestions.xml b/addons/mail/static/src/xml/suggestions.xml index 7b550f43d0e..2104e96a891 100644 --- a/addons/mail/static/src/xml/suggestions.xml +++ b/addons/mail/static/src/xml/suggestions.xml @@ -2,18 +2,17 @@ + \ No newline at end of file diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py index 7642af47fea..5b9f2bb1116 100644 --- a/addons/mail/tests/test_mail_features.py +++ b/addons/mail/tests/test_mail_features.py @@ -67,12 +67,12 @@ class TestMailFeatures(TestMail): inbox_act_id = self.ref('mail.action_mail_inbox_feeds') action = self.env['mail.thread'].with_context({'params': {}}).sudo(self.user_employee).message_redirect_action() self.assertEqual( - action.get('type'), 'ir.actions.client', - 'URL redirection: action without parameters should redirect to client action Inbox' + action.get('type'), 'ir.actions.act_window', + 'URL redirection: action without parameters should redirect to action act window Inbox' ) self.assertEqual( action.get('id'), inbox_act_id, - 'URL redirection: action without parameters should redirect to client action Inbox' + 'URL redirection: action without parameters should redirect to action act window Inbox' ) def test_inbox_redirection_document(self): @@ -114,12 +114,12 @@ class TestMailFeatures(TestMail): 'params': {'message_id': message.id} }).sudo(self.user_public).message_redirect_action() self.assertEqual( - action.get('type'), 'ir.actions.client', - 'URL redirection: action without parameters should redirect to client action Inbox' + action.get('type'), 'ir.actions.act_window', + 'URL redirection: action without parameters should redirect to action act window Inbox' ) self.assertEqual( action.get('id'), inbox_act_id, - 'URL redirection: action without parameters should redirect to client action Inbox' + 'URL redirection: action without parameters should redirect to action act window Inbox' ) @mute_logger('openerp.models') @@ -130,12 +130,12 @@ class TestMailFeatures(TestMail): 'params': {'model': 'mail.group', 'res_id': self.group_pigs.id} }).sudo(self.user_public).message_redirect_action() self.assertEqual( - action.get('type'), 'ir.actions.client', - 'URL redirection: action without parameters should redirect to client action Inbox' + action.get('type'), 'ir.actions.act_window', + 'URL redirection: action without parameters should redirect to action act window Inbox' ) self.assertEqual( action.get('id'), inbox_act_id, - 'URL redirection: action without parameters should redirect to client action Inbox' + 'URL redirection: action without parameters should redirect to action act window Inbox' ) @mute_logger('openerp.addons.mail.models.mail_mail') diff --git a/addons/mail/tests/test_message_read.py b/addons/mail/tests/test_message_read.py index 90f26332809..f43ef6881f9 100644 --- a/addons/mail/tests/test_message_read.py +++ b/addons/mail/tests/test_message_read.py @@ -17,68 +17,72 @@ class TestMessageRead(TestMail): self.msg_4 = self.group_pigs.message_post(body='2-1', subtype='mt_comment', parent_id=self.msg_2.id) self.msg_5 = self.group_pigs.message_post(body='1-2', subtype='mt_comment', parent_id=self.msg_1.id) self.msg_6 = self.group_pigs.message_post(body='2-2', subtype='mt_comment', parent_id=self.msg_2.id) - self.msg_7 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=self.msg_3.id) - self.msg_8 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=self.msg_4.id) - self.msg_9 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=self.msg_3.id) - self.msg_10 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=self.msg_4.id) + self.msg_7 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=self.msg_1.id) + self.msg_8 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=self.msg_2.id) + self.msg_9 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=self.msg_1.id) + self.msg_10 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=self.msg_2.id) self.msg_ids = [self.msg_10.id, self.msg_9.id, self.msg_8.id, self.msg_7.id, self.msg_6.id, self.msg_5.id, self.msg_4.id, self.msg_3.id, self.msg_2.id, self.msg_1.id, self.msg_0.id] - self.ordered_msg_ids = [self.msg_2.id, self.msg_4.id, self.msg_6.id, self.msg_8.id, self.msg_10.id, self.msg_1.id, self.msg_3.id, self.msg_5.id, self.msg_7.id, self.msg_9.id, self.msg_0.id] + self.ordered_msg_ids = [self.msg_10.id, self.msg_8.id, self.msg_6.id, self.msg_4.id, self.msg_2.id, self.msg_9.id, self.msg_7.id, self.msg_5.id, self.msg_3.id, self.msg_1.id, self.msg_0.id] # TODO: with_context({'mail_read_set_read': True}) def test_message_read_ids(self): """ message_read: test reading specific ids """ messages = self.env['mail.message'].sudo(self.user_employee).browse(self.msg_ids[2:4]).message_read(domain=[('body', 'like', 'dummy')]) - read_msg_ids = [msg.get('id') for msg in messages] - self.assertEqual(read_msg_ids, self.msg_ids[2:4], 'message_read with direct ids should read only the requested ids') + read_msg_list = messages['threads'][0] + msg_ids = [msg.get('id') for msg in read_msg_list] + self.assertEqual(msg_ids, self.msg_ids[2:4], 'message_read with direct ids should read only the requested ids') def test_message_read_domain(self): - messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], limit=200) - read_msg_ids = [msg.get('id') for msg in messages] + messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], limit=200, child_limit=200) + read_msg_list = [msg for thread in messages['threads'] for msg in thread] + read_msg_ids = [msg.get('id') for msg in read_msg_list] self.assertEqual(read_msg_ids, self.msg_ids, 'message_read flat with domain on Pigs should equal all messages of Pigs') def test_message_read_domain_thread(self): - messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], limit=200, thread_level=1) - read_msg_ids = [msg.get('id') for msg in messages] + messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], thread_level=1, limit=200, child_limit=200) + read_msg_list = [msg for thread in messages['threads'] for msg in thread] + read_msg_ids = [msg.get('id') for msg in read_msg_list] self.assertEqual(read_msg_ids, self.ordered_msg_ids, 'message_read threaded with domain on Pigs should equal all messages of Pigs, and sort them with newer thread first, last message last in thread') def test_message_read_expandable(self): - messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], limit=1, thread_level=1) - read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable'] - # TDE TODO: test expandables order - type_list = map(lambda item: item.get('type'), messages) + messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], thread_level=1, limit=1, child_limit=1) + read_msg_list = [msg for thread in messages['threads'] for msg in thread] + read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable'] + read_ancestor_ids = [msg.get('parent_id') if msg.get('parent_id') != 0 else msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable'] + type_list = [msg.get('message_type') for msg in read_msg_list] # Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables - self.assertEqual(len(messages), 4, 'message_read on last Pigs message should return 2 messages and 2 expandables') + self.assertEqual(len(type_list), 4, 'message_read on last Pigs message should return 2 messages and 2 expandables') self.assertEqual(set([self.msg_2.id, self.msg_10.id]), set(read_msg_ids), 'message_read on the last Pigs message should also get its parent') - self.assertEqual(messages[1].get('parent_id'), messages[0].get('id'), 'message_read should set the ancestor to the thread header') + self.assertEqual(read_ancestor_ids[0], read_ancestor_ids[1], 'message_read should set the ancestor to the thread header') # Data: get expandables new_threads_exp, new_msg_exp = None, None - for msg in messages: - if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('max_limit'): - new_threads_exp = msg - elif msg.get('type') == 'expandable': + new_threads_exp = read_msg_list[-1] + for msg in read_msg_list: + if msg.get('parent_id') and msg.get('message_type') == 'expandable': new_msg_exp = msg - # Do: fetch new messages in first thread, domain from expandable self.assertIsNotNone(new_msg_exp, 'message_read on last Pigs message should have returned a new messages expandable') domain = new_msg_exp.get('domain', []) # Test: expandable, conditions in domain - self.assertIn(('id', 'child_of', self.msg_2.id), domain, 'new messages expandable domain should contain a child_of condition') - self.assertIn(('id', '>=', self.msg_4.id), domain, 'new messages expandable domain should contain an id greater than condition') - self.assertIn(('id', '<=', self.msg_8.id), domain, 'new messages expandable domain should contain an id less than condition') + self.assertIn(('parent_id', '=', self.msg_2.id), domain, 'new messages expandable domain should contain a child_of condition') + self.assertIn(('id', '>', self.msg_2.id), domain, 'new messages expandable domain should contain an id greater than condition') + self.assertIn(('id', '<', self.msg_10.id), domain, 'new messages expandable domain should contain an id less than condition') self.assertEqual(new_msg_exp.get('parent_id'), self.msg_2.id, 'new messages expandable should have parent_id set to the thread header') # Do: message_read with domain, thread_level=0, parent_id=msg_id2 (should be imposed by JS), 2 messages - messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, limit=2, thread_level=0, parent_id=self.msg_2.id) - read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable'] - new_msg_exp = [msg for msg in messages if msg.get('type') == 'expandable'][0] + messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, thread_level=0, limit=2) + read_msg_list = [msg for thread in messages['threads'] for msg in thread] + read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable'] + new_msg_exp = [msg for msg in read_msg_list if msg.get('message_type') == 'expandable'][0] # Test: structure content, 2 messages and 1 thread expandable - self.assertEqual(len(messages), 3, 'message_read in Pigs thread should return 2 messages and 1 expandables') + self.assertEqual(len(read_msg_list), 3, 'message_read in Pigs thread should return 2 messages and 1 expandables') self.assertEqual(set([self.msg_6.id, self.msg_8.id]), set(read_msg_ids), 'message_read in Pigs thread should return 2 more previous messages in thread') # Do: read the last message - messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=new_msg_exp.get('domain'), limit=2, thread_level=0, parent_id=self.msg_2.id) - read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable'] + messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=new_msg_exp.get('domain'), thread_level=0, limit=2) + read_msg_list = [msg for thread in messages['threads'] for msg in thread] + read_msg_ids = [msg.get('id') for msg in read_msg_list] # Test: structure content, 1 message - self.assertEqual(len(messages), 1, 'message_read in Pigs thread should return 1 message') + self.assertEqual(len(read_msg_list), 1, 'message_read in Pigs thread should return 1 message') self.assertEqual(set([self.msg_4.id]), set(read_msg_ids), 'message_read in Pigs thread should return the last message in thread') # Do: fetch a new thread, domain from expandable self.assertIsNotNone(new_threads_exp, 'message_read on last Pigs message should have returned a new threads expandable') @@ -88,33 +92,35 @@ class TestMessageRead(TestMail): self.assertIn(condition, domain, 'new threads expandable domain should contain the message_read domain parameter') self.assertFalse(new_threads_exp.get('parent_id'), 'new threads expandable should not have an parent_id') # Do: message_read with domain, thread_level=1 (should be imposed by JS) - messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, limit=1, thread_level=1) - read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable'] + messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, thread_level=1, limit=1, child_limit=1) + read_msg_list = [msg for thread in messages['threads'] for msg in thread] + read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable'] + read_ancestor_ids = [msg.get('parent_id') if msg.get('parent_id') != 0 else msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable'] + type_list = [msg.get('message_type') for msg in read_msg_list] # Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables - self.assertEqual(len(messages), 4, 'message_read on Pigs should return 2 messages and 2 expandables') + self.assertEqual(len(type_list), 4, 'message_read on Pigs should return 2 messages and 2 expandables') self.assertEqual(set([self.msg_1.id, self.msg_9.id]), set(read_msg_ids), 'message_read on a Pigs message should also get its parent') - self.assertEqual(messages[1].get('parent_id'), messages[0].get('id'), 'message_read should set the ancestor to the thread header') + self.assertEqual(read_ancestor_ids[0], read_ancestor_ids[1], 'message_read should set the ancestor to the thread header') # Data: get expandables new_threads_exp, new_msg_exp = None, None - for msg in messages: - if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('max_limit'): - new_threads_exp = msg - elif msg.get('type') == 'expandable': + new_threads_exp = read_msg_list[-1] + for msg in read_msg_list: + if msg.get('parent_id') and msg.get('message_type') == 'expandable': new_msg_exp = msg # Do: fetch new messages in second thread, domain from expandable self.assertIsNotNone(new_msg_exp, 'message_read on Pigs message should have returned a new messages expandable') domain = new_msg_exp.get('domain', []) # Test: expandable, conditions in domain - self.assertIn(('id', 'child_of', self.msg_1.id), domain, 'new messages expandable domain should contain a child_of condition') - self.assertIn(('id', '>=', self.msg_3.id), domain, 'new messages expandable domain should contain an id greater than condition') - self.assertIn(('id', '<=', self.msg_7.id), domain, 'new messages expandable domain should contain an id less than condition') + self.assertIn(('parent_id', '=', self.msg_1.id), domain, 'new messages expandable domain should contain a child_of condition') + self.assertIn(('id', '>', self.msg_1.id), domain, 'new messages expandable domain should contain an id greater than condition') + self.assertIn(('id', '<', self.msg_9.id), domain, 'new messages expandable domain should contain an id less than condition') self.assertEqual(new_msg_exp.get('parent_id'), self.msg_1.id, 'new messages expandable should have ancestor_id set to the thread header') # Do: message_read with domain, thread_level=0, parent_id=msg_id1 (should be imposed by JS) - messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, limit=200, thread_level=0, parent_id=self.msg_1.id) - read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable'] + messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, thread_level=0, limit=200) + read_msg_list = [msg for thread in messages['threads'] for msg in thread] + read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable'] # Test: other message in thread have been fetch self.assertEqual(set([self.msg_3.id, self.msg_5.id, self.msg_7.id]), set(read_msg_ids), 'message_read on the last Pigs message should also get its parent') - # Test: fetch a new thread, domain from expandable self.assertIsNotNone(new_threads_exp, 'message_read should have returned a new threads expandable') domain = new_threads_exp.get('domain', []) @@ -122,10 +128,12 @@ class TestMessageRead(TestMail): for condition in [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)]: self.assertIn(condition, domain, 'general expandable domain should contain the message_read domain parameter') # Do: message_read with domain, thread_level=1 (should be imposed by JS) - messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, limit=1, thread_level=1) - read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable'] + messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, thread_level=1, limit=1, child_limit=1) + read_msg_list = [msg for thread in messages['threads'] for msg in thread] + read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable'] + type_list = [msg.get('message_type') for msg in read_msg_list] # Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables - self.assertEqual(len(messages), 1, 'message_read on Pigs should return 1 message because everything else has been fetched') + self.assertEqual(len(type_list), 1, 'message_read on Pigs should return 1 message because everything else has been fetched') self.assertEqual([self.msg_0.id], read_msg_ids, 'message_read after 2 More should return only 1 last message') # ---------------------------------------- @@ -133,28 +141,30 @@ class TestMessageRead(TestMail): # ---------------------------------------- # Do: read 2 lasts message, flat - messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], limit=2, thread_level=0) - read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable'] + messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)], thread_level=0, limit=2) + read_msg_list = [msg for thread in messages['threads'] for msg in thread] + read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable'] # Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is not set, 1 expandable - self.assertEqual(len(messages), 3, 'message_read on last Pigs message should return 2 messages and 1 expandable') + self.assertEqual(len(read_msg_list), 3, 'message_read on last Pigs message should return 2 messages and 1 expandable') self.assertEqual(set([self.msg_9.id, self.msg_10.id]), set(read_msg_ids), 'message_read flat on Pigs last messages should only return those messages') - self.assertFalse(messages[0].get('parent_id'), 'message_read flat should set the ancestor as False') - self.assertFalse(messages[1].get('parent_id'), 'message_read flat should set the ancestor as False') + self.assertFalse(read_msg_list[0].get('parent_id'), 'message_read flat should set the ancestor as False') + self.assertFalse(read_msg_list[1].get('parent_id'), 'message_read flat should set the ancestor as False') # Data: get expandables - new_threads_exp, new_msg_exp = None, None - for msg in messages: - if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('max_limit'): - new_threads_exp = msg + new_msg_exp = None + for msg in read_msg_list: + if msg.get('message_type') == 'expandable': + new_msg_exp = msg # Do: fetch new messages, domain from expandable - self.assertIsNotNone(new_threads_exp, 'message_read flat on the 2 last Pigs messages should have returns a new threads expandable') - domain = new_threads_exp.get('domain', []) + self.assertIsNotNone(new_msg_exp, 'message_read flat on the 2 last Pigs messages should have returns a new threads expandable') + domain = new_msg_exp.get('domain', []) # Test: expandable, conditions in domain for condition in [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs.id)]: self.assertIn(condition, domain, 'new threads expandable domain should contain the message_read domain parameter') # Do: message_read with domain, thread_level=0 (should be imposed by JS) - messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, limit=20, thread_level=0) - read_msg_ids = [msg.get('id') for msg in messages if msg.get('type') != 'expandable'] + messages = self.env['mail.message'].sudo(self.user_employee).message_read(domain=domain, thread_level=0, limit=20) + read_msg_list = [msg for thread in messages['threads'] for msg in thread] + read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('message_type') != 'expandable'] # Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables - self.assertEqual(len(messages), 9, 'message_read on Pigs should return 9 messages and 0 expandable') + self.assertEqual(len(read_msg_list), 9, 'message_read on Pigs should return 9 messages and 0 expandable') self.assertEqual([self.msg_8.id, self.msg_7.id, self.msg_6.id, self.msg_5.id, self.msg_4.id, self.msg_3.id, self.msg_2.id, self.msg_1.id, self.msg_0.id], read_msg_ids, 'message_read, More on flat, should return all remaning messages') diff --git a/addons/mail/tests/test_message_track.py b/addons/mail/tests/test_message_track.py index f4a74725b5b..195d1d3c770 100644 --- a/addons/mail/tests/test_message_track.py +++ b/addons/mail/tests/test_message_track.py @@ -10,21 +10,60 @@ class TestTracking(TestMail): should probably be. """ def _strip_string_spaces(body): return body.replace(' ', '').replace('\n', '') - self.group_pigs.message_subscribe_users(user_ids=[self.user_employee.id]) - # Data: res.users.group, to test group_public_id automatic logging - group_system_id = self.env['ir.model.data'].xmlid_to_res_id('base.group_system') or False - - # Data: custom subtypes Subtype = self.env['mail.message.subtype'] Data = self.env['ir.model.data'] - mt_private = Subtype.create({'name': 'private', 'description': 'Private public'}) - Data.create({'name': 'mt_private', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_private.id}) - mt_name_supername = Subtype.create({'name': 'name_supername', 'description': 'Supername name'}) - Data.create({'name': 'mt_name_supername', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_name_supername.id}) - mt_group_public_set = Subtype.create({'name': 'group_public_set', 'description': 'Group set'}) - Data.create({'name': 'mt_group_public_set', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_group_public_set.id}) - mt_group_public = Subtype.create({'name': 'group_public', 'description': 'Group changed'}) - Data.create({'name': 'mt_group_public', 'model': 'mail.message.subtype', 'module': 'mail', 'res_id': mt_group_public.id}) + + group_system = self.env.ref('base.group_system') + group_user = self.env.ref('base.group_user') + self.group_pigs.message_subscribe_users(user_ids=[self.user_employee.id]) + + # mt_private: public field (tracked as onchange) set to 'private' (selection) + mt_private = Subtype.create({ + 'name': 'private', + 'description': 'Public field set to private' + }) + Data.create({ + 'name': 'mt_private', + 'model': 'mail.message.subtype', + 'module': 'mail', + 'res_id': mt_private.id + }) + + # mt_name_supername: name field (tracked as always) set to 'supername' (char) + mt_name_supername = Subtype.create({ + 'name': 'name_supername', + 'description': 'Name field set to supername' + }) + Data.create({ + 'name': 'mt_name_supername', + 'model': 'mail.message.subtype', + 'module': 'mail', + 'res_id': mt_name_supername.id + }) + + # mt_group_public_set: group_public field (tracked as onchange) set to something (m2o) + mt_group_public_set = Subtype.create({ + 'name': 'group_public_set', + 'description': 'Group_public field set' + }) + Data.create({ + 'name': 'mt_group_public_set', + 'model': 'mail.message.subtype', + 'module': 'mail', + 'res_id': mt_group_public_set.id + }) + + # mt_group_public_set: group_public field (tracked as onchange) set to nothing (m2o) + mt_group_public_unset = Subtype.create({ + 'name': 'group_public_unset', + 'description': 'Group_public field unset' + }) + Data.create({ + 'name': 'mt_group_public_unset', + 'model': 'mail.message.subtype', + 'module': 'mail', + 'res_id': mt_group_public_unset.id + }) def _track_subtype(self, cr, uid, ids, init_values, context=None): record = self.browse(cr, uid, ids[0], context=context) @@ -35,11 +74,15 @@ class TestTracking(TestMail): elif 'group_public_id' in init_values and record.group_public_id: return 'mail.mt_group_public_set' elif 'group_public_id' in init_values and not record.group_public_id: - return 'mail.mt_group_public' + return 'mail.mt_group_public_unset' return False self.registry('mail.group')._patch_method('_track_subtype', _track_subtype) - visibility = {'public': 'onchange', 'name': 'always', 'group_public_id': 'onchange'} + visibility = { + 'public': 'onchange', + 'name': 'always', + 'group_public_id': 'onchange' + } cls = type(self.env['mail.group']) for key in visibility: self.assertFalse(hasattr(getattr(cls, key), 'track_visibility')) @@ -51,44 +94,38 @@ class TestTracking(TestMail): del getattr(cls, key).track_visibility # Test: change name -> always tracked, not related to a subtype - self.group_pigs.sudo(self.user_employee).write({'public': 'public'}) - self.assertEqual(len(self.group_pigs.message_ids.ids), 1, 'tracked: a message should have been produced') - # Test: first produced message: no subtype, name change tracked + self.group_pigs.sudo(self.user_employee).write({'name': 'my_name'}) + self.assertEqual(len(self.group_pigs.message_ids), 1) last_msg = self.group_pigs.message_ids[-1] - self.assertFalse(last_msg.subtype_id.id, 'tracked: message should not have been linked to a subtype') - self.assertIn(u"Selectedgroupofusers\u2192Everyone", _strip_string_spaces(last_msg.body), 'tracked: message body incorrect') - self.assertIn('Pigs', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field') - # Test: change name as supername, public as private -> 2 subtypes + self.assertFalse(last_msg.subtype_id) + self.assertEqual(len(last_msg.tracking_value_ids), 1) + self.assertEqual(last_msg.tracking_value_ids.field, 'name') + self.assertEqual(last_msg.tracking_value_ids.field_desc, 'Name') + self.assertEqual(last_msg.tracking_value_ids.old_value_char, 'Pigs') + self.assertEqual(last_msg.tracking_value_ids.new_value_char, 'my_name') + + # Test: change name as supername, public as private -> 1 subtype, private self.group_pigs.sudo(self.user_employee).write({'name': 'supername', 'public': 'private'}) self.group_pigs.invalidate_cache() - self.assertEqual(len(self.group_pigs.message_ids.ids), 2, 'tracked: one new message should have been produced') - # Test: first produced message: mt_name_supername - last_msg = self.group_pigs.message_ids[-2] - self.assertEqual(last_msg.subtype_id.id, mt_private.id, 'tracked: message should be linked to mt_private subtype') - self.assertIn('Private public', last_msg.body, 'tracked: message body does not hold the subtype description') - self.assertIn(u'Pigs\u2192supername', _strip_string_spaces(last_msg.body), 'tracked: message body incorrect') + self.assertEqual(len(self.group_pigs.message_ids.ids), 2) + last_msg = self.group_pigs.message_ids[0] + self.assertEqual(last_msg.subtype_id, mt_private) + self.assertEqual(len(last_msg.tracking_value_ids), 2) + self.assertEqual(set(last_msg.tracking_value_ids.mapped('field')), set(['name', 'public'])) + self.assertEqual(set(last_msg.tracking_value_ids.mapped('field_desc')), set(['Name', 'Privacy'])) + self.assertEqual(set(last_msg.tracking_value_ids.mapped('old_value_char')), set(['my_name', 'Selected group of users'])) + self.assertEqual(set(last_msg.tracking_value_ids.mapped('new_value_char')), set(['supername', 'Invited people only'])) - # Test: change public as public, group_public_id -> 1 subtype, name always tracked - self.group_pigs.sudo(self.user_employee).write({'public': 'public', 'group_public_id': group_system_id}) + # Test: change public as public, group_public_id -> 1 subtype, group public set + self.group_pigs.sudo(self.user_employee).write({'public': 'public', 'group_public_id': group_system.id}) self.group_pigs.invalidate_cache() - self.assertEqual(len(self.group_pigs.message_ids), 3, 'tracked: one message should have been produced') - # Test: first produced message: mt_group_public_set_id, with name always tracked, public tracked on change - last_msg = self.group_pigs.message_ids[-3] - self.assertEqual(last_msg.subtype_id, mt_group_public_set, 'tracked: message should be linked to mt_group_public_set_id') - self.assertIn('Group set', last_msg.body, 'tracked: message body does not hold the subtype description') - self.assertIn(u"Invitedpeopleonly\u2192Everyone", _strip_string_spaces(last_msg.body), 'tracked: message body does not hold changed tracked field') - self.assertIn(u'HumanResources/Employee\u2192Administration/Settings', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field') - - # Test: change group_public_id to False -> 1 subtype, name always tracked - self.group_pigs.sudo(self.user_employee).write({'group_public_id': False}) - self.group_pigs.invalidate_cache() - self.assertEqual(len(self.group_pigs.message_ids), 4, 'tracked: one message should have been produced') - # Test: first produced message: mt_group_public_set_id, with name always tracked, public tracked on change - last_msg = self.group_pigs.message_ids[-4] - self.assertEqual(last_msg.subtype_id, mt_group_public, 'tracked: message should be linked to mt_group_public_id') - self.assertIn('Group changed', last_msg.body, 'tracked: message body does not hold the subtype description') - self.assertIn(u'Administration/Settings\u2192', _strip_string_spaces(last_msg.body), 'tracked: message body does not hold always tracked field') - # Test: change not tracked field, no tracking message - self.group_pigs.sudo(self.user_employee).write({'description': 'Dummy'}) - self.group_pigs.invalidate_cache() - self.assertEqual(len(self.group_pigs.message_ids), 4, 'tracked: No message should have been produced') + self.assertEqual(len(self.group_pigs.message_ids), 3) + last_msg = self.group_pigs.message_ids[0] + self.assertEqual(last_msg.subtype_id, mt_group_public_set) + self.assertEqual(len(last_msg.tracking_value_ids), 2) + self.assertEqual(set(last_msg.tracking_value_ids.mapped('field')), set(['group_public_id', 'public'])) + self.assertEqual(set(last_msg.tracking_value_ids.mapped('field_desc')), set(['Authorized Group', 'Privacy'])) + self.assertEqual(set(last_msg.tracking_value_ids.mapped('old_value_char')), set([group_user.name_get()[0][1], 'Invited people only'])) + self.assertEqual(set(last_msg.tracking_value_ids.mapped('new_value_char')), set([group_system.name_get()[0][1], 'Everyone'])) + self.assertEqual(set(last_msg.tracking_value_ids.mapped('old_value_integer')), set([0, group_user.id])) + self.assertEqual(set(last_msg.tracking_value_ids.mapped('new_value_integer')), set([0, group_system.id])) diff --git a/addons/mail/views/mail_group_views.xml b/addons/mail/views/mail_group_views.xml index edd008e52d0..20160dcff94 100644 --- a/addons/mail/views/mail_group_views.xml +++ b/addons/mail/views/mail_group_views.xml @@ -2,20 +2,11 @@ - + Discussion Group - mail.wall mail.message - { - } - { - 'read_action': 'read' - } - -

    - No message in this group. -

    -
    + form + timeline
    diff --git a/addons/mail/views/mail_message_views.xml b/addons/mail/views/mail_message_views.xml index ec19d622553..f7fa1060627 100644 --- a/addons/mail/views/mail_message_views.xml +++ b/addons/mail/views/mail_message_views.xml @@ -126,7 +126,291 @@ kanban,tree,form
    + + mail.message.view.timeline. + mail.message + + + + + + + + + + + + + + + + + +
    + +
    + - - - - - - - - - - - See more messages - - - - - - - - - - - +
    + +
    +
    + +
    +
    + +
    +
    + + + + +
    + +
    +
    + +
    + +
    + + Update + + + + + by + + + + + + + + +
    + + + + + Message + Email + Note + from + + + + + + + + + • + + + + + + Like + Unlike + + + +
    + + + + + + + + +
    +
    + +
    + +
      + +
    • + + : + + + + + +
    • +
      +
    +
    + + + + + + +
    + + + +
    +
    +
    +
    +
    +
    + + +
    + +
    + - - - - - - - - - - - See more messages - - - - - - - - - - - +
    + + + + +
    + +
    +
    +
    + +
    + +
    + +
    + + + + + : + + + + + + + + + No subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + + + + + + + + +
    +
    +
    +
    diff --git a/addons/mail/views/mail_thread_views.xml b/addons/mail/views/mail_thread_views.xml index f4a3e3815d6..d9425d40b5a 100644 --- a/addons/mail/views/mail_thread_views.xml +++ b/addons/mail/views/mail_thread_views.xml @@ -1,168 +1,41 @@ - - - Inbox - mail.wall - mail.message - { - 'default_model': 'res.users', - 'default_res_id': uid, - 'thread_model': 'res.partner', - 'needaction_menu_ref': ['mail.mail_tomefeeds', 'mail.mail_starfeeds'] - } - - -

    - Good Job! Your inbox is empty. -

    - Your inbox contains private messages or emails sent to you - as well as information related to documents or people you - follow. -

    -
    -
    + + + - - To: me - mail.wall - mail.message - { - 'default_model': 'res.users', - 'default_res_id': uid, - 'thread_model': 'res.partner', - 'search_default_message_unread': True, - 'needaction_menu_ref': ['mail.mail_starfeeds', 'mail.mail_inboxfeeds'] - } - - -

    - No private message. -

    - This list contains messages sent to you. -

    -
    -
    + + Inbox + mail.message + form + timeline + { + 'default_model': 'res.users', + 'default_res_id': uid, + 'thread_model': 'res.partner', + 'options' : {'view_mailbox': True, + 'view_inbox': True, + 'show_write_to_followers': False} + } + [ + '|', + ('notification_ids.partner_id.user_ids', 'in', [uid]), + ('author_id.user_ids', 'in', [uid]), + ] + - - To-do - mail.wall - mail.message - { - 'default_model': 'res.users', - 'default_res_id': uid, - 'thread_model': 'res.partner', - 'search_default_message_unread': True - } - - -

    - No todo. -

    - When you process messages in your inbox, you can mark some - as todo. From this menu, you can process all your todo. -

    -
    -
    + + + + Mail Redirection (Document / Inbox) + True + + action = pool.get('mail.thread').message_redirect_action(cr, uid, context) + code + ir.actions.server + - - Archives - mail.wall - { - 'default_model': 'res.users', - 'default_res_id': uid, - 'thread_model': 'res.partner', - 'needaction_menu_ref': ['mail.mail_tomefeeds', 'mail.mail_starfeeds', 'mail.mail_inboxfeeds'] - } - - -

    - No message found and no message sent yet. -

    - Click on the top-right icon to compose a message. This - message will be sent by email if it's an internal contact. -

    -
    -
    - - - Mail Redirection (Document / Inbox) - True - - action = pool.get('mail.thread').message_redirect_action(cr, uid, context) - code - ir.actions.server - - - - - - - - - - - - -
    -
    + + \ No newline at end of file diff --git a/addons/mail/views/mail_tracking_views.xml b/addons/mail/views/mail_tracking_views.xml new file mode 100644 index 00000000000..d359e2c3a3d --- /dev/null +++ b/addons/mail/views/mail_tracking_views.xml @@ -0,0 +1,67 @@ + + + + + + mail.tracking.value.tree + mail.tracking.value + 12 + + + + + + + + + + + + + + + + + + + + mail.tracking.value.form + mail.tracking.value + +
    + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + + Tracking Values + mail.tracking.value + form + tree,form + + + +
    +
    \ No newline at end of file diff --git a/addons/mail_tip/static/src/js/mail_tip.js b/addons/mail_tip/static/src/js/mail_tip.js index 0ac5f47db4f..1e1eda4ac65 100644 --- a/addons/mail_tip/static/src/js/mail_tip.js +++ b/addons/mail_tip/static/src/js/mail_tip.js @@ -4,7 +4,7 @@ odoo.define('mail_tip.mail_tip', function (require) { var mail = require('mail.mail'); var core = require('web.core'); -mail.Thread.include({ +mail.MailThread.include({ message_fetch: function() { return this._super.apply(this, arguments).done(function() { core.bus.trigger('chatter_messages_fetched'); diff --git a/addons/portal/portal_view.xml b/addons/portal/portal_view.xml index 4e0f0df1344..05aab7bdb54 100644 --- a/addons/portal/portal_view.xml +++ b/addons/portal/portal_view.xml @@ -11,101 +11,29 @@ - + Inbox - mail.wall mail.message + form + timeline { - 'default_model': 'res.users', - 'default_res_id': uid, - 'thread_model': 'res.partner', + 'default_model': 'res.users', + 'default_res_id': uid, + 'thread_model': 'res.partner', + 'options' : {'view_mailbox': True, + 'view_inbox': True, + 'show_link': 'False', + 'show_write_to_followers': False} } - - -

    - Good Job! Your inbox is empty. -

    - Your inbox contains private messages or emails sent to you - as well as information related to documents or people you - follow. -

    -
    -
    - - - - To-do - mail.wall - mail.message - { - 'default_model': 'res.users', - 'default_res_id': uid, - 'thread_model': 'res.partner', - 'search_default_message_unread': True - } - - -

    - No todo. -

    - When you process messages in your inbox, you can mark some - as todo. From this menu, you can process all your todo. -

    -
    -
    - - - - - Archives - mail.wall - { - 'default_model': 'res.users', - 'default_res_id': uid, - 'thread_model': 'res.partner', - } - - -

    - No message found and no message sent yet. -

    - Click on the top-right icon to compose a message. This - message will be sent by email if it's an internal contact. -

    -
    + [ + '|', + ('notification_ids.partner_id.user_ids', 'in', [uid]), + ('author_id.user_ids', 'in', [uid]), + ]
    -