From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/mail/static/src/audio/ting.mp3 | Bin 0 -> 10866 bytes addons/mail/static/src/audio/ting.ogg | Bin 0 -> 14654 bytes addons/mail/static/src/bugfix/bugfix.js | 191 + addons/mail/static/src/bugfix/bugfix.scss | 6 + addons/mail/static/src/bugfix/bugfix.xml | 11 + addons/mail/static/src/bugfix/bugfix_tests.js | 18 + .../use_drag_visible_dropzone.js | 93 + .../src/component_hooks/use_refs/use_refs.js | 21 + .../src/component_hooks/use_store/use_store.js | 126 + .../component_hooks/use_store/use_store_tests.js | 189 + .../static/src/components/activity/activity.js | 199 + .../static/src/components/activity/activity.scss | 186 + .../static/src/components/activity/activity.xml | 153 + .../src/components/activity/activity_tests.js | 1157 +++++ .../src/components/activity_box/activity_box.js | 64 + .../src/components/activity_box/activity_box.scss | 45 + .../src/components/activity_box/activity_box.xml | 45 + .../activity_mark_done_popover.js | 122 + .../activity_mark_done_popover.scss | 20 + .../activity_mark_done_popover.xml | 23 + .../activity_mark_done_popover_tests.js | 297 ++ .../static/src/components/attachment/attachment.js | 204 + .../src/components/attachment/attachment.scss | 204 + .../src/components/attachment/attachment.xml | 115 + .../src/components/attachment/attachment_tests.js | 762 ++++ .../components/attachment_box/attachment_box.js | 124 + .../components/attachment_box/attachment_box.scss | 46 + .../components/attachment_box/attachment_box.xml | 50 + .../attachment_box/attachment_box_tests.js | 337 ++ .../attachment_delete_confirm_dialog.js | 92 + .../attachment_delete_confirm_dialog.xml | 12 + .../components/attachment_list/attachment_list.js | 119 + .../attachment_list/attachment_list.scss | 29 + .../components/attachment_list/attachment_list.xml | 39 + .../attachment_viewer/attachment_viewer.js | 598 +++ .../attachment_viewer/attachment_viewer.scss | 198 + .../attachment_viewer/attachment_viewer.xml | 93 + .../autocomplete_input/autocomplete_input.js | 174 + .../autocomplete_input/autocomplete_input.xml | 8 + .../src/components/chat_window/chat_window.js | 363 ++ .../src/components/chat_window/chat_window.scss | 93 + .../src/components/chat_window/chat_window.xml | 54 + .../chat_window_header/chat_window_header.js | 118 + .../chat_window_header/chat_window_header.scss | 95 + .../chat_window_header/chat_window_header.xml | 56 + .../chat_window_hidden_menu.js | 141 + .../chat_window_hidden_menu.scss | 90 + .../chat_window_hidden_menu.xml | 31 + .../chat_window_manager/chat_window_manager.js | 51 + .../chat_window_manager/chat_window_manager.scss | 16 + .../chat_window_manager/chat_window_manager.xml | 23 + .../chat_window_manager_tests.js | 2423 +++++++++++ .../mail/static/src/components/chatter/chatter.js | 150 + .../static/src/components/chatter/chatter.scss | 42 + .../mail/static/src/components/chatter/chatter.xml | 56 + .../chatter/chatter_suggested_recipient_tests.js | 420 ++ .../static/src/components/chatter/chatter_tests.js | 469 +++ .../chatter_container/chatter_container.js | 139 + .../chatter_container/chatter_container.scss | 25 + .../chatter_container/chatter_container.xml | 15 + .../components/chatter_topbar/chatter_topbar.js | 137 + .../components/chatter_topbar/chatter_topbar.scss | 106 + .../components/chatter_topbar/chatter_topbar.xml | 74 + .../chatter_topbar/chatter_topbar_tests.js | 730 ++++ .../static/src/components/composer/composer.js | 444 ++ .../static/src/components/composer/composer.scss | 273 ++ .../static/src/components/composer/composer.xml | 179 + .../src/components/composer/composer_tests.js | 2153 ++++++++++ .../composer_suggested_recipient.js | 158 + .../composer_suggested_recipient.scss | 5 + .../composer_suggested_recipient.xml | 41 + .../composer_suggested_recipient_list.js | 77 + .../composer_suggested_recipient_list.scss | 3 + .../composer_suggested_recipient_list.xml | 26 + .../composer_suggestion/composer_suggestion.js | 143 + .../composer_suggestion/composer_suggestion.scss | 43 + .../composer_suggestion/composer_suggestion.xml | 33 + .../composer_suggestion_canned_response_tests.js | 154 + .../composer_suggestion_channel_tests.js | 144 + .../composer_suggestion_command_tests.js | 151 + .../composer_suggestion_partner_tests.js | 160 + .../composer_suggestion_list.js | 73 + .../composer_suggestion_list.scss | 27 + .../composer_suggestion_list.xml | 32 + .../composer_text_input/composer_text_input.js | 419 ++ .../composer_text_input/composer_text_input.scss | 40 + .../composer_text_input/composer_text_input.xml | 24 + addons/mail/static/src/components/dialog/dialog.js | 119 + .../mail/static/src/components/dialog/dialog.scss | 23 + .../mail/static/src/components/dialog/dialog.xml | 22 + .../components/dialog_manager/dialog_manager.js | 69 + .../components/dialog_manager/dialog_manager.xml | 17 + .../dialog_manager/dialog_manager_tests.js | 82 + .../mail/static/src/components/discuss/discuss.js | 313 ++ .../static/src/components/discuss/discuss.scss | 114 + .../mail/static/src/components/discuss/discuss.xml | 106 + .../discuss/tests/discuss_domain_tests.js | 408 ++ .../discuss/tests/discuss_inbox_tests.js | 725 ++++ .../discuss/tests/discuss_moderation_tests.js | 1180 ++++++ .../discuss/tests/discuss_pinned_tests.js | 238 ++ .../discuss/tests/discuss_sidebar_tests.js | 163 + .../src/components/discuss/tests/discuss_tests.js | 4447 ++++++++++++++++++++ .../discuss_mobile_mailbox_selection.js | 95 + .../discuss_mobile_mailbox_selection.scss | 26 + .../discuss_mobile_mailbox_selection.xml | 20 + .../discuss_mobile_mailbox_selection_tests.js | 130 + .../components/discuss_sidebar/discuss_sidebar.js | 308 ++ .../discuss_sidebar/discuss_sidebar.scss | 110 + .../components/discuss_sidebar/discuss_sidebar.xml | 81 + .../discuss_sidebar_item/discuss_sidebar_item.js | 220 + .../discuss_sidebar_item/discuss_sidebar_item.scss | 109 + .../discuss_sidebar_item/discuss_sidebar_item.xml | 63 + .../static/src/components/drop_zone/drop_zone.js | 139 + .../static/src/components/drop_zone/drop_zone.scss | 29 + .../static/src/components/drop_zone/drop_zone.xml | 12 + .../src/components/editable_text/editable_text.js | 91 + .../src/components/editable_text/editable_text.xml | 8 + .../components/emojis_popover/emojis_popover.js | 78 + .../components/emojis_popover/emojis_popover.scss | 22 + .../components/emojis_popover/emojis_popover.xml | 14 + .../src/components/file_uploader/file_uploader.js | 241 ++ .../components/file_uploader/file_uploader.scss | 3 + .../src/components/file_uploader/file_uploader.xml | 10 + .../file_uploader/file_uploader_tests.js | 94 + .../src/components/follow_button/follow_button.js | 93 + .../components/follow_button/follow_button.scss | 27 + .../src/components/follow_button/follow_button.xml | 24 + .../follow_button/follow_button_tests.js | 278 ++ .../static/src/components/follower/follower.js | 80 + .../static/src/components/follower/follower.scss | 55 + .../static/src/components/follower/follower.xml | 23 + .../src/components/follower/follower_tests.js | 380 ++ .../follower_list_menu/follower_list_menu.js | 154 + .../follower_list_menu/follower_list_menu.scss | 17 + .../follower_list_menu/follower_list_menu.xml | 38 + .../follower_list_menu/follower_list_menu_tests.js | 424 ++ .../follower_subtype/follower_subtype.js | 71 + .../follower_subtype/follower_subtype.scss | 27 + .../follower_subtype/follower_subtype.xml | 13 + .../follower_subtype/follower_subtype_tests.js | 233 + .../follower_subtype_list/follower_subtype_list.js | 89 + .../follower_subtype_list.scss | 8 + .../follower_subtype_list.xml | 38 + .../src/components/mail_template/mail_template.js | 81 + .../components/mail_template/mail_template.scss | 27 + .../src/components/mail_template/mail_template.xml | 29 + .../mail/static/src/components/message/message.js | 680 +++ .../static/src/components/message/message.scss | 381 ++ .../mail/static/src/components/message/message.xml | 210 + .../static/src/components/message/message_tests.js | 1580 +++++++ .../message_author_prefix/message_author_prefix.js | 67 + .../message_author_prefix.scss | 11 + .../message_author_prefix.xml | 17 + .../src/components/message_list/message_list.js | 600 +++ .../src/components/message_list/message_list.scss | 135 + .../src/components/message_list/message_list.xml | 103 + .../message_seen_indicator.js | 136 + .../message_seen_indicator.scss | 39 + .../message_seen_indicator.xml | 16 + .../message_seen_indicator_tests.js | 294 ++ .../components/messaging_menu/messaging_menu.js | 234 + .../components/messaging_menu/messaging_menu.scss | 143 + .../components/messaging_menu/messaging_menu.xml | 83 + .../messaging_menu/messaging_menu_tests.js | 1039 +++++ .../mobile_messaging_navbar.js | 61 + .../mobile_messaging_navbar.scss | 43 + .../mobile_messaging_navbar.xml | 17 + .../moderation_ban_dialog/moderation_ban_dialog.js | 94 + .../moderation_ban_dialog.xml | 23 + .../moderation_discard_dialog.js | 109 + .../moderation_discard_dialog.xml | 13 + .../moderation_reject_dialog.js | 104 + .../moderation_reject_dialog.xml | 13 + .../notification_alert/notification_alert.js | 54 + .../notification_alert/notification_alert.xml | 14 + .../notification_group/notification_group.js | 93 + .../notification_group/notification_group.scss | 93 + .../notification_group/notification_group.xml | 39 + .../notification_list/notification_list.js | 226 + .../notification_list/notification_list.scss | 37 + .../notification_list/notification_list.xml | 47 + .../notification_list/notification_list_item.scss | 179 + .../notification_list_notification_group_tests.js | 546 +++ .../notification_list/notification_list_tests.js | 162 + .../notification_popover/notification_popover.js | 95 + .../notification_popover/notification_popover.scss | 7 + .../notification_popover/notification_popover.xml | 17 + .../notification_request/notification_request.js | 94 + .../notification_request/notification_request.scss | 77 + .../notification_request/notification_request.xml | 31 + .../partner_im_status_icon.js | 74 + .../partner_im_status_icon.scss | 59 + .../partner_im_status_icon.xml | 38 + .../partner_im_status_icon_tests.js | 145 + .../src/components/thread_icon/thread_icon.js | 64 + .../src/components/thread_icon/thread_icon.scss | 26 + .../src/components/thread_icon/thread_icon.xml | 58 + .../components/thread_icon/thread_icon_tests.js | 118 + .../thread_needaction_preview.js | 151 + .../thread_needaction_preview.scss | 108 + .../thread_needaction_preview.xml | 58 + .../thread_needaction_preview_tests.js | 457 ++ .../components/thread_preview/thread_preview.js | 130 + .../components/thread_preview/thread_preview.scss | 117 + .../components/thread_preview/thread_preview.xml | 63 + .../thread_preview/thread_preview_tests.js | 114 + .../thread_textual_typing_status.js | 52 + .../thread_textual_typing_status.scss | 12 + .../thread_textual_typing_status.xml | 14 + .../thread_textual_typing_status_tests.js | 367 ++ .../thread_typing_icon/thread_typing_icon.js | 41 + .../thread_typing_icon/thread_typing_icon.scss | 108 + .../thread_typing_icon/thread_typing_icon.xml | 29 + .../src/components/thread_view/thread_view.js | 222 + .../src/components/thread_view/thread_view.scss | 38 + .../src/components/thread_view/thread_view.xml | 50 + .../components/thread_view/thread_view_tests.js | 1809 ++++++++ addons/mail/static/src/env/test_env.js | 148 + addons/mail/static/src/img/_al.png | Bin 0 -> 1017 bytes addons/mail/static/src/img/_pinky.png | Bin 0 -> 7725 bytes addons/mail/static/src/img/attachment.png | Bin 0 -> 1142 bytes addons/mail/static/src/img/checklist.png | Bin 0 -> 976 bytes addons/mail/static/src/img/email_icon.png | Bin 0 -> 924 bytes addons/mail/static/src/img/email_template.png | Bin 0 -> 2898 bytes addons/mail/static/src/img/email_template_save.png | Bin 0 -> 3100 bytes addons/mail/static/src/img/formatting.png | Bin 0 -> 3466 bytes addons/mail/static/src/img/groupdefault.png | Bin 0 -> 2461 bytes addons/mail/static/src/img/odoo_o.png | Bin 0 -> 556 bytes addons/mail/static/src/img/odoobot.png | Bin 0 -> 2215 bytes addons/mail/static/src/img/odoobot_transparent.png | Bin 0 -> 4487 bytes addons/mail/static/src/img/smiley/avatar.jpg | Bin 0 -> 6462 bytes addons/mail/static/src/img/smiley/green.png | Bin 0 -> 1239 bytes addons/mail/static/src/img/smiley/mailfailure.jpg | Bin 0 -> 10498 bytes addons/mail/static/src/img/smiley/yellow.png | Bin 0 -> 1235 bytes addons/mail/static/src/js/activity.js | 868 ++++ addons/mail/static/src/js/basic_view.js | 68 + addons/mail/static/src/js/core/translation.js | 28 + addons/mail/static/src/js/custom_filter_item.js | 21 + addons/mail/static/src/js/document_viewer.js | 396 ++ addons/mail/static/src/js/emojis.js | 155 + addons/mail/static/src/js/emojis_mixin.js | 91 + addons/mail/static/src/js/field_char.js | 56 + addons/mail/static/src/js/field_char_emojis.js | 18 + addons/mail/static/src/js/field_emojis_common.js | 136 + addons/mail/static/src/js/field_text_emojis.js | 18 + addons/mail/static/src/js/main.js | 126 + addons/mail/static/src/js/many2many_tags_email.js | 135 + addons/mail/static/src/js/many2one_avatar_user.js | 68 + .../static/src/js/systray/systray_activity_menu.js | 202 + addons/mail/static/src/js/tools/debug_manager.js | 33 + addons/mail/static/src/js/tours/mail.js | 59 + addons/mail/static/src/js/utils.js | 187 + .../static/src/js/views/activity/activity_cell.js | 42 + .../src/js/views/activity/activity_controller.js | 124 + .../static/src/js/views/activity/activity_model.js | 124 + .../src/js/views/activity/activity_record.js | 62 + .../src/js/views/activity/activity_renderer.js | 210 + .../static/src/js/views/activity/activity_view.js | 53 + addons/mail/static/src/model/model_core.js | 125 + addons/mail/static/src/model/model_errors.js | 22 + addons/mail/static/src/model/model_field.js | 820 ++++ .../mail/static/src/model/model_field_command.js | 73 + addons/mail/static/src/model/model_manager.js | 1098 +++++ addons/mail/static/src/models/activity/activity.js | 355 ++ .../src/models/activity_type/activity_type.js | 39 + .../static/src/models/attachment/attachment.js | 439 ++ .../src/models/attachment/attachment_tests.js | 144 + .../models/attachment_viewer/attachment_viewer.js | 59 + .../src/models/canned_response/canned_response.js | 107 + .../src/models/channel_command/channel_command.js | 130 + .../static/src/models/chat_window/chat_window.js | 480 +++ .../chat_window_manager/chat_window_manager.js | 487 +++ addons/mail/static/src/models/chatter/chatter.js | 334 ++ addons/mail/static/src/models/composer/composer.js | 1435 +++++++ addons/mail/static/src/models/country/country.js | 55 + addons/mail/static/src/models/device/device.js | 71 + addons/mail/static/src/models/dialog/dialog.js | 32 + .../src/models/dialog_manager/dialog_manager.js | 52 + addons/mail/static/src/models/discuss/discuss.js | 568 +++ addons/mail/static/src/models/follower/follower.js | 293 ++ .../models/follower_subtype/follower_subtype.js | 82 + .../follower_subtype_list/follower_subtype_list.js | 22 + addons/mail/static/src/models/locale/locale.js | 52 + .../src/models/mail_template/mail_template.js | 83 + addons/mail/static/src/models/message/message.js | 817 ++++ .../static/src/models/message/message_tests.js | 187 + .../message_seen_indicator.js | 358 ++ .../mail/static/src/models/messaging/messaging.js | 253 ++ .../static/src/models/messaging/messaging_tests.js | 126 + .../messaging_initializer/messaging_initializer.js | 304 ++ .../src/models/messaging_menu/messaging_menu.js | 154 + .../messaging_notification_handler.js | 795 ++++ addons/mail/static/src/models/model/model.js | 291 ++ .../static/src/models/notification/notification.js | 80 + .../notification_group/notification_group.js | 126 + .../notification_group_manager.js | 77 + addons/mail/static/src/models/partner/partner.js | 527 +++ .../suggested_recipient_info.js | 116 + addons/mail/static/src/models/thread/thread.js | 2324 ++++++++++ .../mail/static/src/models/thread/thread_tests.js | 150 + .../static/src/models/thread_cache/thread_cache.js | 617 +++ .../thread_partner_seen_info.js | 109 + .../static/src/models/thread_view/thread_view.js | 441 ++ .../static/src/models/thread_view/thread_viewer.js | 296 ++ addons/mail/static/src/models/user/user.js | 254 ++ addons/mail/static/src/scss/activity_view.scss | 132 + addons/mail/static/src/scss/composer.scss | 161 + addons/mail/static/src/scss/discuss.scss | 191 + addons/mail/static/src/scss/emojis.scss | 67 + addons/mail/static/src/scss/kanban_view.scss | 64 + addons/mail/static/src/scss/mail_activity.scss | 242 ++ .../mail/static/src/scss/many2one_avatar_user.scss | 6 + addons/mail/static/src/scss/systray.scss | 137 + addons/mail/static/src/scss/thread.scss | 173 + addons/mail/static/src/scss/variables.scss | 19 + .../chat_window_service/chat_window_service.js | 104 + .../src/services/dialog_service/dialog_service.js | 101 + addons/mail/static/src/utils/deferred/deferred.js | 21 + addons/mail/static/src/utils/test_utils.js | 767 ++++ addons/mail/static/src/utils/throttle/throttle.js | 382 ++ .../static/src/utils/throttle/throttle_tests.js | 407 ++ addons/mail/static/src/utils/timer/timer.js | 165 + addons/mail/static/src/utils/timer/timer_tests.js | 427 ++ addons/mail/static/src/utils/utils.js | 193 + addons/mail/static/src/variables.scss | 16 + addons/mail/static/src/widgets/common.xml | 25 + addons/mail/static/src/widgets/discuss/discuss.js | 397 ++ .../mail/static/src/widgets/discuss/discuss.scss | 36 + addons/mail/static/src/widgets/discuss/discuss.xml | 27 + .../discuss_invite_partner_dialog.js | 124 + .../discuss_invite_partner_dialog.xml | 13 + .../src/widgets/form_renderer/form_renderer.js | 188 + .../src/widgets/form_renderer/form_renderer.scss | 17 + .../widgets/form_renderer/form_renderer_tests.js | 982 +++++ .../src/widgets/messaging_menu/messaging_menu.js | 56 + .../src/widgets/messaging_menu/messaging_menu.xml | 8 + .../notification_alert/notification_alert.js | 45 + .../notification_alert/notification_alert_tests.js | 103 + addons/mail/static/src/xml/activity.xml | 130 + addons/mail/static/src/xml/activity_view.xml | 93 + addons/mail/static/src/xml/composer.xml | 20 + .../mail/static/src/xml/many2one_avatar_user.xml | 12 + addons/mail/static/src/xml/systray.xml | 61 + addons/mail/static/src/xml/text_emojis.xml | 18 + addons/mail/static/src/xml/thread.xml | 102 + addons/mail/static/src/xml/web_kanban_activity.xml | 114 + 346 files changed, 67238 insertions(+) create mode 100644 addons/mail/static/src/audio/ting.mp3 create mode 100644 addons/mail/static/src/audio/ting.ogg create mode 100644 addons/mail/static/src/bugfix/bugfix.js create mode 100644 addons/mail/static/src/bugfix/bugfix.scss create mode 100644 addons/mail/static/src/bugfix/bugfix.xml create mode 100644 addons/mail/static/src/bugfix/bugfix_tests.js create mode 100644 addons/mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js create mode 100644 addons/mail/static/src/component_hooks/use_refs/use_refs.js create mode 100644 addons/mail/static/src/component_hooks/use_store/use_store.js create mode 100644 addons/mail/static/src/component_hooks/use_store/use_store_tests.js create mode 100644 addons/mail/static/src/components/activity/activity.js create mode 100644 addons/mail/static/src/components/activity/activity.scss create mode 100644 addons/mail/static/src/components/activity/activity.xml create mode 100644 addons/mail/static/src/components/activity/activity_tests.js create mode 100644 addons/mail/static/src/components/activity_box/activity_box.js create mode 100644 addons/mail/static/src/components/activity_box/activity_box.scss create mode 100644 addons/mail/static/src/components/activity_box/activity_box.xml create mode 100644 addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.js create mode 100644 addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.scss create mode 100644 addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.xml create mode 100644 addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover_tests.js create mode 100644 addons/mail/static/src/components/attachment/attachment.js create mode 100644 addons/mail/static/src/components/attachment/attachment.scss create mode 100644 addons/mail/static/src/components/attachment/attachment.xml create mode 100644 addons/mail/static/src/components/attachment/attachment_tests.js create mode 100644 addons/mail/static/src/components/attachment_box/attachment_box.js create mode 100644 addons/mail/static/src/components/attachment_box/attachment_box.scss create mode 100644 addons/mail/static/src/components/attachment_box/attachment_box.xml create mode 100644 addons/mail/static/src/components/attachment_box/attachment_box_tests.js create mode 100644 addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.js create mode 100644 addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.xml create mode 100644 addons/mail/static/src/components/attachment_list/attachment_list.js create mode 100644 addons/mail/static/src/components/attachment_list/attachment_list.scss create mode 100644 addons/mail/static/src/components/attachment_list/attachment_list.xml create mode 100644 addons/mail/static/src/components/attachment_viewer/attachment_viewer.js create mode 100644 addons/mail/static/src/components/attachment_viewer/attachment_viewer.scss create mode 100644 addons/mail/static/src/components/attachment_viewer/attachment_viewer.xml create mode 100644 addons/mail/static/src/components/autocomplete_input/autocomplete_input.js create mode 100644 addons/mail/static/src/components/autocomplete_input/autocomplete_input.xml create mode 100644 addons/mail/static/src/components/chat_window/chat_window.js create mode 100644 addons/mail/static/src/components/chat_window/chat_window.scss create mode 100644 addons/mail/static/src/components/chat_window/chat_window.xml create mode 100644 addons/mail/static/src/components/chat_window_header/chat_window_header.js create mode 100644 addons/mail/static/src/components/chat_window_header/chat_window_header.scss create mode 100644 addons/mail/static/src/components/chat_window_header/chat_window_header.xml create mode 100644 addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.js create mode 100644 addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.scss create mode 100644 addons/mail/static/src/components/chat_window_hidden_menu/chat_window_hidden_menu.xml create mode 100644 addons/mail/static/src/components/chat_window_manager/chat_window_manager.js create mode 100644 addons/mail/static/src/components/chat_window_manager/chat_window_manager.scss create mode 100644 addons/mail/static/src/components/chat_window_manager/chat_window_manager.xml create mode 100644 addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js create mode 100644 addons/mail/static/src/components/chatter/chatter.js create mode 100644 addons/mail/static/src/components/chatter/chatter.scss create mode 100644 addons/mail/static/src/components/chatter/chatter.xml create mode 100644 addons/mail/static/src/components/chatter/chatter_suggested_recipient_tests.js create mode 100644 addons/mail/static/src/components/chatter/chatter_tests.js create mode 100644 addons/mail/static/src/components/chatter_container/chatter_container.js create mode 100644 addons/mail/static/src/components/chatter_container/chatter_container.scss create mode 100644 addons/mail/static/src/components/chatter_container/chatter_container.xml create mode 100644 addons/mail/static/src/components/chatter_topbar/chatter_topbar.js create mode 100644 addons/mail/static/src/components/chatter_topbar/chatter_topbar.scss create mode 100644 addons/mail/static/src/components/chatter_topbar/chatter_topbar.xml create mode 100644 addons/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js create mode 100644 addons/mail/static/src/components/composer/composer.js create mode 100644 addons/mail/static/src/components/composer/composer.scss create mode 100644 addons/mail/static/src/components/composer/composer.xml create mode 100644 addons/mail/static/src/components/composer/composer_tests.js create mode 100644 addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js create mode 100644 addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.scss create mode 100644 addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.xml create mode 100644 addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.js create mode 100644 addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.scss create mode 100644 addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.xml create mode 100644 addons/mail/static/src/components/composer_suggestion/composer_suggestion.js create mode 100644 addons/mail/static/src/components/composer_suggestion/composer_suggestion.scss create mode 100644 addons/mail/static/src/components/composer_suggestion/composer_suggestion.xml create mode 100644 addons/mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js create mode 100644 addons/mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js create mode 100644 addons/mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js create mode 100644 addons/mail/static/src/components/composer_suggestion/composer_suggestion_partner_tests.js create mode 100644 addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.js create mode 100644 addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.scss create mode 100644 addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.xml create mode 100644 addons/mail/static/src/components/composer_text_input/composer_text_input.js create mode 100644 addons/mail/static/src/components/composer_text_input/composer_text_input.scss create mode 100644 addons/mail/static/src/components/composer_text_input/composer_text_input.xml create mode 100644 addons/mail/static/src/components/dialog/dialog.js create mode 100644 addons/mail/static/src/components/dialog/dialog.scss create mode 100644 addons/mail/static/src/components/dialog/dialog.xml create mode 100644 addons/mail/static/src/components/dialog_manager/dialog_manager.js create mode 100644 addons/mail/static/src/components/dialog_manager/dialog_manager.xml create mode 100644 addons/mail/static/src/components/dialog_manager/dialog_manager_tests.js create mode 100644 addons/mail/static/src/components/discuss/discuss.js create mode 100644 addons/mail/static/src/components/discuss/discuss.scss create mode 100644 addons/mail/static/src/components/discuss/discuss.xml create mode 100644 addons/mail/static/src/components/discuss/tests/discuss_domain_tests.js create mode 100644 addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js create mode 100644 addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js create mode 100644 addons/mail/static/src/components/discuss/tests/discuss_pinned_tests.js create mode 100644 addons/mail/static/src/components/discuss/tests/discuss_sidebar_tests.js create mode 100644 addons/mail/static/src/components/discuss/tests/discuss_tests.js create mode 100644 addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.js create mode 100644 addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.scss create mode 100644 addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection.xml create mode 100644 addons/mail/static/src/components/discuss_mobile_mailbox_selection/discuss_mobile_mailbox_selection_tests.js create mode 100644 addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.js create mode 100644 addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.scss create mode 100644 addons/mail/static/src/components/discuss_sidebar/discuss_sidebar.xml create mode 100644 addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js create mode 100644 addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.scss create mode 100644 addons/mail/static/src/components/discuss_sidebar_item/discuss_sidebar_item.xml create mode 100644 addons/mail/static/src/components/drop_zone/drop_zone.js create mode 100644 addons/mail/static/src/components/drop_zone/drop_zone.scss create mode 100644 addons/mail/static/src/components/drop_zone/drop_zone.xml create mode 100644 addons/mail/static/src/components/editable_text/editable_text.js create mode 100644 addons/mail/static/src/components/editable_text/editable_text.xml create mode 100644 addons/mail/static/src/components/emojis_popover/emojis_popover.js create mode 100644 addons/mail/static/src/components/emojis_popover/emojis_popover.scss create mode 100644 addons/mail/static/src/components/emojis_popover/emojis_popover.xml create mode 100644 addons/mail/static/src/components/file_uploader/file_uploader.js create mode 100644 addons/mail/static/src/components/file_uploader/file_uploader.scss create mode 100644 addons/mail/static/src/components/file_uploader/file_uploader.xml create mode 100644 addons/mail/static/src/components/file_uploader/file_uploader_tests.js create mode 100644 addons/mail/static/src/components/follow_button/follow_button.js create mode 100644 addons/mail/static/src/components/follow_button/follow_button.scss create mode 100644 addons/mail/static/src/components/follow_button/follow_button.xml create mode 100644 addons/mail/static/src/components/follow_button/follow_button_tests.js create mode 100644 addons/mail/static/src/components/follower/follower.js create mode 100644 addons/mail/static/src/components/follower/follower.scss create mode 100644 addons/mail/static/src/components/follower/follower.xml create mode 100644 addons/mail/static/src/components/follower/follower_tests.js create mode 100644 addons/mail/static/src/components/follower_list_menu/follower_list_menu.js create mode 100644 addons/mail/static/src/components/follower_list_menu/follower_list_menu.scss create mode 100644 addons/mail/static/src/components/follower_list_menu/follower_list_menu.xml create mode 100644 addons/mail/static/src/components/follower_list_menu/follower_list_menu_tests.js create mode 100644 addons/mail/static/src/components/follower_subtype/follower_subtype.js create mode 100644 addons/mail/static/src/components/follower_subtype/follower_subtype.scss create mode 100644 addons/mail/static/src/components/follower_subtype/follower_subtype.xml create mode 100644 addons/mail/static/src/components/follower_subtype/follower_subtype_tests.js create mode 100644 addons/mail/static/src/components/follower_subtype_list/follower_subtype_list.js create mode 100644 addons/mail/static/src/components/follower_subtype_list/follower_subtype_list.scss create mode 100644 addons/mail/static/src/components/follower_subtype_list/follower_subtype_list.xml create mode 100644 addons/mail/static/src/components/mail_template/mail_template.js create mode 100644 addons/mail/static/src/components/mail_template/mail_template.scss create mode 100644 addons/mail/static/src/components/mail_template/mail_template.xml create mode 100644 addons/mail/static/src/components/message/message.js create mode 100644 addons/mail/static/src/components/message/message.scss create mode 100644 addons/mail/static/src/components/message/message.xml create mode 100644 addons/mail/static/src/components/message/message_tests.js create mode 100644 addons/mail/static/src/components/message_author_prefix/message_author_prefix.js create mode 100644 addons/mail/static/src/components/message_author_prefix/message_author_prefix.scss create mode 100644 addons/mail/static/src/components/message_author_prefix/message_author_prefix.xml create mode 100644 addons/mail/static/src/components/message_list/message_list.js create mode 100644 addons/mail/static/src/components/message_list/message_list.scss create mode 100644 addons/mail/static/src/components/message_list/message_list.xml create mode 100644 addons/mail/static/src/components/message_seen_indicator/message_seen_indicator.js create mode 100644 addons/mail/static/src/components/message_seen_indicator/message_seen_indicator.scss create mode 100644 addons/mail/static/src/components/message_seen_indicator/message_seen_indicator.xml create mode 100644 addons/mail/static/src/components/message_seen_indicator/message_seen_indicator_tests.js create mode 100644 addons/mail/static/src/components/messaging_menu/messaging_menu.js create mode 100644 addons/mail/static/src/components/messaging_menu/messaging_menu.scss create mode 100644 addons/mail/static/src/components/messaging_menu/messaging_menu.xml create mode 100644 addons/mail/static/src/components/messaging_menu/messaging_menu_tests.js create mode 100644 addons/mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.js create mode 100644 addons/mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.scss create mode 100644 addons/mail/static/src/components/mobile_messaging_navbar/mobile_messaging_navbar.xml create mode 100644 addons/mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.js create mode 100644 addons/mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.xml create mode 100644 addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js create mode 100644 addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.xml create mode 100644 addons/mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js create mode 100644 addons/mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.xml create mode 100644 addons/mail/static/src/components/notification_alert/notification_alert.js create mode 100644 addons/mail/static/src/components/notification_alert/notification_alert.xml create mode 100644 addons/mail/static/src/components/notification_group/notification_group.js create mode 100644 addons/mail/static/src/components/notification_group/notification_group.scss create mode 100644 addons/mail/static/src/components/notification_group/notification_group.xml create mode 100644 addons/mail/static/src/components/notification_list/notification_list.js create mode 100644 addons/mail/static/src/components/notification_list/notification_list.scss create mode 100644 addons/mail/static/src/components/notification_list/notification_list.xml create mode 100644 addons/mail/static/src/components/notification_list/notification_list_item.scss create mode 100644 addons/mail/static/src/components/notification_list/notification_list_notification_group_tests.js create mode 100644 addons/mail/static/src/components/notification_list/notification_list_tests.js create mode 100644 addons/mail/static/src/components/notification_popover/notification_popover.js create mode 100644 addons/mail/static/src/components/notification_popover/notification_popover.scss create mode 100644 addons/mail/static/src/components/notification_popover/notification_popover.xml create mode 100644 addons/mail/static/src/components/notification_request/notification_request.js create mode 100644 addons/mail/static/src/components/notification_request/notification_request.scss create mode 100644 addons/mail/static/src/components/notification_request/notification_request.xml create mode 100644 addons/mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js create mode 100644 addons/mail/static/src/components/partner_im_status_icon/partner_im_status_icon.scss create mode 100644 addons/mail/static/src/components/partner_im_status_icon/partner_im_status_icon.xml create mode 100644 addons/mail/static/src/components/partner_im_status_icon/partner_im_status_icon_tests.js create mode 100644 addons/mail/static/src/components/thread_icon/thread_icon.js create mode 100644 addons/mail/static/src/components/thread_icon/thread_icon.scss create mode 100644 addons/mail/static/src/components/thread_icon/thread_icon.xml create mode 100644 addons/mail/static/src/components/thread_icon/thread_icon_tests.js create mode 100644 addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js create mode 100644 addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.scss create mode 100644 addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.xml create mode 100644 addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview_tests.js create mode 100644 addons/mail/static/src/components/thread_preview/thread_preview.js create mode 100644 addons/mail/static/src/components/thread_preview/thread_preview.scss create mode 100644 addons/mail/static/src/components/thread_preview/thread_preview.xml create mode 100644 addons/mail/static/src/components/thread_preview/thread_preview_tests.js create mode 100644 addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.js create mode 100644 addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.scss create mode 100644 addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status.xml create mode 100644 addons/mail/static/src/components/thread_textual_typing_status/thread_textual_typing_status_tests.js create mode 100644 addons/mail/static/src/components/thread_typing_icon/thread_typing_icon.js create mode 100644 addons/mail/static/src/components/thread_typing_icon/thread_typing_icon.scss create mode 100644 addons/mail/static/src/components/thread_typing_icon/thread_typing_icon.xml create mode 100644 addons/mail/static/src/components/thread_view/thread_view.js create mode 100644 addons/mail/static/src/components/thread_view/thread_view.scss create mode 100644 addons/mail/static/src/components/thread_view/thread_view.xml create mode 100644 addons/mail/static/src/components/thread_view/thread_view_tests.js create mode 100644 addons/mail/static/src/env/test_env.js create mode 100644 addons/mail/static/src/img/_al.png create mode 100644 addons/mail/static/src/img/_pinky.png create mode 100644 addons/mail/static/src/img/attachment.png create mode 100644 addons/mail/static/src/img/checklist.png create mode 100644 addons/mail/static/src/img/email_icon.png create mode 100644 addons/mail/static/src/img/email_template.png create mode 100644 addons/mail/static/src/img/email_template_save.png create mode 100644 addons/mail/static/src/img/formatting.png create mode 100644 addons/mail/static/src/img/groupdefault.png create mode 100644 addons/mail/static/src/img/odoo_o.png create mode 100644 addons/mail/static/src/img/odoobot.png create mode 100644 addons/mail/static/src/img/odoobot_transparent.png create mode 100644 addons/mail/static/src/img/smiley/avatar.jpg create mode 100644 addons/mail/static/src/img/smiley/green.png create mode 100644 addons/mail/static/src/img/smiley/mailfailure.jpg create mode 100644 addons/mail/static/src/img/smiley/yellow.png create mode 100644 addons/mail/static/src/js/activity.js create mode 100644 addons/mail/static/src/js/basic_view.js create mode 100644 addons/mail/static/src/js/core/translation.js create mode 100644 addons/mail/static/src/js/custom_filter_item.js create mode 100644 addons/mail/static/src/js/document_viewer.js create mode 100644 addons/mail/static/src/js/emojis.js create mode 100644 addons/mail/static/src/js/emojis_mixin.js create mode 100644 addons/mail/static/src/js/field_char.js create mode 100644 addons/mail/static/src/js/field_char_emojis.js create mode 100644 addons/mail/static/src/js/field_emojis_common.js create mode 100644 addons/mail/static/src/js/field_text_emojis.js create mode 100644 addons/mail/static/src/js/main.js create mode 100644 addons/mail/static/src/js/many2many_tags_email.js create mode 100644 addons/mail/static/src/js/many2one_avatar_user.js create mode 100644 addons/mail/static/src/js/systray/systray_activity_menu.js create mode 100644 addons/mail/static/src/js/tools/debug_manager.js create mode 100644 addons/mail/static/src/js/tours/mail.js create mode 100644 addons/mail/static/src/js/utils.js create mode 100644 addons/mail/static/src/js/views/activity/activity_cell.js create mode 100644 addons/mail/static/src/js/views/activity/activity_controller.js create mode 100644 addons/mail/static/src/js/views/activity/activity_model.js create mode 100644 addons/mail/static/src/js/views/activity/activity_record.js create mode 100644 addons/mail/static/src/js/views/activity/activity_renderer.js create mode 100644 addons/mail/static/src/js/views/activity/activity_view.js create mode 100644 addons/mail/static/src/model/model_core.js create mode 100644 addons/mail/static/src/model/model_errors.js create mode 100644 addons/mail/static/src/model/model_field.js create mode 100644 addons/mail/static/src/model/model_field_command.js create mode 100644 addons/mail/static/src/model/model_manager.js create mode 100644 addons/mail/static/src/models/activity/activity.js create mode 100644 addons/mail/static/src/models/activity_type/activity_type.js create mode 100644 addons/mail/static/src/models/attachment/attachment.js create mode 100644 addons/mail/static/src/models/attachment/attachment_tests.js create mode 100644 addons/mail/static/src/models/attachment_viewer/attachment_viewer.js create mode 100644 addons/mail/static/src/models/canned_response/canned_response.js create mode 100644 addons/mail/static/src/models/channel_command/channel_command.js create mode 100644 addons/mail/static/src/models/chat_window/chat_window.js create mode 100644 addons/mail/static/src/models/chat_window_manager/chat_window_manager.js create mode 100644 addons/mail/static/src/models/chatter/chatter.js create mode 100644 addons/mail/static/src/models/composer/composer.js create mode 100644 addons/mail/static/src/models/country/country.js create mode 100644 addons/mail/static/src/models/device/device.js create mode 100644 addons/mail/static/src/models/dialog/dialog.js create mode 100644 addons/mail/static/src/models/dialog_manager/dialog_manager.js create mode 100644 addons/mail/static/src/models/discuss/discuss.js create mode 100644 addons/mail/static/src/models/follower/follower.js create mode 100644 addons/mail/static/src/models/follower_subtype/follower_subtype.js create mode 100644 addons/mail/static/src/models/follower_subtype_list/follower_subtype_list.js create mode 100644 addons/mail/static/src/models/locale/locale.js create mode 100644 addons/mail/static/src/models/mail_template/mail_template.js create mode 100644 addons/mail/static/src/models/message/message.js create mode 100644 addons/mail/static/src/models/message/message_tests.js create mode 100644 addons/mail/static/src/models/message_seen_indicator/message_seen_indicator.js create mode 100644 addons/mail/static/src/models/messaging/messaging.js create mode 100644 addons/mail/static/src/models/messaging/messaging_tests.js create mode 100644 addons/mail/static/src/models/messaging_initializer/messaging_initializer.js create mode 100644 addons/mail/static/src/models/messaging_menu/messaging_menu.js create mode 100644 addons/mail/static/src/models/messaging_notification_handler/messaging_notification_handler.js create mode 100644 addons/mail/static/src/models/model/model.js create mode 100644 addons/mail/static/src/models/notification/notification.js create mode 100644 addons/mail/static/src/models/notification_group/notification_group.js create mode 100644 addons/mail/static/src/models/notification_group_manager/notification_group_manager.js create mode 100644 addons/mail/static/src/models/partner/partner.js create mode 100644 addons/mail/static/src/models/suggested_recipient_info/suggested_recipient_info.js create mode 100644 addons/mail/static/src/models/thread/thread.js create mode 100644 addons/mail/static/src/models/thread/thread_tests.js create mode 100644 addons/mail/static/src/models/thread_cache/thread_cache.js create mode 100644 addons/mail/static/src/models/thread_partner_seen_info/thread_partner_seen_info.js create mode 100644 addons/mail/static/src/models/thread_view/thread_view.js create mode 100644 addons/mail/static/src/models/thread_view/thread_viewer.js create mode 100644 addons/mail/static/src/models/user/user.js create mode 100644 addons/mail/static/src/scss/activity_view.scss create mode 100644 addons/mail/static/src/scss/composer.scss create mode 100644 addons/mail/static/src/scss/discuss.scss create mode 100644 addons/mail/static/src/scss/emojis.scss create mode 100644 addons/mail/static/src/scss/kanban_view.scss create mode 100644 addons/mail/static/src/scss/mail_activity.scss create mode 100644 addons/mail/static/src/scss/many2one_avatar_user.scss create mode 100644 addons/mail/static/src/scss/systray.scss create mode 100644 addons/mail/static/src/scss/thread.scss create mode 100644 addons/mail/static/src/scss/variables.scss create mode 100644 addons/mail/static/src/services/chat_window_service/chat_window_service.js create mode 100644 addons/mail/static/src/services/dialog_service/dialog_service.js create mode 100644 addons/mail/static/src/utils/deferred/deferred.js create mode 100644 addons/mail/static/src/utils/test_utils.js create mode 100644 addons/mail/static/src/utils/throttle/throttle.js create mode 100644 addons/mail/static/src/utils/throttle/throttle_tests.js create mode 100644 addons/mail/static/src/utils/timer/timer.js create mode 100644 addons/mail/static/src/utils/timer/timer_tests.js create mode 100644 addons/mail/static/src/utils/utils.js create mode 100644 addons/mail/static/src/variables.scss create mode 100644 addons/mail/static/src/widgets/common.xml create mode 100644 addons/mail/static/src/widgets/discuss/discuss.js create mode 100644 addons/mail/static/src/widgets/discuss/discuss.scss create mode 100644 addons/mail/static/src/widgets/discuss/discuss.xml create mode 100644 addons/mail/static/src/widgets/discuss_invite_partner_dialog/discuss_invite_partner_dialog.js create mode 100644 addons/mail/static/src/widgets/discuss_invite_partner_dialog/discuss_invite_partner_dialog.xml create mode 100644 addons/mail/static/src/widgets/form_renderer/form_renderer.js create mode 100644 addons/mail/static/src/widgets/form_renderer/form_renderer.scss create mode 100644 addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js create mode 100644 addons/mail/static/src/widgets/messaging_menu/messaging_menu.js create mode 100644 addons/mail/static/src/widgets/messaging_menu/messaging_menu.xml create mode 100644 addons/mail/static/src/widgets/notification_alert/notification_alert.js create mode 100644 addons/mail/static/src/widgets/notification_alert/notification_alert_tests.js create mode 100644 addons/mail/static/src/xml/activity.xml create mode 100644 addons/mail/static/src/xml/activity_view.xml create mode 100644 addons/mail/static/src/xml/composer.xml create mode 100644 addons/mail/static/src/xml/many2one_avatar_user.xml create mode 100644 addons/mail/static/src/xml/systray.xml create mode 100644 addons/mail/static/src/xml/text_emojis.xml create mode 100644 addons/mail/static/src/xml/thread.xml create mode 100644 addons/mail/static/src/xml/web_kanban_activity.xml (limited to 'addons/mail/static/src') diff --git a/addons/mail/static/src/audio/ting.mp3 b/addons/mail/static/src/audio/ting.mp3 new file mode 100644 index 00000000..fd29bf59 Binary files /dev/null and b/addons/mail/static/src/audio/ting.mp3 differ diff --git a/addons/mail/static/src/audio/ting.ogg b/addons/mail/static/src/audio/ting.ogg new file mode 100644 index 00000000..cc8c0f01 Binary files /dev/null and b/addons/mail/static/src/audio/ting.ogg differ diff --git a/addons/mail/static/src/bugfix/bugfix.js b/addons/mail/static/src/bugfix/bugfix.js new file mode 100644 index 00000000..20800dc7 --- /dev/null +++ b/addons/mail/static/src/bugfix/bugfix.js @@ -0,0 +1,191 @@ +/** + * This file allows introducing new JS modules without contaminating other files. + * This is useful when bug fixing requires adding such JS modules in stable + * versions of Odoo. Any module that is defined in this file should be isolated + * in its own file in master. + */ +odoo.define('mail/static/src/bugfix/bugfix.js', function (require) { +'use strict'; + +}); + +// Should be moved to its own file in master. +odoo.define('mail/static/src/component_hooks/use_rendered_values/use_rendered_values.js', function (require) { +'use strict'; + +const { Component } = owl; +const { onMounted, onPatched } = owl.hooks; + +/** + * This hooks provides support for accessing the values returned by the given + * selector at the time of the last render. The values will be updated after + * every mount/patch. + * + * @param {function} selector function that will be executed at the time of the + * render and of which the result will be stored for future reference. + * @returns {function} function to call to retrieve the last rendered values. + */ +function useRenderedValues(selector) { + const component = Component.current; + let renderedValues; + let patchedValues; + + const __render = component.__render.bind(component); + component.__render = function () { + renderedValues = selector(); + return __render(...arguments); + }; + onMounted(onUpdate); + onPatched(onUpdate); + function onUpdate() { + patchedValues = renderedValues; + } + return () => patchedValues; +} + +return useRenderedValues; + +}); + +// Should be moved to its own file in master. +odoo.define('mail/static/src/component_hooks/use_update/use_update.js', function (require) { +'use strict'; + +const { Component } = owl; +const { onMounted, onPatched } = owl.hooks; + +const executionQueue = []; + +function executeNextInQueue() { + if (executionQueue.length === 0) { + return; + } + const { component, func } = executionQueue.shift(); + if (component.__owl__.status !== 5 /* DESTROYED */) { + func(); + } + executeNextInQueue(); +} + +/** + * @param {Object} param0 + * @param {Component} param0.component + * @param {function} param0.func + * @param {integer} param0.priority + */ +async function addFunctionToQueue({ component, func, priority }) { + const index = executionQueue.findIndex(item => item.priority > priority); + const item = { component, func, priority }; + if (index === -1) { + executionQueue.push(item); + } else { + executionQueue.splice(index, 0, item); + } + // Timeout to allow all components to register their function before + // executing any of them, to respect all priorities. + await new Promise(resolve => setTimeout(resolve)); + executeNextInQueue(); +} + +/** + * This hook provides support for executing code after update (render or patch). + * + * @param {Object} param0 + * @param {function} param0.func the function to execute after the update. + * @param {integer} [param0.priority] determines the execution order of the function + * among the update function of other components. Lower priority is executed + * first. If no priority is given, the function is executed immediately. + * This param is deprecated because desynchronized update is causing issue if + * there is a new render planned in the meantime (models data become obsolete + * in the update method). + */ +function useUpdate({ func, priority }) { + const component = Component.current; + onMounted(onUpdate); + onPatched(onUpdate); + function onUpdate() { + if (priority === undefined) { + func(); + return; + } + addFunctionToQueue({ component, func, priority }); + } +} + +return useUpdate; + +}); + +// Should be moved to its own file in master. +odoo.define('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js', function (require) { +'use strict'; + +const { Component } = owl; + +/** + * Compares `a` and `b` up to the given `compareDepth`. + * + * @param {any} a + * @param {any} b + * @param {Object|integer} compareDepth + * @returns {boolean} + */ +function isEqual(a, b, compareDepth) { + const keys = Object.keys(a); + if (Object.keys(b).length !== keys.length) { + return false; + } + for (const key of keys) { + // the depth can be given either as a number (for all keys) or as + // an object (for each key) + let depth; + if (typeof compareDepth === 'number') { + depth = compareDepth; + } else { + depth = compareDepth[key] || 0; + } + if (depth === 0 && a[key] !== b[key]) { + return false; + } + if (depth !== 0) { + let nextDepth; + if (typeof depth === 'number') { + nextDepth = depth - 1; + } else { + nextDepth = depth; + } + if (!isEqual(a[key], b[key], nextDepth)) { + return false; + } + } + } + return true; +} + +/** + * This hook overrides the `shouldUpdate` method to ensure the component is only + * updated if its props actually changed. This is especially useful to use on + * components for which an extra render costs proportionally a lot more than + * comparing props. + * + * @param {Object} [param0={}] + * @param {Object} [param0.compareDepth={}] allows to specify the comparison + * depth to use for each prop. Default is shallow compare (depth = 0). + */ +function useShouldUpdateBasedOnProps({ compareDepth = {} } = {}) { + const component = Component.current; + component.shouldUpdate = nextProps => { + const allNewProps = Object.assign({}, nextProps); + const defaultProps = component.constructor.defaultProps; + for (const key in defaultProps) { + if (allNewProps[key] === undefined) { + allNewProps[key] = defaultProps[key]; + } + } + return !isEqual(component.props, allNewProps, compareDepth); + }; +} + +return useShouldUpdateBasedOnProps; + +}); diff --git a/addons/mail/static/src/bugfix/bugfix.scss b/addons/mail/static/src/bugfix/bugfix.scss new file mode 100644 index 00000000..c4272e52 --- /dev/null +++ b/addons/mail/static/src/bugfix/bugfix.scss @@ -0,0 +1,6 @@ +/** +* This file allows introducing new styles without contaminating other files. +* This is useful when bug fixing requires adding new components for instance in +* stable versions of Odoo. Any style that is defined in this file should be isolated +* in its own file in master. +*/ diff --git a/addons/mail/static/src/bugfix/bugfix.xml b/addons/mail/static/src/bugfix/bugfix.xml new file mode 100644 index 00000000..c17906f7 --- /dev/null +++ b/addons/mail/static/src/bugfix/bugfix.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/addons/mail/static/src/bugfix/bugfix_tests.js b/addons/mail/static/src/bugfix/bugfix_tests.js new file mode 100644 index 00000000..a5ecddde --- /dev/null +++ b/addons/mail/static/src/bugfix/bugfix_tests.js @@ -0,0 +1,18 @@ +odoo.define('mail/static/src/bugfix/bugfix_tests.js', function (require) { +'use strict'; + +/** + * This file allows introducing new QUnit test modules without contaminating + * other test files. This is useful when bug fixing requires adding new + * components for instance in stable versions of Odoo. Any test that is defined + * in this file should be isolated in its own file in master. + */ +QUnit.module('mail', {}, function () { +QUnit.module('bugfix', {}, function () { +QUnit.module('bugfix_tests.js', { + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js b/addons/mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js new file mode 100644 index 00000000..f96eb265 --- /dev/null +++ b/addons/mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js @@ -0,0 +1,93 @@ +odoo.define('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js', function (require) { +'use strict'; + +const { useState, onMounted, onWillUnmount } = owl.hooks; + +/** + * This hook handle the visibility of the dropzone based on drag & drop events. + * It needs a ref to a dropzone, so you need to specify a t-ref="dropzone" in + * the template of your component. + * + * @returns {Object} + */ +function useDragVisibleDropZone() { + /** + * Determine whether the drop zone should be visible or not. + * Note that this is an observed value, and primitive types such as + * boolean cannot be observed, hence this is an object with boolean + * value accessible from `.value` + */ + const isVisible = useState({ value: false }); + + /** + * Counts how many drag enter/leave happened globally. This is the only + * way to know if a file has been dragged out of the browser window. + */ + let dragCount = 0; + + // COMPONENTS HOOKS + onMounted(() => { + document.addEventListener('dragenter', _onDragenterListener, true); + document.addEventListener('dragleave', _onDragleaveListener, true); + document.addEventListener('drop', _onDropListener, true); + + // Thoses Events prevent the browser to open or download the file if + // it's dropped outside of the dropzone + window.addEventListener('dragover', ev => ev.preventDefault()); + window.addEventListener('drop', ev => ev.preventDefault()); + }); + + onWillUnmount(() => { + document.removeEventListener('dragenter', _onDragenterListener, true); + document.removeEventListener('dragleave', _onDragleaveListener, true); + document.removeEventListener('drop', _onDropListener, true); + + window.removeEventListener('dragover', ev => ev.preventDefault()); + window.removeEventListener('drop', ev => ev.preventDefault()); + }); + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Shows the dropzone when entering the browser window, to let the user know + * where he can drop its file. + * Avoids changing state when entering inner dropzones. + * + * @private + * @param {DragEvent} ev + */ + function _onDragenterListener(ev) { + if (dragCount === 0) { + isVisible.value = true; + } + dragCount++; + } + + /** + * @private + * @param {DragEvent} ev + */ + function _onDragleaveListener(ev) { + dragCount--; + if (dragCount === 0) { + isVisible.value = false; + } + } + + /** + * @private + * @param {DragEvent} ev + */ + function _onDropListener(ev) { + dragCount = 0; + isVisible.value = false; + } + + return isVisible; +} + +return useDragVisibleDropZone; + +}); diff --git a/addons/mail/static/src/component_hooks/use_refs/use_refs.js b/addons/mail/static/src/component_hooks/use_refs/use_refs.js new file mode 100644 index 00000000..6c84f195 --- /dev/null +++ b/addons/mail/static/src/component_hooks/use_refs/use_refs.js @@ -0,0 +1,21 @@ +odoo.define('mail/static/src/component_hooks/use_refs/use_refs.js', function (require) { +'use strict'; + +const { Component } = owl; + +/** + * This hook provides support for dynamic-refs. + * + * @returns {function} returns object whose keys are t-ref values of active refs. + * and values are refs. + */ +function useRefs() { + const component = Component.current; + return function () { + return component.__owl__.refs || {}; + }; +} + +return useRefs; + +}); diff --git a/addons/mail/static/src/component_hooks/use_store/use_store.js b/addons/mail/static/src/component_hooks/use_store/use_store.js new file mode 100644 index 00000000..7ff5ea3a --- /dev/null +++ b/addons/mail/static/src/component_hooks/use_store/use_store.js @@ -0,0 +1,126 @@ +odoo.define('mail/static/src/component_hooks/use_store/use_store.js', function (require) { +'use strict'; + +/** + * Similar to owl.hooks.useStore but to decide if a new render has to be done it + * compares the keys on the result, with an optional level of depth for each + * key, given as options `compareDepth`. + * + * It assumes that the result of the selector is always an object (or array). + * + * @param {function} selector function passed as selector of original `useStore` + * with 1st parameter extended as store state. @see owl.hooks.useStore + * @param {object} [options={}] @see owl.hooks.useStore + * @param {number|object} [options.compareDepth=0] the comparison depth, either + * as number (applies to all keys) or as an object (depth for specific keys) + * @returns {Proxy} @see owl.hooks.useStore + */ +function useStore(selector, options = {}) { + const store = options.store || owl.Component.current.env.store; + const hashFn = store.observer.revNumber.bind(store.observer); + const isEqual = options.isEqual || ((a, b) => a === b); + + /** + * Returns a function comparing whether two values are the same, which is just + * calling `isEqual` on primitive values and objects, but which also works for + * Owl Proxy in a temporal way: the current result of hashFn is compared to the + * previous result of hashFn (from the last time the function was called). + * + * It means that when this function is given Proxy the first call will always + * return false, and consecutive calls will not lead to the same result: + * it returns true the first time after a change happended inside the Proxy, and + * then always returns false until a new change is made. + * + * @returns {function} function taking two arguments, and comparing them as + * explained above. + */ + function proxyComparator() { + /** + * It is important to locally save the old `revNumber` of each resulting + * value because when the "old" and "new" values are the same proxy it is + * impossible to compare them based on their current value (since it was + * updated in "both" due to the fact it is a proxy in the first place). + * + * And if the values are not proxy, `revNumber` will be 0 and the `isEqual` + * will be used to compare them. + */ + let oldRevNumber; + + function compare(a, b) { + let ok = true; + const newRevNumber = hashFn(b); + if (a === b && newRevNumber > 0) { + ok = oldRevNumber === newRevNumber; + } else { + ok = isEqual(a, b); + } + oldRevNumber = newRevNumber; + return ok; + } + + return compare; + } + + /** + * @see proxyComparator, but instead of comparing the given values, it compares + * their respective keys, with `compareDepth` level of depth. + * 0 = compare key, 1 = also compare subkeys, ... + * + * This assumes the given values are objects or arrays. + * + * @param {number|object} compareDepth the comparison depth, either as number + * (applies to all keys) or as an object (depth for specific keys) + * @returns {function} + */ + function proxyComparatorDeep(compareDepth = 0) { + const comparator = proxyComparator(); + const comparators = {}; + + function compare(a, b) { + // If a and b are (the same) proxy, it is already managing the depth + // by itself, and a simple comparator can be used. + if (a === b && hashFn(b) > 0) { + return comparator(a, b); + } + let ok = true; + const newKeys = Object.keys(b); + if (!a || (Object.keys(a).length !== newKeys.length)) { + ok = false; + } + for (const key of newKeys) { + // the depth can be given either as a number (for all keys) or as + // an object (for each key) + let depth; + if (typeof compareDepth === 'number') { + depth = compareDepth; + } else { + depth = compareDepth[key] || 0; + } + if (!(key in comparators)) { + if (depth > 0) { + comparators[key] = proxyComparatorDeep(depth - 1); + } else { + comparators[key] = proxyComparator(); + } + } + // It is important to not break too early, the comparator has to + // be called for every key to remember their current states. + if (!comparators[key](a ? a[key] : a, b[key])) { + ok = false; + } + } + return ok; + } + + return compare; + } + + const extendedSelector = (state, props) => selector(props); + return owl.hooks.useStore(extendedSelector, Object.assign({}, options, { + isEqual: proxyComparatorDeep(options.compareDepth), + })); +} + +return useStore; + +}); diff --git a/addons/mail/static/src/component_hooks/use_store/use_store_tests.js b/addons/mail/static/src/component_hooks/use_store/use_store_tests.js new file mode 100644 index 00000000..f1a4b054 --- /dev/null +++ b/addons/mail/static/src/component_hooks/use_store/use_store_tests.js @@ -0,0 +1,189 @@ +odoo.define('mail/static/src/component_hooks/use_store/use_store_tests.js', function (require) { +'use strict'; + +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const { + afterNextRender, + nextAnimationFrame, +} = require('mail/static/src/utils/test_utils.js'); + +const { Component, QWeb, Store } = owl; +const { onPatched, useGetters } = owl.hooks; +const { xml } = owl.tags; + +QUnit.module('mail', {}, function () { +QUnit.module('component_hooks', {}, function () { +QUnit.module('use_store', {}, function () { +QUnit.module('use_store_tests.js', { + beforeEach() { + const qweb = new QWeb(); + this.env = { qweb }; + }, + afterEach() { + this.env = undefined; + this.store = undefined; + }, +}); + + +QUnit.test("compare keys, no depth, primitives", async function (assert) { + assert.expect(8); + this.store = new Store({ + env: this.env, + getters: { + get({ state }, key) { + return state[key]; + }, + }, + state: { + obj: { + subObj1: 'a', + subObj2: 'b', + use1: true, + }, + }, + }); + this.env.store = this.store; + let count = 0; + class MyComponent extends Component { + constructor() { + super(...arguments); + this.storeGetters = useGetters(); + this.storeProps = useStore(props => { + const obj = this.storeGetters.get('obj'); + return { + res: obj.use1 ? obj.subObj1 : obj.subObj2, + }; + }); + onPatched(() => { + count++; + }); + } + } + Object.assign(MyComponent, { + env: this.env, + props: {}, + template: xml`
`, + }); + + const fixture = document.querySelector('#qunit-fixture'); + + const myComponent = new MyComponent(); + await myComponent.mount(fixture); + assert.strictEqual(count, 0, + 'should not detect an update initially'); + assert.strictEqual(fixture.textContent, 'a', + 'should display the content of subObj1'); + + await afterNextRender(() => { + this.store.state.obj.use1 = false; + }); + assert.strictEqual(count, 1, + 'should detect an update because the selector is returning a different value (was subObj1, now is subObj2)'); + assert.strictEqual(fixture.textContent, 'b', + 'should display the content of subObj2'); + + this.store.state.obj.subObj2 = 'b'; + // there must be no render here + await nextAnimationFrame(); + assert.strictEqual(count, 1, + 'should not detect an update because the same primitive value was assigned (subObj2 was already "b")'); + assert.strictEqual(fixture.textContent, 'b', + 'should still display the content of subObj2'); + + await afterNextRender(() => { + this.store.state.obj.subObj2 = 'd'; + }); + assert.strictEqual(count, 2, + 'should detect an update because the selector is returning a different value for subObj2'); + assert.strictEqual(fixture.textContent, 'd', + 'should display the new content of subObj2'); + + myComponent.destroy(); +}); + +QUnit.test("compare keys, depth 1, proxy", async function (assert) { + assert.expect(8); + this.store = new Store({ + env: this.env, + getters: { + get({ state }, key) { + return state[key]; + }, + }, + state: { + obj: { + subObj1: { a: 'a' }, + subObj2: { a: 'b' }, + use1: true, + }, + }, + }); + this.env.store = this.store; + let count = 0; + class MyComponent extends Component { + constructor() { + super(...arguments); + this.storeGetters = useGetters(); + this.storeProps = useStore(props => { + const obj = this.storeGetters.get('obj'); + return { + array: [obj.use1 ? obj.subObj1 : obj.subObj2], + }; + }, { + compareDepth: { + array: 1, + }, + }); + onPatched(() => { + count++; + }); + } + } + Object.assign(MyComponent, { + env: this.env, + props: {}, + template: xml`
`, + }); + + const fixture = document.querySelector('#qunit-fixture'); + + const myComponent = new MyComponent(); + await myComponent.mount(fixture); + assert.strictEqual(count, 0, + 'should not detect an update initially'); + assert.strictEqual(fixture.textContent, 'a', + 'should display the content of subObj1'); + + await afterNextRender(() => { + this.store.state.obj.use1 = false; + }); + assert.strictEqual(count, 1, + 'should detect an update because the selector is returning a different value (was subObj1, now is subObj2)'); + assert.strictEqual(fixture.textContent, 'b', + 'should display the content of subObj2'); + + this.store.state.obj.subObj1.a = 'c'; + // there must be no render here + await nextAnimationFrame(); + assert.strictEqual(count, 1, + 'should not detect an update because subObj1 was changed but only subObj2 is selected'); + assert.strictEqual(fixture.textContent, 'b', + 'should still display the content of subObj2'); + + await afterNextRender(() => { + this.store.state.obj.subObj2.a = 'd'; + }); + assert.strictEqual(count, 2, + 'should detect an update because the value of subObj2 changed'); + assert.strictEqual(fixture.textContent, 'd', + 'should display the new content of subObj2'); + + myComponent.destroy(); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/activity/activity.js b/addons/mail/static/src/components/activity/activity.js new file mode 100644 index 00000000..1ee7ecf3 --- /dev/null +++ b/addons/mail/static/src/components/activity/activity.js @@ -0,0 +1,199 @@ +odoo.define('mail/static/src/components/activity/activity.js', function (require) { +'use strict'; + +const components = { + ActivityMarkDonePopover: require('mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.js'), + FileUploader: require('mail/static/src/components/file_uploader/file_uploader.js'), + MailTemplate: require('mail/static/src/components/mail_template/mail_template.js'), +}; +const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const { + auto_str_to_date, + getLangDateFormat, + getLangDatetimeFormat, +} = require('web.time'); + +const { Component, useState } = owl; +const { useRef } = owl.hooks; + +class Activity extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useShouldUpdateBasedOnProps(); + this.state = useState({ + areDetailsVisible: false, + }); + useStore(props => { + const activity = this.env.models['mail.activity'].get(props.activityLocalId); + return { + activity: activity ? activity.__state : undefined, + assigneeNameOrDisplayName: ( + activity && + activity.assignee && + activity.assignee.nameOrDisplayName + ), + }; + }); + /** + * Reference of the file uploader. + * Useful to programmatically prompts the browser file uploader. + */ + this._fileUploaderRef = useRef('fileUploader'); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {mail.activity} + */ + get activity() { + return this.env.models['mail.activity'].get(this.props.activityLocalId); + } + + /** + * @returns {string} + */ + get assignedUserText() { + return _.str.sprintf(this.env._t("for %s"), this.activity.assignee.nameOrDisplayName); + } + + /** + * @returns {string} + */ + get delayLabel() { + const today = moment().startOf('day'); + const momentDeadlineDate = moment(auto_str_to_date(this.activity.dateDeadline)); + // true means no rounding + const diff = momentDeadlineDate.diff(today, 'days', true); + if (diff === 0) { + return this.env._t("Today:"); + } else if (diff === -1) { + return this.env._t("Yesterday:"); + } else if (diff < 0) { + return _.str.sprintf(this.env._t("%d days overdue:"), Math.abs(diff)); + } else if (diff === 1) { + return this.env._t("Tomorrow:"); + } else { + return _.str.sprintf(this.env._t("Due in %d days:"), Math.abs(diff)); + } + } + + /** + * @returns {string} + */ + get formattedCreateDatetime() { + const momentCreateDate = moment(auto_str_to_date(this.activity.dateCreate)); + const datetimeFormat = getLangDatetimeFormat(); + return momentCreateDate.format(datetimeFormat); + } + + /** + * @returns {string} + */ + get formattedDeadlineDate() { + const momentDeadlineDate = moment(auto_str_to_date(this.activity.dateDeadline)); + const datetimeFormat = getLangDateFormat(); + return momentDeadlineDate.format(datetimeFormat); + } + + /** + * @returns {string} + */ + get MARK_DONE() { + return this.env._t("Mark Done"); + } + + /** + * @returns {string} + */ + get summary() { + return _.str.sprintf(this.env._t("“%s”"), this.activity.summary); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {CustomEvent} ev + * @param {Object} ev.detail + * @param {mail.attachment} ev.detail.attachment + */ + _onAttachmentCreated(ev) { + this.activity.markAsDone({ attachments: [ev.detail.attachment] }); + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClick(ev) { + if ( + ev.target.tagName === 'A' && + ev.target.dataset.oeId && + ev.target.dataset.oeModel + ) { + this.env.messaging.openProfile({ + id: Number(ev.target.dataset.oeId), + model: ev.target.dataset.oeModel, + }); + // avoid following dummy href + ev.preventDefault(); + } + } + + /** + * @private + * @param {MouseEvent} ev + */ + async _onClickCancel(ev) { + ev.preventDefault(); + await this.activity.deleteServerRecord(); + this.trigger('reload', { keepChanges: true }); + } + + /** + * @private + */ + _onClickDetailsButton() { + this.state.areDetailsVisible = !this.state.areDetailsVisible; + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickEdit(ev) { + this.activity.edit(); + } + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickUploadDocument(ev) { + this._fileUploaderRef.comp.openBrowserFileUploader(); + } + +} + +Object.assign(Activity, { + components, + props: { + activityLocalId: String, + }, + template: 'mail.Activity', +}); + +return Activity; + +}); diff --git a/addons/mail/static/src/components/activity/activity.scss b/addons/mail/static/src/components/activity/activity.scss new file mode 100644 index 00000000..64a0ceac --- /dev/null +++ b/addons/mail/static/src/components/activity/activity.scss @@ -0,0 +1,186 @@ +// ------------------------------------------------------------------ +// Layout +// ------------------------------------------------------------------ + +.o_Activity { + display: flex; + flex: 0 0 auto; + padding: map-get($spacers, 2); +} + +.o_Activity_detailsUserAvatar { + margin-inline-end: map-get($spacers, 2); + object-fit: cover; + height: 18px; + width: 18px; +} + +.o_Activity_dueDateText, .o_Activity_summary { + margin-inline-end: map-get($spacers, 2); +} + +.o_Activity_iconContainer { + @include o-position-absolute($top: auto, $left: auto, $bottom: -5px, $right: -5px); + display: flex; + align-items: center; + justify-content: center; + width: 25px; + height: 25px; + border-width: 2px; +} + +.o_Activity_info { + display: flex; + align-items: baseline; +} + +.o_Activity_note p { + margin-bottom: 0; +} + +.o_Activity_sidebar { + display: flex; + flex: 0 0 36px; + margin-right: map-get($spacers, 3); + justify-content: center; +} + +.o_Activity_toolButton { + padding-top: map-get($spacers, 0); +} + +.o_Activity_tools { + display: flex; +} + +.o_Activity_user { + height: 36px; + margin-left: map-get($spacers, 2); + margin-right: map-get($spacers, 2); + position: relative; + width: 36px; +} + +.o_Activity_userAvatar { + height: map-get($sizes, 100); + width: map-get($sizes, 100); +} + +// From python template +.o_mail_note_title { + margin-top: map-get($spacers, 2); +} + +.o_mail_note_title + div p { + margin-bottom: 0; +} + +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ + +$o-mail-activity-default-color: gray('300') !default; +$o-mail-activity-overdue-color: darken(theme-color('danger'), 10%) !default; +$o-mail-activity-planned-color: darken(theme-color('success'), 10%) !default; +$o-mail-activity-today-color: darken(theme-color('warning'), 10%) !default; + + +.o_Activity_deadlineDateText { + &.o-default { + color: $o-mail-activity-default-color; + } + + &.o-overdue { + color: $o-mail-activity-overdue-color; + } + + &.o-planned { + color: $o-mail-activity-planned-color; + } + + &.o-today { + color: $o-mail-activity-today-color; + } +} + +.o_Activity_details { + color: gray('500'); +} + +.o_Activity_detailsCreatorAvatar { + margin-inline-start: map-get($spacers, 2); +} + +.o_Activity_detailsUserAvatar { + border-radius: 50%; +} + +.o_Activity_dueDateText { + font-weight: bolder; + + &.o-default { + color: $o-mail-activity-default-color; + } + + &.o-overdue { + color: $o-mail-activity-overdue-color; + } + + &.o-planned { + color: $o-mail-activity-planned-color; + } + + &.o-today { + color: $o-mail-activity-today-color; + } +} + +/* Needed specifity to counter default bootstrap style */ +a:not([href]):not([tabindex]).o_Activity_detailsButton { + background: transparent; + opacity: 0.5; + color: gray('500'); + + &:hover { + opacity: 1; + color: gray('600'); + } +} + +.o_Activity_detailsCreator { + font-weight: bold; +} + +.o_Activity_iconContainer { + color: white; + border-color: white; + border-radius: 100%; + border-style: solid; +} + +.o_Activity_sidebar { + font-size: smaller; +} + +.o_Activity_summary { + font-weight: bolder; + color: gray('900'); +} + +.o_Activity_toolButton { + opacity: 0.5; + color: gray('500'); + + &:hover { + opacity: 1; + color: gray('600'); + } +} + +.o_Activity_userAvatar { + border-radius: 50%; +} + +.o_Activity_userName { + color: gray('500'); +} diff --git a/addons/mail/static/src/components/activity/activity.xml b/addons/mail/static/src/components/activity/activity.xml new file mode 100644 index 00000000..e5e9832c --- /dev/null +++ b/addons/mail/static/src/components/activity/activity.xml @@ -0,0 +1,153 @@ + + + + +
+ +
+
+ + + +
+ +
+
+
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ + + +
+ + +
+
+ +
Activity type
+
+ +
+
+ +
Created
+
+ + + + + +
+
+ +
Assigned to
+
+ + +
+
+
Due on
+
+ + + +
+
+
+
+ + +
+ +
+
+ + +
+ + + +
+
+ + +
+ + + + + + + + + + + + + + +
+
+
+
+
+
+ +
diff --git a/addons/mail/static/src/components/activity/activity_tests.js b/addons/mail/static/src/components/activity/activity_tests.js new file mode 100644 index 00000000..1c260f07 --- /dev/null +++ b/addons/mail/static/src/components/activity/activity_tests.js @@ -0,0 +1,1157 @@ +odoo.define('mail/static/src/components/activity/activity_tests.js', function (require) { +'use strict'; + +const components = { + Activity: require('mail/static/src/components/activity/activity.js'), +}; + +const { + afterEach, + afterNextRender, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const Bus = require('web.Bus'); +const { date_to_str } = require('web.time'); + +const { Component, tags: { xml } } = owl; + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('activity', {}, function () { +QUnit.module('activity_tests.js', { + beforeEach() { + beforeEach(this); + + this.createActivityComponent = async function (activity) { + await createRootComponent(this, components.Activity, { + props: { activityLocalId: activity.localId }, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('activity simplest layout', async function (assert) { + assert.expect(12); + + await this.start(); + const activity = this.env.models['mail.activity'].create({ + id: 12, + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_sidebar').length, + 1, + "should have activity sidebar" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_core').length, + 1, + "should have activity core" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_user').length, + 1, + "should have activity user" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_info').length, + 1, + "should have activity info" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_note').length, + 0, + "should not have activity note" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_details').length, + 0, + "should not have activity details" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_mailTemplates').length, + 0, + "should not have activity mail templates" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_editButton').length, + 0, + "should not have activity Edit button" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_cancelButton').length, + 0, + "should not have activity Cancel button" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_markDoneButton').length, + 0, + "should not have activity Mark as Done button" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_uploadButton').length, + 0, + "should not have activity Upload button" + ); +}); + +QUnit.test('activity with note layout', async function (assert) { + assert.expect(3); + + await this.start(); + const activity = this.env.models['mail.activity'].create({ + id: 12, + note: 'There is no good or bad note', + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_note').length, + 1, + "should have activity note" + ); + assert.strictEqual( + document.querySelector('.o_Activity_note').textContent, + "There is no good or bad note", + "activity note should be 'There is no good or bad note'" + ); +}); + +QUnit.test('activity info layout when planned after tomorrow', async function (assert) { + assert.expect(4); + + await this.start(); + const today = new Date(); + const fiveDaysFromNow = new Date(); + fiveDaysFromNow.setDate(today.getDate() + 5); + const activity = this.env.models['mail.activity'].create({ + dateDeadline: date_to_str(fiveDaysFromNow), + id: 12, + state: 'planned', + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_dueDateText').length, + 1, + "should have activity delay" + ); + assert.ok( + document.querySelector('.o_Activity_dueDateText').classList.contains('o-planned'), + "activity delay should have the right color modifier class (planned)" + ); + assert.strictEqual( + document.querySelector('.o_Activity_dueDateText').textContent, + "Due in 5 days:", + "activity delay should have 'Due in 5 days:' as label" + ); +}); + +QUnit.test('activity info layout when planned tomorrow', async function (assert) { + assert.expect(4); + + await this.start(); + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + const activity = this.env.models['mail.activity'].create({ + dateDeadline: date_to_str(tomorrow), + id: 12, + state: 'planned', + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_dueDateText').length, + 1, + "should have activity delay" + ); + assert.ok( + document.querySelector('.o_Activity_dueDateText').classList.contains('o-planned'), + "activity delay should have the right color modifier class (planned)" + ); + assert.strictEqual( + document.querySelector('.o_Activity_dueDateText').textContent, + 'Tomorrow:', + "activity delay should have 'Tomorrow:' as label" + ); +}); + +QUnit.test('activity info layout when planned today', async function (assert) { + assert.expect(4); + + await this.start(); + const today = new Date(); + const activity = this.env.models['mail.activity'].create({ + dateDeadline: date_to_str(today), + id: 12, + state: 'today', + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_dueDateText').length, + 1, + "should have activity delay" + ); + assert.ok( + document.querySelector('.o_Activity_dueDateText').classList.contains('o-today'), + "activity delay should have the right color modifier class (today)" + ); + assert.strictEqual( + document.querySelector('.o_Activity_dueDateText').textContent, + "Today:", + "activity delay should have 'Today:' as label" + ); +}); + +QUnit.test('activity info layout when planned yesterday', async function (assert) { + assert.expect(4); + + await this.start(); + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate(today.getDate() - 1); + const activity = this.env.models['mail.activity'].create({ + dateDeadline: date_to_str(yesterday), + id: 12, + state: 'overdue', + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_dueDateText').length, + 1, + "should have activity delay" + ); + assert.ok( + document.querySelector('.o_Activity_dueDateText').classList.contains('o-overdue'), + "activity delay should have the right color modifier class (overdue)" + ); + assert.strictEqual( + document.querySelector('.o_Activity_dueDateText').textContent, + "Yesterday:", + "activity delay should have 'Yesterday:' as label" + ); +}); + +QUnit.test('activity info layout when planned before yesterday', async function (assert) { + assert.expect(4); + + await this.start(); + const today = new Date(); + const fiveDaysBeforeNow = new Date(); + fiveDaysBeforeNow.setDate(today.getDate() - 5); + const activity = this.env.models['mail.activity'].create({ + dateDeadline: date_to_str(fiveDaysBeforeNow), + id: 12, + state: 'overdue', + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_dueDateText').length, + 1, + "should have activity delay" + ); + assert.ok( + document.querySelector('.o_Activity_dueDateText').classList.contains('o-overdue'), + "activity delay should have the right color modifier class (overdue)" + ); + assert.strictEqual( + document.querySelector('.o_Activity_dueDateText').textContent, + "5 days overdue:", + "activity delay should have '5 days overdue:' as label" + ); +}); + +QUnit.test('activity with a summary layout', async function (assert) { + assert.expect(4); + + await this.start(); + const activity = this.env.models['mail.activity'].create({ + id: 12, + summary: 'test summary', + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_summary').length, + 1, + "should have activity summary" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_type').length, + 0, + "should not have the activity type as summary" + ); + assert.strictEqual( + document.querySelector('.o_Activity_summary').textContent.trim(), + "“test summary”", + "should have the specific activity summary in activity summary" + ); +}); + +QUnit.test('activity without summary layout', async function (assert) { + assert.expect(5); + + await this.start(); + const activity = this.env.models['mail.activity'].create({ + id: 12, + thread: [['insert', { id: 42, model: 'res.partner' }]], + type: [['insert', { id: 1, displayName: "Fake type" }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_type').length, + 1, + "activity details should have an activity type section" + ); + assert.strictEqual( + document.querySelector('.o_Activity_type').textContent.trim(), + "Fake type", + "activity details should have the activity type display name in type section" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_summary.o_Activity_type').length, + 1, + "should have activity type as summary" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_summary:not(.o_Activity_type)').length, + 0, + "should not have a specific summary" + ); +}); + +QUnit.test('activity details toggle', async function (assert) { + assert.expect(5); + + await this.start(); + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + const activity = this.env.models['mail.activity'].create({ + creator: [['insert', { id: 1, display_name: "Admin" }]], + dateCreate: date_to_str(today), + dateDeadline: date_to_str(tomorrow), + id: 12, + state: 'planned', + thread: [['insert', { id: 42, model: 'res.partner' }]], + type: [['insert', { id: 1, displayName: "Fake type" }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_details').length, + 0, + "activity details should not be visible by default" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_detailsButton').length, + 1, + "activity should have a details button" + ); + + await afterNextRender(() => + document.querySelector('.o_Activity_detailsButton').click() + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_details').length, + 1, + "activity details should be visible after clicking on details button" + ); + + await afterNextRender(() => + document.querySelector('.o_Activity_detailsButton').click() + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_details').length, + 0, + "activity details should no longer be visible after clicking again on details button" + ); +}); + +QUnit.test('activity details layout', async function (assert) { + assert.expect(11); + + await this.start(); + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + const activity = this.env.models['mail.activity'].create({ + assignee: [['insert', { id: 10, display_name: "Pauvre pomme" }]], + creator: [['insert', { id: 1, display_name: "Admin" }]], + dateCreate: date_to_str(today), + dateDeadline: date_to_str(tomorrow), + id: 12, + state: 'planned', + thread: [['insert', { id: 42, model: 'res.partner' }]], + type: [['insert', { id: 1, displayName: "Fake type" }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_userAvatar').length, + 1, + "should have activity user avatar" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_detailsButton').length, + 1, + "activity should have a details button" + ); + + await afterNextRender(() => + document.querySelector('.o_Activity_detailsButton').click() + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_details').length, + 1, + "activity details should be visible after clicking on details button" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_details .o_Activity_type').length, + 1, + "activity details should have type" + ); + assert.strictEqual( + document.querySelector('.o_Activity_details .o_Activity_type').textContent, + "Fake type", + "activity details type should be 'Fake type'" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_detailsCreation').length, + 1, + "activity details should have creation date " + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_detailsCreator').length, + 1, + "activity details should have creator" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_detailsAssignation').length, + 1, + "activity details should have assignation information" + ); + assert.strictEqual( + document.querySelector('.o_Activity_detailsAssignation').textContent.indexOf('Pauvre pomme'), + 0, + "activity details assignation information should contain creator display name" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_detailsAssignationUserAvatar').length, + 1, + "activity details should have user avatar" + ); +}); + +QUnit.test('activity with mail template layout', async function (assert) { + assert.expect(8); + + await this.start(); + const activity = this.env.models['mail.activity'].create({ + id: 12, + mailTemplates: [['insert', { id: 1, name: "Dummy mail template" }]], + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_sidebar').length, + 1, + "should have activity sidebar" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_mailTemplates').length, + 1, + "should have activity mail templates" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_mailTemplate').length, + 1, + "should have activity mail template" + ); + assert.strictEqual( + document.querySelectorAll('.o_MailTemplate_name').length, + 1, + "should have activity mail template name" + ); + assert.strictEqual( + document.querySelector('.o_MailTemplate_name').textContent, + "Dummy mail template", + "should have activity mail template name" + ); + assert.strictEqual( + document.querySelectorAll('.o_MailTemplate_preview').length, + 1, + "should have activity mail template name preview button" + ); + assert.strictEqual( + document.querySelectorAll('.o_MailTemplate_send').length, + 1, + "should have activity mail template name send button" + ); +}); + +QUnit.test('activity with mail template: preview mail', async function (assert) { + assert.expect(10); + + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action.context.default_res_id, + 42, + 'Action should have the activity res id as default res id in context' + ); + assert.strictEqual( + payload.action.context.default_model, + 'res.partner', + 'Action should have the activity res model as default model in context' + ); + assert.ok( + payload.action.context.default_use_template, + 'Action should have true as default use_template in context' + ); + assert.strictEqual( + payload.action.context.default_template_id, + 1, + 'Action should have the selected mail template id as default template id in context' + ); + assert.strictEqual( + payload.action.type, + "ir.actions.act_window", + 'Action should be of type "ir.actions.act_window"' + ); + assert.strictEqual( + payload.action.res_model, + "mail.compose.message", + 'Action should have "mail.compose.message" as res_model' + ); + }); + + await this.start({ env: { bus } }); + const activity = this.env.models['mail.activity'].create({ + id: 12, + mailTemplates: [['insert', { + id: 1, + name: "Dummy mail template", + }]], + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_MailTemplate_preview').length, + 1, + "should have activity mail template name preview button" + ); + + document.querySelector('.o_MailTemplate_preview').click(); + assert.verifySteps( + ['do_action'], + "should have called 'compose email' action correctly" + ); +}); + +QUnit.test('activity with mail template: send mail', async function (assert) { + assert.expect(7); + + await this.start({ + async mockRPC(route, args) { + if (args.method === 'activity_send_mail') { + assert.step('activity_send_mail'); + assert.strictEqual(args.args[0].length, 1); + assert.strictEqual(args.args[0][0], 42); + assert.strictEqual(args.args[1], 1); + return; + } else { + return this._super(...arguments); + } + }, + }); + const activity = this.env.models['mail.activity'].create({ + id: 12, + mailTemplates: [['insert', { + id: 1, + name: "Dummy mail template", + }]], + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_MailTemplate_send').length, + 1, + "should have activity mail template name send button" + ); + + document.querySelector('.o_MailTemplate_send').click(); + assert.verifySteps( + ['activity_send_mail'], + "should have called activity_send_mail rpc" + ); +}); + +QUnit.test('activity upload document is available', async function (assert) { + assert.expect(3); + + await this.start(); + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + const activity = this.env.models['mail.activity'].create({ + canWrite: true, + category: 'upload_file', + id: 12, + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_uploadButton').length, + 1, + "should have activity upload button" + ); + assert.strictEqual( + document.querySelectorAll('.o_FileUploader').length, + 1, + "should have a file uploader" + ); +}); + +QUnit.test('activity click on mark as done', async function (assert) { + assert.expect(4); + + await this.start(); + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + const activity = this.env.models['mail.activity'].create({ + canWrite: true, + category: 'not_upload_file', + id: 12, + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_markDoneButton').length, + 1, + "should have activity Mark as Done button" + ); + + await afterNextRender(() => { + document.querySelector('.o_Activity_markDoneButton').click(); + }); + assert.strictEqual( + document.querySelectorAll('.o_ActivityMarkDonePopover').length, + 1, + "should have opened the mark done popover" + ); + + await afterNextRender(() => { + document.querySelector('.o_Activity_markDoneButton').click(); + }); + assert.strictEqual( + document.querySelectorAll('.o_ActivityMarkDonePopover').length, + 0, + "should have closed the mark done popover" + ); +}); + +QUnit.test('activity mark as done popover should focus feedback input on open [REQUIRE FOCUS]', async function (assert) { + assert.expect(3); + + await this.start(); + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + const activity = this.env.models['mail.activity'].create({ + canWrite: true, + category: 'not_upload_file', + id: 12, + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + + assert.containsOnce( + document.body, + '.o_Activity', + "should have activity component" + ); + assert.containsOnce( + document.body, + '.o_Activity_markDoneButton', + "should have activity Mark as Done button" + ); + + await afterNextRender(() => { + document.querySelector('.o_Activity_markDoneButton').click(); + }); + assert.strictEqual( + document.querySelector('.o_ActivityMarkDonePopover_feedback'), + document.activeElement, + "the popover textarea should have the focus" + ); +}); + +QUnit.test('activity click on edit', async function (assert) { + assert.expect(9); + + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action.context.default_res_id, + 42, + 'Action should have the activity res id as default res id in context' + ); + assert.strictEqual( + payload.action.context.default_res_model, + 'res.partner', + 'Action should have the activity res model as default res model in context' + ); + assert.strictEqual( + payload.action.type, + "ir.actions.act_window", + 'Action should be of type "ir.actions.act_window"' + ); + assert.strictEqual( + payload.action.res_model, + "mail.activity", + 'Action should have "mail.activity" as res_model' + ); + assert.strictEqual( + payload.action.res_id, + 12, + 'Action should have activity id as res_id' + ); + }); + + await this.start({ env: { bus } }); + const activity = this.env.models['mail.activity'].create({ + canWrite: true, + id: 12, + mailTemplates: [['insert', { id: 1, name: "Dummy mail template" }]], + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_editButton').length, + 1, + "should have activity edit button" + ); + + document.querySelector('.o_Activity_editButton').click(); + assert.verifySteps( + ['do_action'], + "should have called 'schedule activity' action correctly" + ); +}); + +QUnit.test('activity edition', async function (assert) { + assert.expect(14); + + this.data['mail.activity'].records.push({ + can_write: true, + icon: 'fa-times', + id: 12, + res_id: 42, + res_model: 'res.partner', + }); + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.step('do_action'); + assert.strictEqual( + payload.action.context.default_res_id, + 42, + 'Action should have the activity res id as default res id in context' + ); + assert.strictEqual( + payload.action.context.default_res_model, + 'res.partner', + 'Action should have the activity res model as default res model in context' + ); + assert.strictEqual( + payload.action.type, + 'ir.actions.act_window', + 'Action should be of type "ir.actions.act_window"' + ); + assert.strictEqual( + payload.action.res_model, + 'mail.activity', + 'Action should have "mail.activity" as res_model' + ); + assert.strictEqual( + payload.action.res_id, + 12, + 'Action should have activity id as res_id' + ); + this.data['mail.activity'].records[0].icon = 'fa-check'; + payload.options.on_close(); + }); + + await this.start({ env: { bus } }); + const activity = this.env.models['mail.activity'].insert( + this.env.models['mail.activity'].convertData( + this.data['mail.activity'].records[0] + ) + ); + await this.createActivityComponent(activity); + + assert.containsOnce( + document.body, + '.o_Activity', + "should have activity component" + ); + assert.containsOnce( + document.body, + '.o_Activity_editButton', + "should have activity edit button" + ); + assert.containsOnce( + document.body, + '.o_Activity_icon', + "should have activity icon" + ); + assert.containsOnce( + document.body, + '.o_Activity_icon.fa-times', + "should have initial activity icon" + ); + assert.containsNone( + document.body, + '.o_Activity_icon.fa-check', + "should not have new activity icon when not edited yet" + ); + + await afterNextRender(() => { + document.querySelector('.o_Activity_editButton').click(); + }); + assert.verifySteps( + ['do_action'], + "should have called 'schedule activity' action correctly" + ); + assert.containsNone( + document.body, + '.o_Activity_icon.fa-times', + "should no more have initial activity icon once edited" + ); + assert.containsOnce( + document.body, + '.o_Activity_icon.fa-check', + "should now have new activity icon once edited" + ); +}); + +QUnit.test('activity click on cancel', async function (assert) { + assert.expect(7); + + await this.start({ + async mockRPC(route, args) { + if (route === '/web/dataset/call_kw/mail.activity/unlink') { + assert.step('unlink'); + assert.strictEqual(args.args[0].length, 1); + assert.strictEqual(args.args[0][0], 12); + return; + } else { + return this._super(...arguments); + } + }, + }); + const activity = this.env.models['mail.activity'].create({ + canWrite: true, + id: 12, + mailTemplates: [['insert', { + id: 1, + name: "Dummy mail template", + }]], + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + + // Create a parent component to surround the Activity component in order to be able + // to check that activity component has been destroyed + class ParentComponent extends Component { + constructor(...args) { + super(... args); + useStore(props => { + const activity = this.env.models['mail.activity'].get(props.activityLocalId); + return { + activity: activity ? activity.__state : undefined, + }; + }); + } + + /** + * @returns {mail.activity} + */ + get activity() { + return this.env.models['mail.activity'].get(this.props.activityLocalId); + } + } + ParentComponent.env = this.env; + Object.assign(ParentComponent, { + components, + props: { activityLocalId: String }, + template: xml` +
+

