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]), + ]
    -