parent

+ + + +
+ `, + }); + await createRootComponent(this, ParentComponent, { + props: { activityLocalId: activity.localId }, + target: this.widget.el, + }); + + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity_cancelButton').length, + 1, + "should have activity cancel button" + ); + + await afterNextRender(() => + document.querySelector('.o_Activity_cancelButton').click() + ); + assert.verifySteps( + ['unlink'], + "should have called unlink rpc after clicking on cancel" + ); + assert.strictEqual( + document.querySelectorAll('.o_Activity').length, + 0, + "should no longer display activity after clicking on cancel" + ); +}); + +QUnit.test('activity mark done popover close on ESCAPE', async function (assert) { + // This test is not in activity_mark_done_popover_tests.js as it requires the activity mark done + // component to have a parent in order to allow testing interactions the popover. + assert.expect(2); + + await this.start(); + const activity = this.env.models['mail.activity'].create({ + canWrite: true, + category: 'not_upload_file', + id: 12, + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + + await this.createActivityComponent(activity); + await afterNextRender(() => { + document.querySelector('.o_Activity_markDoneButton').click(); + }); + assert.containsOnce( + document.body, + '.o_ActivityMarkDonePopover', + "Popover component should be present" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ActivityMarkDonePopover`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_ActivityMarkDonePopover', + "ESCAPE pressed should have closed the mark done popover" + ); +}); + +QUnit.test('activity mark done popover click on discard', async function (assert) { + // This test is not in activity_mark_done_popover_tests.js as it requires the activity mark done + // component to have a parent in order to allow testing interactions the popover. + assert.expect(3); + + await this.start(); + const activity = this.env.models['mail.activity'].create({ + canWrite: true, + category: 'not_upload_file', + id: 12, + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + await afterNextRender(() => { + document.querySelector('.o_Activity_markDoneButton').click(); + }); + assert.containsOnce( + document.body, + '.o_ActivityMarkDonePopover', + "Popover component should be present" + ); + assert.containsOnce( + document.body, + '.o_ActivityMarkDonePopover_discardButton', + "Popover component should contain the discard button" + ); + await afterNextRender(() => + document.querySelector('.o_ActivityMarkDonePopover_discardButton').click() + ); + assert.containsNone( + document.body, + '.o_ActivityMarkDonePopover', + "Discard button clicked should have closed the mark done popover" + ); +}); + +QUnit.test('data-oe-id & data-oe-model link redirection on click', async function (assert) { + assert.expect(7); + + const bus = new Bus(); + bus.on('do-action', null, payload => { + assert.strictEqual( + payload.action.type, + 'ir.actions.act_window', + "action should open view" + ); + assert.strictEqual( + payload.action.res_model, + 'some.model', + "action should open view on 'some.model' model" + ); + assert.strictEqual( + payload.action.res_id, + 250, + "action should open view on 250" + ); + assert.step('do-action:openFormView_some.model_250'); + }); + await this.start({ env: { bus } }); + const activity = this.env.models['mail.activity'].create({ + canWrite: true, + category: 'not_upload_file', + id: 12, + note: `

some.model_250

`, + thread: [['insert', { id: 42, model: 'res.partner' }]], + }); + await this.createActivityComponent(activity); + assert.containsOnce( + document.body, + '.o_Activity_note', + "activity should have a note" + ); + assert.containsOnce( + document.querySelector('.o_Activity_note'), + 'a', + "activity note should have a link" + ); + + document.querySelector(`.o_Activity_note a`).click(); + assert.verifySteps( + ['do-action:openFormView_some.model_250'], + "should have open form view on related record after click on link" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/mail/static/src/components/activity_box/activity_box.js b/addons/mail/static/src/components/activity_box/activity_box.js new file mode 100644 index 00000000..ca191694 --- /dev/null +++ b/addons/mail/static/src/components/activity_box/activity_box.js @@ -0,0 +1,64 @@ +odoo.define('mail/static/src/components/activity_box/activity_box.js', function (require) { +'use strict'; + +const components = { + Activity: require('mail/static/src/components/activity/activity.js'), +}; +const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const { Component } = owl; + +class ActivityBox extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useShouldUpdateBasedOnProps(); + useStore(props => { + const chatter = this.env.models['mail.chatter'].get(props.chatterLocalId); + const thread = chatter && chatter.thread; + return { + chatter: chatter ? chatter.__state : undefined, + thread: thread && thread.__state, + }; + }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {Chatter} + */ + get chatter() { + return this.env.models['mail.chatter'].get(this.props.chatterLocalId); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onClickTitle() { + this.chatter.toggleActivityBoxVisibility(); + } + +} + +Object.assign(ActivityBox, { + components, + props: { + chatterLocalId: String, + }, + template: 'mail.ActivityBox', +}); + +return ActivityBox; + +}); diff --git a/addons/mail/static/src/components/activity_box/activity_box.scss b/addons/mail/static/src/components/activity_box/activity_box.scss new file mode 100644 index 00000000..64e99347 --- /dev/null +++ b/addons/mail/static/src/components/activity_box/activity_box.scss @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------ +// Layout +// ------------------------------------------------------------------ + +.o_ActivityBox_title { + display: flex; + align-items: center; + flex: 0 0 auto; + margin-top: map-get($spacers, 4); + margin-bottom: map-get($spacers, 4); +} + +.o_ActivityBox_titleBadge { + padding: map-get($spacers, 0) map-get($spacers, 2); +} + +.o_ActivityBox_titleBadges { + margin-inline-end: map-get($spacers, 3); +} + +.o_ActivityBox_titleLine { + flex: 1 1 auto; + width: auto; +} + +.o_ActivityBox_titleText { + margin: map-get($spacers, 0) map-get($spacers, 3); +} + +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ + +.o_ActivityBox_title { + font-weight: bold; +} + +.o_ActivityBox_titleBadge { + font-size: 11px; +} + +.o_ActivityBox_titleLine { + border-color: gray('400'); + border-style: dashed; +} diff --git a/addons/mail/static/src/components/activity_box/activity_box.xml b/addons/mail/static/src/components/activity_box/activity_box.xml new file mode 100644 index 00000000..900b5634 --- /dev/null +++ b/addons/mail/static/src/components/activity_box/activity_box.xml @@ -0,0 +1,45 @@ + + + + + + + + diff --git a/addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.js b/addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.js new file mode 100644 index 00000000..de1ea5ce --- /dev/null +++ b/addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.js @@ -0,0 +1,122 @@ +odoo.define('mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.js', function (require) { +'use strict'; + +const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const { Component } = owl; +const { useRef } = owl.hooks; + +class ActivityMarkDonePopover extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useShouldUpdateBasedOnProps(); + useStore(props => { + const activity = this.env.models['mail.activity'].get(props.activityLocalId); + return { + activity: activity ? activity.__state : undefined, + }; + }); + this._feedbackTextareaRef = useRef('feedbackTextarea'); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + mounted() { + this._feedbackTextareaRef.el.focus(); + if (this.activity.feedbackBackup) { + this._feedbackTextareaRef.el.value = this.activity.feedbackBackup; + } + } + + /** + * @returns {mail.activity} + */ + get activity() { + return this.env.models['mail.activity'].get(this.props.activityLocalId); + } + + /** + * @returns {string} + */ + get DONE_AND_SCHEDULE_NEXT() { + return this.env._t("Done & Schedule Next"); + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _close() { + this.trigger('o-popover-close'); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onBlur() { + this.activity.update({ + feedbackBackup: this._feedbackTextareaRef.el.value, + }); + } + + /** + * @private + */ + _onClickDiscard() { + this._close(); + } + + /** + * @private + */ + async _onClickDone() { + await this.activity.markAsDone({ + feedback: this._feedbackTextareaRef.el.value, + }); + this.trigger('reload', { keepChanges: true }); + } + + /** + * @private + */ + _onClickDoneAndScheduleNext() { + this.activity.markAsDoneAndScheduleNext({ + feedback: this._feedbackTextareaRef.el.value, + }); + } + + /** + * @private + */ + _onKeydown(ev) { + if (ev.key === 'Escape') { + this._close(); + } + } + +} + +Object.assign(ActivityMarkDonePopover, { + props: { + activityLocalId: String, + }, + template: 'mail.ActivityMarkDonePopover', +}); + +return ActivityMarkDonePopover; + +}); diff --git a/addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.scss b/addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.scss new file mode 100644 index 00000000..3479ffc3 --- /dev/null +++ b/addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.scss @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------ +// Layout +// ------------------------------------------------------------------ + +.o_ActivityMarkDonePopover { + min-height: 100px; +} + +.o_ActivityMarkDonePopover_buttons { + margin-top: map-get($spacers, 2); +} + +.o_ActivityMarkDonePopover_doneButton { + margin: map-get($spacers, 0) map-get($spacers, 2); +} + +.o_ActivityMarkDonePopover_feedback { + min-height: 70px; +} + diff --git a/addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.xml b/addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.xml new file mode 100644 index 00000000..357ab59b --- /dev/null +++ b/addons/mail/static/src/components/activity_mark_done_popover/activity_mark_done_popover.xml @@ -0,0 +1,23 @@ + + + + +
+ +