diff --git a/addons/mail/static/src/backend_components/activity_cell_view/activity_cell_view_container.js b/addons/mail/static/src/backend_components/activity_cell_view/activity_cell_view_container.js index a27c07dc364..a02185918b9 100644 --- a/addons/mail/static/src/backend_components/activity_cell_view/activity_cell_view_container.js +++ b/addons/mail/static/src/backend_components/activity_cell_view/activity_cell_view_container.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; -import { insert } from '@mail/model'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; +import { insert } from "@mail/model"; -import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl'; +import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl"; const getNextId = (function () { let tmpId = 0; @@ -18,7 +18,6 @@ const getNextId = (function () { * records are ready before rendering ActivityCellView component. */ export class ActivityCellViewContainer extends Component { - /** * @override */ @@ -28,7 +27,7 @@ export class ActivityCellViewContainer extends Component { this.activityCellView = undefined; this.activityCellViewId = getNextId(); this._insertFromProps(this.props); - onWillUpdateProps(nextProps => this._insertFromProps(nextProps)); + onWillUpdateProps((nextProps) => this._insertFromProps(nextProps)); onWillDestroy(() => this._deleteRecord()); } @@ -53,7 +52,7 @@ export class ActivityCellViewContainer extends Component { this._deleteRecord(); return; } - const activityCellView = messaging.models['ActivityCellView'].insert({ + const activityCellView = messaging.models["ActivityCellView"].insert({ activityType: { id: props.activityTypeId, }, @@ -61,14 +60,16 @@ export class ActivityCellViewContainer extends Component { id: this.activityCellViewId, reloadFunc: props.reloadFunc, thread: { - activities: insert(props.activityIds.map(activityId => { - return { - id: activityId, - type: { - id: props.activityTypeId, - }, - }; - })), + activities: insert( + props.activityIds.map((activityId) => { + return { + id: activityId, + type: { + id: props.activityTypeId, + }, + }; + }) + ), hasActivities: true, id: props.resId, model: props.resModel, @@ -80,7 +81,6 @@ export class ActivityCellViewContainer extends Component { } this.render(); } - } Object.assign(ActivityCellViewContainer, { @@ -95,5 +95,5 @@ Object.assign(ActivityCellViewContainer, { resId: Number, resModel: String, }, - template: 'mail.ActivityCellViewContainer', + template: "mail.ActivityCellViewContainer", }); diff --git a/addons/mail/static/src/backend_components/activity_exception/activity_exception.js b/addons/mail/static/src/backend_components/activity_exception/activity_exception.js index 6df5883cacc..3778b0e3c8f 100644 --- a/addons/mail/static/src/backend_components/activity_exception/activity_exception.js +++ b/addons/mail/static/src/backend_components/activity_exception/activity_exception.js @@ -3,26 +3,26 @@ import { registry } from "@web/core/registry"; import { standardFieldProps } from "@web/views/fields/standard_field_props"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; class ActivityException extends Component { - get textClass() { if (this.props.value) { - return 'text-' + this.props.value + ' fa ' + this.props.record.data.activity_exception_icon; + return ( + "text-" + this.props.value + " fa " + this.props.record.data.activity_exception_icon + ); } return undefined; } - } Object.assign(ActivityException, { props: standardFieldProps, - template: 'mail.ActivityException', + template: "mail.ActivityException", fieldDependencies: { - activity_exception_icon: { type: 'char' }, + activity_exception_icon: { type: "char" }, }, noLabel: true, }); -registry.category('fields').add('activity_exception', ActivityException); +registry.category("fields").add("activity_exception", ActivityException); diff --git a/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js index c88894382c0..1abcdb67139 100644 --- a/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js +++ b/addons/mail/static/src/backend_components/kanban_field_activity_view/kanban_field_activity_view_container.js @@ -1,12 +1,12 @@ /** @odoo-module **/ // ensure components are registered beforehand. -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; import { registry } from "@web/core/registry"; import { standardFieldProps } from "@web/views/fields/standard_field_props"; -import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl'; +import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl"; const getNextId = (function () { let tmpId = 0; @@ -21,7 +21,6 @@ const getNextId = (function () { * records are ready before rendering KanbanFieldActivityView component. */ export class KanbanFieldActivityViewContainer extends Component { - /** * @override */ @@ -31,7 +30,7 @@ export class KanbanFieldActivityViewContainer extends Component { this.kanbanFieldActivityView = undefined; this.kanbanFieldActivityViewId = getNextId(); this._insertFromProps(this.props); - onWillUpdateProps(nextProps => this._insertFromProps(nextProps)); + onWillUpdateProps((nextProps) => this._insertFromProps(nextProps)); onWillDestroy(() => this._deleteRecord()); } @@ -56,10 +55,10 @@ export class KanbanFieldActivityViewContainer extends Component { this._deleteRecord(); return; } - const kanbanFieldActivityView = messaging.models['KanbanFieldActivityView'].insert({ + const kanbanFieldActivityView = messaging.models["KanbanFieldActivityView"].insert({ id: this.kanbanFieldActivityViewId, thread: { - activities: props.value.records.map(activityData => { + activities: props.value.records.map((activityData) => { return { id: activityData.resId, }; @@ -76,22 +75,21 @@ export class KanbanFieldActivityViewContainer extends Component { } this.render(); } - } Object.assign(KanbanFieldActivityViewContainer, { fieldDependencies: { - activity_exception_decoration: { type: 'selection' }, - activity_exception_icon: { type: 'char' }, - activity_state: { type: 'selection' }, - activity_summary: { type: 'char' }, - activity_type_icon: { type: 'char' }, - activity_type_id: { type: 'many2one', relation: 'mail.activity.type' }, + activity_exception_decoration: { type: "selection" }, + activity_exception_icon: { type: "char" }, + activity_state: { type: "selection" }, + activity_summary: { type: "char" }, + activity_type_icon: { type: "char" }, + activity_type_id: { type: "many2one", relation: "mail.activity.type" }, }, props: { ...standardFieldProps, }, - template: 'mail.KanbanFieldActivityViewContainer', + template: "mail.KanbanFieldActivityViewContainer", }); -registry.category('fields').add('kanban_activity', KanbanFieldActivityViewContainer); +registry.category("fields").add("kanban_activity", KanbanFieldActivityViewContainer); diff --git a/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js index bb6c3200085..7e60dd9eed4 100644 --- a/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js +++ b/addons/mail/static/src/backend_components/list_field_activity_view/list_field_activity_view_container.js @@ -1,12 +1,12 @@ /** @odoo-module **/ // ensure components are registered beforehand. -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; import { registry } from "@web/core/registry"; import { standardFieldProps } from "@web/views/fields/standard_field_props"; -import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl'; +import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl"; const getNextId = (function () { let tmpId = 0; @@ -21,7 +21,6 @@ const getNextId = (function () { * records are ready before rendering ListFieldActivityView component. */ export class ListFieldActivityViewContainer extends Component { - /** * @override */ @@ -31,7 +30,7 @@ export class ListFieldActivityViewContainer extends Component { this.listFieldActivityView = undefined; this.listFieldActivityViewId = getNextId(); this._insertFromProps(this.props); - onWillUpdateProps(nextProps => this._insertFromProps(nextProps)); + onWillUpdateProps((nextProps) => this._insertFromProps(nextProps)); onWillDestroy(() => this._deleteRecord()); } @@ -56,10 +55,10 @@ export class ListFieldActivityViewContainer extends Component { this._deleteRecord(); return; } - const listFieldActivityView = messaging.models['ListFieldActivityView'].insert({ + const listFieldActivityView = messaging.models["ListFieldActivityView"].insert({ id: this.listFieldActivityViewId, thread: { - activities: props.value.records.map(activityData => { + activities: props.value.records.map((activityData) => { return { id: activityData.resId, }; @@ -76,22 +75,21 @@ export class ListFieldActivityViewContainer extends Component { } this.render(); } - } Object.assign(ListFieldActivityViewContainer, { fieldDependencies: { - activity_exception_decoration: { type: 'selection' }, - activity_exception_icon: { type: 'char' }, - activity_state: { type: 'selection' }, - activity_summary: { type: 'char' }, - activity_type_icon: { type: 'char' }, - activity_type_id: { type: 'many2one', relation: 'mail.activity.type' }, + activity_exception_decoration: { type: "selection" }, + activity_exception_icon: { type: "char" }, + activity_state: { type: "selection" }, + activity_summary: { type: "char" }, + activity_type_icon: { type: "char" }, + activity_type_id: { type: "many2one", relation: "mail.activity.type" }, }, props: { ...standardFieldProps, }, - template: 'mail.ListFieldActivityViewContainer', + template: "mail.ListFieldActivityViewContainer", }); -registry.category('fields').add('list_activity', ListFieldActivityViewContainer); +registry.category("fields").add("list_activity", ListFieldActivityViewContainer); diff --git a/addons/mail/static/src/component_hooks/use_component_to_model.js b/addons/mail/static/src/component_hooks/use_component_to_model.js index 94ef621ec8b..82681287cb4 100644 --- a/addons/mail/static/src/component_hooks/use_component_to_model.js +++ b/addons/mail/static/src/component_hooks/use_component_to_model.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { clear } from '@mail/model'; +import { clear } from "@mail/model"; -import { onWillUpdateProps, useComponent } from '@odoo/owl'; +import { onWillUpdateProps, useComponent } from "@odoo/owl"; /** * This hook provides support for saving the reference of the component directly @@ -15,7 +15,7 @@ import { onWillUpdateProps, useComponent } from '@odoo/owl'; export function useComponentToModel({ fieldName }) { const component = useComponent(); component.props.record.update({ [fieldName]: component }); - onWillUpdateProps(nextProps => { + onWillUpdateProps((nextProps) => { const currentRecord = component.props.record; const nextRecord = nextProps.record; if (currentRecord.exists() && currentRecord !== nextRecord) { diff --git a/addons/mail/static/src/component_hooks/use_messaging_container.js b/addons/mail/static/src/component_hooks/use_messaging_container.js index e89e0a6223b..3d4fc0e476f 100644 --- a/addons/mail/static/src/component_hooks/use_messaging_container.js +++ b/addons/mail/static/src/component_hooks/use_messaging_container.js @@ -1,16 +1,16 @@ /** @odoo-module **/ -import { useModels } from '@mail/component_hooks/use_models'; +import { useModels } from "@mail/component_hooks/use_models"; import { componentRegistry, getMessagingComponent } from "@mail/utils/messaging_component"; -import { useComponent } from '@odoo/owl'; +import { useComponent } from "@odoo/owl"; export function useMessagingContainer() { const component = useComponent(); component.isLoaded = false; useModels(); component.env.services.messaging.modelManager.messagingCreatedPromise.then(() => { - let components = { ...component.constructor.components }; + const components = { ...component.constructor.components }; for (const name in componentRegistry) { Object.assign(components, { [name]: getMessagingComponent(name) }); } diff --git a/addons/mail/static/src/component_hooks/use_models.js b/addons/mail/static/src/component_hooks/use_models.js index 7bfad8a2ed7..45223d93d61 100644 --- a/addons/mail/static/src/component_hooks/use_models.js +++ b/addons/mail/static/src/component_hooks/use_models.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { Listener } from '@mail/model/model_listener'; +import { Listener } from "@mail/model/model_listener"; -import { onRendered, onWillDestroy, onWillRender, useComponent } from '@odoo/owl'; +import { onRendered, onWillDestroy, onWillRender, useComponent } from "@odoo/owl"; /** * This hook provides support for automatically re-rendering when used records diff --git a/addons/mail/static/src/component_hooks/use_ref_to_model.js b/addons/mail/static/src/component_hooks/use_ref_to_model.js index 9f7971c836e..97284e7e54b 100644 --- a/addons/mail/static/src/component_hooks/use_ref_to_model.js +++ b/addons/mail/static/src/component_hooks/use_ref_to_model.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { clear } from '@mail/model'; +import { clear } from "@mail/model"; -import { onWillUpdateProps, useComponent, useRef } from '@odoo/owl'; +import { onWillUpdateProps, useComponent, useRef } from "@odoo/owl"; /** * This hook provides support for saving the result of useRef directly into the @@ -17,7 +17,7 @@ export function useRefToModel({ fieldName, refName }) { const component = useComponent(); const ref = useRef(refName); component.props.record.update({ [fieldName]: ref }); - onWillUpdateProps(nextProps => { + onWillUpdateProps((nextProps) => { const currentRecord = component.props.record; const nextRecord = nextProps.record; if (currentRecord.exists() && currentRecord !== nextRecord) { diff --git a/addons/mail/static/src/component_hooks/use_update.js b/addons/mail/static/src/component_hooks/use_update.js index 6bc60ab4183..54434434f19 100644 --- a/addons/mail/static/src/component_hooks/use_update.js +++ b/addons/mail/static/src/component_hooks/use_update.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { Listener } from '@mail/model/model_listener'; +import { Listener } from "@mail/model/model_listener"; -import { onMounted, onPatched, onWillDestroy, useComponent } from '@odoo/owl'; +import { onMounted, onPatched, onWillDestroy, useComponent } from "@odoo/owl"; /** * This hook provides support for executing code after update (render or patch). diff --git a/addons/mail/static/src/component_hooks/use_update_to_model.js b/addons/mail/static/src/component_hooks/use_update_to_model.js index a05b81c5ee3..f8337a74466 100644 --- a/addons/mail/static/src/component_hooks/use_update_to_model.js +++ b/addons/mail/static/src/component_hooks/use_update_to_model.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { useUpdate } from '@mail/component_hooks/use_update'; +import { useUpdate } from "@mail/component_hooks/use_update"; -import { useComponent } from '@odoo/owl'; +import { useComponent } from "@odoo/owl"; /** * This hook provides support for binding the onMounted/onPatched hooks to the @@ -13,7 +13,9 @@ import { useComponent } from '@odoo/owl'; */ export function useUpdateToModel({ methodName }) { const component = useComponent(); - useUpdate({ func: () => { - component.props.record[methodName](); - } }); + useUpdate({ + func: () => { + component.props.record[methodName](); + }, + }); } diff --git a/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js b/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js index a960a22cfeb..a2679915715 100644 --- a/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js +++ b/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js @@ -1,28 +1,28 @@ /** @odoo-module **/ // ensure components are registered beforehand. -import '@mail/components/activity_menu_view/activity_menu_view'; +import "@mail/components/activity_menu_view/activity_menu_view"; import { getMessagingComponent } from "@mail/utils/messaging_component"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class ActivityMenuContainer extends Component { - /** * @override */ setup() { super.setup(); this.env.services.messaging.modelManager.messagingCreatedPromise.then(() => { - this.activityMenuView = this.env.services.messaging.modelManager.messaging.models['ActivityMenuView'].insert(); + this.activityMenuView = this.env.services.messaging.modelManager.messaging.models[ + "ActivityMenuView" + ].insert(); this.render(); }); } - } ActivityMenuContainer.props = {}; Object.assign(ActivityMenuContainer, { - components: { ActivityMenuView: getMessagingComponent('ActivityMenuView') }, - template: 'mail.ActivityMenuContainer', + components: { ActivityMenuView: getMessagingComponent("ActivityMenuView") }, + template: "mail.ActivityMenuContainer", }); diff --git a/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js b/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js index cda1efa9e45..e482fe5607e 100644 --- a/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js +++ b/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js @@ -1,17 +1,17 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { registerMessagingComponent } from '@mail/utils/messaging_component'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { registerMessagingComponent } from "@mail/utils/messaging_component"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class ActivityMenuView extends Component { /** * @override */ - setup() { + setup() { super.setup(); - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); } /** * @returns {ActivityMenuView} @@ -23,7 +23,7 @@ export class ActivityMenuView extends Component { Object.assign(ActivityMenuView, { props: { record: Object }, - template: 'mail.ActivityMenuView', + template: "mail.ActivityMenuView", }); registerMessagingComponent(ActivityMenuView); diff --git a/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js b/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js index c656bbdd950..2a95ab5a120 100644 --- a/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js +++ b/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js @@ -1,24 +1,23 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useRefToModel } from '@mail/component_hooks/use_ref_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { registerMessagingComponent } from '@mail/utils/messaging_component'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useRefToModel } from "@mail/component_hooks/use_ref_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { registerMessagingComponent } from "@mail/utils/messaging_component"; -import { Component, onMounted } from '@odoo/owl'; +import { Component, onMounted } from "@odoo/owl"; export class AttachmentViewer extends Component { - /** * @override */ setup() { super.setup(); - useComponentToModel({ fieldName: 'component' }); - useRefToModel({ fieldName: 'imageRef', refName: 'image' }); - useRefToModel({ fieldName: 'zoomerRef', refName: 'zoomer' }); - useRefToModel({ fieldName: 'iframeViewerPdfRef', refName: 'iframeViewerPdf' }); - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useComponentToModel({ fieldName: "component" }); + useRefToModel({ fieldName: "imageRef", refName: "image" }); + useRefToModel({ fieldName: "zoomerRef", refName: "zoomer" }); + useRefToModel({ fieldName: "iframeViewerPdfRef", refName: "iframeViewerPdf" }); + useUpdateToModel({ methodName: "onComponentUpdate" }); onMounted(() => this._mounted()); } @@ -39,12 +38,11 @@ export class AttachmentViewer extends Component { get attachmentViewer() { return this.props.record; } - } Object.assign(AttachmentViewer, { props: { record: Object }, - template: 'mail.AttachmentViewer', + template: "mail.AttachmentViewer", }); registerMessagingComponent(AttachmentViewer); diff --git a/addons/mail/static/src/components/call_invite_request_popup_list/call_invite_request_popup_list.js b/addons/mail/static/src/components/call_invite_request_popup_list/call_invite_request_popup_list.js index 630dc25f1c6..00f411819c8 100644 --- a/addons/mail/static/src/components/call_invite_request_popup_list/call_invite_request_popup_list.js +++ b/addons/mail/static/src/components/call_invite_request_popup_list/call_invite_request_popup_list.js @@ -1,14 +1,14 @@ /** @odoo-module **/ -import { registerMessagingComponent } from '@mail/utils/messaging_component'; +import { registerMessagingComponent } from "@mail/utils/messaging_component"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class CallInviteRequestPopupList extends Component {} Object.assign(CallInviteRequestPopupList, { props: {}, - template: 'mail.CallInviteRequestPopupList', + template: "mail.CallInviteRequestPopupList", }); registerMessagingComponent(CallInviteRequestPopupList); diff --git a/addons/mail/static/src/components/call_settings_menu_device/call_settings_menu_device.js b/addons/mail/static/src/components/call_settings_menu_device/call_settings_menu_device.js index 8d5af64a726..e0b760cf621 100644 --- a/addons/mail/static/src/components/call_settings_menu_device/call_settings_menu_device.js +++ b/addons/mail/static/src/components/call_settings_menu_device/call_settings_menu_device.js @@ -1,14 +1,14 @@ /** @odoo-module **/ -import { registerMessagingComponent } from '@mail/utils/messaging_component'; +import { registerMessagingComponent } from "@mail/utils/messaging_component"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class CallSettingsMenuDevice extends Component {} Object.assign(CallSettingsMenuDevice, { props: { device: Object }, - template: 'mail.CallSettingsMenuDevice', + template: "mail.CallSettingsMenuDevice", }); registerMessagingComponent(CallSettingsMenuDevice); diff --git a/addons/mail/static/src/components/call_systray_menu_container/call_systray_menu_container.js b/addons/mail/static/src/components/call_systray_menu_container/call_systray_menu_container.js index 920c0d4af23..7fa29354925 100644 --- a/addons/mail/static/src/components/call_systray_menu_container/call_systray_menu_container.js +++ b/addons/mail/static/src/components/call_systray_menu_container/call_systray_menu_container.js @@ -1,11 +1,10 @@ /** @odoo-module **/ -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class CallSystrayMenuContainer extends Component { - /** * @override */ @@ -16,10 +15,9 @@ export class CallSystrayMenuContainer extends Component { get messaging() { return this.env.services.messaging.modelManager.messaging; } - } CallSystrayMenuContainer.props = {}; Object.assign(CallSystrayMenuContainer, { - template: 'mail.CallSystrayMenuContainer', + template: "mail.CallSystrayMenuContainer", }); diff --git a/addons/mail/static/src/components/chat_window_manager_container/chat_window_manager_container.js b/addons/mail/static/src/components/chat_window_manager_container/chat_window_manager_container.js index c27e5dce8f8..de34f066771 100644 --- a/addons/mail/static/src/components/chat_window_manager_container/chat_window_manager_container.js +++ b/addons/mail/static/src/components/chat_window_manager_container/chat_window_manager_container.js @@ -1,11 +1,10 @@ /** @odoo-module **/ -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class ChatWindowManagerContainer extends Component { - /** * @override */ @@ -20,5 +19,5 @@ export class ChatWindowManagerContainer extends Component { ChatWindowManagerContainer.props = {}; Object.assign(ChatWindowManagerContainer, { - template: 'mail.ChatWindowManagerContainer', + template: "mail.ChatWindowManagerContainer", }); diff --git a/addons/mail/static/src/components/chatter_container/chatter_container.js b/addons/mail/static/src/components/chatter_container/chatter_container.js index bce73b478e6..6e70645db10 100644 --- a/addons/mail/static/src/components/chatter_container/chatter_container.js +++ b/addons/mail/static/src/components/chatter_container/chatter_container.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; -import { clear } from '@mail/model'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; +import { clear } from "@mail/model"; -import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl'; +import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl"; export const getChatterNextTemporaryId = (function () { let tmpId = 0; @@ -22,7 +22,6 @@ export const getChatterNextTemporaryId = (function () { * this component delays the mounting of chatter until it becomes initialized. */ export class ChatterContainer extends Component { - /** * @override */ @@ -31,7 +30,7 @@ export class ChatterContainer extends Component { super.setup(); this.localChatter = undefined; this._insertFromProps(this.props); - onWillUpdateProps(nextProps => { + onWillUpdateProps((nextProps) => { this._insertFromProps(nextProps); }); onWillDestroy(() => this.deleteLocalChatter()); @@ -67,7 +66,10 @@ export class ChatterContainer extends Component { } const hasToCreateChatter = !props.chatter && !this.localChatter; if (hasToCreateChatter) { - this.localChatter = messaging.models['Chatter'].insert({ id: getChatterNextTemporaryId(), ...values }); + this.localChatter = messaging.models["Chatter"].insert({ + id: getChatterNextTemporaryId(), + ...values, + }); } const chatter = props.chatter || this.localChatter; if (!hasToCreateChatter) { @@ -98,7 +100,6 @@ export class ChatterContainer extends Component { } this.render(); } - } Object.assign(ChatterContainer, { @@ -163,7 +164,7 @@ Object.assign(ChatterContainer, { webRecord: { type: Object, optional: true, - } + }, }, - template: 'mail.ChatterContainer', + template: "mail.ChatterContainer", }); diff --git a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js index 4a1e3710565..8bf6cba6e33 100644 --- a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js +++ b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js @@ -1,28 +1,27 @@ /** @odoo-module **/ -import { useUpdate } from '@mail/component_hooks/use_update'; -import { registerMessagingComponent } from '@mail/utils/messaging_component'; +import { useUpdate } from "@mail/component_hooks/use_update"; +import { registerMessagingComponent } from "@mail/utils/messaging_component"; import { useService } from "@web/core/utils/hooks"; -import { FormViewDialog } from '@web/views/view_dialogs/form_view_dialog'; +import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog"; -import { Component, useRef } from '@odoo/owl'; +import { Component, useRef } from "@odoo/owl"; export class ComposerSuggestedRecipientView extends Component { - /** * @override */ setup() { super.setup(); - this.id = _.uniqueId('o_ComposerSuggestedRecipientView_'); + this.id = _.uniqueId("o_ComposerSuggestedRecipientView_"); useUpdate({ func: () => this._update() }); /** * Reference of the checkbox. Useful to know whether it was checked or * not, to properly update the corresponding state in the record or to * prompt the user with the partner creation dialog. */ - this._checkboxRef = useRef('checkbox'); + this._checkboxRef = useRef("checkbox"); this.dialogService = useService("dialog"); } @@ -70,13 +69,17 @@ export class ComposerSuggestedRecipientView extends Component { if (isChecked) { this.dialogService.add(FormViewDialog, { context: { - active_id: this.composerSuggestedRecipientView.suggestedRecipientInfo.thread.id, - active_model: 'mail.compose.message', - default_email: this.composerSuggestedRecipientView.suggestedRecipientInfo.email, - default_name: this.composerSuggestedRecipientView.suggestedRecipientInfo.name, - default_lang: this.composerSuggestedRecipientView.suggestedRecipientInfo.lang, + active_id: this.composerSuggestedRecipientView.suggestedRecipientInfo.thread + .id, + active_model: "mail.compose.message", + default_email: this.composerSuggestedRecipientView.suggestedRecipientInfo + .email, + default_name: this.composerSuggestedRecipientView.suggestedRecipientInfo + .name, + default_lang: this.composerSuggestedRecipientView.suggestedRecipientInfo + .lang, force_email: true, - ref: 'compound_context', + ref: "compound_context", }, onRecordSaved: () => this._onDialogSaved(), resModel: "res.partner", @@ -93,20 +96,19 @@ export class ComposerSuggestedRecipientView extends Component { if (!this.composerSuggestedRecipientView.exists()) { return; } - const thread = ( + const thread = this.composerSuggestedRecipientView.suggestedRecipientInfo && - this.composerSuggestedRecipientView.suggestedRecipientInfo.thread - ); + this.composerSuggestedRecipientView.suggestedRecipientInfo.thread; if (!thread) { return; } - thread.fetchData(['suggestedRecipients']); + thread.fetchData(["suggestedRecipients"]); } } Object.assign(ComposerSuggestedRecipientView, { props: { record: Object }, - template: 'mail.ComposerSuggestedRecipientView', + template: "mail.ComposerSuggestedRecipientView", }); registerMessagingComponent(ComposerSuggestedRecipientView); diff --git a/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js b/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js index 7f2206859e4..de7be9fe065 100644 --- a/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js +++ b/addons/mail/static/src/components/dialog_manager_container/dialog_manager_container.js @@ -1,15 +1,14 @@ /** @odoo-module **/ -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class DialogManagerContainer extends Component { - /** * @override */ - setup() { + setup() { useMessagingContainer(); } @@ -20,5 +19,5 @@ export class DialogManagerContainer extends Component { DialogManagerContainer.props = {}; Object.assign(DialogManagerContainer, { - template: 'mail.DialogManagerContainer', + template: "mail.DialogManagerContainer", }); diff --git a/addons/mail/static/src/components/discuss_container/discuss_container.js b/addons/mail/static/src/components/discuss_container/discuss_container.js index 39218b6d8d1..c4dd217c48c 100644 --- a/addons/mail/static/src/components/discuss_container/discuss_container.js +++ b/addons/mail/static/src/components/discuss_container/discuss_container.js @@ -1,11 +1,10 @@ /** @odoo-module **/ -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; -import { Component, onWillDestroy } from '@odoo/owl'; +import { Component, onWillDestroy } from "@odoo/owl"; export class DiscussContainer extends Component { - /** * @override */ @@ -18,7 +17,7 @@ export class DiscussContainer extends Component { const initActiveId = (action.context && action.context.active_id) || (action.params && action.params.default_active_id) || - 'mail.box_inbox'; + "mail.box_inbox"; this.discuss = this.messaging.discuss; this.discuss.update({ discussView: { @@ -58,7 +57,6 @@ export class DiscussContainer extends Component { this.discuss.close(); } } - } Object.assign(DiscussContainer, { @@ -68,5 +66,5 @@ Object.assign(DiscussContainer, { className: String, globalState: { type: Object, optional: 1 }, }, - template: 'mail.DiscussContainer', + template: "mail.DiscussContainer", }); diff --git a/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js b/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js index d7382c7a15c..99a7a931da8 100644 --- a/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js +++ b/addons/mail/static/src/components/discuss_public_view_container/discuss_public_view_container.js @@ -1,20 +1,23 @@ /** @odoo-module **/ -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class DiscussPublicViewContainer extends Component { - /** * @override */ setup() { super.setup(); useMessagingContainer(); - this.env.services.messaging.get().then(messaging => { - messaging.models['Thread'].insert(messaging.models['Thread'].convertData(this.props.data.channelData)); - this.discussPublicView = messaging.models['DiscussPublicView'].insert(this.props.data.discussPublicViewData); + this.env.services.messaging.get().then((messaging) => { + messaging.models["Thread"].insert( + messaging.models["Thread"].convertData(this.props.data.channelData) + ); + this.discussPublicView = messaging.models["DiscussPublicView"].insert( + this.props.data.discussPublicViewData + ); if (this.discussPublicView.shouldDisplayWelcomeViewInitially) { this.discussPublicView.switchToWelcomeView(); } else { @@ -23,11 +26,10 @@ export class DiscussPublicViewContainer extends Component { this.render(); }); } - } Object.assign(DiscussPublicViewContainer, { - template: 'mail.DiscussPublicViewContainer', + template: "mail.DiscussPublicViewContainer", props: { data: Object, }, diff --git a/addons/mail/static/src/components/message_list/message_list.js b/addons/mail/static/src/components/message_list/message_list.js index ae6f3da12ed..d103d94c9b7 100644 --- a/addons/mail/static/src/components/message_list/message_list.js +++ b/addons/mail/static/src/components/message_list/message_list.js @@ -1,24 +1,23 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useRefToModel } from '@mail/component_hooks/use_ref_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { registerMessagingComponent } from '@mail/utils/messaging_component'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useRefToModel } from "@mail/component_hooks/use_ref_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { registerMessagingComponent } from "@mail/utils/messaging_component"; import { Transition } from "@web/core/transition"; -import { Component, onWillPatch } from '@odoo/owl'; +import { Component, onWillPatch } from "@odoo/owl"; export class MessageListView extends Component { - /** * @override */ setup() { super.setup(); - useComponentToModel({ fieldName: 'component' }); - useRefToModel({ fieldName: 'loadMoreRef', refName: 'loadMore' }); - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useComponentToModel({ fieldName: "component" }); + useRefToModel({ fieldName: "loadMoreRef", refName: "loadMore" }); + useUpdateToModel({ methodName: "onComponentUpdate" }); /** * Snapshot computed during willPatch, which is used by patched. */ @@ -46,13 +45,12 @@ export class MessageListView extends Component { get messageListView() { return this.props.record; } - } Object.assign(MessageListView, { components: { Transition }, props: { record: Object }, - template: 'mail.MessageListView', + template: "mail.MessageListView", }); registerMessagingComponent(MessageListView); diff --git a/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js b/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js index afeaab92037..3ec753133b2 100644 --- a/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js +++ b/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js @@ -1,11 +1,10 @@ /** @odoo-module **/ -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class MessagingMenuContainer extends Component { - /** * @override */ @@ -20,5 +19,5 @@ export class MessagingMenuContainer extends Component { MessagingMenuContainer.props = {}; Object.assign(MessagingMenuContainer, { - template: 'mail.MessagingMenuContainer', + template: "mail.MessagingMenuContainer", }); diff --git a/addons/mail/static/src/components/notification_alert/notification_alert.js b/addons/mail/static/src/components/notification_alert/notification_alert.js index 78332f58e2a..e1c6d9056c0 100644 --- a/addons/mail/static/src/components/notification_alert/notification_alert.js +++ b/addons/mail/static/src/components/notification_alert/notification_alert.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { registerMessagingComponent } from '@mail/utils/messaging_component'; +import { registerMessagingComponent } from "@mail/utils/messaging_component"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class NotificationAlert extends Component {} Object.assign(NotificationAlert, { - template: 'mail.NotificationAlert', + template: "mail.NotificationAlert", }); registerMessagingComponent(NotificationAlert); diff --git a/addons/mail/static/src/components/notification_alert_container/notification_alert_container.js b/addons/mail/static/src/components/notification_alert_container/notification_alert_container.js index f63566c3b4c..c53f81d2732 100644 --- a/addons/mail/static/src/components/notification_alert_container/notification_alert_container.js +++ b/addons/mail/static/src/components/notification_alert_container/notification_alert_container.js @@ -3,19 +3,16 @@ import { registry } from "@web/core/registry"; import { getMessagingComponent } from "@mail/utils/messaging_component"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class NotificationAlertContainer extends Component {} Object.assign(NotificationAlertContainer, { - components: { NotificationAlert: getMessagingComponent('NotificationAlert') }, - template: 'mail.NotificationAlertContainer', + components: { NotificationAlert: getMessagingComponent("NotificationAlert") }, + template: "mail.NotificationAlertContainer", props: { // Should call standard_widget_props.js readonly: { type: Boolean, optional: true }, record: { type: Object }, }, }); -registry.category("view_widgets").add( - "notification_alert", - NotificationAlertContainer -); +registry.category("view_widgets").add("notification_alert", NotificationAlertContainer); diff --git a/addons/mail/static/src/components/popover_manager_container/popover_manager_container.js b/addons/mail/static/src/components/popover_manager_container/popover_manager_container.js index f2eafcb2ce2..d06a631864a 100644 --- a/addons/mail/static/src/components/popover_manager_container/popover_manager_container.js +++ b/addons/mail/static/src/components/popover_manager_container/popover_manager_container.js @@ -1,11 +1,10 @@ /** @odoo-module **/ -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class PopoverManagerContainer extends Component { - /** * @override */ @@ -20,5 +19,5 @@ export class PopoverManagerContainer extends Component { PopoverManagerContainer.props = {}; Object.assign(PopoverManagerContainer, { - template: 'mail.PopoverManagerContainer', + template: "mail.PopoverManagerContainer", }); diff --git a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js index bd204b44a50..b88302b07cf 100644 --- a/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js +++ b/addons/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js @@ -1,18 +1,17 @@ /** @odoo-module **/ -import { useRefToModel } from '@mail/component_hooks/use_ref_to_model'; -import { registerMessagingComponent } from '@mail/utils/messaging_component'; +import { useRefToModel } from "@mail/component_hooks/use_ref_to_model"; +import { registerMessagingComponent } from "@mail/utils/messaging_component"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class ThreadNeedactionPreviewView extends Component { - /** * @override */ setup() { super.setup(); - useRefToModel({ fieldName: 'markAsReadRef', refName: 'markAsRead' }); + useRefToModel({ fieldName: "markAsReadRef", refName: "markAsRead" }); } //-------------------------------------------------------------------------- @@ -29,7 +28,7 @@ export class ThreadNeedactionPreviewView extends Component { return this.threadNeedactionPreviewView.thread.moduleIcon; } if (!this.threadNeedactionPreviewView.thread.channel) { - return '/mail/static/src/img/smiley/avatar.jpg'; + return "/mail/static/src/img/smiley/avatar.jpg"; } if (this.threadNeedactionPreviewView.thread.channel.correspondent) { return this.threadNeedactionPreviewView.thread.channel.correspondent.avatarUrl; @@ -43,12 +42,11 @@ export class ThreadNeedactionPreviewView extends Component { get threadNeedactionPreviewView() { return this.props.record; } - } Object.assign(ThreadNeedactionPreviewView, { props: { record: Object }, - template: 'mail.ThreadNeedactionPreviewView', + template: "mail.ThreadNeedactionPreviewView", }); registerMessagingComponent(ThreadNeedactionPreviewView); diff --git a/addons/mail/static/src/components/tracking_value/tracking_value.js b/addons/mail/static/src/components/tracking_value/tracking_value.js index 9915a11f163..3633e44c8bd 100644 --- a/addons/mail/static/src/components/tracking_value/tracking_value.js +++ b/addons/mail/static/src/components/tracking_value/tracking_value.js @@ -1,23 +1,21 @@ /** @odoo-module **/ -import { registerMessagingComponent } from '@mail/utils/messaging_component'; +import { registerMessagingComponent } from "@mail/utils/messaging_component"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; export class TrackingValue extends Component { - /** * @returns {TrackingValue} */ get trackingValue() { return this.props.value; } - } Object.assign(TrackingValue, { props: { value: Object }, - template: 'mail.TrackingValue', + template: "mail.TrackingValue", }); registerMessagingComponent(TrackingValue); diff --git a/addons/mail/static/src/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container.js b/addons/mail/static/src/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container.js index f8cd0c4ddc6..2c08f9b32de 100644 --- a/addons/mail/static/src/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container.js +++ b/addons/mail/static/src/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container'; +import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container"; -import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl'; +import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl"; const getNextId = (function () { let tmpId = 0; @@ -22,7 +22,6 @@ const getNextId = (function () { * mounting of attachment view until it becomes initialized. */ export class WebClientViewAttachmentViewContainer extends Component { - /** * @override */ @@ -32,7 +31,7 @@ export class WebClientViewAttachmentViewContainer extends Component { this.webClientViewAttachmentView = undefined; this.webClientViewAttachmentViewId = getNextId(); this._insertFromProps(this.props); - onWillUpdateProps(nextProps => this._insertFromProps(nextProps)); + onWillUpdateProps((nextProps) => this._insertFromProps(nextProps)); onWillDestroy(() => this._deleteRecord()); } @@ -61,17 +60,16 @@ export class WebClientViewAttachmentViewContainer extends Component { this._deleteRecord(); return; } - const thread = messaging.models['Thread'].insert({ + const thread = messaging.models["Thread"].insert({ id: props.threadId, model: props.threadModel, }); - this.webClientViewAttachmentView = messaging.models['WebClientViewAttachmentView'].insert({ + this.webClientViewAttachmentView = messaging.models["WebClientViewAttachmentView"].insert({ id: this.webClientViewAttachmentViewId, thread, }); this.render(); } - } Object.assign(WebClientViewAttachmentViewContainer, { @@ -82,5 +80,5 @@ Object.assign(WebClientViewAttachmentViewContainer, { }, threadModel: String, }, - template: 'mail.WebClientViewAttachmentViewContainer', + template: "mail.WebClientViewAttachmentViewContainer", }); diff --git a/addons/mail/static/src/core_models/device.js b/addons/mail/static/src/core_models/device.js index cd41c4a50d7..560b0323314 100644 --- a/addons/mail/static/src/core_models/device.js +++ b/addons/mail/static/src/core_models/device.js @@ -1,19 +1,19 @@ /** @odoo-module **/ -import { attr, Model } from '@mail/model'; +import { attr, Model } from "@mail/model"; import { browser } from "@web/core/browser/browser"; import { isMobileOS } from "@web/core/browser/feature_detection"; -import { SIZES } from '@web/core/ui/ui_service'; +import { SIZES } from "@web/core/ui/ui_service"; Model({ - name: 'Device', + name: "Device", lifecycleHooks: { _created() { this._refresh(); this._onResize = _.debounce(() => this._refresh(), 100); }, _willDelete() { - browser.removeEventListener('resize', this._onResize); + browser.removeEventListener("resize", this._onResize); }, }, recordMethods: { @@ -21,7 +21,7 @@ Model({ * Called when messaging is started. */ start() { - browser.addEventListener('resize', this._onResize); + browser.addEventListener("resize", this._onResize); }, /** * @private @@ -39,7 +39,10 @@ Model({ fields: { globalWindowInnerHeight: attr(), globalWindowInnerWidth: attr(), - hasCanvasFilterSupport: attr({ default: typeof document.createElement('canvas').getContext('2d').filter !== 'undefined' }), + hasCanvasFilterSupport: attr({ + default: + typeof document.createElement("canvas").getContext("2d").filter !== "undefined", + }), hasRtcSupport: attr({ compute() { return Boolean(window.RTCPeerConnection && window.MediaStream); diff --git a/addons/mail/static/src/core_models/locale.js b/addons/mail/static/src/core_models/locale.js index 04ff51c36f5..007654afd4e 100644 --- a/addons/mail/static/src/core_models/locale.js +++ b/addons/mail/static/src/core_models/locale.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, Model } from '@mail/model'; +import { attr, Model } from "@mail/model"; -import { localization } from '@web/core/l10n/localization'; +import { localization } from "@web/core/l10n/localization"; Model({ - name: 'Locale', + name: "Locale", fields: { /** * Language used by interface, formatted like {language ISO 2}_{country ISO 2} (eg: fr_FR). diff --git a/addons/mail/static/src/core_models/messaging.js b/addons/mail/static/src/core_models/messaging.js index bc1df7e12fe..4f50efcdb00 100644 --- a/addons/mail/static/src/core_models/messaging.js +++ b/addons/mail/static/src/core_models/messaging.js @@ -1,14 +1,14 @@ /** @odoo-module **/ -import { attr, many, one, Model } from '@mail/model'; -import { makeDeferred } from '@mail/utils/deferred'; +import { attr, many, one, Model } from "@mail/model"; +import { makeDeferred } from "@mail/utils/deferred"; -import { browser } from '@web/core/browser/browser'; +import { browser } from "@web/core/browser/browser"; -import { EventBus } from '@odoo/owl'; +import { EventBus } from "@odoo/owl"; Model({ - name: 'Messaging', + name: "Messaging", lifecycleHooks: { _created() { odoo.__DEBUG__.messaging = this; @@ -33,23 +33,35 @@ Model({ const { args = [], method, model, kwargs = {} } = params; const { domain, fields, groupBy, ...remainingKwargs } = kwargs; - const ormService = 'shadow' in options ? this.env.services.orm.silent : this.env.services.orm; + const ormService = + "shadow" in options ? this.env.services.orm.silent : this.env.services.orm; switch (method) { - case 'create': { + case "create": { const { vals_list, ...createKwargs } = kwargs; return ormService.create(model, args[0] || vals_list, createKwargs); } - case 'read': + case "read": return ormService.read(model, args[0], args[1] || fields, remainingKwargs); - case 'read_group': - return ormService.readGroup(model, args[0] || domain, args[1] || fields, args[2] || groupBy, remainingKwargs); - case 'search': + case "read_group": + return ormService.readGroup( + model, + args[0] || domain, + args[1] || fields, + args[2] || groupBy, + remainingKwargs + ); + case "search": return ormService.search(model, args[0] || domain, remainingKwargs); - case 'search_read': - return ormService.searchRead(model, args[0] || domain, args[1] || fields, remainingKwargs); - case 'unlink': + case "search_read": + return ormService.searchRead( + model, + args[0] || domain, + args[1] || fields, + remainingKwargs + ); + case "unlink": return ormService.unlink(model, args[0], kwargs); - case 'write': { + case "write": { const { vals, ...writeKwargs } = kwargs; return ormService.write(model, args[0], args[1] || vals, writeKwargs); } @@ -71,27 +83,29 @@ Model({ * Inverse of the messaging field present on all models. This field * therefore contains all existing records. */ - allRecords: many('Record', { inverse: 'messaging', isCausal: true }), + allRecords: many("Record", { inverse: "messaging", isCausal: true }), browser: attr({ compute() { return browser; }, }), - device: one('Device', { default: {}, isCausal: true, readonly: true }), + device: one("Device", { default: {}, isCausal: true, readonly: true }), /** * Promise that will be resolved when messaging is initialized. */ - initializedPromise: attr({ required: true, + initializedPromise: attr({ + required: true, compute() { return makeDeferred(); }, }), isInitialized: attr({ default: false }), - locale: one('Locale', { default: {}, isCausal: true, readonly: true }), + locale: one("Locale", { default: {}, isCausal: true, readonly: true }), /** * Determines the bus that is used to communicate messaging events. */ - messagingBus: attr({ required: true, + messagingBus: attr({ + required: true, compute() { if (this.messagingBus) { return; // avoid overwrite if already provided (example in tests) diff --git a/addons/mail/static/src/core_models/record.js b/addons/mail/static/src/core_models/record.js index 2c6d494186d..ead1395d419 100644 --- a/addons/mail/static/src/core_models/record.js +++ b/addons/mail/static/src/core_models/record.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; /** * The object below defines a model. Instances of such model (or inherited @@ -13,7 +13,7 @@ Model({ * Name of the model. Important to refer to appropriate model like in * relational fields. Name of models must be unique. */ - name: 'Record', + name: "Record", lifecycleHooks: { /** * This function is called after the record has been created, more @@ -178,6 +178,11 @@ Model({ * States the messaging singleton. Automatically assigned by the model * manager at creation. */ - messaging: one('Messaging', { default: {}, inverse: 'allRecords', readonly: true, required: true }), + messaging: one("Messaging", { + default: {}, + inverse: "allRecords", + readonly: true, + required: true, + }), }, }); diff --git a/addons/mail/static/src/core_models/timer.js b/addons/mail/static/src/core_models/timer.js index b0656fea5df..9da0ce36fbd 100644 --- a/addons/mail/static/src/core_models/timer.js +++ b/addons/mail/static/src/core_models/timer.js @@ -1,13 +1,15 @@ /** @odoo-module **/ -import { attr, clear, Model } from '@mail/model'; +import { attr, clear, Model } from "@mail/model"; Model({ - name: 'Timer', - identifyingMode: 'xor', + name: "Timer", + identifyingMode: "xor", lifecycleHooks: { _created() { - this.update({ timeoutId: this.messaging.browser.setTimeout(this._onTimeout, this.duration) }); + this.update({ + timeoutId: this.messaging.browser.setTimeout(this._onTimeout, this.duration), + }); }, _willDelete() { this.messaging.browser.clearTimeout(this.timeoutId); @@ -39,7 +41,8 @@ Model({ * Duration, in milliseconds, until timer times out and calls the * timeout function. */ - duration: attr({ required: true, + duration: attr({ + required: true, compute() { return clear(); }, @@ -53,7 +56,7 @@ Model({ onChanges: [ { dependencies: ["doReset"], - methodName: '_onChangeDoReset', + methodName: "_onChangeDoReset", }, ], }); diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index f60fc3f9678..20b2de3c972 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import AbstractField from 'web.AbstractField'; -import config from 'web.config'; -import core from 'web.core'; -import field_registry from 'web.field_registry'; -import session from 'web.session'; -import { sprintf } from '@web/core/utils/strings'; -import framework from 'web.framework'; -import time from 'web.time'; +import AbstractField from "web.AbstractField"; +import config from "web.config"; +import core from "web.core"; +import field_registry from "web.field_registry"; +import session from "web.session"; +import { sprintf } from "@web/core/utils/strings"; +import framework from "web.framework"; +import time from "web.time"; const QWeb = core.qweb; const _t = core._t; @@ -17,21 +17,21 @@ const _lt = core._lt; // Activities Widget for Kanban views ('kanban_activity' widget) // ----------------------------------------------------------------------------- const KanbanActivity = AbstractField.extend({ - template: 'mail.KanbanActivity', + template: "mail.KanbanActivity", events: { - 'change input.o_input_file': '_onFileChanged', - 'click .o_activity_template_preview': '_onPreviewMailTemplate', - 'click .o_activity_template_send': '_onSendMailTemplate', - 'click .o_edit_activity': '_onEditActivity', - 'click .o_mark_as_done': '_onMarkActivityDone', - 'click .o_mark_as_done_upload_file': '_onMarkActivityDoneUploadFile', - 'click .o_schedule_activity': '_onScheduleActivity', - 'show.bs.dropdown': '_onDropdownShow', + "change input.o_input_file": "_onFileChanged", + "click .o_activity_template_preview": "_onPreviewMailTemplate", + "click .o_activity_template_send": "_onSendMailTemplate", + "click .o_edit_activity": "_onEditActivity", + "click .o_mark_as_done": "_onMarkActivityDone", + "click .o_mark_as_done_upload_file": "_onMarkActivityDoneUploadFile", + "click .o_schedule_activity": "_onScheduleActivity", + "show.bs.dropdown": "_onDropdownShow", }, fieldDependencies: { - activity_exception_decoration: { type: 'selection' }, - activity_exception_icon: { type: 'char' }, - activity_state: { type: 'selection' }, + activity_exception_decoration: { type: "selection" }, + activity_exception_icon: { type: "char" }, + activity_state: { type: "selection" }, }, /** @@ -72,8 +72,8 @@ const KanbanActivity = AbstractField.extend({ const feedback = params.feedback || false; const attachmentIds = params.attachmentIds || []; await this._rpc({ - model: 'mail.activity', - method: 'action_feedback', + model: "mail.activity", + method: "action_feedback", args: [[activityID]], kwargs: { feedback: feedback, @@ -97,8 +97,8 @@ const KanbanActivity = AbstractField.extend({ const activityID = params.activityID; const feedback = params.feedback; const action = await this._rpc({ - model: 'mail.activity', - method: 'action_feedback_schedule_next', + model: "mail.activity", + method: "action_feedback_schedule_next", args: [[activityID]], kwargs: { feedback: feedback }, context: this.record.getContext(), @@ -106,11 +106,11 @@ const KanbanActivity = AbstractField.extend({ if (action) { this.do_action(action, { on_close: () => { - this.trigger_up('reload', { keepChanges: true }); + this.trigger_up("reload", { keepChanges: true }); }, }); } else { - this.trigger_up('reload', { keepChanges: true }); + this.trigger_up("reload", { keepChanges: true }); } }, /** @@ -120,9 +120,12 @@ const KanbanActivity = AbstractField.extend({ */ async _onEditActivity(ev) { ev.preventDefault(); - const activityId = $(ev.currentTarget).data('activity-id'); - const thread = this.messaging.models['Thread'].insert({ id: this.res_id, model: this.model }); - const activity = this.messaging.models['Activity'].insert({ id: activityId, thread }); + const activityId = $(ev.currentTarget).data("activity-id"); + const thread = this.messaging.models["Thread"].insert({ + id: this.res_id, + model: this.model, + }); + const activity = this.messaging.models["Activity"].insert({ id: activityId, thread }); await this.messaging.openActivityForm({ activity, defaultActivityTypeId: this.defaultActivityType, @@ -137,7 +140,7 @@ const KanbanActivity = AbstractField.extend({ _onFileChanged(ev) { ev.stopPropagation(); ev.preventDefault(); - const $form = $(ev.currentTarget).closest('form'); + const $form = $(ev.currentTarget).closest("form"); $form.submit(); framework.blockUI(); }, @@ -157,143 +160,157 @@ const KanbanActivity = AbstractField.extend({ ev.preventDefault(); const self = this; const $markDoneBtn = $(ev.currentTarget); - const activityID = $markDoneBtn.data('activity-id'); - const previousActivityTypeID = $markDoneBtn.data('previous-activity-type-id') || false; - const chainingTypeActivity = $markDoneBtn.data('chaining-type-activity'); + const activityID = $markDoneBtn.data("activity-id"); + const previousActivityTypeID = $markDoneBtn.data("previous-activity-type-id") || false; + const chainingTypeActivity = $markDoneBtn.data("chaining-type-activity"); - if ($markDoneBtn[0].dataset.bsToggle === 'collapse') { - const $actLi = $markDoneBtn.parents('.o_log_activity'); - const $panel = this.$('#o_activity_form_' + activityID); + if ($markDoneBtn[0].dataset.bsToggle === "collapse") { + const $actLi = $markDoneBtn.parents(".o_log_activity"); + const $panel = this.$("#o_activity_form_" + activityID); - if (!$panel.data('bs.collapse')) { - const $form = $(QWeb.render('mail.activity_feedback_form', { - previous_activity_type_id: previousActivityTypeID, - chaining_type: chainingTypeActivity - })); + if (!$panel.data("bs.collapse")) { + const $form = $( + QWeb.render("mail.activity_feedback_form", { + previous_activity_type_id: previousActivityTypeID, + chaining_type: chainingTypeActivity, + }) + ); $panel.append($form); this._onMarkActivityDoneActions($markDoneBtn, $form, activityID); // Close and reset any other open panels - _.each($panel.siblings('.o_activity_form'), function (el) { - if ($(el).data('bs.collapse')) { - $(el).empty().collapse('dispose').removeClass('show'); + _.each($panel.siblings(".o_activity_form"), function (el) { + if ($(el).data("bs.collapse")) { + $(el).empty().collapse("dispose").removeClass("show"); } - $(el).data('bs.collapse', false); + $(el).data("bs.collapse", false); }); // Scroll to selected activity - $markDoneBtn.parents('.o_activity_log_container').scrollTo($actLi.position().top, 100); - $panel.data('bs.collapse', true); + $markDoneBtn + .parents(".o_activity_log_container") + .scrollTo($actLi.position().top, 100); + $panel.data("bs.collapse", true); } // Empty and reset panel on close - $panel.on('hidden.bs.collapse', function () { - if ($panel.data('bs.collapse')) { - $actLi.removeClass('o_activity_selected'); - $panel.collapse('dispose'); + $panel.on("hidden.bs.collapse", function () { + if ($panel.data("bs.collapse")) { + $actLi.removeClass("o_activity_selected"); + $panel.collapse("dispose"); $panel.empty(); } - $panel.data('bs.collapse', false); + $panel.data("bs.collapse", false); }); - this.$('.o_activity_selected').removeClass('o_activity_selected'); - $actLi.toggleClass('o_activity_selected'); - $panel.collapse('toggle'); - $panel.find('#activity_feedback').focus(); - - } else if (!$markDoneBtn.data('bs.popover')) { - $markDoneBtn.popover({ - template: $(Popover.Default.template).addClass('o_mail_activity_feedback')[0].outerHTML, // Ugly but cannot find another way - container: $markDoneBtn, - title: _t("Feedback"), - html: true, - trigger: 'manual', - placement: 'right', // FIXME: this should work, maybe a bug in the popper lib - content: () => { - const $popover = $(QWeb.render('mail.activity_feedback_form', { - previous_activity_type_id: previousActivityTypeID, - chaining_type: chainingTypeActivity - })); - this._onMarkActivityDoneActions($markDoneBtn, $popover, activityID); - return $popover; - }, - }).on('shown.bs.popover', function () { - const $popover = $($(this).data("bs.popover").tip); - $(".o_mail_activity_feedback.popover").not($popover).popover("hide"); - $popover.addClass('o_mail_activity_feedback').attr('tabindex', 0); - $popover.find('#activity_feedback').focus(); - const activityID = $(this).data('activity-id'); - $popover.off('focusout'); - $popover.focusout(e => { - // outside click of popover hide the popover - // e.relatedTarget is the element receiving the focus - if (!$popover.is(e.relatedTarget) && !$popover.find(e.relatedTarget).length) { - self._draftFeedback[activityID] = $popover.find('#activity_feedback').val(); - $popover.popover('hide'); - } - }); - }).popover('show'); + this.$(".o_activity_selected").removeClass("o_activity_selected"); + $actLi.toggleClass("o_activity_selected"); + $panel.collapse("toggle"); + $panel.find("#activity_feedback").focus(); + } else if (!$markDoneBtn.data("bs.popover")) { + $markDoneBtn + .popover({ + template: $(Popover.Default.template).addClass("o_mail_activity_feedback")[0] + .outerHTML, // Ugly but cannot find another way + container: $markDoneBtn, + title: _t("Feedback"), + html: true, + trigger: "manual", + placement: "right", // FIXME: this should work, maybe a bug in the popper lib + content: () => { + const $popover = $( + QWeb.render("mail.activity_feedback_form", { + previous_activity_type_id: previousActivityTypeID, + chaining_type: chainingTypeActivity, + }) + ); + this._onMarkActivityDoneActions($markDoneBtn, $popover, activityID); + return $popover; + }, + }) + .on("shown.bs.popover", function () { + const $popover = $($(this).data("bs.popover").tip); + $(".o_mail_activity_feedback.popover").not($popover).popover("hide"); + $popover.addClass("o_mail_activity_feedback").attr("tabindex", 0); + $popover.find("#activity_feedback").focus(); + const activityID = $(this).data("activity-id"); + $popover.off("focusout"); + $popover.focusout((e) => { + // outside click of popover hide the popover + // e.relatedTarget is the element receiving the focus + if ( + !$popover.is(e.relatedTarget) && + !$popover.find(e.relatedTarget).length + ) { + self._draftFeedback[activityID] = $popover + .find("#activity_feedback") + .val(); + $popover.popover("hide"); + } + }); + }) + .popover("show"); } else { - const popover = $markDoneBtn.data('bs.popover'); - if ($('#' + popover.tip.id).length === 0) { - popover.show(); + const popover = $markDoneBtn.data("bs.popover"); + if ($("#" + popover.tip.id).length === 0) { + popover.show(); } } }, /** - * Bind all necessary actions to the 'mark as done' form - * - * @private - * @param {Object} $form - * @param {integer} activityID - */ + * Bind all necessary actions to the 'mark as done' form + * + * @private + * @param {Object} $form + * @param {integer} activityID + */ _onMarkActivityDoneActions($btn, $form, activityID) { - $form.find('#activity_feedback').val(this._draftFeedback[activityID]); - $form.on('keydown', '#activity_feedback', ev => { - if (ev.key === 'Enter') { + $form.find("#activity_feedback").val(this._draftFeedback[activityID]); + $form.on("keydown", "#activity_feedback", (ev) => { + if (ev.key === "Enter") { ev.stopPropagation(); // Prevent list view actions } }); - $form.on('click', '.o_activity_popover_done', ev => { + $form.on("click", ".o_activity_popover_done", (ev) => { ev.stopPropagation(); this._markActivityDone({ activityID: activityID, - feedback: $form.find('#activity_feedback').val(), + feedback: $form.find("#activity_feedback").val(), }); }); - $form.on('keydown', '.o_activity_popover_done', ev => { - if (ev.key === 'Enter') { + $form.on("keydown", ".o_activity_popover_done", (ev) => { + if (ev.key === "Enter") { ev.stopPropagation(); // Prevent list view actions ev.preventDefault(); this._markActivityDone({ activityID, - feedback: $form.find('#activity_feedback').val(), + feedback: $form.find("#activity_feedback").val(), }); } }); - $form.on('click', '.o_activity_popover_done_next', ev => { + $form.on("click", ".o_activity_popover_done_next", (ev) => { ev.stopPropagation(); this._markActivityDoneAndScheduleNext({ activityID: activityID, - feedback: $form.find('#activity_feedback').val(), + feedback: $form.find("#activity_feedback").val(), }); }); - $form.on('keydown', '.o_activity_popover_done_next', ev => { - if (ev.key === 'Enter') { + $form.on("keydown", ".o_activity_popover_done_next", (ev) => { + if (ev.key === "Enter") { ev.stopPropagation(); // Prevent list view actions ev.preventDefault(); this._markActivityDoneAndScheduleNext({ activityID, - feedback: $form.find('#activity_feedback').val(), + feedback: $form.find("#activity_feedback").val(), }); } }); - $form.on('click', '.o_activity_popover_discard', ev => { + $form.on("click", ".o_activity_popover_discard", (ev) => { ev.stopPropagation(); - if ($btn.data('bs.popover')) { - $btn.popover('hide'); - } else if ($btn[0].dataset.bsToggle === 'collapse') { - this.$('#o_activity_form_' + activityID).collapse('hide'); + if ($btn.data("bs.popover")) { + $btn.popover("hide"); + } else if ($btn[0].dataset.bsToggle === "collapse") { + this.$("#o_activity_form_" + activityID).collapse("hide"); } }); }, @@ -304,7 +321,7 @@ const KanbanActivity = AbstractField.extend({ _onMarkActivityDoneUploadFile(ev) { ev.preventDefault(); ev.stopPropagation(); - const fileuploadID = $(ev.currentTarget).data('fileupload-id'); + const fileuploadID = $(ev.currentTarget).data("fileupload-id"); const $input = this.$("[target='" + fileuploadID + "'] > input.o_input_file"); $input.click(); }, @@ -316,13 +333,13 @@ const KanbanActivity = AbstractField.extend({ _onPreviewMailTemplate(ev) { ev.stopPropagation(); ev.preventDefault(); - const templateID = $(ev.currentTarget).data('template-id'); + const templateID = $(ev.currentTarget).data("template-id"); const action = { - name: _t('Compose Email'), - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - views: [[false, 'form']], - target: 'new', + name: _t("Compose Email"), + type: "ir.actions.act_window", + res_model: "mail.compose.message", + views: [[false, "form"]], + target: "new", context: { default_res_id: this.res_id, default_model: this.model, @@ -333,7 +350,7 @@ const KanbanActivity = AbstractField.extend({ }; return this.do_action(action, { on_close: () => { - this.trigger_up('reload', { keepChanges: true }); + this.trigger_up("reload", { keepChanges: true }); }, }); }, @@ -344,7 +361,10 @@ const KanbanActivity = AbstractField.extend({ */ async _onScheduleActivity(ev) { ev.preventDefault(); - const thread = this.messaging.models['Thread'].insert({ id: this.res_id, model: this.model }); + const thread = this.messaging.models["Thread"].insert({ + id: this.res_id, + model: this.model, + }); await this.messaging.openActivityForm({ defaultActivityTypeId: this.defaultActivityType, thread, @@ -359,10 +379,10 @@ const KanbanActivity = AbstractField.extend({ async _onSendMailTemplate(ev) { ev.stopPropagation(); ev.preventDefault(); - const templateID = $(ev.currentTarget).data('template-id'); + const templateID = $(ev.currentTarget).data("template-id"); await this._rpc({ model: this.model, - method: 'activity_send_mail', + method: "activity_send_mail", args: [[this.res_id], templateID], }); this._reload(); @@ -371,7 +391,7 @@ const KanbanActivity = AbstractField.extend({ * @private */ _reload() { - this.trigger_up('reload', { db_id: this.record.id, keepChanges: true }); + this.trigger_up("reload", { db_id: this.record.id, keepChanges: true }); }, /** * @override @@ -380,16 +400,16 @@ const KanbanActivity = AbstractField.extend({ _render() { // span classes need to be updated manually because the template cannot // be re-rendered eaasily (because of the dropdown state) - const spanClasses = ['fa', 'fa-lg', 'fa-fw']; - spanClasses.push('o_activity_color_' + (this.record.data.activity_state || 'default')); + const spanClasses = ["fa", "fa-lg", "fa-fw"]; + spanClasses.push("o_activity_color_" + (this.record.data.activity_state || "default")); if (this.recordData.activity_exception_decoration) { - spanClasses.push('text-' + this.recordData.activity_exception_decoration); + spanClasses.push("text-" + this.recordData.activity_exception_decoration); spanClasses.push(this.recordData.activity_exception_icon); } else { - spanClasses.push('fa-clock-o'); + spanClasses.push("fa-clock-o"); } - this.$('.o_activity_btn > span').removeClass().addClass(spanClasses.join(' ')); - if (this.$el.hasClass('show')) { + this.$(".o_activity_btn > span").removeClass().addClass(spanClasses.join(" ")); + if (this.$el.hasClass("show")) { // note: this part of the rendering might be asynchronous this._renderDropdown(); } @@ -398,38 +418,42 @@ const KanbanActivity = AbstractField.extend({ * @private */ async _renderDropdown() { - this.$('.o_activity') - .toggleClass('dropdown-menu-end', config.device.isMobile) - .html(QWeb.render('mail.KanbanActivityLoading')); + this.$(".o_activity") + .toggleClass("dropdown-menu-end", config.device.isMobile) + .html(QWeb.render("mail.KanbanActivityLoading")); let context = this.getSession().user_context; if (this.record && !_.isEmpty(this.record.getContext())) { context = this.record.getContext(); } - const activities = this.value.res_ids.length ? await this._rpc({ - model: 'mail.activity', - method: 'activity_format', - args: [this.value.res_ids], - context: context, - }) : []; - const today = moment().startOf('day'); + const activities = this.value.res_ids.length + ? await this._rpc({ + model: "mail.activity", + method: "activity_format", + args: [this.value.res_ids], + context: context, + }) + : []; + const today = moment().startOf("day"); for (const activity of activities) { activity.create_date = moment(time.auto_str_to_date(activity.create_date)); activity.date_deadline = moment(time.auto_str_to_date(activity.date_deadline)); - if (activity.activity_category === 'upload_file') { - activity.fileuploadID = _.uniqueId('o_fileupload'); + if (activity.activity_category === "upload_file") { + activity.fileuploadID = _.uniqueId("o_fileupload"); } - let toDisplay = ''; - const diff = activity.date_deadline.diff(today, 'days', true); // true means no rounding + let toDisplay = ""; + const diff = activity.date_deadline.diff(today, "days", true); // true means no rounding if (diff === 0) { toDisplay = _t("Today"); } else { - if (diff < 0) { // overdue + if (diff < 0) { + // overdue if (diff === -1) { toDisplay = _t("Yesterday"); } else { toDisplay = sprintf(_t("%s days overdue"), Math.round(Math.abs(diff))); } - } else { // due + } else { + // due if (diff === 1) { toDisplay = _t("Tomorrow"); } else { @@ -439,12 +463,14 @@ const KanbanActivity = AbstractField.extend({ } activity.label_delay = toDisplay; } - const sortedActivities = _.sortBy(activities, 'date_deadline'); - this.$('.o_activity').html(QWeb.render('mail.KanbanActivityDropdown', { - records: _.groupBy(sortedActivities, 'state'), - session: session, - widget: this, - })); + const sortedActivities = _.sortBy(activities, "date_deadline"); + this.$(".o_activity").html( + QWeb.render("mail.KanbanActivityDropdown", { + records: _.groupBy(sortedActivities, "state"), + session: session, + widget: this, + }) + ); for (const activity of sortedActivities) { if (activity.fileuploadID) { $(window).on(activity.fileuploadID, async () => { @@ -453,9 +479,9 @@ const KanbanActivity = AbstractField.extend({ const files = Array.prototype.slice.call(arguments, 1); await this._markActivityDone({ activityID: activity.id, - attachmentIds: _.pluck(files, 'id') + attachmentIds: _.pluck(files, "id"), }); - this.trigger_up('reload', { keepChanges: true }); + this.trigger_up("reload", { keepChanges: true }); }); } } @@ -477,16 +503,16 @@ const KanbanActivity = AbstractField.extend({ // Activities Widget for List views ('list_activity' widget) // ----------------------------------------------------------------------------- const ListActivity = KanbanActivity.extend({ - template: 'mail.ListActivity', + template: "mail.ListActivity", events: Object.assign({}, KanbanActivity.prototype.events, { - 'click .dropdown-menu.o_activity': '_onDropdownClicked', + "click .dropdown-menu.o_activity": "_onDropdownClicked", }), fieldDependencies: _.extend({}, KanbanActivity.prototype.fieldDependencies, { - activity_summary: { type: 'char' }, - activity_type_id: { type: 'many2one', relation: 'mail.activity.type' }, - activity_type_icon: { type: 'char' }, + activity_summary: { type: "char" }, + activity_type_id: { type: "many2one", relation: "mail.activity.type" }, + activity_type_icon: { type: "char" }, }), - label: _lt('Next Activity'), + label: _lt("Next Activity"), //-------------------------------------------------------------------------- // Private @@ -501,19 +527,22 @@ const ListActivity = KanbanActivity.extend({ // set the 'special_click' prop on the activity icon to prevent from // opening the record when the user clicks on it (as it opens the // activity dropdown instead) - this.$('.o_activity_btn > span').prop('special_click', true); + this.$(".o_activity_btn > span").prop("special_click", true); if (this.value.count) { let text; if (this.recordData.activity_exception_decoration) { - text = _t('Warning'); + text = _t("Warning"); } else { - text = this.recordData.activity_summary || - this.recordData.activity_type_id.data.display_name; + text = + this.recordData.activity_summary || + this.recordData.activity_type_id.data.display_name; } - this.$('.o_activity_summary').text(text); + this.$(".o_activity_summary").text(text); } if (this.recordData.activity_type_icon) { - this.el.querySelector('.o_activity_btn > span').classList.replace('fa-clock-o', this.recordData.activity_type_icon); + this.el + .querySelector(".o_activity_btn > span") + .classList.replace("fa-clock-o", this.recordData.activity_type_icon); } }, @@ -540,7 +569,7 @@ const ListActivity = KanbanActivity.extend({ const ActivityException = AbstractField.extend({ noLabel: true, fieldDependencies: _.extend({}, AbstractField.prototype.fieldDependencies, { - activity_exception_icon: { type: 'char' }, + activity_exception_icon: { type: "char" }, }), //------------------------------------------------------------ @@ -567,14 +596,18 @@ const ActivityException = AbstractField.extend({ this.$el.empty(); if (this.value) { this.$el.attr({ - title: _t('This record has an exception activity.'), - class: "float-end mt-1 text-" + this.value + " fa " + this.recordData.activity_exception_icon + title: _t("This record has an exception activity."), + class: + "float-end mt-1 text-" + + this.value + + " fa " + + this.recordData.activity_exception_icon, }); } - } + }, }); field_registry - .add('kanban_activity', KanbanActivity) - .add('list_activity', ListActivity) - .add('activity_exception', ActivityException); + .add("kanban_activity", KanbanActivity) + .add("list_activity", ListActivity) + .add("activity_exception", ActivityException); diff --git a/addons/mail/static/src/js/basic_view.js b/addons/mail/static/src/js/basic_view.js index 45ac2503c68..75cac9d437a 100644 --- a/addons/mail/static/src/js/basic_view.js +++ b/addons/mail/static/src/js/basic_view.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import BasicView from 'web.BasicView'; +import BasicView from "web.BasicView"; -const mailWidgets = ['kanban_activity']; +const mailWidgets = ["kanban_activity"]; const chatterFields = ["message_ids", "message_follower_ids", "activity_ids"]; @@ -13,19 +13,22 @@ BasicView.include({ this._super.apply(this, arguments); this.rendererParams.hasChatter = this.hasChatter; this.rendererParams.hasAttachmentViewerFeature = this.hasAttachmentViewerFeature; - const post_refresh = this._getFieldOption('message_ids', 'post_refresh', false); - const followers_post_refresh = this._getFieldOption('message_follower_ids', 'post_refresh', false); + const post_refresh = this._getFieldOption("message_ids", "post_refresh", false); + const followers_post_refresh = this._getFieldOption( + "message_follower_ids", + "post_refresh", + false + ); this.chatterFields = { - hasActivityIds: this._hasField('activity_ids'), - hasMessageFollowerIds: this._hasField('message_follower_ids'), - hasMessageIds: this._hasField('message_ids'), - hasRecordReloadOnAttachmentsChanged: post_refresh === 'always', + hasActivityIds: this._hasField("activity_ids"), + hasMessageFollowerIds: this._hasField("message_follower_ids"), + hasMessageIds: this._hasField("message_ids"), + hasRecordReloadOnAttachmentsChanged: post_refresh === "always", hasRecordReloadOnMessagePosted: !!post_refresh, hasRecordReloadOnFollowersUpdate: !!followers_post_refresh, - isAttachmentBoxVisibleInitially: ( - this._getFieldOption('message_ids', 'open_attachments', false) || - this._getFieldOption('message_follower_ids', 'open_attachments', false) - ), + isAttachmentBoxVisibleInitially: + this._getFieldOption("message_ids", "open_attachments", false) || + this._getFieldOption("message_follower_ids", "open_attachments", false), }; const fieldsInfo = this.fieldsInfo[this.viewType]; this.rendererParams.chatterFields = this.chatterFields; @@ -82,7 +85,7 @@ BasicView.include({ const fieldsInfo = fv.fieldsInfo[viewType]; const fields = fv.viewFields; for (const child of node.children) { - if (child.tag === 'field' && chatterFields.includes(child.attrs.name)) { + if (child.tag === "field" && chatterFields.includes(child.attrs.name)) { const attrs = { ...child.attrs, modifiers: {}, __no_fetch: true }; const fieldName = attrs.name; fieldsInfo[fieldName] = this._processField(viewType, fields[fieldName], attrs); @@ -92,7 +95,7 @@ BasicView.include({ } return false; } - if (node.tag === 'div' && node.attrs.class === 'o_attachment_preview') { + if (node.tag === "div" && node.attrs.class === "o_attachment_preview") { this.hasAttachmentViewerFeature = true; } return this._super(...arguments); diff --git a/addons/mail/static/src/js/core/translation.js b/addons/mail/static/src/js/core/translation.js index c89f1a2f984..e58a44e8ee8 100644 --- a/addons/mail/static/src/js/core/translation.js +++ b/addons/mail/static/src/js/core/translation.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { TranslationDataBase } from 'web.translation'; +import { TranslationDataBase } from "web.translation"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; TranslationDataBase.include({ /** @@ -14,7 +14,7 @@ TranslationDataBase.include({ // During boot `env.services` might not even be set yet, in this // case this can safely be ignored as messaging will then load // locale information during its initialization. - Component.env.services.messaging.get().then(messaging => { + Component.env.services.messaging.get().then((messaging) => { // Update messaging locale whenever the translation bundle changes. // In particular if messaging is created before the end of the // `load_translations` RPC, the default values have to be diff --git a/addons/mail/static/src/js/custom_filter_item.js b/addons/mail/static/src/js/custom_filter_item.js index 3de2c39984a..9e82dfc9b09 100644 --- a/addons/mail/static/src/js/custom_filter_item.js +++ b/addons/mail/static/src/js/custom_filter_item.js @@ -1,19 +1,20 @@ /** @odoo-module **/ -import CustomFilterItem from 'web.CustomFilterItem'; -import { patch } from 'web.utils'; - -patch(CustomFilterItem.prototype, 'mail.CustomFilterItem', { +import CustomFilterItem from "web.CustomFilterItem"; +import { patch } from "web.utils"; +patch(CustomFilterItem.prototype, "mail.CustomFilterItem", { /** * With the `mail` module installed, we want to filter out some of the * available fields in 'Add custom filter' menu (@see CustomFilterItem). * @override */ _validateField(field) { - return this._super(...arguments) && - field.relation !== 'mail.message' && - field.name !== 'message_ids'; + return ( + this._super(...arguments) && + field.relation !== "mail.message" && + field.name !== "message_ids" + ); }, }); diff --git a/addons/mail/static/src/js/emojis.js b/addons/mail/static/src/js/emojis.js index 6d5c8c22243..6495173cd4c 100644 --- a/addons/mail/static/src/js/emojis.js +++ b/addons/mail/static/src/js/emojis.js @@ -20,116 +20,116 @@ * representation in the system. */ const data = { - ":)": "😊", - ":-)": "😊", // alternative (alt.) - "=)": "😊", // alt. - ":]": "😊", // alt. - ":D": "😃", - ":-D": "😃", // alt. - "=D": "😃", // alt. - "xD": "😆", - "XD": "😆", // alt. - "x'D": "😂", - ";)": "😉", - ";-)": "😉", // alt. - "B)": "😎", - "8)": "😎", // alt. - "B-)": "😎", // alt. - "8-)": "😎", // alt. - ";p": "😜", - ";P": "😜", // alt. - ":p": "😋", - ":P": "😋", // alt. - ":-p": "😋", // alt. - ":-P": "😋", // alt. - "=P": "😋", // alt. - "xp": "😝", - "xP": "😝", // alt. - "o_o": "😳", - ":|": "😐", - ":-|": "😐", // alt. - ":/": "😕", // alt. - ":-/": "😕", // alt. - ":(": "😞", - ":@": "😱", - ":O": "😲", - ":-O": "😲", // alt. - ":o": "😲", // alt. - ":-o": "😲", // alt. - ":'o": "😨", - "3:(": "😠", - ">:(": "😠", // alt. - "3:)": "😈", - ">:)": "😈", // alt. - ":*": "😘", - ":-*": "😘", // alt. - "o:)": "😇", - ":'(": "😢", - ":'-(": "😭", - ":\"(": "😭", // alt. - "<3": "❤️", - "<3": "❤️", - ":heart": "❤️", // alt. - ":(": "😠", // alt. + "3:)": "😈", + ">:)": "😈", // alt. + ":*": "😘", + ":-*": "😘", // alt. + "o:)": "😇", + ":'(": "😢", + ":'-(": "😭", + ':"(': "😭", // alt. + "<3": "❤️", + "<3": "❤️", + ":heart": "❤️", // alt. + " { - new Dropdown(this.toggleRef.el, { - popperConfig: { placement: 'bottom-end', strategy: 'fixed' }, + new window.Dropdown(this.toggleRef.el, { + popperConfig: { placement: "bottom-end", strategy: "fixed" }, }); }); } -}; -EmojisDropdown.template = 'mail.EmojisDropdown'; +} +EmojisDropdown.template = "mail.EmojisDropdown"; diff --git a/addons/mail/static/src/js/emojis_mixin.js b/addons/mail/static/src/js/emojis_mixin.js index b7d4a466d86..adafdf7cec1 100644 --- a/addons/mail/static/src/js/emojis_mixin.js +++ b/addons/mail/static/src/js/emojis_mixin.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import { escape } from '@web/core/utils/strings'; -import emojis from '@mail/js/emojis'; +import { escape } from "@web/core/utils/strings"; +import emojis from "@mail/js/emojis"; /** * This mixin gathers a few methods that are used to handle emojis. @@ -33,9 +33,15 @@ export default { const textInput = this._getTargetTextElement($(ev.currentTarget)); const selectionStart = textInput.selectionStart; - textInput.value = textInput.value.slice(0, selectionStart) + unicode + textInput.value.slice(selectionStart); + textInput.value = + textInput.value.slice(0, selectionStart) + + unicode + + textInput.value.slice(selectionStart); textInput.focus(); - textInput.setSelectionRange(selectionStart + unicode.length, selectionStart + unicode.length); + textInput.setSelectionRange( + selectionStart + unicode.length, + selectionStart + unicode.length + ); }, //-------------------------------------------------------------------------- @@ -52,7 +58,7 @@ export default { _formatText(message) { message = escape(message); message = this._wrapEmojis(message); - message = message.replace(/(?:\r\n|\r|\n)/g, '
'); + message = message.replace(/(?:\r\n|\r|\n)/g, "
"); return message; }, @@ -66,11 +72,11 @@ export default { _wrapEmojis(message) { emojis.forEach(function (emoji) { message = message.replace( - new RegExp(emoji.unicode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), - '' + emoji.unicode + '' + new RegExp(emoji.unicode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), + '' + emoji.unicode + "" ); }); return message; - } + }, }; diff --git a/addons/mail/static/src/js/field_char.js b/addons/mail/static/src/js/field_char.js index d176eae6e57..2383f2d26bc 100644 --- a/addons/mail/static/src/js/field_char.js +++ b/addons/mail/static/src/js/field_char.js @@ -1,10 +1,9 @@ /** @odoo-module **/ import { debounce } from "@web/core/utils/timing"; -import { FieldChar } from 'web.basic_fields'; +import { FieldChar } from "web.basic_fields"; FieldChar.include({ - //-------------------------------------------------------------------------- // Public //------------------------------------------------------------------------- @@ -20,10 +19,12 @@ FieldChar.include({ if (this.nodeOptions.keydown_debounce_delay === undefined) { this.nodeOptions.keydown_debounce_delay = 2000; } - this._triggerOnchange = debounce(this._triggerOnchange, this.nodeOptions.keydown_debounce_delay); + this._triggerOnchange = debounce( + this._triggerOnchange, + this.nodeOptions.keydown_debounce_delay + ); }, - //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -55,6 +56,6 @@ FieldChar.include({ * @private */ _triggerOnchange: function () { - this.$input.trigger('change'); + this.$input.trigger("change"); }, }); diff --git a/addons/mail/static/src/js/m2x_avatar_user.js b/addons/mail/static/src/js/m2x_avatar_user.js index 557b420a72c..82820d7c76d 100644 --- a/addons/mail/static/src/js/m2x_avatar_user.js +++ b/addons/mail/static/src/js/m2x_avatar_user.js @@ -1,12 +1,16 @@ /** @odoo-module **/ -import core from 'web.core'; -import fieldRegistry from 'web.field_registry'; -import { FieldMany2ManyTagsAvatar, KanbanMany2ManyTagsAvatar, ListMany2ManyTagsAvatar, Many2OneAvatar } from 'web.relational_fields'; -import session from 'web.session'; - -import { Component } from '@odoo/owl'; +import core from "web.core"; +import fieldRegistry from "web.field_registry"; +import { + FieldMany2ManyTagsAvatar, + KanbanMany2ManyTagsAvatar, + ListMany2ManyTagsAvatar, + Many2OneAvatar, +} from "web.relational_fields"; +import session from "web.session"; +import { Component } from "@odoo/owl"; // This module defines extensions of the Many2OneAvatar and Many2ManyAvatar // widgets, which are integrated with the messaging system. They are designed @@ -23,14 +27,18 @@ import { Component } from '@odoo/owl'; // models than 'res.users'. const M2XAvatarMixin = { - supportedModels: ['res.users'], + supportedModels: ["res.users"], init() { this._super(...arguments); if (!this.supportedModels.includes(this.field.relation)) { - throw new Error(`This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify(this.supportedModels)}`); + throw new Error( + `This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify( + this.supportedModels + )}` + ); } - this.className = `${this.className || ''} o_clickable_m2x_avatar`.trim(); + this.className = `${this.className || ""} o_clickable_m2x_avatar`.trim(); }, //-------------------------------------------------------------------------- @@ -51,14 +59,14 @@ const M2XAvatarMixin = { export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, { events: Object.assign({}, Many2OneAvatar.prototype.events, { - 'click .o_m2o_avatar > img': '_onAvatarClicked', + "click .o_m2o_avatar > img": "_onAvatarClicked", }), on_attach_callback() { - this._registerCommandAssignTo() + this._registerCommandAssignTo(); }, - on_detach_callback(){ - this._unregisterCommandAssignTo() + on_detach_callback() { + this._unregisterCommandAssignTo(); }, //-------------------------------------------------------------------------- @@ -69,24 +77,24 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, { * @override * @private */ - _registerCommandAssignTo() { + _registerCommandAssignTo() { const self = this; if (self.viewType === "form") { - let provide = async (env, options) => { + const provide = async (env, options) => { if (self.isDestroyed()) { - return + return; } - const records = await self._searchAssignTo(options.searchValue, 10) + const records = await self._searchAssignTo(options.searchValue, 10); return records.map((record) => ({ name: record[1], action: () => { if (self.isDestroyed()) { - return + return; } self.reinitialize({ id: record[0], display_name: record[1] }); - } - })) - } + }, + })); + }; let getCommandDefinition = (env) => ({ name: env._t("Assign to ..."), options: { @@ -106,8 +114,12 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, { }; }, }); - core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignTo", getCommandDefinition, self.el); - + core.bus.trigger( + "set_legacy_command", + "web.Many2OneAvatar.assignTo", + getCommandDefinition, + self.el + ); getCommandDefinition = (env) => ({ name: env._t("Assign/unassign to me"), @@ -118,19 +130,23 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, { }, action() { if (self.isDestroyed()) { - return + return; } if (self.value.res_id === session.user_id[0]) { self._setValue({ - operation: 'DELETE', + operation: "DELETE", ids: [session.user_id[0]], - }); + }); } else { self.reinitialize({ id: session.user_id[0], display_name: session.name }); } }, }); - core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignToMe", getCommandDefinition); + core.bus.trigger( + "set_legacy_command", + "web.Many2OneAvatar.assignToMe", + getCommandDefinition + ); } }, @@ -152,13 +168,13 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, { const domain = this.record.getDomain(this.recordParams); const context = Object.assign( this.record.getContext(this.recordParams), - this.additionalContext, + this.additionalContext ); // Exclude black-listed ids from the domain const blackListedIds = this._getSearchBlacklist(); if (blackListedIds.length) { - domain.push(['id', 'not in', blackListedIds]); + domain.push(["id", "not in", blackListedIds]); } const nameSearch = this._rpc({ @@ -170,7 +186,7 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, { operator: "ilike", limit: limit + 1, context, - } + }, }); return this.orderer.add(nameSearch); }, @@ -188,11 +204,11 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, { _onAvatarClicked(ev) { ev.stopPropagation(); // in list view, prevent from opening the record this._openChat({ userId: this.value.res_id }); - } + }, }); export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({ - _template: 'mail.KanbanMany2OneAvatarUser', + _template: "mail.KanbanMany2OneAvatarUser", init() { this._super(...arguments); @@ -202,7 +218,7 @@ export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({ const M2MAvatarMixin = Object.assign(M2XAvatarMixin, { events: Object.assign({}, FieldMany2ManyTagsAvatar.prototype.events, { - 'click .o_m2m_avatar': '_onAvatarClicked', + "click .o_m2m_avatar": "_onAvatarClicked", }), //---------------------------------------------------------------------- @@ -215,18 +231,17 @@ const M2MAvatarMixin = Object.assign(M2XAvatarMixin, { */ _onAvatarClicked(ev) { ev.stopPropagation(); // in list view, prevent from opening the record - const userId = parseInt(ev.target.getAttribute('data-id'), 10); + const userId = parseInt(ev.target.getAttribute("data-id"), 10); this._openChat({ userId: userId }); }, }); export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixin, { - on_attach_callback() { - this._registerCommandAssignTo() + this._registerCommandAssignTo(); }, - on_detach_callback(){ - this._unregisterCommandAssignTo() + on_detach_callback() { + this._unregisterCommandAssignTo(); }, //-------------------------------------------------------------------------- @@ -237,15 +252,15 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi * @override * @private */ - _registerCommandAssignTo() { + _registerCommandAssignTo() { const self = this; if (self.viewType === "form") { const provide = async (env, options) => { if (self.isDestroyed()) { - return + return; } const many2one = new Many2OneAvatarUser(self, self.name, self.record, { - mode: 'edit', + mode: "edit", noOpen: true, noCreate: !self.canCreate, viewType: self.viewType, @@ -254,19 +269,19 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi many2one._getSearchBlacklist = function () { return self.value.res_ids; }; - const records = await many2one._searchAssignTo(options.searchValue, 10) + const records = await many2one._searchAssignTo(options.searchValue, 10); return records.map((record) => ({ name: record[1], action: () => { if (self.isDestroyed()) { - return + return; } self._setValue({ - operation: 'ADD_M2M', - ids: { id: record[0], display_name: record[1] } + operation: "ADD_M2M", + ids: { id: record[0], display_name: record[1] }, }); }, - })) + })); }; let getCommandDefinition = (env) => ({ name: env._t("Assign to ..."), @@ -287,7 +302,11 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi }; }, }); - core.bus.trigger("set_legacy_command", "web.FieldMany2ManyTagsAvatar.assignTo", getCommandDefinition) + core.bus.trigger( + "set_legacy_command", + "web.FieldMany2ManyTagsAvatar.assignTo", + getCommandDefinition + ); getCommandDefinition = (env) => ({ name: env._t("Assign/unassign to me"), @@ -298,22 +317,26 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi }, action() { if (self.isDestroyed()) { - return + return; } if (self.value.res_ids.includes(session.user_id[0])) { self._setValue({ - operation: 'DELETE', + operation: "DELETE", ids: [session.user_id[0]], - }); + }); } else { self._setValue({ - operation: 'ADD_M2M', - ids: { id: session.user_id[0], display_name: session.name } + operation: "ADD_M2M", + ids: { id: session.user_id[0], display_name: session.name }, }); } }, }); - core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignToMe", getCommandDefinition); + core.bus.trigger( + "set_legacy_command", + "web.Many2OneAvatar.assignToMe", + getCommandDefinition + ); } }, @@ -321,19 +344,19 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi * @override * @private */ - _unregisterCommandAssignTo() { + _unregisterCommandAssignTo() { core.bus.trigger("remove_legacy_command", "web.FieldMany2ManyTagsAvatar.assignTo"); core.bus.trigger("remove_legacy_command", "web.Many2OneAvatar.assignToMe"); - } + }, }); export const KanbanMany2ManyAvatarUser = KanbanMany2ManyTagsAvatar.extend(M2MAvatarMixin, {}); export const ListMany2ManyAvatarUser = ListMany2ManyTagsAvatar.extend(M2MAvatarMixin, {}); -fieldRegistry.add('many2one_avatar_user', Many2OneAvatarUser); -fieldRegistry.add('kanban.many2one_avatar_user', KanbanMany2OneAvatarUser); -fieldRegistry.add('activity.many2one_avatar_user', KanbanMany2OneAvatarUser); -fieldRegistry.add('many2many_avatar_user', Many2ManyAvatarUser); -fieldRegistry.add('kanban.many2many_avatar_user', KanbanMany2ManyAvatarUser); -fieldRegistry.add('list.many2many_avatar_user', ListMany2ManyAvatarUser); +fieldRegistry.add("many2one_avatar_user", Many2OneAvatarUser); +fieldRegistry.add("kanban.many2one_avatar_user", KanbanMany2OneAvatarUser); +fieldRegistry.add("activity.many2one_avatar_user", KanbanMany2OneAvatarUser); +fieldRegistry.add("many2many_avatar_user", Many2ManyAvatarUser); +fieldRegistry.add("kanban.many2many_avatar_user", KanbanMany2ManyAvatarUser); +fieldRegistry.add("list.many2many_avatar_user", ListMany2ManyAvatarUser); diff --git a/addons/mail/static/src/js/tours/mail.js b/addons/mail/static/src/js/tours/mail.js index 71bcae85235..a7e0b1ba6d7 100644 --- a/addons/mail/static/src/js/tours/mail.js +++ b/addons/mail/static/src/js/tours/mail.js @@ -1,59 +1,89 @@ /** @odoo-module **/ -import { _t } from 'web.core'; -import { Markup } from 'web.utils'; -import tour from 'web_tour.tour'; +import { _t } from "web.core"; +import { Markup } from "web.utils"; +import tour from "web_tour.tour"; -tour.register('mail_tour', { - url: "/web#action=mail.action_discuss", - sequence: 80, -}, [{ - trigger: '.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd', - content: Markup(_t("

Channels make it easy to organize information across different topics and groups.

Try to create your first channel (e.g. sales, marketing, product XYZ, after work party, etc).

")), - position: 'bottom', -}, { - trigger: '.o_DiscussSidebarCategory_addingItemInput', - content: Markup(_t("

Create a channel here.

")), - position: 'bottom', - auto: true, - run: function (actions) { - var t = new Date().getTime(); - actions.text("SomeChannel_" + t, this.$anchor); +tour.register( + "mail_tour", + { + url: "/web#action=mail.action_discuss", + sequence: 80, }, -}, { - trigger: ".o_DiscussSidebarCategory_newChannelAutocompleteSuggestions", - content: Markup(_t("

Create a public or private channel.

")), - position: 'right', - run() { - this.$consumeEventAnchors.find('li:first').click(); - }, -}, { - trigger: '.o_DiscussView_thread .o_ComposerTextInputView_textarea', - content: Markup(_t("

Write a message to the members of the channel here.

You can notify someone with '@' or link another channel with '#'. Start your message with '/' to get the list of possible commands.

")), - position: "top", - width: 350, - run: function (actions) { - var t = new Date().getTime(); - actions.text("SomeText_" + t, this.$anchor); - }, -}, { - trigger: '.o_DiscussView_thread .o_ComposerView_buttonSend', - content: _t("Post your message on the thread"), - position: "top", -}, { - trigger: '.o_DiscussView_thread .o_MessageView', - content: _t("Click on your message"), - position: "top", -}, { - trigger: '.o_DiscussView_thread .o_MessageActionView_actionToggleStar', - content: Markup(_t("Messages can be starred to remind you to check back later.")), - position: "bottom", -}, { - trigger: '.o_DiscussSidebarMailboxView.o-starred-box', - content: _t("Once a message has been starred, you can come back and review it at any time here."), - position: "bottom", -}, { - trigger: '.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_commandAdd', - content: Markup(_t("

Chat with coworkers in real-time using direct messages.

You might need to invite users from the Settings app first.

")), - position: 'bottom', -}]); + [ + { + trigger: ".o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd", + content: Markup( + _t( + "

Channels make it easy to organize information across different topics and groups.

Try to create your first channel (e.g. sales, marketing, product XYZ, after work party, etc).

" + ) + ), + position: "bottom", + }, + { + trigger: ".o_DiscussSidebarCategory_addingItemInput", + content: Markup(_t("

Create a channel here.

")), + position: "bottom", + auto: true, + run: function (actions) { + var t = new Date().getTime(); + actions.text("SomeChannel_" + t, this.$anchor); + }, + }, + { + trigger: ".o_DiscussSidebarCategory_newChannelAutocompleteSuggestions", + content: Markup(_t("

Create a public or private channel.

")), + position: "right", + run() { + this.$consumeEventAnchors.find("li:first").click(); + }, + }, + { + trigger: ".o_DiscussView_thread .o_ComposerTextInputView_textarea", + content: Markup( + _t( + "

Write a message to the members of the channel here.

You can notify someone with '@' or link another channel with '#'. Start your message with '/' to get the list of possible commands.

" + ) + ), + position: "top", + width: 350, + run: function (actions) { + var t = new Date().getTime(); + actions.text("SomeText_" + t, this.$anchor); + }, + }, + { + trigger: ".o_DiscussView_thread .o_ComposerView_buttonSend", + content: _t("Post your message on the thread"), + position: "top", + }, + { + trigger: ".o_DiscussView_thread .o_MessageView", + content: _t("Click on your message"), + position: "top", + }, + { + trigger: ".o_DiscussView_thread .o_MessageActionView_actionToggleStar", + content: Markup( + _t("Messages can be starred to remind you to check back later.") + ), + position: "bottom", + }, + { + trigger: ".o_DiscussSidebarMailboxView.o-starred-box", + content: _t( + "Once a message has been starred, you can come back and review it at any time here." + ), + position: "bottom", + }, + { + trigger: ".o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_commandAdd", + content: Markup( + _t( + "

Chat with coworkers in real-time using direct messages.

You might need to invite users from the Settings app first.

" + ) + ), + position: "bottom", + }, + ] +); diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js index b66ae7e8ff3..42d1778884f 100644 --- a/addons/mail/static/src/js/utils.js +++ b/addons/mail/static/src/js/utils.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import core from 'web.core'; -import { escape } from '@web/core/utils/strings'; +import core from "web.core"; +import { escape } from "@web/core/utils/strings"; var _t = core._t; @@ -19,12 +19,16 @@ function parseAndTransform(htmlString, transformFunction) { var string = htmlString.replace(/</g, openToken); var children; try { - children = $('
').html(string).contents(); + children = $("
").html(string).contents(); } catch { - children = $('
').html('
' + string + '
').contents(); + children = $("
") + .html("
" + string + "
") + .contents(); } - return _parseAndTransform(children, transformFunction) - .replace(new RegExp(openToken, "g"), "<"); + return _parseAndTransform(children, transformFunction).replace( + new RegExp(openToken, "g"), + "<" + ); } /** @@ -55,25 +59,26 @@ var urlRegexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\. function linkify(text, attrs) { attrs = attrs || {}; if (attrs.target === undefined) { - attrs.target = '_blank'; + attrs.target = "_blank"; } - if (attrs.target === '_blank') { - attrs.rel = 'noreferrer noopener'; + if (attrs.target === "_blank") { + attrs.rel = "noreferrer noopener"; } attrs = _.map(attrs, function (value, key) { return key + '="' + _.escape(value) + '"'; - }).join(' '); + }).join(" "); return text.replace(urlRegexp, function (url) { - var href = (!/^https?:\/\//i.test(url)) ? "http://" + url : url; - return '' + url + ''; + var href = !/^https?:\/\//i.test(url) ? "http://" + url : url; + return "' + url + ""; }); } function addLink(node, transformChildren) { - if (node.nodeType === 3) { // text node + if (node.nodeType === 3) { + // text node const linkified = linkify(node.data); if (linkified !== node.data) { - const div = document.createElement('div'); + const div = document.createElement("div"); div.innerHTML = linkified; for (const childNode of [...div.childNodes]) { node.parentNode.insertBefore(childNode, node); @@ -83,7 +88,9 @@ function addLink(node, transformChildren) { } return node.textContent; } - if (node.tagName === "A") return node.outerHTML; + if (node.tagName === "A") { + return node.outerHTML; + } transformChildren(); return node.outerHTML; } @@ -94,39 +101,50 @@ function addLink(node, transformChildren) { */ function htmlToTextContentInline(htmlString) { const fragment = document.createDocumentFragment(); - const div = document.createElement('div'); + const div = document.createElement("div"); fragment.appendChild(div); - htmlString = htmlString.replace(//gi,' '); + htmlString = htmlString.replace(//gi, " "); try { div.innerHTML = htmlString; } catch { div.innerHTML = `
${htmlString}
`; } - return div - .textContent + return div.textContent .trim() - .replace(/[\n\r]/g, '') - .replace(/\s\s+/g, ' '); + .replace(/[\n\r]/g, "") + .replace(/\s\s+/g, " "); } function stripHTML(node, transformChildren) { - if (node.nodeType === 3) return node.data; // text node - if (node.tagName === "BR") return "\n"; + if (node.nodeType === 3) { + return node.data; + } // text node + if (node.tagName === "BR") { + return "\n"; + } return transformChildren(); } function inline(node, transform_children) { - if (node.nodeType === 3) return node.data; - if (node.nodeType === 8) return ""; - if (node.tagName === "BR") return " "; - if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children(); + if (node.nodeType === 3) { + return node.data; + } + if (node.nodeType === 8) { + return ""; + } + if (node.tagName === "BR") { + return " "; + } + if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) { + return transform_children(); + } node.innerHTML = transform_children(); return node.outerHTML; } // Parses text to find email: Tagada -> [Tagada, address@mail.fr] or False function parseEmail(text) { - if (text){ + if (text) { var result = text.match(/(.*)<(.*@.*)>/); if (result) { return [_.str.trim(result[1]), _.str.trim(result[2])]; @@ -148,11 +166,11 @@ function parseEmail(text) { function escapeAndCompactTextContent(content) { //Removing unwanted extra spaces from message let value = escape(content).trim(); - value = value.replace(/(\r|\n){2,}/g, '

'); - value = value.replace(/(\r|\n)/g, '
'); + value = value.replace(/(\r|\n){2,}/g, "

"); + value = value.replace(/(\r|\n)/g, "
"); // prevent html space collapsing - value = value.replace(/ /g, ' ').replace(/([^>]) ([^<])/g, '$1 $2'); + value = value.replace(/ /g, " ").replace(/([^>]) ([^<])/g, "$1 $2"); return value; } @@ -160,12 +178,12 @@ function escapeAndCompactTextContent(content) { // TDE note : should be done server-side, in Python -> use mail.compose.message ? function getTextToHTML(text) { return text - .replace(/((?:https?|ftp):\/\/[\S]+)/g,'$1 ') - .replace(/[\n\r]/g,'
'); + .replace(/((?:https?|ftp):\/\/[\S]+)/g, '$1 ') + .replace(/[\n\r]/g, "
"); } function timeFromNow(date) { - if (moment().diff(date, 'seconds') < 45) { + if (moment().diff(date, "seconds") < 45) { return _t("now"); } return date.fromNow(); diff --git a/addons/mail/static/src/js/views/activity/activity_controller.js b/addons/mail/static/src/js/views/activity/activity_controller.js index 903549a5802..a7a1cae5b00 100644 --- a/addons/mail/static/src/js/views/activity/activity_controller.js +++ b/addons/mail/static/src/js/views/activity/activity_controller.js @@ -1,22 +1,22 @@ /** @odoo-module **/ -import '@mail/js/activity'; +import "@mail/js/activity"; -import BasicController from 'web.BasicController'; -import core from 'web.core'; -import { sprintf } from '@web/core/utils/strings'; +import BasicController from "web.BasicController"; +import core from "web.core"; +import { sprintf } from "@web/core/utils/strings"; -import { SelectCreateDialog } from '@web/views/view_dialogs/select_create_dialog'; +import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; var _t = core._t; var ActivityController = BasicController.extend({ custom_events: _.extend({}, BasicController.prototype.custom_events, { - empty_cell_clicked: '_onEmptyCell', - reload_data: '_onReloadData', - send_mail_template: '_onSendMailTemplate', - schedule_activity: '_onScheduleActivity', + empty_cell_clicked: "_onEmptyCell", + reload_data: "_onReloadData", + send_mail_template: "_onSendMailTemplate", + schedule_activity: "_onScheduleActivity", }), //-------------------------------------------------------------------------- @@ -58,7 +58,7 @@ var ActivityController = BasicController.extend({ * @private */ _onReloadData() { - this.trigger_up('reload'); + this.trigger_up("reload"); }, /** * @private @@ -73,11 +73,14 @@ var ActivityController = BasicController.extend({ noCreate: !this.activeActions.create, multiSelect: false, context: state.context, - onSelected: async resIds => { + onSelected: async (resIds) => { const messaging = await owl.Component.env.services.messaging.get(); - const thread = messaging.models['Thread'].insert({ id: resIds[0], model: this.model.modelName }); + const thread = messaging.models["Thread"].insert({ + id: resIds[0], + model: this.model.modelName, + }); await messaging.openActivityForm({ thread }); - this.trigger_up('reload'); + this.trigger_up("reload"); }, }); }, @@ -87,22 +90,25 @@ var ActivityController = BasicController.extend({ */ _onEmptyCell: function (ev) { var state = this.model.get(this.handle); - this.do_action({ - type: 'ir.actions.act_window', - res_model: 'mail.activity', - view_mode: 'form', - view_type: 'form', - views: [[false, 'form']], - target: 'new', - context: { - default_res_id: ev.data.resId, - default_res_model: state.model, - default_activity_type_id: ev.data.activityTypeId, + this.do_action( + { + type: "ir.actions.act_window", + res_model: "mail.activity", + view_mode: "form", + view_type: "form", + views: [[false, "form"]], + target: "new", + context: { + default_res_id: ev.data.resId, + default_res_model: state.model, + default_activity_type_id: ev.data.activityTypeId, + }, + res_id: false, }, - res_id: false, - }, { - on_close: this.reload.bind(this), - }); + { + on_close: this.reload.bind(this), + } + ); }, /** * @private @@ -123,7 +129,7 @@ var ActivityController = BasicController.extend({ }); this._rpc({ model: this.model.modelName, - method: 'activity_send_mail', + method: "activity_send_mail", args: [resIDS, templateID], }); }, diff --git a/addons/mail/static/src/js/views/activity/activity_model.js b/addons/mail/static/src/js/views/activity/activity_model.js index 8b07fa48b2e..ad02489e756 100644 --- a/addons/mail/static/src/js/views/activity/activity_model.js +++ b/addons/mail/static/src/js/views/activity/activity_model.js @@ -1,10 +1,9 @@ /** @odoo-module **/ -import BasicModel from 'web.BasicModel'; -import session from 'web.session'; +import BasicModel from "web.BasicModel"; +import session from "web.session"; const ActivityModel = BasicModel.extend({ - //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- @@ -19,7 +18,7 @@ const ActivityModel = BasicModel.extend({ */ __get: function () { var result = this._super.apply(this, arguments); - if (result && result.model === this.modelName && result.type === 'list') { + if (result && result.model === this.modelName && result.type === "list") { _.extend(result, this.additionalData); } return result; @@ -30,7 +29,7 @@ const ActivityModel = BasicModel.extend({ */ __load: function (params) { this.originalDomain = _.extend([], params.domain); - params.domain.push(['activity_ids', '!=', false]); + params.domain.push(["activity_ids", "!=", false]); this.domain = params.domain; this.modelName = params.modelName; params.groupedBy = []; @@ -44,12 +43,12 @@ const ActivityModel = BasicModel.extend({ * @param {Array[]} [params.domain] */ __reload: function (handle, params) { - if (params && 'domain' in params) { + if (params && "domain" in params) { this.originalDomain = _.extend([], params.domain); - params.domain.push(['activity_ids', '!=', false]); + params.domain.push(["activity_ids", "!=", false]); this.domain = params.domain; } - if (params && 'groupBy' in params) { + if (params && "groupBy" in params) { params.groupBy = []; } var def = this._super.apply(this, arguments); @@ -72,12 +71,12 @@ const ActivityModel = BasicModel.extend({ var self = this; return this._rpc({ model: "mail.activity", - method: 'get_activity_data', + method: "get_activity_data", kwargs: { res_model: this.modelName, domain: this.domain, context: session.user_context, - } + }, }).then(function (result) { self.additionalData = result; }); diff --git a/addons/mail/static/src/js/views/activity/activity_record.js b/addons/mail/static/src/js/views/activity/activity_record.js index bd7d248085e..8bc4e9a7477 100644 --- a/addons/mail/static/src/js/views/activity/activity_record.js +++ b/addons/mail/static/src/js/views/activity/activity_record.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import KanbanRecord from 'web.KanbanRecord'; +import KanbanRecord from "web.KanbanRecord"; var ActivityRecord = KanbanRecord.extend({ /** * @override */ init: function (parent, state) { - this._super.apply(this,arguments); + this._super.apply(this, arguments); this.fieldsInfo = state.fieldsInfo.activity; }, @@ -22,9 +22,9 @@ var ActivityRecord = KanbanRecord.extend({ */ _render: function () { this.defs = []; - this._replaceElement(this.qweb.render('activity-box', this.qweb_context)); - this.$el.on('click', this._onGlobalClick.bind(this)); - this.$el.addClass('o_activity_record'); + this._replaceElement(this.qweb.render("activity-box", this.qweb_context)); + this.$el.on("click", this._onGlobalClick.bind(this)); + this.$el.addClass("o_activity_record"); this._processFields(); this._setupColor(); return Promise.all(this.defs); @@ -38,7 +38,7 @@ var ActivityRecord = KanbanRecord.extend({ // attribute muted if (this.fieldsInfo[fieldName].muted) { - $el.addClass('text-muted'); + $el.addClass("text-muted"); } }, /** diff --git a/addons/mail/static/src/js/views/activity/activity_renderer.js b/addons/mail/static/src/js/views/activity/activity_renderer.js index 6b5989a6353..0526098d67a 100644 --- a/addons/mail/static/src/js/views/activity/activity_renderer.js +++ b/addons/mail/static/src/js/views/activity/activity_renderer.js @@ -1,17 +1,17 @@ /** @odoo-module **/ -import ActivityRecord from '@mail/js/views/activity/activity_record'; -import { ActivityCellViewContainer } from '@mail/backend_components/activity_cell_view/activity_cell_view_container'; +import ActivityRecord from "@mail/js/views/activity/activity_record"; +import { ActivityCellViewContainer } from "@mail/backend_components/activity_cell_view/activity_cell_view_container"; -import AbstractRendererOwl from 'web.AbstractRendererOwl'; -import core from 'web.core'; -import KanbanColumnProgressBar from 'web.KanbanColumnProgressBar'; -import { ComponentAdapter } from 'web.OwlCompatibility'; -import QWeb from 'web.QWeb'; -import session from 'web.session'; -import utils from 'web.utils'; +import AbstractRendererOwl from "web.AbstractRendererOwl"; +import core from "web.core"; +import KanbanColumnProgressBar from "web.KanbanColumnProgressBar"; +import { ComponentAdapter } from "web.OwlCompatibility"; +import QWeb from "web.QWeb"; +import session from "web.session"; +import utils from "web.utils"; -import { useState } from '@odoo/owl'; +import { useState } from "@odoo/owl"; const _t = core._t; /** @@ -20,7 +20,7 @@ const _t = core._t; */ class ActivityRecordAdapter extends ComponentAdapter { renderWidget() { - _.invoke(_.pluck(this.widget.subWidgets, '$el'), 'detach'); + _.invoke(_.pluck(this.widget.subWidgets, "$el"), "detach"); this.widget._render(); } @@ -55,11 +55,13 @@ class KanbanColumnProgressBarAdapter extends ComponentAdapter { // but we don't need to listen to them in our case. if (this.el) { if (ev.name === "set_progress_bar_state") { - this.props.onSetProgressBarState(new CustomEvent("set-progress-bar-state", { - bubbles: true, - cancelable: true, - detail: ev.data, - })); + this.props.onSetProgressBarState( + new CustomEvent("set-progress-bar-state", { + bubbles: true, + cancelable: true, + detail: ev.data, + }) + ); } super._trigger_up(ev); } @@ -69,12 +71,12 @@ class KanbanColumnProgressBarAdapter extends ComponentAdapter { class ActivityRenderer extends AbstractRendererOwl { setup() { super.setup(...arguments); - this.qweb = new QWeb(this.env.isDebug(), {_s: session.origin}); + this.qweb = new QWeb(this.env.isDebug(), { _s: session.origin }); this.qweb.add_template(utils.json_node_to_xml(this.props.templates)); this.activeFilter = useState({ state: null, activityTypeId: null, - resIds: [] + resIds: [], }); this.widgetComponents = { ActivityRecord, @@ -92,8 +94,8 @@ class ActivityRenderer extends AbstractRendererOwl { * @returns filtered resIds first then the rest. */ get activityResIds() { - const copiedActivityResIds = Array.from(this.props.activity_res_ids) - return copiedActivityResIds.sort((a, b) => this.activeFilter.resIds.includes(a) ? -1 : 0); + const copiedActivityResIds = Array.from(this.props.activity_res_ids); + return copiedActivityResIds.sort((a, b) => (this.activeFilter.resIds.includes(a) ? -1 : 0)); } /** @@ -119,7 +121,7 @@ class ActivityRenderer extends AbstractRendererOwl { getProgressBarColumnState(typeId) { const counts = { planned: 0, today: 0, overdue: 0 }; - for (let activities of Object.values(this.props.grouped_activities)) { + for (const activities of Object.values(this.props.grouped_activities)) { if (typeId in activities) { counts[activities[typeId].state] += 1; } @@ -128,17 +130,17 @@ class ActivityRenderer extends AbstractRendererOwl { count: Object.values(counts).reduce((x, y) => x + y), fields: { activity_state: { - type: 'selection', + type: "selection", selection: [ - ['planned', _t('Planned')], - ['today', _t('Today')], - ['overdue', _t('Overdue')], + ["planned", _t("Planned")], + ["today", _t("Today")], + ["overdue", _t("Overdue")], ], }, }, progressBarValues: { - field: 'activity_state', - colors: { planned: 'success', today: 'warning', overdue: 'danger' }, + field: "activity_state", + colors: { planned: "success", today: "warning", overdue: "danger" }, counts: counts, }, }; @@ -152,7 +154,7 @@ class ActivityRenderer extends AbstractRendererOwl { * @param {MouseEvent} ev */ _onEmptyCellClicked(ev) { - this.trigger('empty_cell_clicked', { + this.trigger("empty_cell_clicked", { resId: parseInt(ev.currentTarget.dataset.resId, 10), activityTypeId: parseInt(ev.currentTarget.dataset.activityTypeId, 10), }); @@ -162,7 +164,7 @@ class ActivityRenderer extends AbstractRendererOwl { * @param {MouseEvent} ev */ _onSendMailTemplateClicked(ev) { - this.trigger('send_mail_template', { + this.trigger("send_mail_template", { activityTypeID: parseInt(ev.currentTarget.dataset.activityTypeId, 10), templateID: parseInt(ev.currentTarget.dataset.templateId, 10), }); @@ -176,8 +178,11 @@ class ActivityRenderer extends AbstractRendererOwl { this.activeFilter.state = ev.detail.values.activeFilter; this.activeFilter.activityTypeId = ev.detail.columnID; this.activeFilter.resIds = Object.entries(this.props.grouped_activities) - .filter(([, resIds]) => ev.detail.columnID in resIds && - resIds[ev.detail.columnID].state === ev.detail.values.activeFilter.value) + .filter( + ([, resIds]) => + ev.detail.columnID in resIds && + resIds[ev.detail.columnID].state === ev.detail.values.activeFilter.value + ) .map(([key]) => parseInt(key)); } else { this.activeFilter.state = null; @@ -196,6 +201,6 @@ ActivityRenderer.components = { ActivityRecordAdapter, KanbanColumnProgressBarAdapter, }; -ActivityRenderer.template = 'mail.ActivityRenderer'; +ActivityRenderer.template = "mail.ActivityRenderer"; export default ActivityRenderer; diff --git a/addons/mail/static/src/js/views/activity/activity_view.js b/addons/mail/static/src/js/views/activity/activity_view.js index c631f17df8a..ab89493ce52 100644 --- a/addons/mail/static/src/js/views/activity/activity_view.js +++ b/addons/mail/static/src/js/views/activity/activity_view.js @@ -1,27 +1,27 @@ -/** @odoo-module **/; +/** @odoo-module **/ -import ActivityController from '@mail/js/views/activity/activity_controller'; -import ActivityModel from '@mail/js/views/activity/activity_model'; -import ActivityRenderer from '@mail/js/views/activity/activity_renderer'; +import ActivityController from "@mail/js/views/activity/activity_controller"; +import ActivityModel from "@mail/js/views/activity/activity_model"; +import ActivityRenderer from "@mail/js/views/activity/activity_renderer"; -import BasicView from 'web.BasicView'; -import core from 'web.core'; -import RendererWrapper from 'web.RendererWrapper'; -import view_registry from 'web.view_registry'; +import BasicView from "web.BasicView"; +import core from "web.core"; +import RendererWrapper from "web.RendererWrapper"; +import view_registry from "web.view_registry"; const _lt = core._lt; const ActivityView = BasicView.extend({ accesskey: "a", - display_name: _lt('Activity'), - icon: 'fa fa-clock-o', + display_name: _lt("Activity"), + icon: "fa fa-clock-o", config: _.extend({}, BasicView.prototype.config, { Controller: ActivityController, Model: ActivityModel, Renderer: ActivityRenderer, }), - viewType: 'activity', - searchMenuTypes: ['filter', 'favorite'], + viewType: "activity", + searchMenuTypes: ["filter", "favorite"], /** * @override @@ -31,11 +31,11 @@ const ActivityView = BasicView.extend({ const { search_view_id } = params.action || {}; this.controllerParams.searchViewId = search_view_id ? search_view_id[0] : false; - this.loadParams.type = 'list'; + this.loadParams.type = "list"; // limit makes no sense in this view as we display all records having activities this.loadParams.limit = false; - this.rendererParams.templates = _.findWhere(this.arch.children, { 'tag': 'templates' }); + this.rendererParams.templates = _.findWhere(this.arch.children, { tag: "templates" }); this.controllerParams.title = this.arch.attrs.string; }, /** @@ -48,6 +48,6 @@ const ActivityView = BasicView.extend({ }, }); -view_registry.add('activity', ActivityView); +view_registry.add("activity", ActivityView); export default ActivityView; diff --git a/addons/mail/static/src/main.js b/addons/mail/static/src/main.js index 91b025eaf31..94d4a38863a 100644 --- a/addons/mail/static/src/main.js +++ b/addons/mail/static/src/main.js @@ -1,29 +1,35 @@ /** @odoo-module **/ -import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container'; -import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container'; -import { DiscussContainer } from '@mail/components/discuss_container/discuss_container'; -import { PopoverManagerContainer } from '@mail/components/popover_manager_container/popover_manager_container'; -import { messagingService } from '@mail/services/messaging_service'; -import { systrayService } from '@mail/services/systray_service'; -import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env'; +import { ChatWindowManagerContainer } from "@mail/components/chat_window_manager_container/chat_window_manager_container"; +import { DialogManagerContainer } from "@mail/components/dialog_manager_container/dialog_manager_container"; +import { DiscussContainer } from "@mail/components/discuss_container/discuss_container"; +import { PopoverManagerContainer } from "@mail/components/popover_manager_container/popover_manager_container"; +import { messagingService } from "@mail/services/messaging_service"; +import { systrayService } from "@mail/services/systray_service"; +import { makeMessagingToLegacyEnv } from "@mail/utils/make_messaging_to_legacy_env"; -import { registry } from '@web/core/registry'; +import { registry } from "@web/core/registry"; const messagingValuesService = { start() { return {}; - } + }, }; -const serviceRegistry = registry.category('services'); -serviceRegistry.add('messaging', messagingService); -serviceRegistry.add('messagingValues', messagingValuesService); -serviceRegistry.add('systray_service', systrayService); -serviceRegistry.add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv(owl.Component.env)); +const serviceRegistry = registry.category("services"); +serviceRegistry.add("messaging", messagingService); +serviceRegistry.add("messagingValues", messagingValuesService); +serviceRegistry.add("systray_service", systrayService); +serviceRegistry.add("messaging_service_to_legacy_env", makeMessagingToLegacyEnv(owl.Component.env)); -registry.category('actions').add('mail.action_discuss', DiscussContainer); +registry.category("actions").add("mail.action_discuss", DiscussContainer); -registry.category('main_components').add('DialogManagerContainer', { Component: DialogManagerContainer }); -registry.category('main_components').add('ChatWindowManagerContainer', { Component: ChatWindowManagerContainer }); -registry.category('main_components').add('PopoverManagerContainer', { Component: PopoverManagerContainer }); +registry + .category("main_components") + .add("DialogManagerContainer", { Component: DialogManagerContainer }); +registry + .category("main_components") + .add("ChatWindowManagerContainer", { Component: ChatWindowManagerContainer }); +registry + .category("main_components") + .add("PopoverManagerContainer", { Component: PopoverManagerContainer }); diff --git a/addons/mail/static/src/model.js b/addons/mail/static/src/model.js index 0cc5d73a573..5df5df978f5 100644 --- a/addons/mail/static/src/model.js +++ b/addons/mail/static/src/model.js @@ -1,5 +1,5 @@ /** @odoo-module **/ -export * from '@mail/model/model_core'; -export * from '@mail/model/model_field'; -export * from '@mail/model/model_field_command'; +export * from "@mail/model/model_core"; +export * from "@mail/model/model_field"; +export * from "@mail/model/model_field_command"; diff --git a/addons/mail/static/src/model/model_core.js b/addons/mail/static/src/model/model_core.js index 8bc0618ac71..bee3a2adae0 100644 --- a/addons/mail/static/src/model/model_core.js +++ b/addons/mail/static/src/model/model_core.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; +import { makeDeferred } from "@mail/utils/deferred"; /** * Module that contains registry for adding new models or patching models. @@ -42,10 +42,12 @@ function addContextToErrors(func, contextMessage) { * @param {Function} componentSetup The componentSetup function to be added. */ function addComponentSetup(modelName, componentSetup) { - if (typeof componentSetup !== 'function') { - throw new Error(`Cannot add componentSetup to model ${modelName}: componentSetup must be a function (current type: "${typeof componentSetup}").`); + if (typeof componentSetup !== "function") { + throw new Error( + `Cannot add componentSetup to model ${modelName}: componentSetup must be a function (current type: "${typeof componentSetup}").` + ); } - registry.get(modelName).set('componentSetup', componentSetup); + registry.get(modelName).set("componentSetup", componentSetup); } /** @@ -60,11 +62,11 @@ function addFields(modelName, fields) { addContextToErrors(() => { assertNameIsAvailableOnRecords(fieldName, definition); }, `Cannot add field "${fieldName}" to model "${modelName}": `); - definition.get('fields').set(fieldName, field); + definition.get("fields").set(fieldName, field); } } -const validHookNames = ['_created', '_willDelete']; +const validHookNames = ["_created", "_willDelete"]; /** * Adds the provided hooks to the model specified by the `modelName`. @@ -78,10 +80,12 @@ function addLifecycleHooks(modelName, hooks) { addContextToErrors(() => { assertIsFunction(handler); if (!validHookNames.includes(name)) { - throw new Error(`unsupported hook name. Possible values: ${validHookNames.join(", ")}.`); + throw new Error( + `unsupported hook name. Possible values: ${validHookNames.join(", ")}.` + ); } }, `Cannot add lifecycle hook "${name}" to model "${modelName}": `); - definition.get('lifecycleHooks').set(name, handler); + definition.get("lifecycleHooks").set(name, handler); } } @@ -102,7 +106,7 @@ function addModelGetters(modelName, modelGetters) { assertIsFunction(getter); assertNameIsAvailableOnModel(getterName, definition); }, `Cannot add model getter "${getterName}" to model "${modelName}": `); - definition.get('modelGetters').set(getterName, getter); + definition.get("modelGetters").set(getterName, getter); } } @@ -119,7 +123,7 @@ function addModelMethods(modelName, modelMethods) { assertIsFunction(method); assertNameIsAvailableOnModel(name, definition); }, `Cannot add model method "${name}" to model "${modelName}": `); - definition.get('modelMethods').set(name, method); + definition.get("modelMethods").set(name, method); } } @@ -132,37 +136,48 @@ function addModelMethods(modelName, modelMethods) { function addOnChanges(modelName, onChanges) { addContextToErrors(() => { for (const onChange of onChanges) { - if (!Object.prototype.hasOwnProperty.call(onChange, 'dependencies')) { - throw new Error("at least one onChange definition lacks dependencies (the list of fields to be watched for changes)."); + if (!Object.prototype.hasOwnProperty.call(onChange, "dependencies")) { + throw new Error( + "at least one onChange definition lacks dependencies (the list of fields to be watched for changes)." + ); } - if (!Object.prototype.hasOwnProperty.call(onChange, 'methodName')) { - throw new Error("at least one onChange definition lacks a methodName (the name of the method to be called on change)."); + if (!Object.prototype.hasOwnProperty.call(onChange, "methodName")) { + throw new Error( + "at least one onChange definition lacks a methodName (the name of the method to be called on change)." + ); } if (!Array.isArray(onChange.dependencies)) { throw new Error("onChange dependencies must be an array of strings."); } - if (typeof onChange.methodName !== 'string') { + if (typeof onChange.methodName !== "string") { throw new Error("onChange methodName must be a string."); } - const allowedKeys = ['dependencies', 'methodName']; + const allowedKeys = ["dependencies", "methodName"]; for (const key of Object.keys(onChange)) { if (!allowedKeys.includes(key)) { - throw new Error(`unknown key "${key}" in onChange definition. Allowed keys: ${allowedKeys.join(", ")}.`); + throw new Error( + `unknown key "${key}" in onChange definition. Allowed keys: ${allowedKeys.join( + ", " + )}.` + ); } } // paths of dependencies are splitted now to avoid having to do it // each time the path is followed. const splittedDependencies = []; for (const dependency of onChange.dependencies) { - if (typeof dependency !== 'string') { + if (typeof dependency !== "string") { throw new Error("onChange dependencies must be an array of strings."); } - splittedDependencies.push(dependency.split('.')); + splittedDependencies.push(dependency.split(".")); } onChange.dependencies = splittedDependencies; } }, `Cannot add onChanges to model "${modelName}": `); - registry.get(modelName).get('onChanges').push(...onChanges); + registry + .get(modelName) + .get("onChanges") + .push(...onChanges); } /** @@ -178,7 +193,7 @@ function addRecordMethods(modelName, recordMethods) { assertIsFunction(method); assertNameIsAvailableOnRecords(name, definition); }, `Cannot add record method "${name}" to model "${modelName}": `); - definition.get('recordMethods').set(name, method); + definition.get("recordMethods").set(name, method); } } @@ -199,7 +214,7 @@ function addRecordGetters(modelName, recordGetters) { assertIsFunction(getter); assertNameIsAvailableOnRecords(getterName, definition); }, `Cannot add record getter "${getterName}" to model "${modelName}": `); - definition.get('recordGetters').set(getterName, getter); + definition.get("recordGetters").set(getterName, getter); } } @@ -210,7 +225,7 @@ function addRecordGetters(modelName, recordGetters) { * @throws {Error} when `toAssert` is not typeof function. */ function assertIsFunction(toAssert) { - if (typeof toAssert !== 'function') { + if (typeof toAssert !== "function") { throw new Error(`"${toAssert}" must be a function`); } } @@ -223,7 +238,7 @@ function assertIsFunction(toAssert) { * @throws {Error} when `name` is already used as a key on the model. */ function assertNameIsAvailableOnModel(name, modelDefinition) { - if (['modelGetters', 'modelMethods'].some(x => modelDefinition.get(x).has(name))) { + if (["modelGetters", "modelMethods"].some((x) => modelDefinition.get(x).has(name))) { throw new Error(`there is already a key with this name on the model.`); } } @@ -236,41 +251,51 @@ function assertNameIsAvailableOnModel(name, modelDefinition) { * @throws {Error} when `name` is already used as a key on the records. */ function assertNameIsAvailableOnRecords(name, modelDefinition) { - if (['fields', 'recordGetters', 'recordMethods'].some(x => modelDefinition.get(x).has(name))) { + if ( + ["fields", "recordGetters", "recordMethods"].some((x) => modelDefinition.get(x).has(name)) + ) { throw new Error(`there is already a key with this name on the records.`); } } function patchComponentSetup({ name, componentSetup }) { - const originalComponentSetup = registry.get(name).get('componentSetup'); + const originalComponentSetup = registry.get(name).get("componentSetup"); if (!originalComponentSetup) { addComponentSetup(name, componentSetup); return; } - if (typeof componentSetup !== 'function') { - throw new Error(`Cannot patch componentSetup of model ${name}: componentSetup must be a function (current type: "${typeof componentSetup}").`); + if (typeof componentSetup !== "function") { + throw new Error( + `Cannot patch componentSetup of model ${name}: componentSetup must be a function (current type: "${typeof componentSetup}").` + ); } - registry.get(name).set('componentSetup', function () { + registry.get(name).set("componentSetup", function () { this._super = originalComponentSetup; return componentSetup.call(this); - }) + }); } function patchFields(patch) { const newFieldsToAdd = Object.create(null); for (const [fieldName, fieldData] of Object.entries(patch.fields)) { - const originalFieldDefinition = registry.get(patch.name).get('fields').get(fieldName); + const originalFieldDefinition = registry.get(patch.name).get("fields").get(fieldName); if (!originalFieldDefinition) { newFieldsToAdd[fieldName] = fieldData; } else { - for (const [attributeName, attributeData] of Object.entries(fieldData)) + for (const [attributeName, attributeData] of Object.entries(fieldData)) { switch (attributeName) { - case 'compute': + case "compute": { if (!originalFieldDefinition.compute) { - throw new Error(`Cannot patch compute of field ${patch.name}/${fieldName}: the field is not a compute in the original definition.`); + throw new Error( + `Cannot patch compute of field ${patch.name}/${fieldName}: the field is not a compute in the original definition.` + ); } - if (typeof attributeData !== 'function') { - throw new Error(`Cannot patch compute of field ${patch.name}/${fieldName}: the compute must be a function (found: "${typeof attributeData}").`); + if (typeof attributeData !== "function") { + throw new Error( + `Cannot patch compute of field ${ + patch.name + }/${fieldName}: the compute must be a function (found: "${typeof attributeData}").` + ); } const computeBeforePatch = originalFieldDefinition.compute; originalFieldDefinition.compute = function () { @@ -278,38 +303,56 @@ function patchFields(patch) { return attributeData.call(this); }; break; - case 'sort': + } + case "sort": if (originalFieldDefinition.sort) { - if (typeof attributeData !== 'function') { - throw new Error(`Cannot patch sorting rules of field ${patch.name}/${fieldName}: the value of 'sort' must be a function to apply to the sorting rules array (found: "${typeof attributeData}").`); + if (typeof attributeData !== "function") { + throw new Error( + `Cannot patch sorting rules of field ${ + patch.name + }/${fieldName}: the value of 'sort' must be a function to apply to the sorting rules array (found: "${typeof attributeData}").` + ); } - originalFieldDefinition.sort = attributeData.call({ _super: originalFieldDefinition.sort }); + originalFieldDefinition.sort = attributeData.call({ + _super: originalFieldDefinition.sort, + }); } else { if (!Array.isArray(attributeData)) { - throw new Error(`Cannot add sorting rules to field ${patch.name}/${fieldName}: sorting rules must be an array.`); + throw new Error( + `Cannot add sorting rules to field ${patch.name}/${fieldName}: sorting rules must be an array.` + ); } originalFieldDefinition.sort = attributeData; } break; - case 'fieldType': - throw new Error(`Cannot patch field ${patch.name}/${fieldName}: patches do not need field type (attr, one, many).`); + case "fieldType": + throw new Error( + `Cannot patch field ${patch.name}/${fieldName}: patches do not need field type (attr, one, many).` + ); default: - throw new Error(`Cannot patch field ${patch.name}/${fieldName}: unsupported field attribute "${attributeName}".`); + throw new Error( + `Cannot patch field ${patch.name}/${fieldName}: unsupported field attribute "${attributeName}".` + ); } + } } } addFields(patch.name, newFieldsToAdd); } function patchLifecycleHooks(patch) { - const originalLifecycleHooksDefinition = registry.get(patch.name).get('lifecycleHooks'); + const originalLifecycleHooksDefinition = registry.get(patch.name).get("lifecycleHooks"); const newLifecycleHooksToAdd = Object.create(null); for (const [hookName, hookHandler] of Object.entries(patch.lifecycleHooks)) { if (!originalLifecycleHooksDefinition.has(hookName)) { newLifecycleHooksToAdd[hookName] = hookHandler; } else { - if (typeof hookHandler !== 'function') { - throw new Error(`Cannot patch hook "${hookName}" on model ${patch.name}: the hook handler must be a function (current type: "${typeof hookHandler}").`); + if (typeof hookHandler !== "function") { + throw new Error( + `Cannot patch hook "${hookName}" on model ${ + patch.name + }: the hook handler must be a function (current type: "${typeof hookHandler}").` + ); } const hookHandlerBeforePatch = originalLifecycleHooksDefinition.get(hookName); originalLifecycleHooksDefinition.set(hookName, function () { @@ -322,14 +365,18 @@ function patchLifecycleHooks(patch) { } function patchModelMethods(patch) { - const originalModelMethodsDefinition = registry.get(patch.name).get('modelMethods'); + const originalModelMethodsDefinition = registry.get(patch.name).get("modelMethods"); const newModelMethodsToAdd = Object.create(null); for (const [methodName, method] of Object.entries(patch.modelMethods)) { if (!originalModelMethodsDefinition.has(methodName)) { newModelMethodsToAdd[methodName] = method; } else { - if (typeof method !== 'function') { - throw new Error(`Cannot patch model method "${methodName}" on model ${patch.name}: the method must be a function (current type: "${typeof method}").`); + if (typeof method !== "function") { + throw new Error( + `Cannot patch model method "${methodName}" on model ${ + patch.name + }: the method must be a function (current type: "${typeof method}").` + ); } const methodBeforePatch = originalModelMethodsDefinition.get(methodName); originalModelMethodsDefinition.set(methodName, function (...args) { @@ -342,14 +389,18 @@ function patchModelMethods(patch) { } function patchRecordMethods(patch) { - const originalRecordMethods = registry.get(patch.name).get('recordMethods'); + const originalRecordMethods = registry.get(patch.name).get("recordMethods"); const newRecordMethodsToAdd = Object.create(null); for (const [methodName, method] of Object.entries(patch.recordMethods)) { if (!originalRecordMethods.has(methodName)) { newRecordMethodsToAdd[methodName] = method; } else { - if (typeof method !== 'function') { - throw new Error(`Cannot patch record method "${methodName}" on model ${patch.name}: the method must be a function (current type: "${typeof method}").`); + if (typeof method !== "function") { + throw new Error( + `Cannot patch record method "${methodName}" on model ${ + patch.name + }: the method must be a function (current type: "${typeof method}").` + ); } const methodBeforePatch = originalRecordMethods.get(methodName); originalRecordMethods.set(methodName, function (...args) { @@ -376,32 +427,65 @@ function patchRecordMethods(patch) { * @param {Object} [definition.recordMethods] * @param {string} [definition.template] */ -export function Model({ componentSetup, fields, identifyingMode = 'and', isLegacyComponent = false, lifecycleHooks, modelGetters, modelMethods, name, onChanges, recordGetters, recordMethods, template }) { +export function Model({ + componentSetup, + fields, + identifyingMode = "and", + isLegacyComponent = false, + lifecycleHooks, + modelGetters, + modelMethods, + name, + onChanges, + recordGetters, + recordMethods, + template, +}) { if (!name) { throw new Error("Model is lacking a name."); } if (registry.has(name)) { throw new Error(`Cannot register model "${name}": model has already been registered.`); } - const sectionNames = ['name', 'template', 'componentSetup', 'identifyingMode', 'isLegacyComponent', 'lifecycleHooks', 'modelMethods', 'modelGetters', 'recordMethods', 'recordGetters', 'fields', 'onChanges']; - const invalidSectionNames = Object.keys(arguments[0]).filter(x => !sectionNames.includes(x)); + const sectionNames = [ + "name", + "template", + "componentSetup", + "identifyingMode", + "isLegacyComponent", + "lifecycleHooks", + "modelMethods", + "modelGetters", + "recordMethods", + "recordGetters", + "fields", + "onChanges", + ]; + const invalidSectionNames = Object.keys(arguments[0]).filter((x) => !sectionNames.includes(x)); if (invalidSectionNames.length > 0) { - throw new Error(`Cannot register model "${name}": model definition contains unknown key(s): ${invalidSectionNames.join(", ")}`); + throw new Error( + `Cannot register model "${name}": model definition contains unknown key(s): ${invalidSectionNames.join( + ", " + )}` + ); } - registry.set(name, new Map([ - ['name', name], - ['identifyingMode', identifyingMode], - ['isLegacyComponent', isLegacyComponent], - ['lifecycleHooks', new Map()], - ['modelMethods', new Map()], - ['modelGetters', new Map()], - ['recordMethods', new Map()], - ['recordGetters', new Map()], - ['fields', new Map()], - ['onChanges', []], - ])); + registry.set( + name, + new Map([ + ["name", name], + ["identifyingMode", identifyingMode], + ["isLegacyComponent", isLegacyComponent], + ["lifecycleHooks", new Map()], + ["modelMethods", new Map()], + ["modelGetters", new Map()], + ["recordMethods", new Map()], + ["recordGetters", new Map()], + ["fields", new Map()], + ["onChanges", []], + ]) + ); if (template) { - registry.get(name).set('template', template); + registry.get(name).set("template", template); if (componentSetup) { addComponentSetup(name, componentSetup); } @@ -429,14 +513,36 @@ export function Model({ componentSetup, fields, identifyingMode = 'and', isLegac } } -export function Patch({ componentSetup, fields, lifecycleHooks, modelMethods, name, onChanges, recordMethods }) { +export function Patch({ + componentSetup, + fields, + lifecycleHooks, + modelMethods, + name, + onChanges, + recordMethods, +}) { if (!name) { throw new Error("Patch is lacking the name of the model to be patched."); } - const allowedSectionNames = ['name', 'componentSetup', 'lifecycleHooks', 'modelMethods', 'recordMethods', 'fields', 'onChanges']; - const invalidSectionNames = Object.keys(arguments['0']).filter(x => !allowedSectionNames.includes(x)); + const allowedSectionNames = [ + "name", + "componentSetup", + "lifecycleHooks", + "modelMethods", + "recordMethods", + "fields", + "onChanges", + ]; + const invalidSectionNames = Object.keys(arguments["0"]).filter( + (x) => !allowedSectionNames.includes(x) + ); if (invalidSectionNames.length > 0) { - throw new Error(`Error while registering patch for model "${name}": patch definition contains unsupported key(s): ${invalidSectionNames.join(", ")}`); + throw new Error( + `Error while registering patch for model "${name}": patch definition contains unsupported key(s): ${invalidSectionNames.join( + ", " + )}` + ); } patches.push({ name, @@ -452,13 +558,13 @@ export function Patch({ componentSetup, fields, lifecycleHooks, modelMethods, na export const patchesAppliedPromise = makeDeferred(); (async function applyPatches() { - if (document.readyState !== 'complete') { - await new Promise(resolve => { + if (document.readyState !== "complete") { + await new Promise((resolve) => { /** * Called when all JS resources are loaded. This is useful to * ensure all definitions have been parsed before applying patches. */ - window.addEventListener('load', resolve); + window.addEventListener("load", resolve); }); } /** @@ -467,11 +573,13 @@ export const patchesAppliedPromise = makeDeferred(); * therefore they should be processed *at most* asynchronously at * "Promise time". */ - await new Promise(resolve => setTimeout(resolve)); + await new Promise((resolve) => setTimeout(resolve)); for (const patch of patches) { const definition = registry.get(patch.name); if (!definition) { - throw new Error(`Cannot patch model "${patch.name}": there is no model registered under this name.`); + throw new Error( + `Cannot patch model "${patch.name}": there is no model registered under this name.` + ); } if (patch.componentSetup) { patchComponentSetup(patch); diff --git a/addons/mail/static/src/model/model_field.js b/addons/mail/static/src/model/model_field.js index 4850ec2f05f..22a5263b77d 100644 --- a/addons/mail/static/src/model/model_field.js +++ b/addons/mail/static/src/model/model_field.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import { clear, FieldCommand, link, unlink, unlinkAll } from '@mail/model/model_field_command'; -import { IS_RECORD } from '@mail/model/model_core'; +import { clear, FieldCommand, link, unlink, unlinkAll } from "@mail/model/model_field_command"; +import { IS_RECORD } from "@mail/model/model_core"; /** * Class whose instances represent field on a model. @@ -9,7 +9,6 @@ import { IS_RECORD } from '@mail/model/model_core'; * `fields` on the model. */ export class ModelField { - //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- @@ -103,7 +102,7 @@ export class ModelField { * * Empty value is systematically undefined. * null or empty string are NOT considered empty value, meaning these values meet the requirement. - */ + */ this.required = required; /** * Determines the name of the function on record that returns the @@ -154,7 +153,7 @@ export class ModelField { */ if (this.identifying) { this.readonly = true; - if (this.model.identifyingMode === 'and') { + if (this.model.identifyingMode === "and") { this.required = true; } } @@ -173,9 +172,11 @@ export class ModelField { const relatedPathSet = new Set(); for (const [compareRule, pathAsString] of sort) { relatedPathSet.add(pathAsString); - this.sort.push([compareRule, pathAsString.split('.')]); + this.sort.push([compareRule, pathAsString.split(".")]); } - this.sortedFieldSplittedPaths = [...relatedPathSet].map(pathAsString => pathAsString.split('.')); + this.sortedFieldSplittedPaths = [...relatedPathSet].map((pathAsString) => + pathAsString.split(".") + ); } } @@ -186,7 +187,7 @@ export class ModelField { * @returns {Object} */ static attr(options) { - return Object.assign({ fieldType: 'attribute' }, options); + return Object.assign({ fieldType: "attribute" }, options); } /** @@ -197,7 +198,10 @@ export class ModelField { * @returns {Object} */ static many(modelName, options) { - return ModelField._relation(modelName, Object.assign({}, options, { relationType: 'many' })); + return ModelField._relation( + modelName, + Object.assign({}, options, { relationType: "many" }) + ); } /** @@ -208,7 +212,7 @@ export class ModelField { * @returns {Object} */ static one(modelName, options) { - return ModelField._relation(modelName, Object.assign({}, options, { relationType: 'one' })); + return ModelField._relation(modelName, Object.assign({}, options, { relationType: "one" })); } /** @@ -224,7 +228,7 @@ export class ModelField { */ clear(record, options) { let hasChanged = false; - if (this.fieldType === 'relation') { + if (this.fieldType === "relation") { if (this.parseAndExecuteCommands(record, unlinkAll(), options)) { hasChanged = true; } @@ -245,9 +249,9 @@ export class ModelField { * @param {Record} record */ computeRelated(record) { - const [relationName, relatedFieldName] = this.related.split('.'); + const [relationName, relatedFieldName] = this.related.split("."); const relationField = this.model.__fieldMap.get(relationName); - if (relationField.relationType === 'many') { + if (relationField.relationType === "many") { const newVal = []; for (const otherRecord of record[relationName]) { const otherValue = otherRecord[relatedFieldName]; @@ -261,7 +265,7 @@ export class ModelField { } } } - if (this.fieldType === 'relation') { + if (this.fieldType === "relation") { return newVal; } return newVal; @@ -272,7 +276,7 @@ export class ModelField { if (newVal === undefined) { return clear(); } - if (this.fieldType === 'relation') { + if (this.fieldType === "relation") { return newVal; } return newVal; @@ -291,25 +295,38 @@ export class ModelField { return [newVal]; } else if (newVal instanceof Array && newVal[0] instanceof FieldCommand) { return newVal; - } else if (this.fieldType === 'relation') { - if (newVal instanceof Array && newVal[0] instanceof Array && ['clear', 'insert', 'insert-and-replace', 'insert-and-unlink', 'link', 'replace', 'unlink', 'unlink-all'].includes(newVal[0][0])) { + } else if (this.fieldType === "relation") { + if ( + newVal instanceof Array && + newVal[0] instanceof Array && + [ + "clear", + "insert", + "insert-and-replace", + "insert-and-unlink", + "link", + "replace", + "unlink", + "unlink-all", + ].includes(newVal[0][0]) + ) { // newVal: [['insert', ...], ...] return newVal.map(([name, value]) => new FieldCommand(name, value)); } else if (newVal instanceof Array && newVal[0] && !newVal[0][IS_RECORD]) { // newVal: [data, ...] - return [new FieldCommand('insert-and-replace', newVal)]; + return [new FieldCommand("insert-and-replace", newVal)]; } else if (newVal instanceof Array && newVal[0]) { // newVal: [record, ...] - return [new FieldCommand('replace', newVal)]; + return [new FieldCommand("replace", newVal)]; } else if (!newVal[IS_RECORD]) { // newVal: data - return [new FieldCommand('insert-and-replace', newVal)]; + return [new FieldCommand("insert-and-replace", newVal)]; } else { // newVal: record - return [new FieldCommand('replace', newVal)]; + return [new FieldCommand("replace", newVal)]; } } else { - return [new FieldCommand('set', newVal)]; + return [new FieldCommand("set", newVal)]; } } @@ -333,11 +350,11 @@ export class ModelField { * @returns {any} */ get(record) { - if (this.fieldType === 'attribute') { + if (this.fieldType === "attribute") { return this.read(record); } - if (this.fieldType === 'relation') { - if (this.relationType === 'one') { + if (this.fieldType === "relation") { + if (this.relationType === "one") { return this.read(record); } return [...this.read(record)]; @@ -375,75 +392,79 @@ export class ModelField { for (const command of commandList) { const commandName = command.name; const newVal = command.value; - if (this.fieldType === 'attribute') { + if (this.fieldType === "attribute") { switch (commandName) { - case 'clear': + case "clear": if (this.clear(record, options)) { hasChanged = true; } break; - case 'decrement': + case "decrement": if (this.decrement(record, newVal)) { hasChanged = true; } break; - case 'increment': + case "increment": if (this.increment(record, newVal)) { hasChanged = true; } break; - case 'set': + case "set": if (this._setAttribute(record, newVal)) { hasChanged = true; } break; default: - throw new Error(`Field "${this}" (${this.fieldType} type) does not support command "${commandName}"`); + throw new Error( + `Field "${this}" (${this.fieldType} type) does not support command "${commandName}"` + ); } - } else if (this.fieldType === 'relation') { + } else if (this.fieldType === "relation") { switch (commandName) { - case 'clear': + case "clear": if (this.clear(record, options)) { hasChanged = true; } break; - case 'insert': + case "insert": if (this._setRelationInsert(record, newVal, options)) { hasChanged = true; } break; - case 'insert-and-replace': + case "insert-and-replace": if (this._setRelationInsertAndReplace(record, newVal, options)) { hasChanged = true; } break; - case 'insert-and-unlink': + case "insert-and-unlink": if (this._setRelationInsertAndUnlink(record, newVal, options)) { hasChanged = true; } break; - case 'link': + case "link": if (this._setRelationLink(record, newVal, options)) { hasChanged = true; } break; - case 'replace': + case "replace": if (this._setRelationReplace(record, newVal, options)) { hasChanged = true; } break; - case 'unlink': + case "unlink": if (this._setRelationUnlink(record, newVal, options)) { hasChanged = true; } break; - case 'unlink-all': + case "unlink-all": if (this._setRelationUnlink(record, this.get(record), options)) { hasChanged = true; } break; default: - throw new Error(`Field "${this}" (${this.fieldType} type) does not support command "${commandName}"`); + throw new Error( + `Field "${this}" (${this.fieldType} type) does not support command "${commandName}"` + ); } } } @@ -478,10 +499,13 @@ export class ModelField { * @param {Object} [options] */ static _relation(modelName, options) { - return Object.assign({ - fieldType: 'relation', - to: modelName, - }, options); + return Object.assign( + { + fieldType: "relation", + to: modelName, + }, + options + ); } /** @@ -496,7 +520,7 @@ export class ModelField { * @returns {Record[]} */ _convertX2ManyValue(newValue, { hasToVerify = true } = {}) { - if (typeof newValue[Symbol.iterator] === 'function') { + if (typeof newValue[Symbol.iterator] === "function") { if (hasToVerify) { for (const value of newValue) { this._verifyRelationalValue(value); @@ -523,10 +547,10 @@ export class ModelField { _insertOtherRecord(record, data, options) { const otherModel = record.models[this.to]; const otherField = otherModel.__fieldMap.get(this.inverse); - const isMulti = typeof data[Symbol.iterator] === 'function'; + const isMulti = typeof data[Symbol.iterator] === "function"; const dataList = isMulti ? data : [data]; for (const recordData of dataList) { - if (otherField.relationType === 'one') { + if (otherField.relationType === "one") { recordData[this.inverse] = record; } else { recordData[this.inverse] = link(record); @@ -612,9 +636,9 @@ export class ModelField { */ _setRelationLink(record, newValue, options) { switch (this.relationType) { - case 'many': + case "many": return this._setRelationLinkX2Many(record, newValue, options); - case 'one': + case "one": return this._setRelationLinkX2One(record, newValue, options); } } @@ -705,7 +729,7 @@ export class ModelField { * @returns {boolean} whether the value changed for the current field */ _setRelationReplace(record, newValue, options) { - if (this.relationType === 'one') { + if (this.relationType === "one") { // for x2one replace is just link return this._setRelationLinkX2One(record, newValue, options); } @@ -771,9 +795,9 @@ export class ModelField { */ _setRelationUnlink(record, newValue, options) { switch (this.relationType) { - case 'many': + case "many": return this._setRelationUnlinkX2Many(record, newValue, options); - case 'one': + case "one": return this._setRelationUnlinkX2One(record, options); } } @@ -792,10 +816,7 @@ export class ModelField { * @returns {boolean} whether the value changed for the current field */ _setRelationUnlinkX2Many(record, newValue, { hasToUpdateInverse = true } = {}) { - const recordsToUnlink = this._convertX2ManyValue( - newValue, - { hasToVerify: false } - ); + const recordsToUnlink = this._convertX2ManyValue(newValue, { hasToVerify: false }); const otherRecords = this.read(record); let hasChanged = false; @@ -884,10 +905,14 @@ export class ModelField { */ _verifyRelationalValue(record) { if (!record) { - throw Error(`record is undefined. Did you try to link() or insert() empty value? Considering clear() instead.`); + throw Error( + `record is undefined. Did you try to link() or insert() empty value? Considering clear() instead.` + ); } if (!record[IS_RECORD]) { - throw Error(`${record} is not a record. Did you try to use link() instead of insert() with data?`); + throw Error( + `${record} is not a record. Did you try to use link() instead of insert() with data?` + ); } const otherModel = record.modelManager.models[this.to]; if (otherModel.__records.has(record)) { @@ -904,7 +929,6 @@ export class ModelField { } throw Error(`Record ${record} is not valid for relational field ${this.fieldName}.`); } - } export const attr = ModelField.attr; diff --git a/addons/mail/static/src/model/model_field_command.js b/addons/mail/static/src/model/model_field_command.js index 6922f7bd128..b7b69ba3297 100644 --- a/addons/mail/static/src/model/model_field_command.js +++ b/addons/mail/static/src/model/model_field_command.js @@ -40,7 +40,7 @@ class FieldCommand { * @returns {FieldCommand} */ function clear() { - return new FieldCommand('clear'); + return new FieldCommand("clear"); } /** @@ -52,7 +52,7 @@ function clear() { * @returns {FieldCommand} */ function create(data) { - return new FieldCommand('create', data); + return new FieldCommand("create", data); } /** @@ -64,7 +64,7 @@ function create(data) { * @returns {FieldCommand} */ function decrement(amount = 1) { - return new FieldCommand('decrement', amount); + return new FieldCommand("decrement", amount); } /** @@ -76,7 +76,7 @@ function decrement(amount = 1) { * @returns {FieldCommand} */ function increment(amount = 1) { - return new FieldCommand('increment', amount); + return new FieldCommand("increment", amount); } /** @@ -90,7 +90,7 @@ function increment(amount = 1) { * @returns {FieldCommand} */ function insert(data) { - return new FieldCommand('insert', data); + return new FieldCommand("insert", data); } /** @@ -104,7 +104,7 @@ function insert(data) { * @returns {FieldCommand} */ export function insertAndUnlink(data = {}) { - return new FieldCommand('insert-and-unlink', data); + return new FieldCommand("insert-and-unlink", data); } /** @@ -118,7 +118,7 @@ export function insertAndUnlink(data = {}) { * @returns {FieldCommand} */ function link(newValue) { - return new FieldCommand('link', newValue); + return new FieldCommand("link", newValue); } /** @@ -129,7 +129,7 @@ function link(newValue) { * @param {any} newValue - value to be written on the field value. */ function set(newValue) { - return new FieldCommand('set', newValue); + return new FieldCommand("set", newValue); } /** @@ -144,7 +144,7 @@ function set(newValue) { * @returns {FieldCommand} */ function unlink(data) { - return new FieldCommand('unlink', data); + return new FieldCommand("unlink", data); } /** @@ -155,18 +155,7 @@ function unlink(data) { * @returns {FieldCommand} */ function unlinkAll() { - return new FieldCommand('unlink-all'); + return new FieldCommand("unlink-all"); } -export { - FieldCommand, - clear, - create, - decrement, - increment, - insert, - link, - set, - unlink, - unlinkAll, -}; +export { FieldCommand, clear, create, decrement, increment, insert, link, set, unlink, unlinkAll }; diff --git a/addons/mail/static/src/model/model_field_relation_set.js b/addons/mail/static/src/model/model_field_relation_set.js index 0e6caf9c699..c6a1b35367e 100644 --- a/addons/mail/static/src/model/model_field_relation_set.js +++ b/addons/mail/static/src/model/model_field_relation_set.js @@ -1,16 +1,15 @@ /** @odoo-module */ -import { decrement, increment } from '@mail/model/model_field_command'; -import { Listener } from '@mail/model/model_listener'; -import { followRelations } from '@mail/model/model_utils'; -import { cleanSearchTerm } from '@mail/utils/utils'; +import { decrement, increment } from "@mail/model/model_field_command"; +import { Listener } from "@mail/model/model_listener"; +import { followRelations } from "@mail/model/model_utils"; +import { cleanSearchTerm } from "@mail/utils/utils"; /** * Defines a set containing the relation records of the given field on the given * record. The behavior of this set depends on the field properties. */ export class RelationSet { - /** * @param {Record} record * @param {ModelField} field @@ -53,7 +52,7 @@ export class RelationSet { const listener = new Listener({ isPartOfUpdateCycle: true, name: `sort of ${value} in ${this.field} of ${this.record}`, - onChange: info => { + onChange: (info) => { // access all useful values of current record (and relations) to mark them as dependencies this.record.modelManager.startListening(listener); const compareDefinition = this.field.sort; @@ -67,7 +66,7 @@ export class RelationSet { const valA = followRelations(a, relatedPath); const valB = followRelations(b, relatedPath); switch (compareMethod) { - case 'truthy-first': { + case "truthy-first": { if (valA === valB) { break; } @@ -79,7 +78,7 @@ export class RelationSet { } break; } - case 'falsy-first': { + case "falsy-first": { if (valA === valB) { break; } @@ -91,8 +90,8 @@ export class RelationSet { } break; } - case 'case-insensitive-asc': { - if (typeof valA !== 'string' || typeof valB !== 'string') { + case "case-insensitive-asc": { + if (typeof valA !== "string" || typeof valB !== "string") { break; } const cleanedValA = cleanSearchTerm(valA); @@ -102,23 +101,23 @@ export class RelationSet { } return cleanedValA < cleanedValB ? -1 : 1; } - case 'smaller-first': - if (typeof valA !== 'number' || typeof valB !== 'number') { + case "smaller-first": + if (typeof valA !== "number" || typeof valB !== "number") { break; } if (valA === valB) { break; } return valA - valB; - case 'greater-first': - if (typeof valA !== 'number' || typeof valB !== 'number') { + case "greater-first": + if (typeof valA !== "number" || typeof valB !== "number") { break; } if (valA === valB) { break; } return valB - valA; - case 'most-recent-first': + case "most-recent-first": if (!(valA instanceof Date) || !(valB instanceof Date)) { break; } @@ -127,7 +126,9 @@ export class RelationSet { } return valB - valA; default: - throw Error(`sort compare method "${compareMethod}" is not supported.`); + throw Error( + `sort compare method "${compareMethod}" is not supported.` + ); } } return 0; @@ -142,20 +143,23 @@ export class RelationSet { }, }); this.sortListenerByValue.set(value, listener); - listener.onChange({ reason: 'initial call' }); + listener.onChange({ reason: "initial call" }); } - for (const { from: contributionFieldName, to: sumFieldName } of this.field.sumContributions) { + for (const { from: contributionFieldName, to: sumFieldName } of this.field + .sumContributions) { this.sumByValueByField.get(sumFieldName).set(value, 0); const listener = new Listener({ isPartOfUpdateCycle: true, name: `sum of field(${sumFieldName}) of ${this.record} from field(${contributionFieldName}) of ${value} through relation ${this.field}`, - onChange: info => { + onChange: (info) => { // listen to the contribution to mark it as dependency this.record.modelManager.startListening(listener); const contribution = value[contributionFieldName]; this.record.modelManager.stopListening(listener); // retrieve the previous contribution and update it - const previousContribution = this.sumByValueByField.get(sumFieldName).get(value); + const previousContribution = this.sumByValueByField + .get(sumFieldName) + .get(value); this.sumByValueByField.get(sumFieldName).set(value, contribution); this.record.modelManager._update( this.record, @@ -165,12 +169,12 @@ export class RelationSet { increment(contribution), ], }, - { allowWriteReadonly: true }, + { allowWriteReadonly: true } ); }, }); this.sumListenerByValueByField.get(sumFieldName).set(value, listener); - listener.onChange({ reason: 'initial call' }); + listener.onChange({ reason: "initial call" }); } } @@ -205,7 +209,11 @@ export class RelationSet { // remove contribution of current value const contribution = this.sumByValueByField.get(sumFieldName).get(value); this.sumByValueByField.get(sumFieldName).delete(value); - this.record.modelManager._update(this.record, { [sumFieldName]: decrement(contribution) }, { allowWriteReadonly: true }); + this.record.modelManager._update( + this.record, + { [sumFieldName]: decrement(contribution) }, + { allowWriteReadonly: true } + ); // remove listener on current value const listener = this.sumListenerByValueByField.get(sumFieldName).get(value); this.sumListenerByValueByField.get(sumFieldName).delete(value); @@ -231,5 +239,4 @@ export class RelationSet { } return this.set[Symbol.iterator](); } - } diff --git a/addons/mail/static/src/model/model_index_and.js b/addons/mail/static/src/model/model_index_and.js index 698f388ae96..bc2bd412387 100644 --- a/addons/mail/static/src/model/model_index_and.js +++ b/addons/mail/static/src/model/model_index_and.js @@ -1,7 +1,6 @@ /** @odoo-module **/ export class ModelIndexAnd { - constructor(model) { this.model = model; this.recordsByValuesTree = new Map(); @@ -20,7 +19,9 @@ export class ModelIndexAnd { for (const fieldName of this.model.__identifyingFieldNames) { const fieldValue = data[fieldName]; if (fieldValue === undefined) { - throw new Error(`Identifying field "${fieldName}" is lacking a value on ${this.model} with 'and' identifying mode`); + throw new Error( + `Identifying field "${fieldName}" is lacking a value on ${this.model} with 'and' identifying mode` + ); } valuesOfRecord.push(fieldValue); if (!res.has(fieldValue)) { @@ -39,7 +40,9 @@ export class ModelIndexAnd { for (const fieldName of this.model.__identifyingFieldNames) { const fieldValue = data[fieldName]; if (fieldValue === undefined) { - throw new Error(`Identifying field "${fieldName}" is lacking a value on ${this.model} with 'and' identifying mode`); + throw new Error( + `Identifying field "${fieldName}" is lacking a value on ${this.model} with 'and' identifying mode` + ); } res = res.get(fieldValue); if (!res) { @@ -66,5 +69,4 @@ export class ModelIndexAnd { } this.valuesByRecords.delete(record); } - } diff --git a/addons/mail/static/src/model/model_index_xor.js b/addons/mail/static/src/model/model_index_xor.js index 574e55dd56f..e2e0626f437 100644 --- a/addons/mail/static/src/model/model_index_xor.js +++ b/addons/mail/static/src/model/model_index_xor.js @@ -1,7 +1,6 @@ /** @odoo-module **/ export class ModelIndexXor { - constructor(model) { this.model = model; this.recordsByValuesByFields = new Map(); @@ -9,16 +8,21 @@ export class ModelIndexXor { } addRecord(record, data) { - const [fieldName, fieldValue] = [...this.model.__identifyingFieldNames].reduce(([fieldName, fieldValue], currentFieldName) => { - const currentFieldValue = data[currentFieldName]; - if (currentFieldValue === undefined) { - return [fieldName, fieldValue]; - } - if (fieldName) { - throw new Error(`Identifying field on ${this.model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".`); - } - return [currentFieldName, currentFieldValue]; - }, [undefined, undefined]); + const [fieldName, fieldValue] = [...this.model.__identifyingFieldNames].reduce( + ([fieldName, fieldValue], currentFieldName) => { + const currentFieldValue = data[currentFieldName]; + if (currentFieldValue === undefined) { + return [fieldName, fieldValue]; + } + if (fieldName) { + throw new Error( + `Identifying field on ${this.model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".` + ); + } + return [currentFieldName, currentFieldValue]; + }, + [undefined, undefined] + ); if (!this.recordsByValuesByFields.has(fieldName)) { this.recordsByValuesByFields.set(fieldName, new Map()); } @@ -27,16 +31,21 @@ export class ModelIndexXor { } findRecord(data) { - const [fieldName, fieldValue] = [...this.model.__identifyingFieldNames].reduce(([fieldName, fieldValue], currentFieldName) => { - const currentFieldValue = data[currentFieldName]; - if (currentFieldValue === undefined) { - return [fieldName, fieldValue]; - } - if (fieldName) { - throw new Error(`Identifying field on ${this.model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".`); - } - return [currentFieldName, currentFieldValue]; - }, [undefined, undefined]); + const [fieldName, fieldValue] = [...this.model.__identifyingFieldNames].reduce( + ([fieldName, fieldValue], currentFieldName) => { + const currentFieldValue = data[currentFieldName]; + if (currentFieldValue === undefined) { + return [fieldName, fieldValue]; + } + if (fieldName) { + throw new Error( + `Identifying field on ${this.model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".` + ); + } + return [currentFieldName, currentFieldValue]; + }, + [undefined, undefined] + ); if (!this.recordsByValuesByFields.has(fieldName)) { this.recordsByValuesByFields.set(fieldName, new Map()); } @@ -48,5 +57,4 @@ export class ModelIndexXor { this.recordsByValuesByFields.get(fieldName).delete(fieldValue); this.fieldNameAndValueByRecords.delete(record); } - } diff --git a/addons/mail/static/src/model/model_manager.js b/addons/mail/static/src/model/model_manager.js index 248377a9da9..af3dfa8f458 100644 --- a/addons/mail/static/src/model/model_manager.js +++ b/addons/mail/static/src/model/model_manager.js @@ -1,20 +1,23 @@ /** @odoo-module **/ -import { useRefToModel } from '@mail/component_hooks/use_ref_to_model'; -import { IS_RECORD, patchesAppliedPromise, registry } from '@mail/model/model_core'; -import { ModelField } from '@mail/model/model_field'; -import { ModelIndexAnd } from '@mail/model/model_index_and'; -import { ModelIndexXor } from '@mail/model/model_index_xor'; -import { FieldCommand, unlinkAll } from '@mail/model/model_field_command'; -import { RelationSet } from '@mail/model/model_field_relation_set'; -import { Listener } from '@mail/model/model_listener'; -import { followRelations } from '@mail/model/model_utils'; -import { makeDeferred } from '@mail/utils/deferred'; -import { registerMessagingComponent, unregisterMessagingComponent } from '@mail/utils/messaging_component'; +import { useRefToModel } from "@mail/component_hooks/use_ref_to_model"; +import { IS_RECORD, patchesAppliedPromise, registry } from "@mail/model/model_core"; +import { ModelField } from "@mail/model/model_field"; +import { ModelIndexAnd } from "@mail/model/model_index_and"; +import { ModelIndexXor } from "@mail/model/model_index_xor"; +import { FieldCommand, unlinkAll } from "@mail/model/model_field_command"; +import { RelationSet } from "@mail/model/model_field_relation_set"; +import { Listener } from "@mail/model/model_listener"; +import { followRelations } from "@mail/model/model_utils"; +import { makeDeferred } from "@mail/utils/deferred"; +import { + registerMessagingComponent, + unregisterMessagingComponent, +} from "@mail/utils/messaging_component"; -import { LegacyComponent } from '@web/legacy/legacy_component'; +import { LegacyComponent } from "@web/legacy/legacy_component"; -import { Component } from '@odoo/owl'; +import { Component } from "@odoo/owl"; /** * Object that manage models and records, notably their update cycle: whenever @@ -23,7 +26,6 @@ import { Component } from '@odoo/owl'; * direct field & and computed field updates. */ export class ModelManager { - //-------------------------------------------------------------------------- // Public //-------------------------------------------------------------------------- @@ -117,7 +119,7 @@ export class ModelManager { /** * Create the messaging singleton record. */ - this.models['Messaging'].insert(values); + this.models["Messaging"].insert(values); this.messagingCreatedPromise.resolve(); await this.messaging.start(); this.messagingInitializedPromise.resolve(); @@ -254,7 +256,7 @@ export class ModelManager { * @returns {Record|Record[]} created or updated record(s). */ insert(model, data) { - const isMulti = typeof data[Symbol.iterator] === 'function'; + const isMulti = typeof data[Symbol.iterator] === "function"; const records = this._insert(model, isMulti ? data : [data]); this._flushUpdateCycle(); return isMulti ? records : records[0]; @@ -280,7 +282,7 @@ export class ModelManager { */ multiModelInsert(data) { for (const [modelName, recordsData] of Object.entries(data)) { - const isMulti = typeof recordsData[Symbol.iterator] === 'function'; + const isMulti = typeof recordsData[Symbol.iterator] === "function"; this._insert(this.models[modelName], isMulti ? recordsData : [recordsData]); } this._flushUpdateCycle(); @@ -292,12 +294,12 @@ export class ModelManager { * @returns {Messaging|undefined} */ get messaging() { - if (!this.models['Messaging']) { + if (!this.models["Messaging"]) { return undefined; } // Use "findFromIdentifyingData" specifically to ensure the record still // exists and to ensure listeners are properly notified of this access. - return this.models['Messaging'].findFromIdentifyingData({}); + return this.models["Messaging"].findFromIdentifyingData({}); } /** @@ -397,36 +399,38 @@ export class ModelManager { */ _applyModelDefinition(model) { const definition = registry.get(model.name); - if (definition.get('template')) { - const ComponentClass = definition.get('isLegacyComponent') ? LegacyComponent : Component; + if (definition.get("template")) { + const ComponentClass = definition.get("isLegacyComponent") + ? LegacyComponent + : Component; const ModelComponent = { [model.name]: class extends ComponentClass {} }[model.name]; Object.assign(ModelComponent, { props: { record: Object }, - template: definition.get('template'), + template: definition.get("template"), }); registerMessagingComponent(ModelComponent); model.__messagingComponent = ModelComponent; model.__fieldAndRefNames = []; } - Object.assign(model, Object.fromEntries(definition.get('modelMethods'))); - Object.assign(model.prototype, Object.fromEntries(definition.get('recordMethods'))); - for (const [getterName, getter] of definition.get('modelGetters')) { + Object.assign(model, Object.fromEntries(definition.get("modelMethods"))); + Object.assign(model.prototype, Object.fromEntries(definition.get("recordMethods"))); + for (const [getterName, getter] of definition.get("modelGetters")) { Object.defineProperty(model, getterName, { get: getter }); } - for (const [getterName, getter] of definition.get('recordGetters')) { + for (const [getterName, getter] of definition.get("recordGetters")) { Object.defineProperty(model.prototype, getterName, { get: getter }); } // Make model manager accessible from model. model.modelManager = this; model.fields = {}; - model.identifyingMode = definition.get('identifyingMode'); + model.identifyingMode = definition.get("identifyingMode"); model.__records = new Set(); model.__recordCount = 0; model.__recordsIndex = (() => { switch (model.identifyingMode) { - case 'and': + case "and": return new ModelIndexAnd(model); - case 'xor': + case "xor": return new ModelIndexXor(model); } })(); @@ -440,109 +444,150 @@ export class ModelManager { */ _checkDeclaredFieldsOnModels() { for (const model of Object.values(this.models)) { - for (const [fieldName, field] of registry.get(model.name).get('fields')) { + for (const [fieldName, field] of registry.get(model.name).get("fields")) { // 0. Forbidden name. if (fieldName in model.prototype) { throw new Error(`Field ${model}/${fieldName} has a forbidden name.`); } // 1. Field type is required. - if (!(['attribute', 'relation'].includes(field.fieldType))) { - throw new Error(`Field ${model}/${fieldName} has unsupported type "${field.fieldType}".`); + if (!["attribute", "relation"].includes(field.fieldType)) { + throw new Error( + `Field ${model}/${fieldName} has unsupported type "${field.fieldType}".` + ); } // 2. Invalid keys based on field type. - if (field.fieldType === 'attribute') { - const invalidKeys = Object.keys(field).filter(key => - ![ - 'compute', - 'default', - 'fieldType', - 'identifying', - 'readonly', - 'ref', - 'related', - 'required', - 'sum', - ].includes(key) + if (field.fieldType === "attribute") { + const invalidKeys = Object.keys(field).filter( + (key) => + ![ + "compute", + "default", + "fieldType", + "identifying", + "readonly", + "ref", + "related", + "required", + "sum", + ].includes(key) ); if (invalidKeys.length > 0) { - throw new Error(`Field ${model}/${fieldName} contains some invalid keys: "${invalidKeys.join(", ")}".`); + throw new Error( + `Field ${model}/${fieldName} contains some invalid keys: "${invalidKeys.join( + ", " + )}".` + ); } } - if (field.fieldType === 'relation') { - const invalidKeys = Object.keys(field).filter(key => - ![ - 'compute', - 'default', - 'fieldType', - 'identifying', - 'inverse', - 'isCausal', - 'readonly', - 'related', - 'relationType', - 'required', - 'sort', - 'to', - ].includes(key) + if (field.fieldType === "relation") { + const invalidKeys = Object.keys(field).filter( + (key) => + ![ + "compute", + "default", + "fieldType", + "identifying", + "inverse", + "isCausal", + "readonly", + "related", + "relationType", + "required", + "sort", + "to", + ].includes(key) ); if (invalidKeys.length > 0) { - throw new Error(`Field ${model}/${fieldName} contains some invalid keys: "${invalidKeys.join(", ")}".`); + throw new Error( + `Field ${model}/${fieldName} contains some invalid keys: "${invalidKeys.join( + ", " + )}".` + ); } if (!this.models[field.to]) { - throw new Error(`Relational field ${model}/${fieldName} targets to unknown model name "${field.to}".`); + throw new Error( + `Relational field ${model}/${fieldName} targets to unknown model name "${field.to}".` + ); } - if (field.required && field.relationType !== 'one') { - throw new Error(`Relational field ${model}/${fieldName} has "required" true with a relation of type "${field.relationType}" but "required" is only supported for "one".`); + if (field.required && field.relationType !== "one") { + throw new Error( + `Relational field ${model}/${fieldName} has "required" true with a relation of type "${field.relationType}" but "required" is only supported for "one".` + ); } - if (field.sort && field.relationType !== 'many') { - throw new Error(`Relational field "${model}/${fieldName}" has "sort" with a relation of type "${field.relationType}" but "sort" is only supported for "many".`); + if (field.sort && field.relationType !== "many") { + throw new Error( + `Relational field "${model}/${fieldName}" has "sort" with a relation of type "${field.relationType}" but "sort" is only supported for "many".` + ); } } // 3. Check for redundant or unsupported attributes on identifying fields. if (field.identifying) { - if ('readonly' in field) { - throw new Error(`Identifying field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for identifying fields).`); + if ("readonly" in field) { + throw new Error( + `Identifying field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for identifying fields).` + ); } - if ('required' in field && model.identifyingMode === 'and') { - throw new Error(`Identifying field ${model}/${fieldName} has unnecessary "required" attribute (required is implicit for AND identifying fields).`); + if ("required" in field && model.identifyingMode === "and") { + throw new Error( + `Identifying field ${model}/${fieldName} has unnecessary "required" attribute (required is implicit for AND identifying fields).` + ); } - if ('default' in field) { - throw new Error(`Identifying field ${model}/${fieldName} has "default" attribute, but default values are not supported for identifying fields.`); + if ("default" in field) { + throw new Error( + `Identifying field ${model}/${fieldName} has "default" attribute, but default values are not supported for identifying fields.` + ); } } // 4. Computed field. if (field.compute) { - if (typeof field.compute !== 'function') { - throw new Error(`Property "compute" of field ${model}/${fieldName} must be a string (instance method name) or a function (the actual compute).`); + if (typeof field.compute !== "function") { + throw new Error( + `Property "compute" of field ${model}/${fieldName} must be a string (instance method name) or a function (the actual compute).` + ); } - if ('readonly' in field) { - throw new Error(`Computed field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for computed fields).`); + if ("readonly" in field) { + throw new Error( + `Computed field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for computed fields).` + ); } } // 5. Related field. if (field.related) { if (field.compute) { - throw new Error(`field ${model}/${fieldName} cannot be a related and compute field at the same time.`); + throw new Error( + `field ${model}/${fieldName} cannot be a related and compute field at the same time.` + ); } - if (!(typeof field.related === 'string')) { - throw new Error(`Property "related" of field ${model}/${fieldName} has invalid format.`); + if (!(typeof field.related === "string")) { + throw new Error( + `Property "related" of field ${model}/${fieldName} has invalid format.` + ); } - const [relationName, relatedFieldName, other] = field.related.split('.'); + const [relationName, relatedFieldName, other] = field.related.split("."); if (!relationName || !relatedFieldName || other) { - throw new Error(`Property "related" of field ${model}/${fieldName} has invalid format.`); + throw new Error( + `Property "related" of field ${model}/${fieldName} has invalid format.` + ); } // find relation on self or parents. let relatedRelation; let targetModel = model; while (this.models[targetModel.name] && !relatedRelation) { - relatedRelation = registry.get(targetModel.name).get('fields').get(relationName); + relatedRelation = registry + .get(targetModel.name) + .get("fields") + .get(relationName); targetModel = targetModel.__proto__; } if (!relatedRelation) { - throw new Error(`Related field ${model}/${fieldName} relates to unknown relation name "${relationName}".`); + throw new Error( + `Related field ${model}/${fieldName} relates to unknown relation name "${relationName}".` + ); } - if (relatedRelation.fieldType !== 'relation') { - throw new Error(`Related field ${model}/${fieldName} relates to non-relational field "${relationName}".`); + if (relatedRelation.fieldType !== "relation") { + throw new Error( + `Related field ${model}/${fieldName} relates to non-relational field "${relationName}".` + ); } // Assuming related relation is valid... // find field name on related model or any parents. @@ -550,28 +595,38 @@ export class ModelManager { let relatedField; targetModel = relatedModel; while (this.models[targetModel.name] && !relatedField) { - relatedField = registry.get(targetModel.name).get('fields').get(relatedFieldName); + relatedField = registry + .get(targetModel.name) + .get("fields") + .get(relatedFieldName); targetModel = targetModel.__proto__; } if (!relatedField) { - throw new Error(`Related field ${model}/${fieldName} relates to unknown related model field "${relatedFieldName}".`); + throw new Error( + `Related field ${model}/${fieldName} relates to unknown related model field "${relatedFieldName}".` + ); } if (relatedField.fieldType !== field.fieldType) { - throw new Error(`Related field ${model}/${fieldName} has mismatched type with its related model field.`); + throw new Error( + `Related field ${model}/${fieldName} has mismatched type with its related model field.` + ); } - if ( - relatedField.fieldType === 'relation' && - relatedField.to !== field.to - ) { - throw new Error(`Related field ${model}/${fieldName} has mismatched target model name with its related model field.`); + if (relatedField.fieldType === "relation" && relatedField.to !== field.to) { + throw new Error( + `Related field ${model}/${fieldName} has mismatched target model name with its related model field.` + ); } - if ('readonly' in field) { - throw new Error(`Related field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for related fields).`); + if ("readonly" in field) { + throw new Error( + `Related field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for related fields).` + ); } } if (field.ref) { if (!model.__messagingComponent) { - throw new Error(`Field ${model}/${fieldName} has a 'ref' attribute but its model is not linked to any component.`); + throw new Error( + `Field ${model}/${fieldName} has a 'ref' attribute but its model is not linked to any component.` + ); } } } @@ -584,17 +639,25 @@ export class ModelManager { */ _checkOnChangesOnModels() { for (const model of Object.values(this.models)) { - for (const { dependencies, methodName } of registry.get(model.name).get('onChanges')) { + for (const { dependencies, methodName } of registry.get(model.name).get("onChanges")) { for (const dependency of dependencies) { let currentModel = model; let currentField; for (const fieldName of dependency) { if (!currentModel) { - throw new Error(`OnChange '${methodName}' defines a dependency with path '${dependency.join('.')}', but this dependency does not resolve: ${currentField} is not a relational field, therefore there is no relation to follow.`); + throw new Error( + `OnChange '${methodName}' defines a dependency with path '${dependency.join( + "." + )}', but this dependency does not resolve: ${currentField} is not a relational field, therefore there is no relation to follow.` + ); } currentField = currentModel.__fieldMap.get(fieldName); if (!currentField) { - throw new Error(`OnChange '${methodName}' defines a dependency with path '${dependency.join('.')}', but this path does not resolve: ${currentModel}/${fieldName} does not exist.`); + throw new Error( + `OnChange '${methodName}' defines a dependency with path '${dependency.join( + "." + )}', but this path does not resolve: ${currentModel}/${fieldName} does not exist.` + ); } if (currentField.to) { currentModel = this.models[currentField.to]; @@ -613,55 +676,79 @@ export class ModelManager { */ _checkProcessedFieldsOnModels() { for (const model of Object.values(this.models)) { - if (!['and', 'xor'].includes(model.identifyingMode)) { - throw new Error(`Unsupported identifying mode "${model.identifyingMode}" on ${model}. Must be one of 'and' or 'xor'.`); + if (!["and", "xor"].includes(model.identifyingMode)) { + throw new Error( + `Unsupported identifying mode "${model.identifyingMode}" on ${model}. Must be one of 'and' or 'xor'.` + ); } for (const field of model.__fieldList) { const fieldName = field.fieldName; - if (!(['attribute', 'relation'].includes(field.fieldType))) { + if (!["attribute", "relation"].includes(field.fieldType)) { throw new Error(`${field} has unsupported type "${field.fieldType}".`); } if (field.compute && field.related) { - throw new Error(`${field} cannot be a related and compute field at the same time.`); + throw new Error( + `${field} cannot be a related and compute field at the same time.` + ); } - if (field.fieldType === 'attribute') { + if (field.fieldType === "attribute") { continue; } if (!field.relationType) { throw new Error(`${field} must define a relation type in "relationType".`); } - if (!(['many', 'one'].includes(field.relationType))) { + if (!["many", "one"].includes(field.relationType)) { throw new Error(`${field} has invalid relation type "${field.relationType}".`); } if (!field.inverse) { throw new Error(`${field} must define an inverse relation name in "inverse".`); } if (!field.to) { - throw new Error(`${field} must define a model name in "to" (1st positional parameter of relation field helpers).`); + throw new Error( + `${field} must define a model name in "to" (1st positional parameter of relation field helpers).` + ); } const relatedModel = this.models[field.to]; if (!relatedModel) { - throw new Error(`${field} defines a relation to model ${field.to}, but there is no model registered with this name.`); + throw new Error( + `${field} defines a relation to model ${field.to}, but there is no model registered with this name.` + ); } const inverseField = relatedModel.__fieldMap.get(field.inverse); if (!inverseField) { - throw new Error(`${field} defines its inverse as field ${relatedModel}/${field.inverse}, but it does not exist.`); + throw new Error( + `${field} defines its inverse as field ${relatedModel}/${field.inverse}, but it does not exist.` + ); } if (inverseField.inverse !== fieldName) { - throw new Error(`The name of ${field} does not match with the name defined in its inverse ${inverseField}.`); + throw new Error( + `The name of ${field} does not match with the name defined in its inverse ${inverseField}.` + ); } - if (![model.name, 'Record'].includes(inverseField.to)) { - throw new Error(`${field} has its inverse ${inverseField} referring to an invalid model (${inverseField.to}).`); + if (![model.name, "Record"].includes(inverseField.to)) { + throw new Error( + `${field} has its inverse ${inverseField} referring to an invalid model (${inverseField.to}).` + ); } if (field.sort) { for (const path of field.sortedFieldSplittedPaths) { let currentField = field; for (const fieldName of path) { if (!currentField.to) { - throw new Error(`Field ${field} defines a sort with path '${path.join('.')}', but this path does not resolve: ${currentField} is not a relational field, therefore there is no relation to follow.`); + throw new Error( + `Field ${field} defines a sort with path '${path.join( + "." + )}', but this path does not resolve: ${currentField} is not a relational field, therefore there is no relation to follow.` + ); } if (!this.models[currentField.to].__fieldMap.has(fieldName)) { - throw new Error(`Field ${field} defines a sort with path '${path.join('.')}', but this path does not resolve: ${this.models[currentField.to]}/${fieldName} does not exist.`); + throw new Error( + `Field ${field} defines a sort with path '${path.join( + "." + )}', but this path does not resolve: ${ + this.models[currentField.to] + }/${fieldName} does not exist.` + ); } currentField = this.models[currentField.to].__fieldMap.get(fieldName); } @@ -671,16 +758,22 @@ export class ModelManager { for (const identifyingField of model.__identifyingFieldNames) { const field = model.__fieldMap.get(identifyingField); if (!field) { - throw new Error(`Identifying field "${model}/${identifyingField}" is not a field on ${model}.`); + throw new Error( + `Identifying field "${model}/${identifyingField}" is not a field on ${model}.` + ); } if (field.to) { - if (field.relationType !== 'one') { - throw new Error(`Identifying field "${model}/${identifyingField}" has a relation of type "${field.relationType}" but identifying field is only supported for "one".`); + if (field.relationType !== "one") { + throw new Error( + `Identifying field "${model}/${identifyingField}" has a relation of type "${field.relationType}" but identifying field is only supported for "one".` + ); } const relatedModel = this.models[field.to]; const inverseField = relatedModel.__fieldMap.get(field.inverse); if (!inverseField.isCausal) { - throw new Error(`Identifying field "${model}/${identifyingField}" has an inverse "${inverseField}" not declared as "isCausal".`); + throw new Error( + `Identifying field "${model}/${identifyingField}" has an inverse "${inverseField}" not declared as "isCausal".` + ); } } } @@ -719,26 +812,31 @@ export class ModelManager { __values: new Map(), [IS_RECORD]: true, }); - const record = owl.markRaw(!this.isDebug ? nonProxyRecord : new Proxy(nonProxyRecord, { - get: function getFromProxy(record, prop) { - if ( - (model.__fieldMap && !model.__fieldMap.has(prop)) && - !['_super', 'then', 'localId'].includes(prop) && - typeof prop !== 'symbol' && - !(prop in record) - ) { - console.warn(`non-field read "${prop}" on ${record}`); - } - return record[prop]; - }, - })); + const record = owl.markRaw( + !this.isDebug + ? nonProxyRecord + : new Proxy(nonProxyRecord, { + get: function getFromProxy(record, prop) { + if ( + model.__fieldMap && + !model.__fieldMap.has(prop) && + !["_super", "then", "localId"].includes(prop) && + typeof prop !== "symbol" && + !(prop in record) + ) { + console.warn(`non-field read "${prop}" on ${record}`); + } + return record[prop]; + }, + }) + ); if (this.isDebug) { record.__proxifiedRecord = record; } // Ensure X2many relations are Set initially (other fields can stay undefined). for (const field of model.__fieldList) { - if (field.fieldType === 'relation') { - if (field.relationType === 'many') { + if (field.fieldType === "relation") { + if (field.relationType === "many") { record.__values.set(field.fieldName, new RelationSet(record, field)); } } @@ -750,7 +848,7 @@ export class ModelManager { /** * Auto-bind record methods so that `this` always refer to the record. */ - const recordMethods = registry.get(model.name).get('recordMethods'); + const recordMethods = registry.get(model.name).get("recordMethods"); for (const methodName of recordMethods.keys()) { record[methodName] = record[methodName].bind(record); } @@ -783,15 +881,15 @@ export class ModelManager { if (!record.exists()) { throw Error(`Cannot delete already deleted record ${record}.`); } - const lifecycleHooks = registry.get(model.name).get('lifecycleHooks'); - if (lifecycleHooks.has('_willDelete')) { - lifecycleHooks.get('_willDelete').call(record); + const lifecycleHooks = registry.get(model.name).get("lifecycleHooks"); + if (lifecycleHooks.has("_willDelete")) { + lifecycleHooks.get("_willDelete").call(record); } for (const listener of record.__listeners) { this.removeListener(listener); } for (const field of model.__fieldList) { - if (field.fieldType === 'relation') { + if (field.fieldType === "relation") { // ensure inverses are properly unlinked field.parseAndExecuteCommands(record, unlinkAll(), { allowWriteReadonly: true }); if (!record.exists()) { @@ -835,7 +933,9 @@ export class ModelManager { _ensureNoLockingListener() { for (const listener of this._listeners) { if (listener.isLocking) { - throw Error(`Model manager locked by ${listener}. It is not allowed to insert/update/delete from inside a lock.`); + throw Error( + `Model manager locked by ${listener}. It is not allowed to insert/update/delete from inside a lock.` + ); } } } @@ -862,7 +962,11 @@ export class ModelManager { this.startListening(listener); const res = field.compute.call(record); this.stopListening(listener); - this._update(record, { [field.fieldName]: res }, { allowWriteReadonly: true }); + this._update( + record, + { [field.fieldName]: res }, + { allowWriteReadonly: true } + ); }, }); listeners.push(listener); @@ -875,7 +979,11 @@ export class ModelManager { this.startListening(listener); const res = field.computeRelated(record); this.stopListening(listener); - this._update(record, { [field.fieldName]: res }, { allowWriteReadonly: true }); + this._update( + record, + { [field.fieldName]: res }, + { allowWriteReadonly: true } + ); }, }); listeners.push(listener); @@ -905,9 +1013,9 @@ export class ModelManager { if (!record.exists()) { throw Error(`Cannot call _created for already deleted ${record}.`); } - const lifecycleHooks = registry.get(record.constructor.name).get('lifecycleHooks'); - if (lifecycleHooks.has('_created')) { - lifecycleHooks.get('_created').call(record); + const lifecycleHooks = registry.get(record.constructor.name).get("lifecycleHooks"); + if (lifecycleHooks.has("_created")) { + lifecycleHooks.get("_created").call(record); } } } @@ -923,7 +1031,7 @@ export class ModelManager { if (!record.exists()) { throw Error(`Cannot call onChange for already deleted ${record}.`); } - for (const onChange of registry.get(record.constructor.name).get('onChanges')) { + for (const onChange of registry.get(record.constructor.name).get("onChanges")) { const listener = new Listener({ name: `${onChange} of ${record}`, onChange: (info) => { @@ -947,7 +1055,6 @@ export class ModelManager { } } - /** * Executes the check of the required field of updated records. */ @@ -984,13 +1091,13 @@ export class ModelManager { _generateModels() { // Create the model through a class to give it a meaningful name to be // displayed in stack traces and stuff. - const model = { 'Record': class {} }['Record']; + const model = { Record: class {} }["Record"]; this._applyModelDefinition(model); // Record is generated separately and before the other models since // it is the dependency of all of them. - const allModelNamesButRecord = [...registry.keys()].filter(name => name !== 'Record'); + const allModelNamesButRecord = [...registry.keys()].filter((name) => name !== "Record"); for (const modelName of allModelNamesButRecord) { - const model = { [modelName]: class extends this.models['Record'] {} }[modelName]; + const model = { [modelName]: class extends this.models["Record"] {} }[modelName]; this._applyModelDefinition(model); } /** @@ -1046,17 +1153,15 @@ export class ModelManager { * @returns {ModelField} */ _makeInverseRelationField(model, field) { - const inverseField = new ModelField(Object.assign( - {}, - ModelField.many(model.name, { inverse: field.fieldName }), - { + const inverseField = new ModelField( + Object.assign({}, ModelField.many(model.name, { inverse: field.fieldName }), { fieldName: `_inverse_${model}/${field.fieldName}`, // Allows the inverse of an identifying field to be // automatically generated. isCausal: field.identifying, model: this.models[field.to], - }, - )); + }) + ); return inverseField; } @@ -1098,7 +1203,8 @@ export class ModelManager { * @param {ModelField} field */ _markRecordFieldAsChanged(record, field) { - for (const [listener, infoList] of record.__listenersObservingFieldsOfRecord.get(field) || []) { + for (const [listener, infoList] of record.__listenersObservingFieldsOfRecord.get(field) || + []) { this._markListenerToNotify(listener, { listener, reason: this.isDebug && `_update: ${field} of ${record}`, @@ -1166,23 +1272,33 @@ export class ModelManager { } const commands = field.convertToFieldCommandList(data[fieldName]); if (commands.length !== 1) { - throw new Error(`Identifying field "${model}/${fieldName}" should receive a single command.`); + throw new Error( + `Identifying field "${model}/${fieldName}" should receive a single command.` + ); } const [command] = commands; if (!(command instanceof FieldCommand)) { - throw new Error(`Identifying field "${model}/${fieldName}" should receive a command.`); + throw new Error( + `Identifying field "${model}/${fieldName}" should receive a command.` + ); } - if (!['insert-and-replace', 'replace'].includes(command._name)) { - throw new Error(`Identifying field "${model}/${fieldName}" should receive a "replace" or "insert-and-replace" command.`); + if (!["insert-and-replace", "replace"].includes(command._name)) { + throw new Error( + `Identifying field "${model}/${fieldName}" should receive a "replace" or "insert-and-replace" command.` + ); } - if (command._name === 'replace') { + if (command._name === "replace") { continue; } if (!command._value) { - throw new Error(`Identifying field "${model}/${fieldName}" is lacking a relation value.`); + throw new Error( + `Identifying field "${model}/${fieldName}" is lacking a relation value.` + ); } - if (typeof command._value[Symbol.iterator] === 'function') { - throw new Error(`Identifying field "${model}/${fieldName}" should receive a single data object.`); + if (typeof command._value[Symbol.iterator] === "function") { + throw new Error( + `Identifying field "${model}/${fieldName}" should receive a single data object.` + ); } const [record] = this._insert(this.models[field.to], [command._value]); data[fieldName] = record; @@ -1206,13 +1322,15 @@ export class ModelManager { for (const model of Object.values(this.models)) { const sumContributionsByFieldName = new Map(); // Make fields aware of their field name. - for (const [fieldName, fieldData] of registry.get(model.name).get('fields')) { - model.fields[fieldName] = new ModelField(Object.assign({}, fieldData, { - fieldName, - model, - })); + for (const [fieldName, fieldData] of registry.get(model.name).get("fields")) { + model.fields[fieldName] = new ModelField( + Object.assign({}, fieldData, { + fieldName, + model, + }) + ); if (fieldData.sum) { - const [relationFieldName, contributionFieldName] = fieldData.sum.split('.'); + const [relationFieldName, contributionFieldName] = fieldData.sum.split("."); if (!sumContributionsByFieldName.has(relationFieldName)) { sumContributionsByFieldName.set(relationFieldName, []); } @@ -1230,8 +1348,8 @@ export class ModelManager { } if (model.__messagingComponent) { const setupFunctions = []; - if (registry.get(model.name).has('componentSetup')) { - setupFunctions.push(registry.get(model.name).get('componentSetup')); + if (registry.get(model.name).has("componentSetup")) { + setupFunctions.push(registry.get(model.name).get("componentSetup")); } if (model.__fieldAndRefNames.length > 0) { setupFunctions.push(function () { @@ -1256,7 +1374,7 @@ export class ModelManager { */ for (const model of Object.values(this.models)) { for (const field of Object.values(model.fields)) { - if (field.fieldType !== 'relation') { + if (field.fieldType !== "relation") { continue; } if (field.inverse) { @@ -1302,9 +1420,7 @@ export class ModelManager { model.__fieldMap = new Map(Object.entries(model.__combinedFields)); // List of all fields, for iterating. model.__fieldList = [...model.__fieldMap.values()]; - model.__requiredFieldsList = model.__fieldList.filter( - field => field.required - ); + model.__requiredFieldsList = model.__fieldList.filter((field) => field.required); model.__identifyingFieldNames = new Set(); for (const [fieldName, field] of model.__fieldMap) { if (field.identifying) { @@ -1312,11 +1428,13 @@ export class ModelManager { } // Add field accessors on model. Object.defineProperty(model.prototype, fieldName, { - get: function getFieldValue() { // this is bound to record + get: function getFieldValue() { + // this is bound to record const record = this.modelManager.isDebug ? this.__proxifiedRecord : this; if (this.modelManager._listeners.size) { - let entryRecord = record.__listenersObservingRecord; - const reason = record.modelManager.isDebug && `getField - ${field} of ${record}`; + const entryRecord = record.__listenersObservingRecord; + const reason = + record.modelManager.isDebug && `getField - ${field} of ${record}`; let entryField = record.__listenersObservingFieldsOfRecord.get(field); if (!entryField) { entryField = new Map(); @@ -1347,7 +1465,8 @@ export class ModelManager { if (model.__messagingComponent) { // Add field accessors on related component Object.defineProperty(model.__messagingComponent.prototype, fieldName, { - get: function getFieldValue() { // this is bound to record + get: function getFieldValue() { + // this is bound to record return this.props.record[fieldName]; }, }); @@ -1355,20 +1474,21 @@ export class ModelManager { } if (model.__messagingComponent) { // Add record method accessors + localId - Object.defineProperty(model.__messagingComponent.prototype, 'localId', { - get: function getFieldValue() { // this is bound to record + Object.defineProperty(model.__messagingComponent.prototype, "localId", { + get: function getFieldValue() { + // this is bound to record return this.props.record.localId; }, }); const definition = registry.get(model.name); - for (const name of definition.get('recordMethods').keys()) { + for (const name of definition.get("recordMethods").keys()) { Object.defineProperty(model.__messagingComponent.prototype, name, { get() { return this.props.record[name]; }, }); } - for (const name of definition.get('recordGetters').keys()) { + for (const name of definition.get("recordGetters").keys()) { Object.defineProperty(model.__messagingComponent.prototype, name, { get() { return this.props.record[name]; @@ -1405,7 +1525,9 @@ export class ModelManager { } const field = model.__fieldMap.get(fieldName); if (!field) { - console.warn(`Cannot create/update record with data unrelated to a field. (record: "${record}", non-field attempted update: "${fieldName}")`); + console.warn( + `Cannot create/update record with data unrelated to a field. (record: "${record}", non-field attempted update: "${fieldName}")` + ); continue; } const newVal = data[fieldName]; @@ -1423,5 +1545,4 @@ export class ModelManager { } return hasChanged; } - } diff --git a/addons/mail/static/src/models/activity.js b/addons/mail/static/src/models/activity.js index 680049351a8..9f22f002f09 100644 --- a/addons/mail/static/src/models/activity.js +++ b/addons/mail/static/src/models/activity.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, clear, insert, many, one, Model } from '@mail/model'; +import { attr, clear, insert, many, one, Model } from "@mail/model"; -import { markup } from '@odoo/owl'; +import { markup } from "@odoo/owl"; Model({ - name: 'Activity', + name: "Activity", modelMethods: { /** * @param {Object} data @@ -13,39 +13,39 @@ Model({ */ convertData(data) { const data2 = {}; - if ('activity_category' in data) { + if ("activity_category" in data) { data2.category = data.activity_category; } - if ('can_write' in data) { + if ("can_write" in data) { data2.canWrite = data.can_write; } - if ('create_date' in data) { + if ("create_date" in data) { data2.dateCreate = data.create_date; } - if ('date_deadline' in data) { + if ("date_deadline" in data) { data2.dateDeadline = data.date_deadline; } - if ('chaining_type' in data) { + if ("chaining_type" in data) { data2.chaining_type = data.chaining_type; } - if ('icon' in data) { + if ("icon" in data) { data2.icon = data.icon; } - if ('id' in data) { + if ("id" in data) { data2.id = data.id; } - if ('note' in data) { + if ("note" in data) { data2.rawNote = data.note; } - if ('state' in data) { + if ("state" in data) { data2.state = data.state; } - if ('summary' in data) { + if ("summary" in data) { data2.summary = data.summary; } // relation - if ('activity_type_id' in data) { + if ("activity_type_id" in data) { if (!data.activity_type_id) { data2.type = clear(); } else { @@ -55,7 +55,7 @@ Model({ }); } } - if ('create_uid' in data) { + if ("create_uid" in data) { if (!data.create_uid) { data2.creator = clear(); } else { @@ -65,7 +65,7 @@ Model({ }); } } - if ('mail_template_ids' in data) { + if ("mail_template_ids" in data) { data2.mailTemplates = insert(data.mail_template_ids); } if (data.res_id && data.res_model) { @@ -74,7 +74,7 @@ Model({ model: data.res_model, }); } - if ('user_id' in data) { + if ("user_id" in data) { if (!data.user_id) { data2.assignee = clear(); } else { @@ -84,7 +84,7 @@ Model({ }); } } - if ('request_partner_id' in data) { + if ("request_partner_id" in data) { if (!data.request_partner_id) { data2.requestingPartner = clear(); } else { @@ -104,8 +104,8 @@ Model({ */ async deleteServerRecord() { await this.messaging.rpc({ - model: 'mail.activity', - method: 'unlink', + model: "mail.activity", + method: "unlink", args: [[this.id]], }); if (!this.exists()) { @@ -126,25 +126,30 @@ Model({ } }, async fetchAndUpdate() { - const [data] = await this.messaging.rpc({ - model: 'mail.activity', - method: 'activity_format', - args: [this.id], - }, { shadow: true }).catch(e => { - const errorName = e.message && e.message.data && e.message.data.name; - if ([errorName, e.exceptionName].includes('odoo.exceptions.MissingError')) { - return []; - } else { - throw e; - } - }); + const [data] = await this.messaging + .rpc( + { + model: "mail.activity", + method: "activity_format", + args: [this.id], + }, + { shadow: true } + ) + .catch((e) => { + const errorName = e.message && e.message.data && e.message.data.name; + if ([errorName, e.exceptionName].includes("odoo.exceptions.MissingError")) { + return []; + } else { + throw e; + } + }); let shouldDelete = false; if (data) { this.update(this.constructor.convertData(data)); } else { shouldDelete = true; } - this.thread.fetchData(['activities', 'attachments', 'messages']); + this.thread.fetchData(["activities", "attachments", "messages"]); if (shouldDelete) { this.delete(); } @@ -155,11 +160,11 @@ Model({ * @param {string|boolean} [param0.feedback=false] */ async markAsDone({ attachments = [], feedback = false }) { - const attachmentIds = attachments.map(attachment => attachment.id); + const attachmentIds = attachments.map((attachment) => attachment.id); const thread = this.thread; await this.messaging.rpc({ - model: 'mail.activity', - method: 'action_feedback', + model: "mail.activity", + method: "action_feedback", args: [[this.id]], kwargs: { attachment_ids: attachmentIds, @@ -167,7 +172,7 @@ Model({ }, }); if (thread.exists()) { - thread.fetchData(['attachments', 'messages']); + thread.fetchData(["attachments", "messages"]); } if (!this.exists()) { return; @@ -181,13 +186,13 @@ Model({ async markAsDoneAndScheduleNext({ feedback }) { const thread = this.thread; const action = await this.messaging.rpc({ - model: 'mail.activity', - method: 'action_feedback_schedule_next', + model: "mail.activity", + method: "action_feedback_schedule_next", args: [[this.id]], kwargs: { feedback }, }); if (thread.exists()) { - thread.fetchData(['activities', 'attachments', 'messages']); + thread.fetchData(["activities", "attachments", "messages"]); } if (this.exists()) { this.delete(); @@ -195,27 +200,24 @@ Model({ if (!action) { return; } - await new Promise(resolve => { - this.env.services.action.doAction( - action, - { - onClose: resolve, - }, - ); + await new Promise((resolve) => { + this.env.services.action.doAction(action, { + onClose: resolve, + }); }); if (!thread.exists()) { return; } - thread.fetchData(['activities']); + thread.fetchData(["activities"]); }, }, fields: { - activityViews: many('ActivityView', { inverse: 'activity' }), - assignee: one('User', { inverse: 'activitiesAsAssignee' }), - attachments: many('Attachment', { inverse: 'activities' }), + activityViews: many("ActivityView", { inverse: "activity" }), + assignee: one("User", { inverse: "activitiesAsAssignee" }), + attachments: many("Attachment", { inverse: "activities" }), canWrite: attr({ default: false }), category: attr(), - creator: one('User'), + creator: one("User"), dateCreate: attr(), dateDeadline: attr(), /** @@ -224,7 +226,7 @@ Model({ * In all other cases, this field value should not be trusted. */ feedbackBackup: attr(), - chaining_type: attr({ default: 'suggest' }), + chaining_type: attr({ default: "suggest" }), icon: attr(), id: attr({ identifying: true }), isCurrentPartnerAssignee: attr({ @@ -236,7 +238,7 @@ Model({ }, default: false, }), - mailTemplates: many('MailTemplate', { inverse: 'activities' }), + mailTemplates: many("MailTemplate", { inverse: "activities" }), /** * This value is meant to be returned by the server * (and has been sanitized before stored into db). @@ -250,7 +252,7 @@ Model({ * value, to reduce the size the empty note takes on the UI. */ compute() { - if (this.rawNote === '


') { + if (this.rawNote === "


") { return clear(); } return this.rawNote; @@ -269,14 +271,14 @@ Model({ * Also, be useful when the assigned user is different from the * "source" or "requesting" partner. */ - requestingPartner: one('Partner'), + requestingPartner: one("Partner"), state: attr(), summary: attr(), /** * Determines to which "thread" (using `mail.activity.mixin` on the * server) `this` belongs to. */ - thread: one('Thread', { inverse: 'activities' }), - type: one('ActivityType', { inverse: 'activities' }), + thread: one("Thread", { inverse: "activities" }), + type: one("ActivityType", { inverse: "activities" }), }, }); diff --git a/addons/mail/static/src/models/activity_button_view.js b/addons/mail/static/src/models/activity_button_view.js index 4a696813afb..657d7a92a75 100644 --- a/addons/mail/static/src/models/activity_button_view.js +++ b/addons/mail/static/src/models/activity_button_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'ActivityButtonView', - template: 'mail.ActivityButtonView', - identifyingMode: 'xor', + name: "ActivityButtonView", + template: "mail.ActivityButtonView", + identifyingMode: "xor", recordMethods: { onClick(ev) { if (!this.exists()) { @@ -18,7 +18,9 @@ Model({ }, }, fields: { - activityListPopoverView: one('PopoverView', { inverse: 'activityButtonViewOwnerAsActivityList' }), + activityListPopoverView: one("PopoverView", { + inverse: "activityButtonViewOwnerAsActivityList", + }), buttonClass: attr({ compute() { if (!this.thread) { @@ -26,26 +28,26 @@ Model({ } const classes = []; switch (this.webRecord.data.activity_state) { - case 'overdue': - classes.push('text-danger'); + case "overdue": + classes.push("text-danger"); break; - case 'today': - classes.push('text-warning'); + case "today": + classes.push("text-warning"); break; - case 'planned': - classes.push('text-success'); + case "planned": + classes.push("text-success"); break; default: - classes.push('text-muted'); + classes.push("text-muted"); break; } switch (this.webRecord.data.activity_exception_decoration) { - case 'warning': - classes.push('text-warning'); + case "warning": + classes.push("text-warning"); classes.push(this.webRecord.data.activity_exception_icon); break; - case 'danger': - classes.push('text-danger'); + case "danger": + classes.push("text-danger"); classes.push(this.webRecord.data.activity_exception_icon); break; default: @@ -53,16 +55,23 @@ Model({ classes.push(this.webRecord.data.activity_type_icon); break; } - classes.push('fa-clock-o'); + classes.push("fa-clock-o"); break; } - return classes.join(' '); + return classes.join(" "); }, }), - buttonRef: attr({ ref: 'button' }), - kanbanFieldActivityViewOwner: one('KanbanFieldActivityView', { identifying: true, inverse: 'activityButtonView' }), - listFieldActivityViewOwner: one('ListFieldActivityView', { identifying: true, inverse: 'activityButtonView' }), - thread: one('Thread', { required: true, + buttonRef: attr({ ref: "button" }), + kanbanFieldActivityViewOwner: one("KanbanFieldActivityView", { + identifying: true, + inverse: "activityButtonView", + }), + listFieldActivityViewOwner: one("ListFieldActivityView", { + identifying: true, + inverse: "activityButtonView", + }), + thread: one("Thread", { + required: true, compute() { if (this.kanbanFieldActivityViewOwner) { return this.kanbanFieldActivityViewOwner.thread; @@ -73,7 +82,8 @@ Model({ return clear(); }, }), - webRecord: attr({ required: true, + webRecord: attr({ + required: true, compute() { if (this.kanbanFieldActivityViewOwner) { return this.kanbanFieldActivityViewOwner.webRecord; diff --git a/addons/mail/static/src/models/activity_cell_view.js b/addons/mail/static/src/models/activity_cell_view.js index d2fb4877e83..b671f5ed22b 100644 --- a/addons/mail/static/src/models/activity_cell_view.js +++ b/addons/mail/static/src/models/activity_cell_view.js @@ -1,18 +1,20 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'ActivityCellView', - template: 'mail.ActivityCellView', + name: "ActivityCellView", + template: "mail.ActivityCellView", recordMethods: { onClick() { this.update({ activityListPopoverView: this.activityListPopoverView ? clear() : {} }); }, }, fields: { - activityListPopoverView: one('PopoverView', { inverse: 'activityCellViewOwnerAsActivityList' }), - activityType: one('ActivityType', { required: true }), + activityListPopoverView: one("PopoverView", { + inverse: "activityCellViewOwnerAsActivityList", + }), + activityType: one("ActivityType", { required: true }), closestDeadline: attr({ required: true }), closestDeadlineFormatted: attr({ compute() { @@ -20,21 +22,24 @@ Model({ // To remove year only if current year if (moment().year() === moment(date).year()) { return date.toLocaleDateString(moment().locale(), { - day: 'numeric', month: 'short' + day: "numeric", + month: "short", }); } else { - return moment(date).format('ll'); + return moment(date).format("ll"); } }, }), - contentRef: attr({ ref: 'content' }), - filteredActivities: many('Activity', { + contentRef: attr({ ref: "content" }), + filteredActivities: many("Activity", { compute() { - return this.thread.activities.filter(activity => activity.type === this.activityType); + return this.thread.activities.filter( + (activity) => activity.type === this.activityType + ); }, }), id: attr({ identifying: true }), reloadFunc: attr({ required: true }), - thread: one('Thread', { required: true }), + thread: one("Thread", { required: true }), }, }); diff --git a/addons/mail/static/src/models/activity_group.js b/addons/mail/static/src/models/activity_group.js index 7450469cb69..a37c268829f 100644 --- a/addons/mail/static/src/models/activity_group.js +++ b/addons/mail/static/src/models/activity_group.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, many, one, Model } from '@mail/model'; +import { attr, many, one, Model } from "@mail/model"; Model({ - name: 'ActivityGroup', + name: "ActivityGroup", modelMethods: { convertData(data) { return { @@ -28,16 +28,16 @@ Model({ * @private */ _onChangeTotalCount() { - if (this.type === 'activity' && this.total_count === 0) { + if (this.type === "activity" && this.total_count === 0) { this.delete(); } }, }, fields: { actions: attr(), - activityGroupViews: many('ActivityGroupView', { inverse: 'activityGroup' }), + activityGroupViews: many("ActivityGroupView", { inverse: "activityGroup" }), domain: attr(), - irModel: one('ir.model', { identifying: true, inverse: 'activityGroup' }), + irModel: one("ir.model", { identifying: true, inverse: "activityGroup" }), overdue_count: attr({ default: 0 }), planned_count: attr({ default: 0 }), today_count: attr({ default: 0 }), @@ -46,8 +46,8 @@ Model({ }, onChanges: [ { - dependencies: ['total_count', 'type'], - methodName: '_onChangeTotalCount', + dependencies: ["total_count", "type"], + methodName: "_onChangeTotalCount", }, ], }); diff --git a/addons/mail/static/src/models/activity_group_view.js b/addons/mail/static/src/models/activity_group_view.js index 1e137f5bbef..6091d5ffc2d 100644 --- a/addons/mail/static/src/models/activity_group_view.js +++ b/addons/mail/static/src/models/activity_group_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; -import session from 'web.session'; +import session from "web.session"; Model({ - name: 'ActivityGroupView', + name: "ActivityGroupView", recordMethods: { /** * @param {MouseEvent} ev @@ -14,26 +14,29 @@ Model({ ev.stopPropagation(); this.activityMenuViewOwner.update({ isOpen: false }); const targetAction = $(ev.currentTarget); - const actionXmlid = targetAction.data('action_xmlid'); + const actionXmlid = targetAction.data("action_xmlid"); if (actionXmlid) { this.env.services.action.doAction(actionXmlid); } else { - let domain = [['activity_ids.user_id', '=', session.uid]]; - if (targetAction.data('domain')) { - domain = domain.concat(targetAction.data('domain')); + let domain = [["activity_ids.user_id", "=", session.uid]]; + if (targetAction.data("domain")) { + domain = domain.concat(targetAction.data("domain")); } - this.env.services['action'].doAction( + this.env.services["action"].doAction( { domain, - name: targetAction.data('model_name'), - res_model: targetAction.data('res_model'), - type: 'ir.actions.act_window', - views: this.activityGroup.irModel.availableWebViews.map(viewName => [false, viewName]), + name: targetAction.data("model_name"), + res_model: targetAction.data("res_model"), + type: "ir.actions.act_window", + views: this.activityGroup.irModel.availableWebViews.map((viewName) => [ + false, + viewName, + ]), }, { clearBreadcrumbs: true, - viewType: 'activity', - }, + viewType: "activity", + } ); } }, @@ -45,37 +48,43 @@ Model({ // fetch the data from the button otherwise fetch the ones from the parent (.o_ActivityMenuView_activityGroup). const data = _.extend({}, $(ev.currentTarget).data(), $(ev.target).data()); const context = {}; - if (data.filter === 'my') { - context['search_default_activities_overdue'] = 1; - context['search_default_activities_today'] = 1; + if (data.filter === "my") { + context["search_default_activities_overdue"] = 1; + context["search_default_activities_today"] = 1; } else { - context['search_default_activities_' + data.filter] = 1; + context["search_default_activities_" + data.filter] = 1; } // Necessary because activity_ids of mail.activity.mixin has auto_join // So, duplicates are faking the count and "Load more" doesn't show up - context['force_search_count'] = 1; - let domain = [['activity_ids.user_id', '=', session.uid]]; + context["force_search_count"] = 1; + let domain = [["activity_ids.user_id", "=", session.uid]]; if (data.domain) { domain = domain.concat(data.domain); } - this.env.services['action'].doAction( + this.env.services["action"].doAction( { context, domain, name: data.model_name, res_model: data.res_model, search_view_id: [false], - type: 'ir.actions.act_window', - views: this.activityGroup.irModel.availableWebViews.map(viewName => [false, viewName]), + type: "ir.actions.act_window", + views: this.activityGroup.irModel.availableWebViews.map((viewName) => [ + false, + viewName, + ]), }, { clearBreadcrumbs: true, - }, + } ); }, }, fields: { - activityGroup: one('ActivityGroup', { identifying: true, inverse: 'activityGroupViews' }), - activityMenuViewOwner: one('ActivityMenuView', { identifying: true, inverse: 'activityGroupViews' }), + activityGroup: one("ActivityGroup", { identifying: true, inverse: "activityGroupViews" }), + activityMenuViewOwner: one("ActivityMenuView", { + identifying: true, + inverse: "activityGroupViews", + }), }, }); diff --git a/addons/mail/static/src/models/activity_list_view.js b/addons/mail/static/src/models/activity_list_view.js index 6578dd64ba9..eed8dc70de4 100644 --- a/addons/mail/static/src/models/activity_list_view.js +++ b/addons/mail/static/src/models/activity_list_view.js @@ -1,29 +1,34 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; -import session from 'web.session'; +import session from "web.session"; Model({ - name: 'ActivityListView', - template: 'mail.ActivityListView', + name: "ActivityListView", + template: "mail.ActivityListView", lifecycleHooks: { async _created() { if (this.activities.length === 0) { return; } const messaging = this.messaging; - const activitiesData = await this.messaging.rpc({ - model: 'mail.activity', - method: 'activity_format', - args: [this.activities.map(activity => activity.id)], - kwargs: { context: session.user_context }, - }, { shadow: true }); + const activitiesData = await this.messaging.rpc( + { + model: "mail.activity", + method: "activity_format", + args: [this.activities.map((activity) => activity.id)], + kwargs: { context: session.user_context }, + }, + { shadow: true } + ); if (!messaging.exists()) { return; } - messaging.models['Activity'].insert( - activitiesData.map(activityData => messaging.models['Activity'].convertData(activityData)) + messaging.models["Activity"].insert( + activitiesData.map((activityData) => + messaging.models["Activity"].convertData(activityData) + ) ); }, }, @@ -32,54 +37,65 @@ Model({ const reloadFunc = this.reloadFunc; const thread = this.thread; const webRecord = this.webRecord; - this.messaging.openActivityForm({ - defaultActivityTypeId: this.popoverViewOwner.activityCellViewOwnerAsActivityList - ? this.popoverViewOwner.activityCellViewOwnerAsActivityList.activityType.id - : undefined, - thread, - }).then(() => { - thread.fetchData(['activities']); - if (reloadFunc) { - reloadFunc(); - } - if (webRecord) { - webRecord.model.load({ resId: thread.id }); - } - }); + this.messaging + .openActivityForm({ + defaultActivityTypeId: this.popoverViewOwner.activityCellViewOwnerAsActivityList + ? this.popoverViewOwner.activityCellViewOwnerAsActivityList.activityType.id + : undefined, + thread, + }) + .then(() => { + thread.fetchData(["activities"]); + if (reloadFunc) { + reloadFunc(); + } + if (webRecord) { + webRecord.model.load({ resId: thread.id }); + } + }); this.popoverViewOwner.delete(); }, }, fields: { - activities: many('Activity', { + activities: many("Activity", { compute() { if (this.popoverViewOwner.activityCellViewOwnerAsActivityList) { - return this.popoverViewOwner.activityCellViewOwnerAsActivityList.filteredActivities; + return this.popoverViewOwner.activityCellViewOwnerAsActivityList + .filteredActivities; } return this.thread && this.thread.activities; }, sort: [ - ['truthy-first', 'dateDeadline'], - ['case-insensitive-asc', 'dateDeadline'], + ["truthy-first", "dateDeadline"], + ["case-insensitive-asc", "dateDeadline"], ], }), - activityListViewItems: many('ActivityListViewItem', { inverse: 'activityListViewOwner', + activityListViewItems: many("ActivityListViewItem", { + inverse: "activityListViewOwner", compute() { - return this.activities.map(activity => { + return this.activities.map((activity) => { return { activity, }; }); }, }), - overdueActivityListViewItems: many('ActivityListViewItem', { inverse: 'activityListViewOwnerAsOverdue' }), - plannedActivityListViewItems: many('ActivityListViewItem', { inverse: 'activityListViewOwnerAsPlanned' }), - popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'activityListView' }), + overdueActivityListViewItems: many("ActivityListViewItem", { + inverse: "activityListViewOwnerAsOverdue", + }), + plannedActivityListViewItems: many("ActivityListViewItem", { + inverse: "activityListViewOwnerAsPlanned", + }), + popoverViewOwner: one("PopoverView", { identifying: true, inverse: "activityListView" }), reloadFunc: attr({ compute() { - return this.popoverViewOwner.activityCellViewOwnerAsActivityList ? this.popoverViewOwner.activityCellViewOwnerAsActivityList.reloadFunc : clear(); + return this.popoverViewOwner.activityCellViewOwnerAsActivityList + ? this.popoverViewOwner.activityCellViewOwnerAsActivityList.reloadFunc + : clear(); }, }), - thread: one('Thread', { required: true, + thread: one("Thread", { + required: true, compute() { if (this.popoverViewOwner.activityButtonViewOwnerAsActivityList) { return this.popoverViewOwner.activityButtonViewOwnerAsActivityList.thread; @@ -90,7 +106,9 @@ Model({ return clear(); }, }), - todayActivityListViewItems: many('ActivityListViewItem', { inverse: 'activityListViewOwnerAsToday' }), + todayActivityListViewItems: many("ActivityListViewItem", { + inverse: "activityListViewOwnerAsToday", + }), webRecord: attr({ compute() { if (this.popoverViewOwner.activityButtonViewOwnerAsActivityList) { diff --git a/addons/mail/static/src/models/activity_list_view_item.js b/addons/mail/static/src/models/activity_list_view_item.js index 97d63c6b086..2d651f05a3e 100644 --- a/addons/mail/static/src/models/activity_list_view_item.js +++ b/addons/mail/static/src/models/activity_list_view_item.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; -import { auto_str_to_date } from 'web.time'; -import { sprintf } from '@web/core/utils/strings'; +import { auto_str_to_date } from "web.time"; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'ActivityListViewItem', - template: 'mail.ActivityListViewItem', + name: "ActivityListViewItem", + template: "mail.ActivityListViewItem", recordMethods: { onClickEditActivityButton() { const popoverViewOwner = this.activityListViewOwner.popoverViewOwner; @@ -36,24 +36,33 @@ Model({ }, }, fields: { - activity: one('Activity', { identifying: true }), - activityListViewOwner: one('ActivityListView', { identifying: true, inverse: 'activityListViewItems' }), - activityListViewOwnerAsOverdue: one('ActivityListView', { inverse: 'overdueActivityListViewItems', + activity: one("Activity", { identifying: true }), + activityListViewOwner: one("ActivityListView", { + identifying: true, + inverse: "activityListViewItems", + }), + activityListViewOwnerAsOverdue: one("ActivityListView", { + inverse: "overdueActivityListViewItems", compute() { - return this.activity.state === 'overdue' ? this.activityListViewOwner : clear(); + return this.activity.state === "overdue" ? this.activityListViewOwner : clear(); }, }), - activityListViewOwnerAsPlanned: one('ActivityListView', { inverse: 'plannedActivityListViewItems', + activityListViewOwnerAsPlanned: one("ActivityListView", { + inverse: "plannedActivityListViewItems", compute() { - return this.activity.state === 'planned' ? this.activityListViewOwner : clear(); + return this.activity.state === "planned" ? this.activityListViewOwner : clear(); }, }), - activityListViewOwnerAsToday: one('ActivityListView', { inverse: 'todayActivityListViewItems', + activityListViewOwnerAsToday: one("ActivityListView", { + inverse: "todayActivityListViewItems", compute() { - return this.activity.state === 'today' ? this.activityListViewOwner : clear(); + return this.activity.state === "today" ? this.activityListViewOwner : clear(); }, }), - clockWatcher: one('ClockWatcher', { default: { clock: { frequency: 60 * 1000 } }, inverse: 'activityListViewItemOwner' }), + clockWatcher: one("ClockWatcher", { + default: { clock: { frequency: 60 * 1000 } }, + inverse: "activityListViewItemOwner", + }), /** * Compute the label for "when" the activity is due. */ @@ -65,10 +74,10 @@ Model({ if (!this.clockWatcher.clock.date) { return clear(); } - const today = moment(this.clockWatcher.clock.date.getTime()).startOf('day'); + const today = moment(this.clockWatcher.clock.date.getTime()).startOf("day"); const momentDeadlineDate = moment(auto_str_to_date(this.activity.dateDeadline)); // true means no rounding - const diff = momentDeadlineDate.diff(today, 'days', true); + const diff = momentDeadlineDate.diff(today, "days", true); if (diff === 0) { return this.env._t("Today"); } else if (diff === -1) { @@ -82,14 +91,15 @@ Model({ } }, }), - fileUploader: one('FileUploader', { inverse: 'activityListViewItemOwner', + fileUploader: one("FileUploader", { + inverse: "activityListViewItemOwner", compute() { - return this.activity.category === 'upload_file' ? {} : clear(); + return this.activity.category === "upload_file" ? {} : clear(); }, }), hasEditButton: attr({ compute() { - return this.activity.chaining_type === 'suggest' && this.activity.canWrite; + return this.activity.chaining_type === "suggest" && this.activity.canWrite; }, }), hasMarkDoneButton: attr({ @@ -97,20 +107,27 @@ Model({ return !this.fileUploader; }, }), - mailTemplateViews: many('MailTemplateView', { inverse: 'activityListViewItemOwner', + mailTemplateViews: many("MailTemplateView", { + inverse: "activityListViewItemOwner", compute() { - return this.activity.mailTemplates.map(mailTemplate => ({ mailTemplate })); + return this.activity.mailTemplates.map((mailTemplate) => ({ mailTemplate })); }, }), - markDoneView: one('ActivityMarkDonePopoverContentView', { inverse: 'activityListViewItemOwner' }), + markDoneView: one("ActivityMarkDonePopoverContentView", { + inverse: "activityListViewItemOwner", + }), reloadFunc: attr({ compute() { - return this.activityListViewOwner.reloadFunc ? this.activityListViewOwner.reloadFunc : clear(); + return this.activityListViewOwner.reloadFunc + ? this.activityListViewOwner.reloadFunc + : clear(); }, }), webRecord: attr({ compute() { - return this.activityListViewOwner.webRecord ? this.activityListViewOwner.webRecord : clear(); + return this.activityListViewOwner.webRecord + ? this.activityListViewOwner.webRecord + : clear(); }, }), }, diff --git a/addons/mail/static/src/models/activity_mark_done_popover_content_view.js b/addons/mail/static/src/models/activity_mark_done_popover_content_view.js index 40992933cab..262cce4ca4e 100644 --- a/addons/mail/static/src/models/activity_mark_done_popover_content_view.js +++ b/addons/mail/static/src/models/activity_mark_done_popover_content_view.js @@ -1,16 +1,16 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; -import { onMounted } from '@odoo/owl'; +import { onMounted } from "@odoo/owl"; Model({ - name: 'ActivityMarkDonePopoverContentView', - template: 'mail.ActivityMarkDonePopoverContentView', + name: "ActivityMarkDonePopoverContentView", + template: "mail.ActivityMarkDonePopoverContentView", componentSetup() { onMounted(this.onMounted); }, - identifyingMode: 'xor', + identifyingMode: "xor", recordMethods: { /** * Handles blur on this feedback textarea. @@ -62,7 +62,9 @@ Model({ const reloadFunc = this.reloadFunc; const webRecord = this.webRecord; const thread = this.activity.thread; - const activityListViewOwner = this.activityListViewItemOwner && this.activityListViewItemOwner.activityListViewOwner; + const activityListViewOwner = + this.activityListViewItemOwner && + this.activityListViewItemOwner.activityListViewOwner; const activity = this.activity; const feedback = this.feedbackTextareaRef.el.value; if (activityListViewOwner && activityListViewOwner.exists()) { @@ -83,7 +85,7 @@ Model({ * Handles keydown on this activity mark done. */ onKeydown(ev) { - if (ev.key === 'Escape') { + if (ev.key === "Escape") { this._close(); } }, @@ -111,7 +113,8 @@ Model({ }, }, fields: { - activity: one('Activity', { required: true, + activity: one("Activity", { + required: true, compute() { if (this.activityListViewItemOwner) { return this.activityListViewItemOwner.activity; @@ -122,8 +125,11 @@ Model({ return clear(); }, }), - activityListViewItemOwner: one('ActivityListViewItem', { identifying: true, inverse: 'markDoneView', }), - activityViewOwner: one('ActivityView', { + activityListViewItemOwner: one("ActivityListViewItem", { + identifying: true, + inverse: "markDoneView", + }), + activityViewOwner: one("ActivityView", { compute() { if (this.popoverViewOwner && this.popoverViewOwner.activityViewOwnerAsMarkDone) { return this.popoverViewOwner.activityViewOwnerAsMarkDone; @@ -131,7 +137,7 @@ Model({ return clear(); }, }), - feedbackTextareaRef: attr({ ref: 'feedbackTextarea' }), + feedbackTextareaRef: attr({ ref: "feedbackTextarea" }), hasHeader: attr({ compute() { return Boolean(this.popoverViewOwner); @@ -145,7 +151,10 @@ Model({ return this.env._t("Mark Done"); }, }), - popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'activityMarkDonePopoverContentView', }), + popoverViewOwner: one("PopoverView", { + identifying: true, + inverse: "activityMarkDonePopoverContentView", + }), reloadFunc: attr({ compute() { if (this.activityListViewItemOwner) { diff --git a/addons/mail/static/src/models/activity_menu_view.js b/addons/mail/static/src/models/activity_menu_view.js index ead1d6b9783..a69a1a96966 100644 --- a/addons/mail/static/src/models/activity_menu_view.js +++ b/addons/mail/static/src/models/activity_menu_view.js @@ -1,18 +1,18 @@ /** @odoo-module **/ -import { attr, many, Model } from '@mail/model'; +import { attr, many, Model } from "@mail/model"; -import session from 'web.session'; +import session from "web.session"; Model({ - name: 'ActivityMenuView', + name: "ActivityMenuView", lifecycleHooks: { _created() { this.fetchData(); - document.addEventListener('click', this._onClickCaptureGlobal, true); + document.addEventListener("click", this._onClickCaptureGlobal, true); }, _willDelete() { - document.removeEventListener('click', this._onClickCaptureGlobal, true); + document.removeEventListener("click", this._onClickCaptureGlobal, true); }, }, recordMethods: { @@ -21,13 +21,15 @@ Model({ }, async fetchData() { const data = await this.messaging.rpc({ - model: 'res.users', - method: 'systray_get_activities', + model: "res.users", + method: "systray_get_activities", args: [], kwargs: { context: session.user_context }, }); this.update({ - activityGroups: data.map(vals => this.messaging.models['ActivityGroup'].convertData(vals)), + activityGroups: data.map((vals) => + this.messaging.models["ActivityGroup"].convertData(vals) + ), extraCount: 0, }); }, @@ -63,12 +65,13 @@ Model({ }, }, fields: { - activityGroups: many('ActivityGroup', { - sort: [['smaller-first', 'irModel.id']], + activityGroups: many("ActivityGroup", { + sort: [["smaller-first", "irModel.id"]], }), - activityGroupViews: many('ActivityGroupView', { inverse: 'activityMenuViewOwner', + activityGroupViews: many("ActivityGroupView", { + inverse: "activityMenuViewOwner", compute() { - return this.activityGroups.map(activityGroup => { + return this.activityGroups.map((activityGroup) => { return { activityGroup, }; @@ -78,7 +81,10 @@ Model({ component: attr(), counter: attr({ compute() { - return this.activityGroups.reduce((total, group) => total + group.total_count, this.extraCount); + return this.activityGroups.reduce( + (total, group) => total + group.total_count, + this.extraCount + ); }, }), /** diff --git a/addons/mail/static/src/models/activity_type.js b/addons/mail/static/src/models/activity_type.js index 3af25d0e5b2..8fc9144ecb3 100644 --- a/addons/mail/static/src/models/activity_type.js +++ b/addons/mail/static/src/models/activity_type.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, many, Model } from '@mail/model'; +import { attr, many, Model } from "@mail/model"; Model({ - name: 'ActivityType', + name: "ActivityType", fields: { - activities: many('Activity', { inverse: 'type' }), + activities: many("Activity", { inverse: "type" }), displayName: attr(), id: attr({ identifying: true }), }, diff --git a/addons/mail/static/src/models/activity_view.js b/addons/mail/static/src/models/activity_view.js index 84284e97154..344e86c2750 100644 --- a/addons/mail/static/src/models/activity_view.js +++ b/addons/mail/static/src/models/activity_view.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; -import { auto_str_to_date, getLangDateFormat, getLangDatetimeFormat } from 'web.time'; -import { sprintf } from '@web/core/utils/strings'; +import { auto_str_to_date, getLangDateFormat, getLangDatetimeFormat } from "web.time"; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'ActivityView', - template: 'mail.ActivityView', + name: "ActivityView", + template: "mail.ActivityView", recordMethods: { /** * Handles the click on a link inside the activity. @@ -15,11 +15,7 @@ Model({ * @param {MouseEvent} ev */ onClickActivity(ev) { - if ( - ev.target.tagName === 'A' && - ev.target.dataset.oeId && - ev.target.dataset.oeModel - ) { + if (ev.target.tagName === "A" && ev.target.dataset.oeId && ev.target.dataset.oeModel) { this.messaging.openProfile({ id: Number(ev.target.dataset.oeId), model: ev.target.dataset.oeModel, @@ -67,7 +63,7 @@ Model({ }, }, fields: { - activity: one('Activity', { identifying: true, inverse: 'activityViews' }), + activity: one("Activity", { identifying: true, inverse: "activityViews" }), /** * Determines whether the details are visible. */ @@ -83,8 +79,11 @@ Model({ return sprintf(this.env._t("for %s"), this.activity.assignee.nameOrDisplayName); }, }), - chatterOwner: one('Chatter', { identifying: true, inverse: 'activityViews' }), - clockWatcher: one('ClockWatcher', { default: { clock: { frequency: 60 * 1000 } }, inverse: 'activityViewOwner' }), + chatterOwner: one("Chatter", { identifying: true, inverse: "activityViews" }), + clockWatcher: one("ClockWatcher", { + default: { clock: { frequency: 60 * 1000 } }, + inverse: "activityViewOwner", + }), /** * Compute the label for "when" the activity is due. */ @@ -96,10 +95,10 @@ Model({ if (!this.clockWatcher.clock.date) { return clear(); } - const today = moment(this.clockWatcher.clock.date.getTime()).startOf('day'); + const today = moment(this.clockWatcher.clock.date.getTime()).startOf("day"); const momentDeadlineDate = moment(auto_str_to_date(this.activity.dateDeadline)); // true means no rounding - const diff = momentDeadlineDate.diff(today, 'days', true); + const diff = momentDeadlineDate.diff(today, "days", true); if (diff === 0) { return this.env._t("Today:"); } else if (diff === -1) { @@ -113,9 +112,10 @@ Model({ } }, }), - fileUploader: one('FileUploader', { inverse: 'activityView', + fileUploader: one("FileUploader", { + inverse: "activityView", compute() { - return this.activity.category === 'upload_file' ? {} : clear(); + return this.activity.category === "upload_file" ? {} : clear(); }, }), /** @@ -144,13 +144,14 @@ Model({ return momentDeadlineDate.format(datetimeFormat); }, }), - mailTemplateViews: many('MailTemplateView', { inverse: 'activityViewOwner', + mailTemplateViews: many("MailTemplateView", { + inverse: "activityViewOwner", compute() { - return this.activity.mailTemplates.map(mailTemplate => ({ mailTemplate })); + return this.activity.mailTemplates.map((mailTemplate) => ({ mailTemplate })); }, }), - markDoneButtonRef: attr({ ref: 'markDoneButton' }), - markDonePopoverView: one('PopoverView', { inverse: 'activityViewOwnerAsMarkDone', }), + markDoneButtonRef: attr({ ref: "markDoneButton" }), + markDonePopoverView: one("PopoverView", { inverse: "activityViewOwnerAsMarkDone" }), /** * Label for mark as done. This is just for translations purpose. */ diff --git a/addons/mail/static/src/models/attachment.js b/addons/mail/static/src/models/attachment.js index 5771320373a..a4522a6676d 100644 --- a/addons/mail/static/src/models/attachment.js +++ b/addons/mail/static/src/models/attachment.js @@ -1,19 +1,19 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'Attachment', + name: "Attachment", recordMethods: { /** * Send the attachment for the browser to download. */ download() { - const downloadLink = document.createElement('a'); - downloadLink.setAttribute('href', this.downloadUrl); + const downloadLink = document.createElement("a"); + downloadLink.setAttribute("href", this.downloadUrl); // Adding 'download' attribute into a link prevents open a new tab or change the current location of the window. // This avoids interrupting the activity in the page such as rtc call. - downloadLink.setAttribute('download', ''); + downloadLink.setAttribute("download", ""); downloadLink.click(); }, /** @@ -35,13 +35,16 @@ Model({ if (!this.isUploading) { this.update({ isUnlinkPending: true }); try { - await this.messaging.rpc({ - route: `/mail/attachment/delete`, - params: { - access_token: this.accessToken, - attachment_id: this.id, + await this.messaging.rpc( + { + route: `/mail/attachment/delete`, + params: { + access_token: this.accessToken, + attachment_id: this.id, + }, }, - }, { shadow: true }); + { shadow: true } + ); } finally { if (this.exists()) { this.update({ isUnlinkPending: false }); @@ -53,24 +56,24 @@ Model({ if (!this.exists()) { return; } - this.messaging.messagingBus.trigger('o-attachment-deleted', { attachment: this }); + this.messaging.messagingBus.trigger("o-attachment-deleted", { attachment: this }); this.delete(); }, }, fields: { accessToken: attr(), - activities: many('Activity', { inverse: 'attachments' }), - allThreads: many('Thread', { inverse: 'allAttachments', readonly: true }), + activities: many("Activity", { inverse: "attachments" }), + allThreads: many("Thread", { inverse: "allAttachments", readonly: true }), /** * States the attachment lists that are displaying this attachment. */ - attachmentLists: many('AttachmentList', { inverse: 'attachments' }), - attachmentViewerViewable: one('AttachmentViewerViewable', { inverse: 'attachmentOwner' }), + attachmentLists: many("AttachmentList", { inverse: "attachments" }), + attachmentViewerViewable: one("AttachmentViewerViewable", { inverse: "attachmentOwner" }), checksum: attr(), /** * States on which composer this attachment is currently being created. */ - composer: one('Composer', { inverse: 'attachments' }), + composer: one("Composer", { inverse: "attachments" }), defaultSource: attr({ compute() { if (this.isImage) { @@ -78,28 +81,38 @@ Model({ } if (this.isPdf) { const pdf_lib = `/web/static/lib/pdfjs/web/viewer.html?file=`; - if (!this.accessToken && this.originThread && this.originThread.model === 'mail.channel') { + if ( + !this.accessToken && + this.originThread && + this.originThread.model === "mail.channel" + ) { return `${pdf_lib}/mail/channel/${this.originThread.id}/attachment/${this.id}`; } - const accessToken = this.accessToken ? `?access_token%3D${this.accessToken}` : ''; + const accessToken = this.accessToken + ? `?access_token%3D${this.accessToken}` + : ""; return `${pdf_lib}/web/content/${this.id}${accessToken}`; } if (this.isUrlYoutube) { - const urlArr = this.url.split('/'); + const urlArr = this.url.split("/"); let token = urlArr[urlArr.length - 1]; - if (token.includes('watch')) { - token = token.split('v=')[1]; - const amp = token.indexOf('&'); + if (token.includes("watch")) { + token = token.split("v=")[1]; + const amp = token.indexOf("&"); if (amp !== -1) { token = token.substring(0, amp); } } return `https://www.youtube.com/embed/${token}`; } - if (!this.accessToken && this.originThread && this.originThread.model === 'mail.channel') { + if ( + !this.accessToken && + this.originThread && + this.originThread.model === "mail.channel" + ) { return `/mail/channel/${this.originThread.id}/attachment/${this.id}`; } - const accessToken = this.accessToken ? `?access_token=${this.accessToken}` : ''; + const accessToken = this.accessToken ? `?access_token=${this.accessToken}` : ""; return `/web/content/${this.id}${accessToken}`; }, }), @@ -118,16 +131,20 @@ Model({ }), downloadUrl: attr({ compute() { - if (!this.accessToken && this.originThread && this.originThread.model === 'mail.channel') { + if ( + !this.accessToken && + this.originThread && + this.originThread.model === "mail.channel" + ) { return `/mail/channel/${this.originThread.id}/attachment/${this.id}?download=true`; } - const accessToken = this.accessToken ? `access_token=${this.accessToken}&` : ''; + const accessToken = this.accessToken ? `access_token=${this.accessToken}&` : ""; return `/web/content/ir.attachment/${this.id}/datas?${accessToken}download=true`; }, }), extension: attr({ compute() { - const extension = this.filename && this.filename.split('.').pop(); + const extension = this.filename && this.filename.split(".").pop(); if (extension) { return extension; } @@ -145,12 +162,18 @@ Model({ return false; } - if (this.messages.length && this.originThread && this.originThread.model === 'mail.channel') { - return this.messages.some(message => ( - message.canBeDeleted || - (message.author && message.author === this.messaging.currentPartner) || - (message.guestAuthor && message.guestAuthor === this.messaging.currentGuest) - )); + if ( + this.messages.length && + this.originThread && + this.originThread.model === "mail.channel" + ) { + return this.messages.some( + (message) => + message.canBeDeleted || + (message.author && message.author === this.messaging.currentPartner) || + (message.guestAuthor && + message.guestAuthor === this.messaging.currentGuest) + ); } return true; }, @@ -161,13 +184,13 @@ Model({ isImage: attr({ compute() { const imageMimetypes = [ - 'image/bmp', - 'image/gif', - 'image/jpeg', - 'image/png', - 'image/svg+xml', - 'image/tiff', - 'image/x-icon', + "image/bmp", + "image/gif", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/tiff", + "image/x-icon", ]; return imageMimetypes.includes(this.mimetype); }, @@ -177,7 +200,7 @@ Model({ */ isPdf: attr({ compute() { - return this.mimetype === 'application/pdf'; + return this.mimetype === "application/pdf"; }, }), /** @@ -186,11 +209,11 @@ Model({ isText: attr({ compute() { const textMimeType = [ - 'application/javascript', - 'application/json', - 'text/css', - 'text/html', - 'text/plain', + "application/javascript", + "application/json", + "text/css", + "text/html", + "text/plain", ]; return textMimeType.includes(this.mimetype); }, @@ -205,7 +228,7 @@ Model({ */ isUrl: attr({ compute() { - return this.type === 'url' && this.url; + return this.type === "url" && this.url; }, }), /** @@ -213,7 +236,7 @@ Model({ */ isUrlYoutube: attr({ compute() { - return !!this.url && this.url.includes('youtu'); + return !!this.url && this.url.includes("youtu"); }, }), /** @@ -222,17 +245,19 @@ Model({ isVideo: attr({ compute() { const videoMimeTypes = [ - 'audio/mpeg', - 'video/x-matroska', - 'video/mp4', - 'video/webm', + "audio/mpeg", + "video/x-matroska", + "video/mp4", + "video/webm", ]; return videoMimeTypes.includes(this.mimetype); }, }), isViewable: attr({ compute() { - return this.isText || this.isImage || this.isVideo || this.isPdf || this.isUrlYoutube; + return ( + this.isText || this.isImage || this.isVideo || this.isPdf || this.isUrlYoutube + ); }, }), /** @@ -240,17 +265,20 @@ Model({ */ mediaType: attr({ compute() { - return this.mimetype && this.mimetype.split('/').shift(); + return this.mimetype && this.mimetype.split("/").shift(); }, }), - messages: many('Message', { inverse: 'attachments' }), - mimetype: attr({ default: '' }), + messages: many("Message", { inverse: "attachments" }), + mimetype: attr({ default: "" }), name: attr(), - originThread: one('Thread', { inverse: 'originThreadAttachments' }), + originThread: one("Thread", { inverse: "originThreadAttachments" }), size: attr(), - threadsAsAttachmentsInWebClientView: many('Thread', { inverse: 'attachmentsInWebClientView', + threadsAsAttachmentsInWebClientView: many("Thread", { + inverse: "attachmentsInWebClientView", compute() { - return (this.isPdf || this.isImage) && !this.isUploading ? this.allThreads : clear(); + return (this.isPdf || this.isImage) && !this.isUploading + ? this.allThreads + : clear(); }, }), type: attr(), @@ -264,8 +292,8 @@ Model({ if (!this.uploadingAbortController) { const abortController = new AbortController(); abortController.signal.onabort = () => { - this.messaging.messagingBus.trigger('o-attachment-upload-abort', { - attachment: this + this.messaging.messagingBus.trigger("o-attachment-upload-abort", { + attachment: this, }); }; return abortController; diff --git a/addons/mail/static/src/models/attachment_card.js b/addons/mail/static/src/models/attachment_card.js index 711f170c2ae..505e21451ae 100644 --- a/addons/mail/static/src/models/attachment_card.js +++ b/addons/mail/static/src/models/attachment_card.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'AttachmentCard', - template: 'mail.AttachmentCard', + name: "AttachmentCard", + template: "mail.AttachmentCard", recordMethods: { /** * Opens the attachment viewer when clicking on viewable attachment. @@ -39,12 +39,14 @@ Model({ /** * Determines the attachment of this card. */ - attachment: one('Attachment', { identifying: true }), - attachmentDeleteConfirmDialog: one('Dialog', { inverse: 'attachmentCardOwnerAsAttachmentDeleteConfirm' }), + attachment: one("Attachment", { identifying: true }), + attachmentDeleteConfirmDialog: one("Dialog", { + inverse: "attachmentCardOwnerAsAttachmentDeleteConfirm", + }), /** * States the attachmentList displaying this card. */ - attachmentList: one('AttachmentList', { identifying: true, inverse: 'attachmentCards' }), + attachmentList: one("AttachmentList", { identifying: true, inverse: "attachmentCards" }), hasMultipleActions: attr({ compute() { return this.attachment.isDeletable && !this.attachmentList.composerViewOwner; diff --git a/addons/mail/static/src/models/attachment_delete_confirm_view.js b/addons/mail/static/src/models/attachment_delete_confirm_view.js index 98ed3434c66..498129acdbc 100644 --- a/addons/mail/static/src/models/attachment_delete_confirm_view.js +++ b/addons/mail/static/src/models/attachment_delete_confirm_view.js @@ -1,14 +1,14 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { attr, clear, one, Model } from '@mail/model'; -import { sprintf } from '@web/core/utils/strings'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { attr, clear, one, Model } from "@mail/model"; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'AttachmentDeleteConfirmView', - template: 'mail.AttachmentDeleteConfirmView', + name: "AttachmentDeleteConfirmView", + template: "mail.AttachmentDeleteConfirmView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); }, recordMethods: { /** @@ -18,7 +18,9 @@ Model({ * @returns {boolean} */ containsElement(element) { - return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element)); + return Boolean( + this.component && this.component.root.el && this.component.root.el.contains(element) + ); }, onClickCancel() { this.dialogOwner.delete(); @@ -32,40 +34,55 @@ Model({ }, }, fields: { - attachment: one('Attachment', { required: true, + attachment: one("Attachment", { + required: true, compute() { - if (this.dialogOwner && this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm) { + if ( + this.dialogOwner && + this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm + ) { return this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachment; } - if (this.dialogOwner && this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm) { - return this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachment; + if ( + this.dialogOwner && + this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm + ) { + return this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm + .attachment; } return clear(); }, }), body: attr({ compute() { - return sprintf(this.env._t(`Do you really want to delete "%s"?`), this.attachment.displayName); + return sprintf( + this.env._t(`Do you really want to delete "%s"?`), + this.attachment.displayName + ); }, }), - chatter: one('Chatter', { + chatter: one("Chatter", { compute() { if ( this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm && - this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachmentList.chatterOwner + this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachmentList + .chatterOwner ) { - return this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachmentList.chatterOwner; + return this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm + .attachmentList.chatterOwner; } if ( this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm && - this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachmentList.chatterOwner + this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachmentList + .chatterOwner ) { - return this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachmentList.chatterOwner; + return this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm + .attachmentList.chatterOwner; } return clear(); }, }), component: attr(), - dialogOwner: one('Dialog', { identifying: true, inverse: 'attachmentDeleteConfirmView' }), + dialogOwner: one("Dialog", { identifying: true, inverse: "attachmentDeleteConfirmView" }), }, }); diff --git a/addons/mail/static/src/models/attachment_image.js b/addons/mail/static/src/models/attachment_image.js index fc40107f08b..b92d1e663d5 100644 --- a/addons/mail/static/src/models/attachment_image.js +++ b/addons/mail/static/src/models/attachment_image.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; -import { isEventHandled, markEventHandled } from '@mail/utils/utils'; +import { attr, clear, one, Model } from "@mail/model"; +import { isEventHandled, markEventHandled } from "@mail/utils/utils"; Model({ - name: 'AttachmentImage', - template: 'mail.AttachmentImage', + name: "AttachmentImage", + template: "mail.AttachmentImage", recordMethods: { /** * Called when clicking on download icon. @@ -13,7 +13,7 @@ Model({ * @param {MouseEvent} ev */ onClickDownload(ev) { - markEventHandled(ev, 'AttachmentImage.onClickDownload'); + markEventHandled(ev, "AttachmentImage.onClickDownload"); if (!this.exists()) { return; } @@ -25,10 +25,10 @@ Model({ * @param {MouseEvent} ev */ onClickImage(ev) { - if (isEventHandled(ev, 'AttachmentImage.onClickDownload')) { + if (isEventHandled(ev, "AttachmentImage.onClickDownload")) { return; } - if (isEventHandled(ev, 'AttachmentImage.onClickUnlink')) { + if (isEventHandled(ev, "AttachmentImage.onClickUnlink")) { return; } if (!this.attachment || !this.attachment.isViewable) { @@ -45,7 +45,7 @@ Model({ * @param {MouseEvent} ev */ onClickUnlink(ev) { - markEventHandled(ev, 'AttachmentImage.onClickUnlink'); + markEventHandled(ev, "AttachmentImage.onClickUnlink"); if (!this.exists()) { return; } @@ -60,16 +60,19 @@ Model({ /** * Determines the attachment of this attachment image.. */ - attachment: one('Attachment', { identifying: true }), - attachmentDeleteConfirmDialog: one('Dialog', { inverse: 'attachmentImageOwnerAsAttachmentDeleteConfirm' }), + attachment: one("Attachment", { identifying: true }), + attachmentDeleteConfirmDialog: one("Dialog", { + inverse: "attachmentImageOwnerAsAttachmentDeleteConfirm", + }), /** * States the attachmentList displaying this attachment image. */ - attachmentList: one('AttachmentList', { identifying: true, inverse: 'attachmentImages' }), + attachmentList: one("AttachmentList", { identifying: true, inverse: "attachmentImages" }), /** * Determines whether `this` should display a download button. */ - hasDownloadButton: attr({ default: false, + hasDownloadButton: attr({ + default: false, compute() { if (!this.attachment || !this.attachmentList) { return clear(); @@ -80,7 +83,8 @@ Model({ /** * Determines the max height of this attachment image in px. */ - height: attr({ required: true, + height: attr({ + required: true, compute() { if (!this.attachmentList) { return clear(); @@ -101,17 +105,24 @@ Model({ if (!this.attachment) { return; } - if (!this.attachment.accessToken && this.attachment.originThread && this.attachment.originThread.model === 'mail.channel') { + if ( + !this.attachment.accessToken && + this.attachment.originThread && + this.attachment.originThread.model === "mail.channel" + ) { return `/mail/channel/${this.attachment.originThread.id}/image/${this.attachment.id}/${this.width}x${this.height}`; } - const accessToken = this.attachment.accessToken ? `?access_token=${this.attachment.accessToken}` : ''; + const accessToken = this.attachment.accessToken + ? `?access_token=${this.attachment.accessToken}` + : ""; return `/web/image/${this.attachment.id}/${this.width}x${this.height}${accessToken}`; }, }), /** * Determines the max width of this attachment image in px. */ - width: attr({ required: true, + width: attr({ + required: true, /** * Returns an arbitrary high value, this is effectively a max-width and * the height should be more constrained. diff --git a/addons/mail/static/src/models/attachment_list.js b/addons/mail/static/src/models/attachment_list.js index 8a572319b62..8e7d38b67e5 100644 --- a/addons/mail/static/src/models/attachment_list.js +++ b/addons/mail/static/src/models/attachment_list.js @@ -1,17 +1,19 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'AttachmentList', - template: 'mail.AttachmentList', - identifyingMode: 'xor', + name: "AttachmentList", + template: "mail.AttachmentList", + identifyingMode: "xor", recordMethods: { /** * Select the next attachment. */ selectNextAttachment() { - const index = this.attachments.findIndex(attachment => attachment === this.selectedAttachment); + const index = this.attachments.findIndex( + (attachment) => attachment === this.selectedAttachment + ); const nextIndex = index === this.attachments.length - 1 ? 0 : index + 1; this.update({ selectedAttachment: this.attachments[nextIndex] }); }, @@ -19,7 +21,9 @@ Model({ * Select the previous attachment. */ selectPreviousAttachment() { - const index = this.attachments.findIndex(attachment => attachment === this.selectedAttachment); + const index = this.attachments.findIndex( + (attachment) => attachment === this.selectedAttachment + ); const prevIndex = index === 0 ? this.attachments.length - 1 : index - 1; this.update({ selectedAttachment: this.attachments[prevIndex] }); }, @@ -28,28 +32,31 @@ Model({ /** * Link with a chatter to handle attachments. */ - chatterOwner: one('Chatter', { identifying: true, inverse: 'attachmentList' }), + chatterOwner: one("Chatter", { identifying: true, inverse: "attachmentList" }), /** * States the attachment cards that are displaying this nonImageAttachments. */ - attachmentCards: many('AttachmentCard', { inverse: 'attachmentList', + attachmentCards: many("AttachmentCard", { + inverse: "attachmentList", compute() { - return this.nonImageAttachments.map(attachment => ({ attachment })); + return this.nonImageAttachments.map((attachment) => ({ attachment })); }, }), /** * States the attachment images that are displaying this imageAttachments. */ - attachmentImages: many('AttachmentImage', { inverse: 'attachmentList', + attachmentImages: many("AttachmentImage", { + inverse: "attachmentList", compute() { - return this.imageAttachments.map(attachment => ({ attachment })); + return this.imageAttachments.map((attachment) => ({ attachment })); }, }), - attachmentListViewDialog: one('Dialog', { inverse: 'attachmentListOwnerAsAttachmentView' }), + attachmentListViewDialog: one("Dialog", { inverse: "attachmentListOwnerAsAttachmentView" }), /** * States the attachments to be displayed by this attachment list. */ - attachments: many('Attachment', { inverse: 'attachmentLists', + attachments: many("Attachment", { + inverse: "attachmentLists", compute() { if (this.messageViewOwner) { return this.messageViewOwner.message.attachments; @@ -66,13 +73,13 @@ Model({ /** * Link with a composer view to handle attachments. */ - composerViewOwner: one('ComposerView', { identifying: true, inverse: 'attachmentList' }), + composerViewOwner: one("ComposerView", { identifying: true, inverse: "attachmentList" }), /** * States the attachment that are an image. */ - imageAttachments: many('Attachment', { + imageAttachments: many("Attachment", { compute() { - return this.attachments.filter(attachment => attachment.isImage); + return this.attachments.filter((attachment) => attachment.isImage); }, }), /** @@ -82,7 +89,7 @@ Model({ compute() { return Boolean( (this.messageViewOwner && this.messageViewOwner.isInDiscuss) || - (this.composerViewOwner && this.composerViewOwner.isInDiscuss) + (this.composerViewOwner && this.composerViewOwner.isInDiscuss) ); }, }), @@ -93,7 +100,7 @@ Model({ compute() { return Boolean( (this.messageViewOwner && this.messageViewOwner.isInChatWindow) || - (this.composerViewOwner && this.composerViewOwner.isInChatWindow) + (this.composerViewOwner && this.composerViewOwner.isInChatWindow) ); }, }), @@ -104,7 +111,7 @@ Model({ compute() { return Boolean( (this.messageViewOwner && this.messageViewOwner.isInChatter) || - (this.composerViewOwner && this.composerViewOwner.isInChatter) + (this.composerViewOwner && this.composerViewOwner.isInChatter) ); }, }), @@ -115,7 +122,8 @@ Model({ compute() { return Boolean( this.composerViewOwner || - (this.messageViewOwner && this.messageViewOwner.message.isCurrentUserOrGuestAuthor) + (this.messageViewOwner && + this.messageViewOwner.message.isCurrentUserOrGuestAuthor) ); }, }), @@ -124,10 +132,7 @@ Model({ */ isInChatWindowAndIsAlignedRight: attr({ compute() { - return Boolean( - this.isInChatWindow && - this.isCurrentUserOrGuestAuthor - ); + return Boolean(this.isInChatWindow && this.isCurrentUserOrGuestAuthor); }, }), /** @@ -135,31 +140,28 @@ Model({ */ isInChatWindowAndIsAlignedLeft: attr({ compute() { - return Boolean( - this.isInChatWindow && - !this.isCurrentUserOrGuestAuthor - ); + return Boolean(this.isInChatWindow && !this.isCurrentUserOrGuestAuthor); }, }), /** * Link with a message view to handle attachments. */ - messageViewOwner: one('MessageView', { identifying: true, inverse: 'attachmentList' }), + messageViewOwner: one("MessageView", { identifying: true, inverse: "attachmentList" }), /** * States the attachment that are not an image. */ - nonImageAttachments: many('Attachment', { + nonImageAttachments: many("Attachment", { compute() { - return this.attachments.filter(attachment => !attachment.isImage); + return this.attachments.filter((attachment) => !attachment.isImage); }, }), - selectedAttachment: one('Attachment'), + selectedAttachment: one("Attachment"), /** * States the attachments that can be viewed inside the browser. */ - viewableAttachments: many('Attachment', { + viewableAttachments: many("Attachment", { compute() { - return this.attachments.filter(attachment => attachment.isViewable); + return this.attachments.filter((attachment) => attachment.isViewable); }, }), }, diff --git a/addons/mail/static/src/models/attachment_viewer.js b/addons/mail/static/src/models/attachment_viewer.js index baa26048545..50e19876012 100644 --- a/addons/mail/static/src/models/attachment_viewer.js +++ b/addons/mail/static/src/models/attachment_viewer.js @@ -1,18 +1,18 @@ /** @odoo-module **/ -import { attr, clear, increment, many, one, Model } from '@mail/model'; +import { attr, clear, increment, many, one, Model } from "@mail/model"; -import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs'; +import { hidePDFJSButtons } from "@web/legacy/js/libs/pdfjs"; Model({ - name: 'AttachmentViewer', - identifyingMode: 'xor', + name: "AttachmentViewer", + identifyingMode: "xor", lifecycleHooks: { _created() { - document.addEventListener('click', this._onClickGlobal); + document.addEventListener("click", this._onClickGlobal); }, _willDelete() { - document.removeEventListener('click', this._onClickGlobal); + document.removeEventListener("click", this._onClickGlobal); }, }, recordMethods: { @@ -29,7 +29,9 @@ Model({ * @returns {boolean} */ containsElement(element) { - return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element)); + return Boolean( + this.component && this.component.root.el && this.component.root.el.contains(element) + ); }, /** * Determine whether the current image is rendered for the 1st time, and if @@ -106,7 +108,7 @@ Model({ * Called when clicking on image. Stop propagation of event to prevent * closing the dialog. * - * @param {MouseEvent} ev + * @param {MouseEvent} ev */ onClickImage(ev) { if (this.isDragging) { @@ -192,28 +194,28 @@ Model({ */ onKeydown(ev) { switch (ev.key) { - case 'ArrowRight': + case "ArrowRight": this.next(); break; - case 'ArrowLeft': + case "ArrowLeft": this.previous(); break; - case 'Escape': + case "Escape": this.close(); break; - case 'q': + case "q": this.close(); break; - case 'r': + case "r": this.rotate(); break; - case '+': + case "+": this.zoomIn(); break; - case '-': + case "-": this.zoomOut(); break; - case '0': + case "0": this.resetZoom(); break; default: @@ -296,7 +298,7 @@ Model({ * Prompt the browser print of this attachment. */ print() { - const printWindow = window.open('about:blank', '_new'); + const printWindow = window.open("about:blank", "_new"); printWindow.document.open(); printWindow.document.write(` @@ -349,20 +351,21 @@ Model({ * defining them in the template, for performance reasons. */ updateZoomerStyle() { - const tx = this.imageRef.offsetWidth * this.scale > this.zoomerRef.el.offsetWidth - ? this.translate.x + this.translate.dx - : 0; - const ty = this.imageRef.offsetHeight * this.scale > this.zoomerRef.el.offsetHeight - ? this.translate.y + this.translate.dy - : 0; + const tx = + this.imageRef.offsetWidth * this.scale > this.zoomerRef.el.offsetWidth + ? this.translate.x + this.translate.dx + : 0; + const ty = + this.imageRef.offsetHeight * this.scale > this.zoomerRef.el.offsetHeight + ? this.translate.y + this.translate.dy + : 0; if (tx === 0) { this.translate.update({ x: 0 }); } if (ty === 0) { this.translate.update({ y: 0 }); } - this.zoomerRef.el.style = `transform: ` + - `translate(${tx}px, ${ty}px)`; + this.zoomerRef.el.style = `transform: ` + `translate(${tx}px, ${ty}px)`; }, /** * Zoom in the image. @@ -384,7 +387,8 @@ Model({ if (this.scale === this.minScale) { return; } - const unflooredAdaptedScale = this.scale - (scroll ? this.scrollZoomStep : this.zoomStep); + const unflooredAdaptedScale = + this.scale - (scroll ? this.scrollZoomStep : this.zoomStep); this.update({ scale: Math.max(this.minScale, unflooredAdaptedScale), }); @@ -410,7 +414,9 @@ Model({ * Angle of the image. Changes when the user rotates it. */ angle: attr({ default: 0 }), - attachmentList: one('AttachmentList', { related: 'dialogOwner.attachmentListOwnerAsAttachmentView' }), + attachmentList: one("AttachmentList", { + related: "dialogOwner.attachmentListOwnerAsAttachmentView", + }), attachmentViewerViewable: one("AttachmentViewerViewable", { compute() { if (this.attachmentList) { @@ -424,7 +430,7 @@ Model({ attachmentViewerViewables: many("AttachmentViewerViewable", { compute() { if (this.attachmentList) { - return this.attachmentList.viewableAttachments.map(attachment => { + return this.attachmentList.viewableAttachments.map((attachment) => { return { attachmentOwner: attachment }; }); } @@ -438,7 +444,11 @@ Model({ /** * Determines the dialog displaying this attachment viewer. */ - dialogOwner: one('Dialog', { identifying: true, inverse: 'attachmentViewer', isCausal: true }), + dialogOwner: one("Dialog", { + identifying: true, + inverse: "attachmentViewer", + isCausal: true, + }), dragStartX: attr({ default: 0 }), dragStartY: attr({ default: 0 }), /** @@ -451,18 +461,18 @@ Model({ */ imageStyle: attr({ compute() { - let style = `transform: ` + + let style = + `transform: ` + `scale3d(${this.scale}, ${this.scale}, 1) ` + `rotate(${this.angle}deg);`; if (this.angle % 180 !== 0) { - style += `` + + style += + `` + `max-height: ${window.innerWidth}px; ` + `max-width: ${window.innerHeight}px;`; } else { - style += `` + - `max-height: 100%; ` + - `max-width: 100%;`; + style += `` + `max-height: 100%; ` + `max-width: 100%;`; } return style; }, @@ -484,7 +494,7 @@ Model({ */ scale: attr({ default: 1 }), scrollZoomStep: attr({ default: 0.1, readonly: true }), - translate: one('AttachmentViewer.Translate', { default: {}, inverse: 'owner' }), + translate: one("AttachmentViewer.Translate", { default: {}, inverse: "owner" }), /** * Reference of the zoomer node. Useful to apply translate * transformation on image visualisation. diff --git a/addons/mail/static/src/models/attachment_viewer__translate.js b/addons/mail/static/src/models/attachment_viewer__translate.js index 49f7698f045..0f3d805c90f 100644 --- a/addons/mail/static/src/models/attachment_viewer__translate.js +++ b/addons/mail/static/src/models/attachment_viewer__translate.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; /** * Tracked translate transformations on image visualisation. This is @@ -10,11 +10,11 @@ import { attr, one, Model } from '@mail/model'; * position changes while dragging) */ Model({ - name: 'AttachmentViewer.Translate', + name: "AttachmentViewer.Translate", fields: { dx: attr({ default: 0 }), dy: attr({ default: 0 }), - owner: one('AttachmentViewer', { identifying: true, inverse: 'translate' }), + owner: one("AttachmentViewer", { identifying: true, inverse: "translate" }), x: attr({ default: 0 }), y: attr({ default: 0 }), }, diff --git a/addons/mail/static/src/models/attachment_viewer_viewable.js b/addons/mail/static/src/models/attachment_viewer_viewable.js index 72d0e8ffb56..80b2ef722d7 100644 --- a/addons/mail/static/src/models/attachment_viewer_viewable.js +++ b/addons/mail/static/src/models/attachment_viewer_viewable.js @@ -8,14 +8,17 @@ import { attr, one, Model } from "@mail/model"; */ Model({ name: "AttachmentViewerViewable", - identifyingMode: 'xor', + identifyingMode: "xor", recordMethods: { download() { return this.attachmentOwner.download(); }, }, fields: { - attachmentOwner: one("Attachment", { identifying: true, inverse: 'attachmentViewerViewable' }), + attachmentOwner: one("Attachment", { + identifying: true, + inverse: "attachmentViewerViewable", + }), defaultSource: attr({ compute() { return this.attachmentOwner.defaultSource; diff --git a/addons/mail/static/src/models/autocomplete_input_view.js b/addons/mail/static/src/models/autocomplete_input_view.js index ebedf6976be..b18d14a885b 100644 --- a/addons/mail/static/src/models/autocomplete_input_view.js +++ b/addons/mail/static/src/models/autocomplete_input_view.js @@ -1,15 +1,15 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { attr, clear, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { attr, clear, one, Model } from "@mail/model"; -import { onMounted, onWillUnmount } from '@odoo/owl'; +import { onMounted, onWillUnmount } from "@odoo/owl"; Model({ - name: 'AutocompleteInputView', - template: 'mail.AutocompleteInputView', + name: "AutocompleteInputView", + template: "mail.AutocompleteInputView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); onMounted(() => { if (!this.root.el) { return; @@ -28,12 +28,12 @@ Model({ html: this.isHtml, }; if (this.customClass) { - args.classes = { 'ui-autocomplete': this.customClass }; + args.classes = { "ui-autocomplete": this.customClass }; } const autoCompleteElem = $(this.root.el).autocomplete(args); // Resize the autocomplete dropdown options to handle the long strings // By setting the width of dropdown based on the width of the input element. - autoCompleteElem.data('ui-autocomplete')._resizeMenu = function () { + autoCompleteElem.data("ui-autocomplete")._resizeMenu = function () { const ul = this.menu.element; ul.outerWidth(this.element.outerWidth()); }; @@ -42,10 +42,10 @@ Model({ if (!this.root.el) { return; } - $(this.root.el).autocomplete('destroy'); + $(this.root.el).autocomplete("destroy"); }); }, - identifyingMode: 'xor', + identifyingMode: "xor", recordMethods: { onBlur() { if (!this.exists()) { @@ -83,7 +83,7 @@ Model({ if (!this.exists()) { return; } - if (ev.key === 'Escape') { + if (ev.key === "Escape") { this.onBlur(); } }, @@ -107,7 +107,10 @@ Model({ return; } if (this.messagingMenuOwnerAsMobileNewMessageInput) { - this.messagingMenuOwnerAsMobileNewMessageInput.onMobileNewMessageInputSelect(ev, ui); + this.messagingMenuOwnerAsMobileNewMessageInput.onMobileNewMessageInputSelect( + ev, + ui + ); return; } }, @@ -128,34 +131,57 @@ Model({ return; } if (this.discussViewOwnerAsMobileAddItemHeader) { - this.discussViewOwnerAsMobileAddItemHeader.onMobileAddItemHeaderInputSource(req, res); + this.discussViewOwnerAsMobileAddItemHeader.onMobileAddItemHeaderInputSource( + req, + res + ); return; } if (this.messagingMenuOwnerAsMobileNewMessageInput) { - this.messagingMenuOwnerAsMobileNewMessageInput.onMobileNewMessageInputSource(req, res); + this.messagingMenuOwnerAsMobileNewMessageInput.onMobileNewMessageInputSource( + req, + res + ); return; } }, }, fields: { - chatWindowOwnerAsNewMessage: one('ChatWindow', { identifying: true, inverse: 'newMessageAutocompleteInputView' }), + chatWindowOwnerAsNewMessage: one("ChatWindow", { + identifying: true, + inverse: "newMessageAutocompleteInputView", + }), component: attr(), - customClass: attr({ default: '', + customClass: attr({ + default: "", compute() { if (this.discussSidebarCategoryOwnerAsAddingItem) { - if (this.discussSidebarCategoryOwnerAsAddingItem === this.messaging.discuss.categoryChannel) { - return 'o_DiscussSidebarCategory_newChannelAutocompleteSuggestions'; + if ( + this.discussSidebarCategoryOwnerAsAddingItem === + this.messaging.discuss.categoryChannel + ) { + return "o_DiscussSidebarCategory_newChannelAutocompleteSuggestions"; } } if (this.messagingMenuOwnerAsMobileNewMessageInput) { - return this.messagingMenuOwnerAsMobileNewMessageInput.viewId + '_mobileNewMessageInputAutocomplete'; + return ( + this.messagingMenuOwnerAsMobileNewMessageInput.viewId + + "_mobileNewMessageInputAutocomplete" + ); } return clear(); }, }), - discussSidebarCategoryOwnerAsAddingItem: one('DiscussSidebarCategory', { identifying: true, inverse: 'addingItemAutocompleteInputView' }), - discussViewOwnerAsMobileAddItemHeader: one('DiscussView', { identifying: true, inverse: 'mobileAddItemHeaderAutocompleteInputView' }), - isFocusOnMount: attr({ default: false, + discussSidebarCategoryOwnerAsAddingItem: one("DiscussSidebarCategory", { + identifying: true, + inverse: "addingItemAutocompleteInputView", + }), + discussViewOwnerAsMobileAddItemHeader: one("DiscussView", { + identifying: true, + inverse: "mobileAddItemHeaderAutocompleteInputView", + }), + isFocusOnMount: attr({ + default: false, compute() { if (this.discussViewOwnerAsMobileAddItemHeader) { return true; @@ -169,18 +195,25 @@ Model({ return clear(); }, }), - isHtml: attr({ default: false, + isHtml: attr({ + default: false, compute() { if (this.discussViewOwnerAsMobileAddItemHeader) { return this.discussViewOwnerAsMobileAddItemHeader.isAddingChannel; } if (this.discussSidebarCategoryOwnerAsAddingItem) { - return this.discussSidebarCategoryOwnerAsAddingItem === this.messaging.discuss.categoryChannel; + return ( + this.discussSidebarCategoryOwnerAsAddingItem === + this.messaging.discuss.categoryChannel + ); } return clear(); }, }), - messagingMenuOwnerAsMobileNewMessageInput: one('MessagingMenu', { identifying: true, inverse: 'mobileNewMessageAutocompleteInputView' }), + messagingMenuOwnerAsMobileNewMessageInput: one("MessagingMenu", { + identifying: true, + inverse: "mobileNewMessageAutocompleteInputView", + }), placeholder: attr({ compute() { if (this.chatWindowOwnerAsNewMessage) { @@ -188,16 +221,19 @@ Model({ } if (this.discussViewOwnerAsMobileAddItemHeader) { if (this.discussViewOwnerAsMobileAddItemHeader.isAddingChannel) { - return this.discussViewOwnerAsMobileAddItemHeader.discuss.addChannelInputPlaceholder; + return this.discussViewOwnerAsMobileAddItemHeader.discuss + .addChannelInputPlaceholder; } else { - return this.discussViewOwnerAsMobileAddItemHeader.discuss.addChatInputPlaceholder; + return this.discussViewOwnerAsMobileAddItemHeader.discuss + .addChatInputPlaceholder; } } if (this.discussSidebarCategoryOwnerAsAddingItem) { return this.discussSidebarCategoryOwnerAsAddingItem.newItemPlaceholderText; } if (this.messagingMenuOwnerAsMobileNewMessageInput) { - return this.messagingMenuOwnerAsMobileNewMessageInput.mobileNewMessageInputPlaceholder; + return this.messagingMenuOwnerAsMobileNewMessageInput + .mobileNewMessageInputPlaceholder; } return clear(); }, diff --git a/addons/mail/static/src/models/blur_manager.js b/addons/mail/static/src/models/blur_manager.js index 92ffc009c6c..02e222a5b01 100644 --- a/addons/mail/static/src/models/blur_manager.js +++ b/addons/mail/static/src/models/blur_manager.js @@ -1,31 +1,37 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; function drawAndBlurImageOnCanvas(image, blurAmount, canvas) { canvas.width = image.width; canvas.height = image.height; if (blurAmount === 0) { - canvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height); + canvas.getContext("2d").drawImage(image, 0, 0, image.width, image.height); return; } - canvas.getContext('2d').clearRect(0, 0, image.width, image.height); - canvas.getContext('2d').save(); + canvas.getContext("2d").clearRect(0, 0, image.width, image.height); + canvas.getContext("2d").save(); // FIXME : Does not work on safari https://bugs.webkit.org/show_bug.cgi?id=198416 - canvas.getContext('2d').filter = `blur(${blurAmount}px)`; - canvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height); - canvas.getContext('2d').restore(); + canvas.getContext("2d").filter = `blur(${blurAmount}px)`; + canvas.getContext("2d").drawImage(image, 0, 0, image.width, image.height); + canvas.getContext("2d").restore(); } Model({ - name: 'BlurManager', + name: "BlurManager", lifecycleHooks: { _willDelete() { - this.video.removeEventListener('loadeddata', this._onVideoPlay); + this.video.removeEventListener("loadeddata", this._onVideoPlay); this.selfieSegmentation.reset(); this.video.srcObject = null; if (this.rejectStreamPromise) { - this.rejectStreamPromise(new Error(this.env._t('The blur manager was removed before the beginning of the blur process'))); + this.rejectStreamPromise( + new Error( + this.env._t( + "The blur manager was removed before the beginning of the blur process" + ) + ) + ); } }, }, @@ -37,8 +43,8 @@ Model({ * @private */ _drawWithCompositing(image, compositeOperation) { - this.canvas.getContext('2d').globalCompositeOperation = compositeOperation; - this.canvas.getContext('2d').drawImage(image, 0, 0); + this.canvas.getContext("2d").globalCompositeOperation = compositeOperation; + this.canvas.getContext("2d").drawImage(image, 0, 0); }, /** * @private @@ -63,7 +69,13 @@ Model({ this.video.srcObject = null; this.selfieSegmentation.reset(); if (this.rejectStreamPromise) { - this.rejectStreamPromise(new Error(this.env._t('The source stream was removed before the beginning of the blur process'))); + this.rejectStreamPromise( + new Error( + this.env._t( + "The source stream was removed before the beginning of the blur process" + ) + ) + ); } if (!this.srcStream) { return; @@ -88,9 +100,9 @@ Model({ modelSelection: 1, }); this.selfieSegmentation.onResults(this._onSelfieSegmentationResults); - this.video.addEventListener('loadeddata', this._onVideoPlay); + this.video.addEventListener("loadeddata", this._onVideoPlay); this.video.autoplay = true; - Promise.resolve(this.video.play()).catch(()=>{}); + Promise.resolve(this.video.play()).catch(() => {}); }, /** * @private @@ -121,32 +133,22 @@ Model({ drawAndBlurImageOnCanvas( results.image, this.userSetting.backgroundBlurAmount, - this.canvasBlur, + this.canvasBlur ); this.canvas.width = this.canvasBlur.width; this.canvas.height = this.canvasBlur.height; drawAndBlurImageOnCanvas( results.segmentationMask, this.userSetting.edgeBlurAmount, - this.canvasMask, + this.canvasMask ); - this.canvas.getContext('2d').save(); - this.canvas.getContext('2d').drawImage( - results.image, - 0, - 0, - this.canvas.width, - this.canvas.height, - ); - this._drawWithCompositing( - this.canvasMask, - 'destination-in', - ); - this._drawWithCompositing( - this.canvasBlur, - 'destination-over', - ); - this.canvas.getContext('2d').restore(); + this.canvas.getContext("2d").save(); + this.canvas + .getContext("2d") + .drawImage(results.image, 0, 0, this.canvas.width, this.canvas.height); + this._drawWithCompositing(this.canvasMask, "destination-in"); + this._drawWithCompositing(this.canvasBlur, "destination-over"); + this.canvas.getContext("2d").restore(); }, /** * @private @@ -171,20 +173,21 @@ Model({ }, }, fields: { - canvas: attr({ default: document.createElement('canvas') }), - canvasBlur: attr({ default: document.createElement('canvas') }), - canvasMask: attr({ default: document.createElement('canvas') }), - canvasStream: one('MediaStream', { isCausal: true, + canvas: attr({ default: document.createElement("canvas") }), + canvasBlur: attr({ default: document.createElement("canvas") }), + canvasMask: attr({ default: document.createElement("canvas") }), + canvasStream: one("MediaStream", { + isCausal: true, compute() { if (this.srcStream) { - this.canvas.getContext('2d'); // canvas.captureStream() doesn't work on firefox before getContext() is called. + this.canvas.getContext("2d"); // canvas.captureStream() doesn't work on firefox before getContext() is called. const webMediaStream = this.canvas.captureStream(); return { webMediaStream, id: webMediaStream.id }; } return clear(); }, }), - frameRequestTimer: one('Timer', { inverse: 'blurManagerOwnerAsFrameRequest' }), + frameRequestTimer: one("Timer", { inverse: "blurManagerOwnerAsFrameRequest" }), isVideoDataLoaded: attr({ default: false }), /** * promise reject function of this.stream promise @@ -194,7 +197,7 @@ Model({ * promise resolve function of this.stream promise */ resolveStreamPromise: attr(), - rtc: one('Rtc', { identifying: true, inverse: 'blurManager' }), + rtc: one("Rtc", { identifying: true, inverse: "blurManager" }), selfieSegmentation: attr({ default: new window.SelfieSegmentation({ locateFile: (file) => { @@ -205,27 +208,27 @@ Model({ /** * mail.MediaStream, source stream for which the blur effect is computed. */ - srcStream: one('MediaStream', { isCausal: true }), + srcStream: one("MediaStream", { isCausal: true }), /** * Promise or undefined, based on this.srcStream, resolved when selfieSegmentation has started painting on the canvas, * resolves into a web.MediaStream that is the blurred version of this.srcStream. */ stream: attr(), - userSetting: one('UserSetting', { related: 'messaging.userSetting' }), - video: attr({ default: document.createElement('video') }), + userSetting: one("UserSetting", { related: "messaging.userSetting" }), + video: attr({ default: document.createElement("video") }), }, onChanges: [ { - dependencies: ['userSetting.edgeBlurAmount'], - methodName: '_onChangeEdgeBlurAmountSetting', + dependencies: ["userSetting.edgeBlurAmount"], + methodName: "_onChangeEdgeBlurAmountSetting", }, { - dependencies: ['userSetting.backgroundBlurAmount'], - methodName: '_onChangeBackgroundBlurAmountSetting', + dependencies: ["userSetting.backgroundBlurAmount"], + methodName: "_onChangeBackgroundBlurAmountSetting", }, { - dependencies: ['srcStream'], - methodName: '_onChangeSrcStream', + dependencies: ["srcStream"], + methodName: "_onChangeSrcStream", }, ], }); diff --git a/addons/mail/static/src/models/call_action_list_view.js b/addons/mail/static/src/models/call_action_list_view.js index 65b05af9450..acc1e68400c 100644 --- a/addons/mail/static/src/models/call_action_list_view.js +++ b/addons/mail/static/src/models/call_action_list_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'CallActionListView', - template: 'mail.CallActionListView', + name: "CallActionListView", + template: "mail.CallActionListView", recordMethods: { /** * @param {MouseEvent} ev @@ -80,7 +80,8 @@ Model({ }, }, fields: { - callButtonTitle: attr({ default: '', + callButtonTitle: attr({ + default: "", compute() { if (!this.thread) { return clear(); @@ -92,9 +93,10 @@ Model({ } }, }), - callMainView: one('CallMainView', { identifying: true, inverse: 'callActionListView' }), - callView: one('CallView', { related: 'callMainView.callView', required: true }), - cameraButtonTitle: attr({ default: '', + callMainView: one("CallMainView", { identifying: true, inverse: "callActionListView" }), + callView: one("CallView", { related: "callMainView.callView", required: true }), + cameraButtonTitle: attr({ + default: "", compute() { if (this.messaging.rtc.sendUserVideo) { return this.env._t("Stop camera"); @@ -103,7 +105,8 @@ Model({ } }, }), - headphoneButtonTitle: attr({ default: '', + headphoneButtonTitle: attr({ + default: "", compute() { if (!this.messaging.rtc.currentRtcSession) { return clear(); @@ -117,7 +120,9 @@ Model({ }), isSmall: attr({ compute() { - return Boolean(this.callView && this.callView.threadView.compact && !this.callView.isFullScreen); + return Boolean( + this.callView && this.callView.threadView.compact && !this.callView.isFullScreen + ); }, }), microphoneButtonTitle: attr({ @@ -132,9 +137,10 @@ Model({ } }, }), - moreButtonRef: attr({ ref: 'moreButton' }), - moreMenuPopoverView: one('PopoverView', { inverse: 'callActionListViewOwnerAsMoreMenu' }), - screenSharingButtonTitle: attr({ default: '', + moreButtonRef: attr({ ref: "moreButton" }), + moreMenuPopoverView: one("PopoverView", { inverse: "callActionListViewOwnerAsMoreMenu" }), + screenSharingButtonTitle: attr({ + default: "", compute() { if (this.messaging.rtc.sendDisplay) { return this.env._t("Stop screen sharing"); @@ -143,6 +149,6 @@ Model({ } }, }), - thread: one('Thread', { related: 'callMainView.thread', required: true }), + thread: one("Thread", { related: "callMainView.thread", required: true }), }, }); diff --git a/addons/mail/static/src/models/call_demo_view.js b/addons/mail/static/src/models/call_demo_view.js index b54cf8556e9..29a0cb9c7aa 100644 --- a/addons/mail/static/src/models/call_demo_view.js +++ b/addons/mail/static/src/models/call_demo_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'CallDemoView', - template: 'mail.CallDemoView', + name: "CallDemoView", + template: "mail.CallDemoView", recordMethods: { /** * Stops recording user's microphone. @@ -102,7 +102,7 @@ Model({ /** * Ref to the audio element used for the audio feedback. */ - audioRef: attr({ ref: 'audio' }), + audioRef: attr({ ref: "audio" }), /** * The MediaStream from the microphone. * @@ -118,8 +118,8 @@ Model({ compute() { return Boolean( navigator.mediaDevices && - navigator.mediaDevices.getUserMedia && - window.MediaStream + navigator.mediaDevices.getUserMedia && + window.MediaStream ); }, }), @@ -142,7 +142,7 @@ Model({ /** * Ref to the video element used for the video feedback. */ - videoRef: attr({ ref: 'video' }), + videoRef: attr({ ref: "video" }), /** * The MediaStream from the camera. * @@ -153,6 +153,6 @@ Model({ /** * States the welcome view containing this media preview. */ - welcomeView: one('WelcomeView', { identifying: true, inverse: 'callDemoView' }), + welcomeView: one("WelcomeView", { identifying: true, inverse: "callDemoView" }), }, }); diff --git a/addons/mail/static/src/models/call_invite_request_popup.js b/addons/mail/static/src/models/call_invite_request_popup.js index 22a086221e9..b06e4ace2a1 100644 --- a/addons/mail/static/src/models/call_invite_request_popup.js +++ b/addons/mail/static/src/models/call_invite_request_popup.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'CallInviteRequestPopup', - template: 'mail.CallInviteRequestPopup', + name: "CallInviteRequestPopup", + template: "mail.CallInviteRequestPopup", recordMethods: { /** * @param {MouseEvent} ev @@ -33,6 +33,6 @@ Model({ }, }, fields: { - thread: one('Thread', { identifying: true, inverse: 'callInviteRequestPopup' }), + thread: one("Thread", { identifying: true, inverse: "callInviteRequestPopup" }), }, }); diff --git a/addons/mail/static/src/models/call_main_view.js b/addons/mail/static/src/models/call_main_view.js index fca6c80eaed..cfb236a5689 100644 --- a/addons/mail/static/src/models/call_main_view.js +++ b/addons/mail/static/src/models/call_main_view.js @@ -1,19 +1,19 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, many, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, many, one, Model } from "@mail/model"; -import { isEventHandled, markEventHandled } from '@mail/utils/utils'; +import { isEventHandled, markEventHandled } from "@mail/utils/utils"; -import { onMounted, onWillUnmount } from '@odoo/owl'; +import { onMounted, onWillUnmount } from "@odoo/owl"; Model({ - name: 'CallMainView', - template: 'mail.CallMainView', + name: "CallMainView", + template: "mail.CallMainView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useComponentToModel({ fieldName: "component" }); + useUpdateToModel({ methodName: "onComponentUpdate" }); onMounted(() => { this.resizeObserver = new ResizeObserver(() => this.onResize()); this.resizeObserver.observe(this.root.el); @@ -92,7 +92,7 @@ Model({ * @param {MouseEvent} ev */ onMouseleave(ev) { - if (ev.relatedTarget && ev.relatedTarget.closest('.o_CallActionListView_popover')) { + if (ev.relatedTarget && ev.relatedTarget.closest(".o_CallActionListView_popover")) { // the overlay should not be hidden when the cursor leaves to enter the controller popover return; } @@ -108,7 +108,7 @@ Model({ if (!this.exists()) { return; } - if (isEventHandled(ev, 'CallMainView.MouseMoveOverlay')) { + if (isEventHandled(ev, "CallMainView.MouseMoveOverlay")) { return; } this._showOverlay(); @@ -120,7 +120,7 @@ Model({ if (!this.exists()) { return; } - markEventHandled(ev, 'CallMainView.MouseMoveOverlay'); + markEventHandled(ev, "CallMainView.MouseMoveOverlay"); this.update({ showOverlay: true, showOverlayTimer: clear(), @@ -175,37 +175,52 @@ Model({ /** * The model for the controller (buttons). */ - callActionListView: one('CallActionListView', { default: {}, inverse: 'callMainView', readonly: true }), - callView: one('CallView', { identifying: true, inverse: 'callMainView' }), + callActionListView: one("CallActionListView", { + default: {}, + inverse: "callMainView", + readonly: true, + }), + callView: one("CallView", { identifying: true, inverse: "callMainView" }), component: attr(), hasSidebarButton: attr({ compute() { - return Boolean(this.callView.activeRtcSession && this.showOverlay && !this.callView.threadView.compact); + return Boolean( + this.callView.activeRtcSession && + this.showOverlay && + !this.callView.threadView.compact + ); }, }), /** * Determines if the controller is an overlay or a bottom bar. */ - isControllerFloating: attr({ default: false, + isControllerFloating: attr({ + default: false, compute() { - return Boolean(this.callView.isFullScreen || this.callView.activeRtcSession && !this.callView.threadView.compact); + return Boolean( + this.callView.isFullScreen || + (this.callView.activeRtcSession && !this.callView.threadView.compact) + ); }, }), - mainTiles: many('CallMainViewTile', { inverse: 'callMainViewOwner', + mainTiles: many("CallMainViewTile", { + inverse: "callMainViewOwner", compute() { if (this.callView.activeRtcSession) { return [{ channelMember: this.callView.activeRtcSession.channelMember }]; } - return this.callView.filteredChannelMembers.map(channelMember => ({ channelMember })); + return this.callView.filteredChannelMembers.map((channelMember) => ({ + channelMember, + })); }, }), /** * Determines if we show the overlay with the control buttons. */ showOverlay: attr({ default: true }), - showOverlayTimer: one('Timer', { inverse: 'callMainViewAsShowOverlay' }), - thread: one('Thread', { related: 'callView.thread', required: true }), - tileContainerRef: attr({ ref: 'tileContainer' }), + showOverlayTimer: one("Timer", { inverse: "callMainViewAsShowOverlay" }), + thread: one("Thread", { related: "callView.thread", required: true }), + tileContainerRef: attr({ ref: "tileContainer" }), tileHeight: attr({ default: 0 }), tileWidth: attr({ default: 0 }), }, diff --git a/addons/mail/static/src/models/call_main_view_tile.js b/addons/mail/static/src/models/call_main_view_tile.js index 84d3288e9da..5f1ef3acff3 100644 --- a/addons/mail/static/src/models/call_main_view_tile.js +++ b/addons/mail/static/src/models/call_main_view_tile.js @@ -1,12 +1,12 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'CallMainViewTile', + name: "CallMainViewTile", fields: { - callMainViewOwner: one('CallMainView', { identifying: true, inverse: 'mainTiles' }), - channelMember: one('ChannelMember', { identifying: true }), - participantCard: one('CallParticipantCard', { default: {}, inverse: 'mainViewTileOwner' }), + callMainViewOwner: one("CallMainView", { identifying: true, inverse: "mainTiles" }), + channelMember: one("ChannelMember", { identifying: true }), + participantCard: one("CallParticipantCard", { default: {}, inverse: "mainViewTileOwner" }), }, }); diff --git a/addons/mail/static/src/models/call_option_menu.js b/addons/mail/static/src/models/call_option_menu.js index c1bdf8660e4..a7c6b2fd943 100644 --- a/addons/mail/static/src/models/call_option_menu.js +++ b/addons/mail/static/src/models/call_option_menu.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { one, Model } from "@mail/model"; Model({ - name: 'CallOptionMenu', - template: 'mail.CallOptionMenu', + name: "CallOptionMenu", + template: "mail.CallOptionMenu", componentSetup() { - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); }, recordMethods: { /** @@ -21,18 +21,22 @@ Model({ return; } const data = window.JSON.stringify(channel.rtc.logs); - const blob = new window.Blob([data], { type: 'application/json' }); + const blob = new window.Blob([data], { type: "application/json" }); const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); + const a = document.createElement("a"); a.href = url; - a.download = `RtcLogs_Channel${channel.id}_Session${channel.rtc.currentRtcSession.id}_${window.moment().format('YYYY-MM-DD_HH-mm')}.json`; + a.download = `RtcLogs_Channel${channel.id}_Session${ + channel.rtc.currentRtcSession.id + }_${window.moment().format("YYYY-MM-DD_HH-mm")}.json`; a.click(); window.URL.revokeObjectURL(url); }, }, fields: { - callActionListView: one('CallActionListView', { related: 'popoverViewOwner.callActionListViewOwnerAsMoreMenu' }), - callView: one('CallView', { related: 'callActionListView.callView', required: true }), - popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'callOptionMenuView' }), + callActionListView: one("CallActionListView", { + related: "popoverViewOwner.callActionListViewOwnerAsMoreMenu", + }), + callView: one("CallView", { related: "callActionListView.callView", required: true }), + popoverViewOwner: one("PopoverView", { identifying: true, inverse: "callOptionMenuView" }), }, }); diff --git a/addons/mail/static/src/models/call_participant_card.js b/addons/mail/static/src/models/call_participant_card.js index 9a8b926ee65..7c0130aae14 100644 --- a/addons/mail/static/src/models/call_participant_card.js +++ b/addons/mail/static/src/models/call_participant_card.js @@ -1,18 +1,18 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; -import { isEventHandled, markEventHandled } from '@mail/utils/utils'; +import { attr, clear, one, Model } from "@mail/model"; +import { isEventHandled, markEventHandled } from "@mail/utils/utils"; Model({ - name: 'CallParticipantCard', - template: 'mail.CallParticipantCard', - identifyingMode: 'xor', + name: "CallParticipantCard", + template: "mail.CallParticipantCard", + identifyingMode: "xor", recordMethods: { /** * @param {MouseEvent} ev */ async onClick(ev) { - if (isEventHandled(ev, 'CallParticipantCard.clickVolumeAnchor')) { + if (isEventHandled(ev, "CallParticipantCard.clickVolumeAnchor")) { return; } if (this.rtcSession) { @@ -24,13 +24,13 @@ Model({ return; } const channel = this.channelMember.channel.thread; - const channelData = await this.messaging.rpc(({ - route: '/mail/rtc/channel/cancel_call_invitation', + const channelData = await this.messaging.rpc({ + route: "/mail/rtc/channel/cancel_call_invitation", params: { channel_id: channel.id, member_ids: [this.channelMember.id], }, - })); + }); if (!channel.exists()) { return; } @@ -42,8 +42,10 @@ Model({ * @param {MouseEvent} ev */ async onClickVolumeAnchor(ev) { - markEventHandled(ev, 'CallParticipantCard.clickVolumeAnchor'); - this.update({ callParticipantCardPopoverView: this.callParticipantCardPopoverView ? clear() : {} }); + markEventHandled(ev, "CallParticipantCard.clickVolumeAnchor"); + this.update({ + callParticipantCardPopoverView: this.callParticipantCardPopoverView ? clear() : {}, + }); }, /** * This listens to the right click event, and used to redirect the event @@ -60,8 +62,9 @@ Model({ }, }, fields: { - callParticipantCardPopoverView: one('PopoverView', { inverse: 'callParticipantCardOwner' }), - channelMember: one('ChannelMember', { inverse: 'callParticipantCards', + callParticipantCardPopoverView: one("PopoverView", { inverse: "callParticipantCardOwner" }), + channelMember: one("ChannelMember", { + inverse: "callParticipantCards", compute() { if (this.sidebarViewTileOwner) { return this.sidebarViewTileOwner.channelMember; @@ -69,11 +72,15 @@ Model({ return this.mainViewTileOwner.channelMember; }, }), - mainViewTileOwner: one('CallMainViewTile', { identifying: true, inverse: 'participantCard', }), + mainViewTileOwner: one("CallMainViewTile", { + identifying: true, + inverse: "participantCard", + }), /** * Determines if this card has to be displayed in a minimized form. */ - isMinimized: attr({ default: false, + isMinimized: attr({ + default: false, compute() { return Boolean(this.callView && this.callView.isMinimized); }, @@ -81,15 +88,19 @@ Model({ /** * Determines if the rtcSession is in a valid "talking" state. */ - isTalking: attr({ default: false, + isTalking: attr({ + default: false, compute() { - return Boolean(this.rtcSession && this.rtcSession.isTalking && !this.rtcSession.isMute); + return Boolean( + this.rtcSession && this.rtcSession.isTalking && !this.rtcSession.isMute + ); }, }), /** * The callView that displays this card. */ - callView: one('CallView', { inverse: 'participantCards', + callView: one("CallView", { + inverse: "participantCards", compute() { if (this.sidebarViewTileOwner) { return this.sidebarViewTileOwner.callSidebarViewOwner.callView; @@ -97,9 +108,16 @@ Model({ return this.mainViewTileOwner.callMainViewOwner.callView; }, }), - rtcSession: one('RtcSession', { inverse: 'callParticipantCards', related: 'channelMember.rtcSession' }), - sidebarViewTileOwner: one('CallSidebarViewTile', { identifying: true, inverse: 'participantCard' }), - callParticipantVideoView: one('CallParticipantVideoView', { inverse: 'callParticipantCardOwner', + rtcSession: one("RtcSession", { + inverse: "callParticipantCards", + related: "channelMember.rtcSession", + }), + sidebarViewTileOwner: one("CallSidebarViewTile", { + identifying: true, + inverse: "participantCard", + }), + callParticipantVideoView: one("CallParticipantVideoView", { + inverse: "callParticipantCardOwner", compute() { if (this.rtcSession && this.rtcSession.videoStream) { return {}; @@ -107,6 +125,6 @@ Model({ return clear(); }, }), - volumeMenuAnchorRef: attr({ ref: 'volumeMenuAnchor' }), + volumeMenuAnchorRef: attr({ ref: "volumeMenuAnchor" }), }, }); diff --git a/addons/mail/static/src/models/call_participant_card_popover_content_view.js b/addons/mail/static/src/models/call_participant_card_popover_content_view.js index d7d71ba7ba6..b11ae86ab12 100644 --- a/addons/mail/static/src/models/call_participant_card_popover_content_view.js +++ b/addons/mail/static/src/models/call_participant_card_popover_content_view.js @@ -1,28 +1,35 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; -import { sprintf } from '@web/core/utils/strings'; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'CallParticipantCardPopoverContentView', - template: 'mail.CallParticipantCardPopoverContentView', + name: "CallParticipantCardPopoverContentView", + template: "mail.CallParticipantCardPopoverContentView", recordMethods: { /** * @param {Event} ev */ onChangeVolume(ev) { - this.callParticipantCard.rtcSession && this.callParticipantCard.rtcSession.setVolume(parseFloat(ev.target.value)); + this.callParticipantCard.rtcSession && + this.callParticipantCard.rtcSession.setVolume(parseFloat(ev.target.value)); }, }, fields: { - callParticipantCard: one('CallParticipantCard', { related: 'popoverViewOwner.callParticipantCardOwner' }), + callParticipantCard: one("CallParticipantCard", { + related: "popoverViewOwner.callParticipantCardOwner", + }), /** * Determines whether or not we show the connection info. */ hasConnectionInfo: attr({ compute() { - return Boolean(this.callParticipantCard.rtcSession && this.env.debug && this.callParticipantCard.channelMember.channel.thread.rtc); + return Boolean( + this.callParticipantCard.rtcSession && + this.env.debug && + this.callParticipantCard.channelMember.channel.thread.rtc + ); }, }), /** @@ -30,16 +37,22 @@ Model({ */ inboundConnectionTypeText: attr({ compute() { - if (!this.callParticipantCard.rtcSession || !this.callParticipantCard.rtcSession.remoteCandidateType) { - return sprintf(this.env._t('From %s: no connection'), this.callParticipantCard.channelMember.persona.name); + if ( + !this.callParticipantCard.rtcSession || + !this.callParticipantCard.rtcSession.remoteCandidateType + ) { + return sprintf( + this.env._t("From %s: no connection"), + this.callParticipantCard.channelMember.persona.name + ); } - return sprintf( - this.env._t('From %(name)s: %(candidateType)s (%(protocol)s)'), { - candidateType: this.callParticipantCard.rtcSession.remoteCandidateType, - name: this.callParticipantCard.channelMember.persona.name, - protocol: this.messaging.rtc.protocolsByCandidateTypes[this.callParticipantCard.rtcSession.remoteCandidateType], - }, - ); + return sprintf(this.env._t("From %(name)s: %(candidateType)s (%(protocol)s)"), { + candidateType: this.callParticipantCard.rtcSession.remoteCandidateType, + name: this.callParticipantCard.channelMember.persona.name, + protocol: this.messaging.rtc.protocolsByCandidateTypes[ + this.callParticipantCard.rtcSession.remoteCandidateType + ], + }); }, }), /** @@ -47,18 +60,27 @@ Model({ */ outboundConnectionTypeText: attr({ compute() { - if (!this.callParticipantCard.rtcSession || !this.callParticipantCard.rtcSession.localCandidateType) { - return sprintf(this.env._t('To %s: no connection'), this.callParticipantCard.channelMember.persona.name); + if ( + !this.callParticipantCard.rtcSession || + !this.callParticipantCard.rtcSession.localCandidateType + ) { + return sprintf( + this.env._t("To %s: no connection"), + this.callParticipantCard.channelMember.persona.name + ); } - return sprintf( - this.env._t('To %(name)s: %(candidateType)s (%(protocol)s)'), { - candidateType: this.callParticipantCard.rtcSession.localCandidateType, - name: this.callParticipantCard.channelMember.persona.name, - protocol: this.messaging.rtc.protocolsByCandidateTypes[this.callParticipantCard.rtcSession.localCandidateType], - }, - ); + return sprintf(this.env._t("To %(name)s: %(candidateType)s (%(protocol)s)"), { + candidateType: this.callParticipantCard.rtcSession.localCandidateType, + name: this.callParticipantCard.channelMember.persona.name, + protocol: this.messaging.rtc.protocolsByCandidateTypes[ + this.callParticipantCard.rtcSession.localCandidateType + ], + }); }, }), - popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'callParticipantCardPopoverContentView' }), + popoverViewOwner: one("PopoverView", { + identifying: true, + inverse: "callParticipantCardPopoverContentView", + }), }, }); diff --git a/addons/mail/static/src/models/call_participant_video_view.js b/addons/mail/static/src/models/call_participant_video_view.js index c719a9af60b..6184f5fa17f 100644 --- a/addons/mail/static/src/models/call_participant_video_view.js +++ b/addons/mail/static/src/models/call_participant_video_view.js @@ -1,15 +1,15 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'CallParticipantVideoView', - template: 'mail.CallParticipantVideoView', + name: "CallParticipantVideoView", + template: "mail.CallParticipantVideoView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useComponentToModel({ fieldName: "component" }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, recordMethods: { /** @@ -37,7 +37,7 @@ Model({ try { await ev.target.play(); } catch (error) { - if (typeof error === 'object' && error.name === 'NotAllowedError') { + if (typeof error === "object" && error.name === "NotAllowedError") { // Ignored as some browsers may reject play() calls that do not // originate from a user input. return; @@ -47,9 +47,12 @@ Model({ }, }, fields: { - callParticipantCardOwner: one('CallParticipantCard', { identifying: true, inverse: 'callParticipantVideoView' }), + callParticipantCardOwner: one("CallParticipantCard", { + identifying: true, + inverse: "callParticipantVideoView", + }), component: attr(), - rtcSession: one('RtcSession', { + rtcSession: one("RtcSession", { compute() { if (this.callParticipantCardOwner.rtcSession) { return this.callParticipantCardOwner.rtcSession; diff --git a/addons/mail/static/src/models/call_settings_menu.js b/addons/mail/static/src/models/call_settings_menu.js index 4aca21d6f57..77887c6aec5 100644 --- a/addons/mail/static/src/models/call_settings_menu.js +++ b/addons/mail/static/src/models/call_settings_menu.js @@ -2,21 +2,23 @@ import { browser } from "@web/core/browser/browser"; -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'CallSettingsMenu', - template: 'mail.CallSettingsMenu', - identifyingMode: 'xor', + name: "CallSettingsMenu", + template: "mail.CallSettingsMenu", + identifyingMode: "xor", lifecycleHooks: { async _created() { - browser.addEventListener('keydown', this._onKeyDown); - browser.addEventListener('keyup', this._onKeyUp); - this.update({ userDevices: await this.messaging.browser.navigator.mediaDevices.enumerateDevices() }); + browser.addEventListener("keydown", this._onKeyDown); + browser.addEventListener("keyup", this._onKeyUp); + this.update({ + userDevices: await this.messaging.browser.navigator.mediaDevices.enumerateDevices(), + }); }, _willDelete() { - browser.removeEventListener('keydown', this._onKeyDown); - browser.removeEventListener('keyup', this._onKeyUp); + browser.removeEventListener("keydown", this._onKeyDown); + browser.removeEventListener("keyup", this._onKeyUp); }, }, recordMethods: { @@ -100,7 +102,7 @@ Model({ }, }, fields: { - callView: one('CallView', { + callView: one("CallView", { compute() { if (this.threadViewOwner) { return this.threadViewOwner.callView; @@ -111,8 +113,8 @@ Model({ return clear(); }, }), - chatWindowOwner: one('ChatWindow', { identifying: true, inverse: 'callSettingsMenu' }), - thread: one('Thread', { + chatWindowOwner: one("ChatWindow", { identifying: true, inverse: "callSettingsMenu" }), + thread: one("Thread", { compute() { if (this.threadViewOwner) { return this.threadViewOwner.thread; @@ -123,8 +125,8 @@ Model({ return clear(); }, }), - threadViewOwner: one('ThreadView', { identifying: true, inverse: 'callSettingsMenu' }), + threadViewOwner: one("ThreadView", { identifying: true, inverse: "callSettingsMenu" }), userDevices: attr({ default: [] }), - userSetting: one('UserSetting', { related: 'messaging.userSetting' }), + userSetting: one("UserSetting", { related: "messaging.userSetting" }), }, }); diff --git a/addons/mail/static/src/models/call_sidebar_view.js b/addons/mail/static/src/models/call_sidebar_view.js index dfca398d50a..509199ee1e0 100644 --- a/addons/mail/static/src/models/call_sidebar_view.js +++ b/addons/mail/static/src/models/call_sidebar_view.js @@ -1,15 +1,18 @@ /** @odoo-module **/ -import { many, one, Model } from '@mail/model'; +import { many, one, Model } from "@mail/model"; Model({ - name: 'CallSidebarView', - template: 'mail.CallSidebarView', + name: "CallSidebarView", + template: "mail.CallSidebarView", fields: { - callView: one('CallView', { identifying: true, inverse: 'callSidebarView' }), - sidebarTiles: many('CallSidebarViewTile', { inverse: 'callSidebarViewOwner', + callView: one("CallView", { identifying: true, inverse: "callSidebarView" }), + sidebarTiles: many("CallSidebarViewTile", { + inverse: "callSidebarViewOwner", compute() { - return this.callView.filteredChannelMembers.map(channelMember => ({ channelMember })); + return this.callView.filteredChannelMembers.map((channelMember) => ({ + channelMember, + })); }, }), }, diff --git a/addons/mail/static/src/models/call_sidebar_view_tile.js b/addons/mail/static/src/models/call_sidebar_view_tile.js index db2864f1366..0a5087e0afe 100644 --- a/addons/mail/static/src/models/call_sidebar_view_tile.js +++ b/addons/mail/static/src/models/call_sidebar_view_tile.js @@ -1,12 +1,18 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'CallSidebarViewTile', + name: "CallSidebarViewTile", fields: { - callSidebarViewOwner: one('CallSidebarView', { identifying: true, inverse: 'sidebarTiles' }), - channelMember: one('ChannelMember', { identifying: true }), - participantCard: one('CallParticipantCard', { default: {}, inverse: 'sidebarViewTileOwner' }), + callSidebarViewOwner: one("CallSidebarView", { + identifying: true, + inverse: "sidebarTiles", + }), + channelMember: one("ChannelMember", { identifying: true }), + participantCard: one("CallParticipantCard", { + default: {}, + inverse: "sidebarViewTileOwner", + }), }, }); diff --git a/addons/mail/static/src/models/call_systray_menu.js b/addons/mail/static/src/models/call_systray_menu.js index 97e7335a87e..40958573ce3 100644 --- a/addons/mail/static/src/models/call_systray_menu.js +++ b/addons/mail/static/src/models/call_systray_menu.js @@ -1,24 +1,25 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; -import { sprintf } from '@web/core/utils/strings'; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'CallSystrayMenu', - template: 'mail.CallSystrayMenu', + name: "CallSystrayMenu", + template: "mail.CallSystrayMenu", fields: { - buttonTitle: attr({ default: '', + buttonTitle: attr({ + default: "", compute() { if (!this.messaging.rtc.channel) { return clear(); } return sprintf( this.env._t("Open conference: %s"), - this.messaging.rtc.channel.displayName, + this.messaging.rtc.channel.displayName ); }, }), - rtc: one('Rtc', { identifying: true, inverse: 'callSystrayMenu' }), + rtc: one("Rtc", { identifying: true, inverse: "callSystrayMenu" }), }, }); diff --git a/addons/mail/static/src/models/call_view.js b/addons/mail/static/src/models/call_view.js index dedd0151af0..261f6e99458 100644 --- a/addons/mail/static/src/models/call_view.js +++ b/addons/mail/static/src/models/call_view.js @@ -2,17 +2,17 @@ import { browser } from "@web/core/browser/browser"; -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'CallView', - template: 'mail.CallView', + name: "CallView", + template: "mail.CallView", lifecycleHooks: { _created() { - browser.addEventListener('fullscreenchange', this._onFullScreenChange); + browser.addEventListener("fullscreenchange", this._onFullScreenChange); }, _willDelete() { - browser.removeEventListener('fullscreenchange', this._onFullScreenChange); + browser.removeEventListener("fullscreenchange", this._onFullScreenChange); }, }, recordMethods: { @@ -41,12 +41,13 @@ Model({ } this.messaging.notify({ message: this.env._t("The FullScreen mode was denied by the browser"), - type: 'warning', + type: "warning", }); } }, async deactivateFullScreen() { - const fullScreenElement = document.webkitFullscreenElement || document.fullscreenElement; + const fullScreenElement = + document.webkitFullscreenElement || document.fullscreenElement; if (fullScreenElement) { if (document.exitFullscreen) { await document.exitFullscreen(); @@ -85,7 +86,8 @@ Model({ * @private */ _onFullScreenChange() { - const fullScreenElement = document.webkitFullscreenElement || document.fullscreenElement; + const fullScreenElement = + document.webkitFullscreenElement || document.fullscreenElement; if (fullScreenElement) { this.update({ isFullScreen: true }); return; @@ -97,20 +99,23 @@ Model({ /** * The rtc session that is the main card of the view. */ - activeRtcSession: one('RtcSession', { related: 'channel.activeRtcSession' }), + activeRtcSession: one("RtcSession", { related: "channel.activeRtcSession" }), /** * The aspect ratio of the tiles. */ - aspectRatio: attr({ default: 16 / 9, + aspectRatio: attr({ + default: 16 / 9, compute() { - const rtcAspectRatio = this.messaging.rtc.videoConfig && this.messaging.rtc.videoConfig.aspectRatio; + const rtcAspectRatio = + this.messaging.rtc.videoConfig && this.messaging.rtc.videoConfig.aspectRatio; const aspectRatio = rtcAspectRatio || 16 / 9; // if we are in minimized mode (round avatar frames), we treat the cards like squares. return this.isMinimized ? 1 : aspectRatio; }, }), - callMainView: one('CallMainView', { default: {}, inverse: 'callView', readonly: true }), - callSidebarView: one('CallSidebarView', { inverse: 'callView', + callMainView: one("CallMainView", { default: {}, inverse: "callView", readonly: true }), + callSidebarView: one("CallSidebarView", { + inverse: "callView", compute() { if (this.activeRtcSession && this.isSidebarOpen && !this.threadView.compact) { return {}; @@ -118,15 +123,19 @@ Model({ return clear(); }, }), - channel: one('Channel', { related: 'thread.channel' }), - filteredChannelMembers: many('ChannelMember', { + channel: one("Channel", { related: "thread.channel" }), + filteredChannelMembers: many("ChannelMember", { compute() { if (!this.channel) { return clear(); } const channelMembers = []; for (const channelMember of this.channel.callParticipants) { - if (this.channel.showOnlyVideo && this.thread.videoCount > 0 && !channelMember.isStreaming) { + if ( + this.channel.showOnlyVideo && + this.thread.videoCount > 0 && + !channelMember.isStreaming + ) { continue; } channelMembers.push(channelMember); @@ -142,7 +151,8 @@ Model({ * Determines if the tiles are in a minimized format: * small circles instead of cards, smaller display area. */ - isMinimized: attr({ default: false, + isMinimized: attr({ + default: false, compute() { if (!this.threadView || !this.thread) { return true; @@ -169,7 +179,7 @@ Model({ * All the participant cards of the call viewer (main card and tile cards). * this is a technical inverse to distinguish from the other relation 'tileParticipantCards'. */ - participantCards: many('CallParticipantCard', { inverse: 'callView', isCausal: true }), + participantCards: many("CallParticipantCard", { inverse: "callView", isCausal: true }), /** * Text content that is displayed on title of the settings dialog. */ @@ -178,20 +188,20 @@ Model({ return this.env._t("Settings"); }, }), - thread: one('Thread', { related: 'threadView.thread', required: true }), + thread: one("Thread", { related: "threadView.thread", required: true }), /** * ThreadView on which the call view is attached. */ - threadView: one('ThreadView', { identifying: true, inverse: 'callView' }), + threadView: one("ThreadView", { identifying: true, inverse: "callView" }), }, onChanges: [ { - dependencies: ['thread.rtc'], - methodName: '_onChangeRtcChannel', + dependencies: ["thread.rtc"], + methodName: "_onChangeRtcChannel", }, { - dependencies: ['thread.videoCount'], - methodName: '_onChangeVideoCount', + dependencies: ["thread.videoCount"], + methodName: "_onChangeVideoCount", }, ], }); diff --git a/addons/mail/static/src/models/canned_response.js b/addons/mail/static/src/models/canned_response.js index 8bd4c31e089..99b09d55496 100644 --- a/addons/mail/static/src/models/canned_response.js +++ b/addons/mail/static/src/models/canned_response.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; -import { cleanSearchTerm } from '@mail/utils/utils'; +import { attr, one, Model } from "@mail/model"; +import { cleanSearchTerm } from "@mail/utils/utils"; Model({ - name: 'CannedResponse', + name: "CannedResponse", modelMethods: { /** * Fetches canned responses matching the given search term to extend the @@ -32,12 +32,18 @@ Model({ getSuggestionSortFunction(searchTerm, { thread } = {}) { const cleanedSearchTerm = cleanSearchTerm(searchTerm); return (a, b) => { - const cleanedAName = cleanSearchTerm(a.source || ''); - const cleanedBName = cleanSearchTerm(b.source || ''); - if (cleanedAName.startsWith(cleanedSearchTerm) && !cleanedBName.startsWith(cleanedSearchTerm)) { + const cleanedAName = cleanSearchTerm(a.source || ""); + const cleanedBName = cleanSearchTerm(b.source || ""); + if ( + cleanedAName.startsWith(cleanedSearchTerm) && + !cleanedBName.startsWith(cleanedSearchTerm) + ) { return -1; } - if (!cleanedAName.startsWith(cleanedSearchTerm) && cleanedBName.startsWith(cleanedSearchTerm)) { + if ( + !cleanedAName.startsWith(cleanedSearchTerm) && + cleanedBName.startsWith(cleanedSearchTerm) + ) { return 1; } if (cleanedAName < cleanedBName) { @@ -61,9 +67,11 @@ Model({ */ searchSuggestions(searchTerm, { thread } = {}) { const cleanedSearchTerm = cleanSearchTerm(searchTerm); - return [this.messaging.cannedResponses.filter(cannedResponse => - cleanSearchTerm(cannedResponse.source).includes(cleanedSearchTerm) - )]; + return [ + this.messaging.cannedResponses.filter((cannedResponse) => + cleanSearchTerm(cannedResponse.source).includes(cleanedSearchTerm) + ), + ]; }, }, fields: { @@ -77,6 +85,11 @@ Model({ * entered. */ substitution: attr(), - suggestable: one('ComposerSuggestable', { default: {}, inverse: 'cannedResponse', readonly: true, required: true }), + suggestable: one("ComposerSuggestable", { + default: {}, + inverse: "cannedResponse", + readonly: true, + required: true, + }), }, }); diff --git a/addons/mail/static/src/models/channel.js b/addons/mail/static/src/models/channel.js index 1edad552f34..2d7ef7a9427 100644 --- a/addons/mail/static/src/models/channel.js +++ b/addons/mail/static/src/models/channel.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, one, many, Model } from '@mail/model'; +import { attr, clear, one, many, Model } from "@mail/model"; Model({ - name: 'Channel', + name: "Channel", modelMethods: { /** * Performs the `channel_get` RPC on `mail.channel`. @@ -19,8 +19,8 @@ Model({ async performRpcCreateChat({ partnerIds, pinForCurrentPartner }) { // TODO FIX: potential duplicate chat task-2276490 const data = await this.messaging.rpc({ - model: 'mail.channel', - method: 'channel_get', + model: "mail.channel", + method: "channel_get", kwargs: { partners_to: partnerIds, pin: pinForCurrentPartner, @@ -29,8 +29,8 @@ Model({ if (!data) { return; } - const { channel } = this.messaging.models['Thread'].insert( - this.messaging.models['Thread'].convertData(data) + const { channel } = this.messaging.models["Thread"].insert( + this.messaging.models["Thread"].convertData(data) ); return channel; }, @@ -38,11 +38,11 @@ Model({ recordMethods: { async fetchChannelMembers() { const channelData = await this.messaging.rpc({ - model: 'mail.channel', - method: 'load_more_members', + model: "mail.channel", + method: "load_more_members", args: [[this.id]], kwargs: { - known_member_ids: this.channelMembers.map(channelMember => channelMember.id), + known_member_ids: this.channelMembers.map((channelMember) => channelMember.id), }, }); if (!this.exists()) { @@ -52,7 +52,7 @@ Model({ }, }, fields: { - activeRtcSession: one('RtcSession'), + activeRtcSession: one("RtcSession"), areAllMembersLoaded: attr({ compute() { return this.memberCount === this.channelMembers.length; @@ -62,7 +62,7 @@ Model({ * Cache key to force a reload of the avatar when avatar is changed. */ avatarCacheKey: attr(), - callParticipants: many('ChannelMember', { + callParticipants: many("ChannelMember", { compute() { if (!this.thread) { return clear(); @@ -74,28 +74,33 @@ Model({ return callParticipants; }, sort: [ - ['truthy-first', 'rtcSession'], - ['smaller-first', 'rtcSession.id'], + ["truthy-first", "rtcSession"], + ["smaller-first", "rtcSession.id"], ], }), - channelMembers: many('ChannelMember', { inverse: 'channel', isCausal: true }), - channelPreviewViews: many('ChannelPreviewView', { inverse: 'channel' }), + channelMembers: many("ChannelMember", { inverse: "channel", isCausal: true }), + channelPreviewViews: many("ChannelPreviewView", { inverse: "channel" }), channel_type: attr(), - correspondent: one('Partner', { + correspondent: one("Partner", { compute() { - if (this.channel_type === 'channel') { + if (this.channel_type === "channel") { return clear(); } const correspondents = this.channelMembers - .filter(member => member.persona && member.persona.partner && !member.isMemberOfCurrentUser) - .map(member => member.persona.partner); + .filter( + (member) => + member.persona && + member.persona.partner && + !member.isMemberOfCurrentUser + ) + .map((member) => member.persona.partner); if (correspondents.length === 1) { // 2 members chat return correspondents[0]; } const partners = this.channelMembers - .filter(member => member.persona && member.persona.partner) - .map(member => member.persona.partner); + .filter((member) => member.persona && member.persona.partner) + .map((member) => member.persona.partner); if (partners.length === 1) { // chat with oneself return partners[0]; @@ -103,12 +108,10 @@ Model({ return clear(); }, }), - correspondentOfDmChat: one('Partner', { inverse: 'dmChatWithCurrentPartner', + correspondentOfDmChat: one("Partner", { + inverse: "dmChatWithCurrentPartner", compute() { - if ( - this.channel_type === 'chat' && - this.correspondent - ) { + if (this.channel_type === "chat" && this.correspondent) { return this.correspondent; } return clear(); @@ -118,13 +121,13 @@ Model({ /** * Useful to compute `discussSidebarCategoryItem`. */ - discussSidebarCategory: one('DiscussSidebarCategory', { + discussSidebarCategory: one("DiscussSidebarCategory", { compute() { switch (this.channel_type) { - case 'channel': + case "channel": return this.messaging.discuss.categoryChannel; - case 'chat': - case 'group': + case "chat": + case "group": return this.messaging.discuss.categoryChat; default: return clear(); @@ -135,7 +138,8 @@ Model({ * Determines the discuss sidebar category item that displays this * channel. */ - discussSidebarCategoryItem: one('DiscussSidebarCategoryItem', { inverse: 'channel', + discussSidebarCategoryItem: one("DiscussSidebarCategoryItem", { + inverse: "channel", compute() { if (!this.thread) { return clear(); @@ -154,13 +158,16 @@ Model({ if (!this.thread) { return; } - if (this.channel_type === 'chat' && this.correspondent) { - return this.custom_channel_name || this.thread.getMemberName(this.correspondent.persona); + if (this.channel_type === "chat" && this.correspondent) { + return ( + this.custom_channel_name || + this.thread.getMemberName(this.correspondent.persona) + ); } - if (this.channel_type === 'group' && !this.thread.name) { + if (this.channel_type === "group" && !this.thread.name) { return this.channelMembers - .filter(channelMember => channelMember.persona) - .map(channelMember => this.thread.getMemberName(channelMember.persona)) + .filter((channelMember) => channelMember.persona) + .map((channelMember) => this.thread.getMemberName(channelMember.persona)) .join(this.env._t(", ")); } return this.thread.name; @@ -179,7 +186,9 @@ Model({ // By default trust the server up to the last message it used // because it's not possible to do better. let baseCounter = this.serverMessageUnreadCounter; - let countFromId = this.thread.serverLastMessage ? this.thread.serverLastMessage.id : 0; + let countFromId = this.thread.serverLastMessage + ? this.thread.serverLastMessage.id + : 0; // But if the client knows the last seen message that the server // returned (and by assumption all the messages that come after), // the counter can be computed fully locally, ignoring potentially @@ -207,17 +216,19 @@ Model({ * States the number of members in this channel according to the server. */ memberCount: attr(), - memberOfCurrentUser: one('ChannelMember', { inverse: 'channelAsMemberOfCurrentUser' }), - orderedOfflineMembers: many('ChannelMember', { inverse: 'channelAsOfflineMember', + memberOfCurrentUser: one("ChannelMember", { inverse: "channelAsMemberOfCurrentUser" }), + orderedOfflineMembers: many("ChannelMember", { + inverse: "channelAsOfflineMember", sort: [ - ['truthy-first', 'persona.name'], - ['case-insensitive-asc', 'persona.name'], + ["truthy-first", "persona.name"], + ["case-insensitive-asc", "persona.name"], ], }), - orderedOnlineMembers: many('ChannelMember', { inverse: 'channelAsOnlineMember', + orderedOnlineMembers: many("ChannelMember", { + inverse: "channelAsOnlineMember", sort: [ - ['truthy-first', 'persona.name'], - ['case-insensitive-asc', 'persona.name'], + ["truthy-first", "persona.name"], + ["case-insensitive-asc", "persona.name"], ], }), /** @@ -235,11 +246,14 @@ Model({ * Determines whether we only display the participants who broadcast a video or all of them. */ showOnlyVideo: attr({ default: false }), - thread: one('Thread', { inverse: 'channel', isCausal: true, required: true, + thread: one("Thread", { + inverse: "channel", + isCausal: true, + required: true, compute() { return { id: this.id, - model: 'mail.channel', + model: "mail.channel", }; }, }), diff --git a/addons/mail/static/src/models/channel_command.js b/addons/mail/static/src/models/channel_command.js index 02d598be280..1cfb8a7bd8f 100644 --- a/addons/mail/static/src/models/channel_command.js +++ b/addons/mail/static/src/models/channel_command.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; -import { cleanSearchTerm } from '@mail/utils/utils'; +import { attr, one, Model } from "@mail/model"; +import { cleanSearchTerm } from "@mail/utils/utils"; Model({ - name: 'ChannelCommand', + name: "ChannelCommand", modelMethods: { /** * Fetches channel commands matching the given search term to extend the @@ -40,12 +40,18 @@ Model({ if (!isATypeSpecific && isBTypeSpecific) { return 1; } - const cleanedAName = cleanSearchTerm(a.name || ''); - const cleanedBName = cleanSearchTerm(b.name || ''); - if (cleanedAName.startsWith(cleanedSearchTerm) && !cleanedBName.startsWith(cleanedSearchTerm)) { + const cleanedAName = cleanSearchTerm(a.name || ""); + const cleanedBName = cleanSearchTerm(b.name || ""); + if ( + cleanedAName.startsWith(cleanedSearchTerm) && + !cleanedBName.startsWith(cleanedSearchTerm) + ) { return -1; } - if (!cleanedAName.startsWith(cleanedSearchTerm) && cleanedBName.startsWith(cleanedSearchTerm)) { + if ( + !cleanedAName.startsWith(cleanedSearchTerm) && + cleanedBName.startsWith(cleanedSearchTerm) + ) { return 1; } if (cleanedAName < cleanedBName) { @@ -72,15 +78,17 @@ Model({ return [[]]; } const cleanedSearchTerm = cleanSearchTerm(searchTerm); - return [this.messaging.commands.filter(command => { - if (!cleanSearchTerm(command.name).includes(cleanedSearchTerm)) { - return false; - } - if (command.channel_types) { - return command.channel_types.includes(thread.channel.channel_type); - } - return true; - })]; + return [ + this.messaging.commands.filter((command) => { + if (!cleanSearchTerm(command.name).includes(cleanedSearchTerm)) { + return false; + } + if (command.channel_types) { + return command.channel_types.includes(thread.channel.channel_type); + } + return true; + }), + ]; }, }, recordMethods: { @@ -91,9 +99,9 @@ Model({ * @param {Thread} param0.channel * @param {Object} [param0.body=''] */ - async execute({ channel, body = '' }) { + async execute({ channel, body = "" }) { return this.messaging.rpc({ - model: 'mail.channel', + model: "mail.channel", method: this.methodName, args: [[channel.id]], kwargs: { body }, @@ -121,6 +129,11 @@ Model({ * The keyword to use a specific command. */ name: attr({ identifying: true }), - suggestable: one('ComposerSuggestable', { default: {}, inverse: 'channelCommand', readonly: true, required: true }), + suggestable: one("ComposerSuggestable", { + default: {}, + inverse: "channelCommand", + readonly: true, + required: true, + }), }, }); diff --git a/addons/mail/static/src/models/channel_invitation_form.js b/addons/mail/static/src/models/channel_invitation_form.js index 3a4ee9ba0e7..2a02848fcb7 100644 --- a/addons/mail/static/src/models/channel_invitation_form.js +++ b/addons/mail/static/src/models/channel_invitation_form.js @@ -1,20 +1,20 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, link, many, one, Model, unlink } from '@mail/model'; -import { cleanSearchTerm } from '@mail/utils/utils'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, link, many, one, Model, unlink } from "@mail/model"; +import { cleanSearchTerm } from "@mail/utils/utils"; -import { sprintf } from '@web/core/utils/strings'; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'ChannelInvitationForm', - template: 'mail.ChannelInvitationForm', + name: "ChannelInvitationForm", + template: "mail.ChannelInvitationForm", componentSetup() { - useComponentToModel({ fieldName: 'component' }); - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useComponentToModel({ fieldName: "component" }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, - identifyingMode: 'xor', + identifyingMode: "xor", recordMethods: { /** * Handles click on the "copy" button. @@ -24,8 +24,8 @@ Model({ async onClickCopy(ev) { await navigator.clipboard.writeText(this.thread.invitationLink); this.messaging.notify({ - message: this.env._t('Link copied!'), - type: 'success', + message: this.env._t("Link copied!"), + type: "success", }); }, /** @@ -34,13 +34,19 @@ Model({ * @param {MouseEvent} ev */ async onClickInvite(ev) { - if (this.thread.channel.channel_type === 'chat') { - const partners_to = [...new Set([ - this.messaging.currentPartner.id, - ...this.thread.channel.channelMembers.filter(member => member.persona && member.persona.partner).map(member => member.persona.partner.id), - ...this.selectedPartners.map(partner => partner.id), - ])]; - const channel = await this.messaging.models['Thread'].createGroupChat({ partners_to }); + if (this.thread.channel.channel_type === "chat") { + const partners_to = [ + ...new Set([ + this.messaging.currentPartner.id, + ...this.thread.channel.channelMembers + .filter((member) => member.persona && member.persona.partner) + .map((member) => member.persona.partner.id), + ...this.selectedPartners.map((partner) => partner.id), + ]), + ]; + const channel = await this.messaging.models["Thread"].createGroupChat({ + partners_to, + }); if (this.thread.rtc) { /** * if we were in a RTC call on the current thread, we move to the new group chat. @@ -49,22 +55,22 @@ Model({ */ await channel.toggleCall({ startWithVideo: !!this.thread.rtc.videoTrack, - videoType: this.thread.rtc.sendUserVideo ? 'user-video' : 'display', + videoType: this.thread.rtc.sendUserVideo ? "user-video" : "display", }); } if (channel.exists()) { channel.open(); } } else { - await this.messaging.rpc(({ - model: 'mail.channel', - method: 'add_members', + await this.messaging.rpc({ + model: "mail.channel", + method: "add_members", args: [[this.thread.id]], kwargs: { - partner_ids: this.selectedPartners.map(partner => partner.id), + partner_ids: this.selectedPartners.map((partner) => partner.id), invite_to_rtc_call: !!this.thread.rtc, }, - })); + }); } if (this.exists()) { this.delete(); @@ -92,7 +98,10 @@ Model({ onComponentUpdate() { if (this.doFocusOnSearchInput && this.searchInputRef.el) { this.searchInputRef.el.focus(); - this.searchInputRef.el.setSelectionRange(this.searchTerm.length, this.searchTerm.length); + this.searchInputRef.el.setSelectionRange( + this.searchTerm.length, + this.searchTerm.length + ); this.update({ doFocusOnSearchInput: clear() }); } }, @@ -135,11 +144,14 @@ Model({ hasSearchRpcInProgress: true, }); try { - const channelId = (this.thread && this.thread.model === 'mail.channel') ? this.thread.id : undefined; + const channelId = + this.thread && this.thread.model === "mail.channel" + ? this.thread.id + : undefined; const { count, partners: partnersData } = await this.messaging.rpc( { - model: 'res.partner', - method: 'search_for_channel_invite', + model: "res.partner", + method: "search_for_channel_invite", kwargs: { channel_id: channelId, search_term: cleanSearchTerm(this.searchTerm), @@ -173,13 +185,12 @@ Model({ if (!this.thread.authorizedGroupFullName) { return clear(); } - return sprintf( - this.env._t('Access restricted to group "%(groupFullName)s"'), - { 'groupFullName': this.thread.authorizedGroupFullName } - ); + return sprintf(this.env._t('Access restricted to group "%(groupFullName)s"'), { + groupFullName: this.thread.authorizedGroupFullName, + }); }, }), - chatWindow: one('ChatWindow', { identifying: true, inverse: 'channelInvitationForm' }), + chatWindow: one("ChatWindow", { identifying: true, inverse: "channelInvitationForm" }), /** * States the OWL component of this channel invitation form. * Useful to be able to close it with popover trigger, or to know when @@ -207,9 +218,9 @@ Model({ return clear(); } switch (this.thread.channel.channel_type) { - case 'chat': + case "chat": return this.env._t("Create group chat"); - case 'group': + case "group": return this.env._t("Invite to group chat"); } return this.env._t("Invite to Channel"); @@ -218,12 +229,16 @@ Model({ /** * If set, this channel invitation form is content of related popover view. */ - popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'channelInvitationForm', isCausal: true }), + popoverViewOwner: one("PopoverView", { + identifying: true, + inverse: "channelInvitationForm", + isCausal: true, + }), /** * States the OWL ref of the "search" input of this channel invitation * form. Useful to be able to focus it. */ - searchInputRef: attr({ ref: 'searchInput' }), + searchInputRef: attr({ ref: "searchInput" }), /** * States the number of results of the last search. */ @@ -236,31 +251,34 @@ Model({ * States all partners that are potential choices according to this * search term. */ - selectablePartners: many('Partner'), - selectablePartnerViews: many('ChannelInvitationFormSelectablePartnerView', { inverse: 'channelInvitationFormOwner', + selectablePartners: many("Partner"), + selectablePartnerViews: many("ChannelInvitationFormSelectablePartnerView", { + inverse: "channelInvitationFormOwner", compute() { if (this.selectablePartners.length === 0) { return clear(); } - return this.selectablePartners.map(partner => ({ partner })); + return this.selectablePartners.map((partner) => ({ partner })); }, }), /** * Determines all partners that are currently selected. */ - selectedPartners: many('Partner'), - selectedPartnerViews: many('ChannelInvitationFormSelectedPartnerView', { inverse: 'channelInvitationFormOwner', + selectedPartners: many("Partner"), + selectedPartnerViews: many("ChannelInvitationFormSelectedPartnerView", { + inverse: "channelInvitationFormOwner", compute() { if (this.selectedPartners.length === 0) { return clear(); } - return this.selectedPartners.map(partner => ({ partner })); + return this.selectedPartners.map((partner) => ({ partner })); }, }), /** * States the thread on which this list operates (if any). */ - thread: one('Thread', { required: true, + thread: one("Thread", { + required: true, compute() { if ( this.popoverViewOwner && diff --git a/addons/mail/static/src/models/channel_invitation_form_selectable_partner_view.js b/addons/mail/static/src/models/channel_invitation_form_selectable_partner_view.js index c2fd826254c..da0183721cf 100644 --- a/addons/mail/static/src/models/channel_invitation_form_selectable_partner_view.js +++ b/addons/mail/static/src/models/channel_invitation_form_selectable_partner_view.js @@ -1,14 +1,21 @@ /** @odoo-module **/ -import { clear, one, Model } from '@mail/model'; +import { clear, one, Model } from "@mail/model"; Model({ - name: 'ChannelInvitationFormSelectablePartnerView', - template: 'mail.ChannelInvitationFormSelectablePartnerView', + name: "ChannelInvitationFormSelectablePartnerView", + template: "mail.ChannelInvitationFormSelectablePartnerView", fields: { - channelInvitationFormOwner: one('ChannelInvitationForm', { identifying: true, inverse: 'selectablePartnerViews' }), - partner: one('Partner', { identifying: true, inverse: 'channelInvitationFormSelectablePartnerViews' }), - personaImStatusIconView: one('PersonaImStatusIconView', { inverse: 'channelInvitationFormSelectablePartnerViewOwner', + channelInvitationFormOwner: one("ChannelInvitationForm", { + identifying: true, + inverse: "selectablePartnerViews", + }), + partner: one("Partner", { + identifying: true, + inverse: "channelInvitationFormSelectablePartnerViews", + }), + personaImStatusIconView: one("PersonaImStatusIconView", { + inverse: "channelInvitationFormSelectablePartnerViewOwner", compute() { return this.partner.isImStatusSet ? {} : clear(); }, diff --git a/addons/mail/static/src/models/channel_invitation_form_selected_partner_view.js b/addons/mail/static/src/models/channel_invitation_form_selected_partner_view.js index 4d9c3017650..5f72946db51 100644 --- a/addons/mail/static/src/models/channel_invitation_form_selected_partner_view.js +++ b/addons/mail/static/src/models/channel_invitation_form_selected_partner_view.js @@ -1,12 +1,18 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'ChannelInvitationFormSelectedPartnerView', - template: 'mail.ChannelInvitationFormSelectedPartnerView', + name: "ChannelInvitationFormSelectedPartnerView", + template: "mail.ChannelInvitationFormSelectedPartnerView", fields: { - channelInvitationFormOwner: one('ChannelInvitationForm', { identifying: true, inverse: 'selectedPartnerViews' }), - partner: one('Partner', { identifying: true, inverse: 'channelInvitationFormSelectedPartnerViews' }), + channelInvitationFormOwner: one("ChannelInvitationForm", { + identifying: true, + inverse: "selectedPartnerViews", + }), + partner: one("Partner", { + identifying: true, + inverse: "channelInvitationFormSelectedPartnerViews", + }), }, }); diff --git a/addons/mail/static/src/models/channel_member.js b/addons/mail/static/src/models/channel_member.js index a6139f7f651..19d78d0b485 100644 --- a/addons/mail/static/src/models/channel_member.js +++ b/addons/mail/static/src/models/channel_member.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, one, many, Model } from '@mail/model'; +import { attr, clear, one, many, Model } from "@mail/model"; Model({ - name: 'ChannelMember', + name: "ChannelMember", fields: { avatarUrl: attr({ compute() { @@ -16,14 +16,19 @@ Model({ return clear(); }, }), - callParticipantCards: many('CallParticipantCard', { inverse: 'channelMember', isCausal: true }), - channel: one('Channel', { inverse: 'channelMembers', readonly: true, required: true }), - channelAsMemberOfCurrentUser: one('Channel', { inverse: 'memberOfCurrentUser', + callParticipantCards: many("CallParticipantCard", { + inverse: "channelMember", + isCausal: true, + }), + channel: one("Channel", { inverse: "channelMembers", readonly: true, required: true }), + channelAsMemberOfCurrentUser: one("Channel", { + inverse: "memberOfCurrentUser", compute() { return this.isMemberOfCurrentUser ? this.channel : clear(); }, }), - channelAsOfflineMember: one('Channel', { inverse: 'orderedOfflineMembers', + channelAsOfflineMember: one("Channel", { + inverse: "orderedOfflineMembers", compute() { if (this.persona.partner) { return !this.persona.partner.isOnline ? this.channel : clear(); @@ -34,7 +39,8 @@ Model({ return clear(); }, }), - channelAsOnlineMember: one('Channel', { inverse: 'orderedOnlineMembers', + channelAsOnlineMember: one("Channel", { + inverse: "orderedOnlineMembers", compute() { if (this.persona.partner) { return this.persona.partner.isOnline ? this.channel : clear(); @@ -45,9 +51,10 @@ Model({ return clear(); }, }), - channelMemberViews: many('ChannelMemberView', { inverse: 'channelMember' }), + channelMemberViews: many("ChannelMemberView", { inverse: "channelMember" }), id: attr({ identifying: true }), - isMemberOfCurrentUser: attr({ default: false, + isMemberOfCurrentUser: attr({ + default: false, compute() { if (this.messaging.currentPartner) { return this.messaging.currentPartner.persona === this.persona; @@ -64,8 +71,10 @@ Model({ }, }), isTyping: attr({ default: false }), - otherMemberLongTypingInThreadTimers: many('OtherMemberLongTypingInThreadTimer', { inverse: 'member' }), - persona: one('Persona', { inverse: 'channelMembers', readonly: true, required: true }), - rtcSession: one('RtcSession', { inverse: 'channelMember' }), + otherMemberLongTypingInThreadTimers: many("OtherMemberLongTypingInThreadTimer", { + inverse: "member", + }), + persona: one("Persona", { inverse: "channelMembers", readonly: true, required: true }), + rtcSession: one("RtcSession", { inverse: "channelMember" }), }, }); diff --git a/addons/mail/static/src/models/channel_member_list_category_view.js b/addons/mail/static/src/models/channel_member_list_category_view.js index ea5cba9fcec..1bbd69e05de 100644 --- a/addons/mail/static/src/models/channel_member_list_category_view.js +++ b/addons/mail/static/src/models/channel_member_list_category_view.js @@ -1,15 +1,16 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; -import { sprintf } from '@web/core/utils/strings'; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'ChannelMemberListCategoryView', - template: 'mail.ChannelMemberListCategoryView', - identifyingMode: 'xor', + name: "ChannelMemberListCategoryView", + template: "mail.ChannelMemberListCategoryView", + identifyingMode: "xor", fields: { - channel: one('Channel', { required: true, + channel: one("Channel", { + required: true, compute() { if (this.channelMemberListViewOwnerAsOffline) { return this.channelMemberListViewOwnerAsOffline.channel; @@ -19,17 +20,24 @@ Model({ } }, }), - channelMemberListViewOwnerAsOffline: one('ChannelMemberListView', { identifying: true, inverse: 'offlineCategoryView' }), - channelMemberListViewOwnerAsOnline: one('ChannelMemberListView', { identifying: true, inverse: 'onlineCategoryView' }), - channelMemberViews: many('ChannelMemberView', { inverse: 'channelMemberListCategoryViewOwner', + channelMemberListViewOwnerAsOffline: one("ChannelMemberListView", { + identifying: true, + inverse: "offlineCategoryView", + }), + channelMemberListViewOwnerAsOnline: one("ChannelMemberListView", { + identifying: true, + inverse: "onlineCategoryView", + }), + channelMemberViews: many("ChannelMemberView", { + inverse: "channelMemberListCategoryViewOwner", compute() { if (this.members.length === 0) { return clear(); } - return this.members.map(channelMember => ({ channelMember })); + return this.members.map((channelMember) => ({ channelMember })); }, }), - members: many('ChannelMember', { + members: many("ChannelMember", { compute() { if (this.channelMemberListViewOwnerAsOnline) { return this.channel.orderedOnlineMembers; @@ -49,13 +57,10 @@ Model({ if (this.channelMemberListViewOwnerAsOffline) { categoryText = this.env._t("Offline"); } - return sprintf( - this.env._t("%(categoryText)s - %(memberCount)s"), - { - categoryText, - memberCount: this.members.length, - } - ); + return sprintf(this.env._t("%(categoryText)s - %(memberCount)s"), { + categoryText, + memberCount: this.members.length, + }); }, }), }, diff --git a/addons/mail/static/src/models/channel_member_list_view.js b/addons/mail/static/src/models/channel_member_list_view.js index cb21fe2f357..e7480455f03 100644 --- a/addons/mail/static/src/models/channel_member_list_view.js +++ b/addons/mail/static/src/models/channel_member_list_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { clear, one, Model } from '@mail/model'; +import { clear, one, Model } from "@mail/model"; Model({ - name: 'ChannelMemberListView', - template: 'mail.ChannelMemberListView', - identifyingMode: 'xor', + name: "ChannelMemberListView", + template: "mail.ChannelMemberListView", + identifyingMode: "xor", lifecycleHooks: { _created() { this.channel.fetchChannelMembers(); @@ -20,7 +20,7 @@ Model({ }, }, fields: { - channel: one('Channel', { + channel: one("Channel", { compute() { if (this.chatWindowOwner) { return this.chatWindowOwner.thread.channel; @@ -31,8 +31,9 @@ Model({ return clear(); }, }), - chatWindowOwner: one('ChatWindow', { identifying: true, inverse: 'channelMemberListView' }), - offlineCategoryView: one('ChannelMemberListCategoryView', { inverse: 'channelMemberListViewOwnerAsOffline', + chatWindowOwner: one("ChatWindow", { identifying: true, inverse: "channelMemberListView" }), + offlineCategoryView: one("ChannelMemberListCategoryView", { + inverse: "channelMemberListViewOwnerAsOffline", compute() { if (this.channel && this.channel.orderedOfflineMembers.length > 0) { return {}; @@ -40,7 +41,8 @@ Model({ return clear(); }, }), - onlineCategoryView: one('ChannelMemberListCategoryView', { inverse: 'channelMemberListViewOwnerAsOnline', + onlineCategoryView: one("ChannelMemberListCategoryView", { + inverse: "channelMemberListViewOwnerAsOnline", compute() { if (this.channel && this.channel.orderedOnlineMembers.length > 0) { return {}; @@ -48,6 +50,6 @@ Model({ return clear(); }, }), - threadViewOwner: one('ThreadView', { identifying: true, inverse: 'channelMemberListView' }), + threadViewOwner: one("ThreadView", { identifying: true, inverse: "channelMemberListView" }), }, }); diff --git a/addons/mail/static/src/models/channel_member_view.js b/addons/mail/static/src/models/channel_member_view.js index 4e6c63c811e..95c0581d65f 100644 --- a/addons/mail/static/src/models/channel_member_view.js +++ b/addons/mail/static/src/models/channel_member_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; -import { isEventHandled } from '@mail/utils/utils'; +import { attr, clear, one, Model } from "@mail/model"; +import { isEventHandled } from "@mail/utils/utils"; Model({ - name: 'ChannelMemberView', - template: 'mail.ChannelMemberView', + name: "ChannelMemberView", + template: "mail.ChannelMemberView", recordMethods: { /** * Handles click on channel member in the member list of this channel. @@ -13,15 +13,21 @@ Model({ * @param {MouseEvent} ev */ onClickMember(ev) { - if (isEventHandled(ev, 'PersonaImStatusIcon.Click') || !this.channelMember.persona.partner) { + if ( + isEventHandled(ev, "PersonaImStatusIcon.Click") || + !this.channelMember.persona.partner + ) { return; } this.channelMember.persona.partner.openChat(); }, }, fields: { - channelMemberListCategoryViewOwner: one('ChannelMemberListCategoryView', { identifying: true, inverse: 'channelMemberViews' }), - channelMember: one('ChannelMember', { identifying: true, inverse: 'channelMemberViews' }), + channelMemberListCategoryViewOwner: one("ChannelMemberListCategoryView", { + identifying: true, + inverse: "channelMemberViews", + }), + channelMember: one("ChannelMember", { identifying: true, inverse: "channelMemberViews" }), hasOpenChat: attr({ compute() { return this.channelMember.persona.partner ? true : false; @@ -29,15 +35,22 @@ Model({ }), memberTitleText: attr({ compute() { - return this.hasOpenChat ? this.env._t("Open chat") : ''; + return this.hasOpenChat ? this.env._t("Open chat") : ""; }, }), - personaImStatusIconView: one('PersonaImStatusIconView', { inverse: 'channelMemberViewOwner', + personaImStatusIconView: one("PersonaImStatusIconView", { + inverse: "channelMemberViewOwner", compute() { - if (this.channelMember.persona.guest && this.channelMember.persona.guest.im_status) { + if ( + this.channelMember.persona.guest && + this.channelMember.persona.guest.im_status + ) { return {}; } - return this.channelMember.persona.partner && this.channelMember.persona.partner.isImStatusSet ? {} : clear(); + return this.channelMember.persona.partner && + this.channelMember.persona.partner.isImStatusSet + ? {} + : clear(); }, }), }, diff --git a/addons/mail/static/src/models/channel_preview_view.js b/addons/mail/static/src/models/channel_preview_view.js index 4ef74e2e256..c9ced66a52b 100644 --- a/addons/mail/static/src/models/channel_preview_view.js +++ b/addons/mail/static/src/models/channel_preview_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; -import { htmlToTextContentInline } from '@mail/js/utils'; +import { attr, clear, one, Model } from "@mail/model"; +import { htmlToTextContentInline } from "@mail/js/utils"; Model({ - name: 'ChannelPreviewView', - template: 'mail.ChannelPreviewView', + name: "ChannelPreviewView", + template: "mail.ChannelPreviewView", recordMethods: { /** * @param {MouseEvent} ev @@ -34,7 +34,7 @@ Model({ }, }, fields: { - channel: one('Channel', { identifying: true, inverse: 'channelPreviewViews' }), + channel: one("Channel", { identifying: true, inverse: "channelPreviewViews" }), imageUrl: attr({ compute() { if (this.channel.correspondent) { @@ -43,7 +43,8 @@ Model({ return `/web/image/mail.channel/${this.channel.id}/avatar_128?unique=${this.channel.avatarCacheKey}`; }, }), - inlineLastMessageBody: attr({ default: "", + inlineLastMessageBody: attr({ + default: "", compute() { if (!this.thread || !this.thread.lastMessage) { return clear(); @@ -56,9 +57,13 @@ Model({ return !this.inlineLastMessageBody && !this.lastTrackingValue; }, }), - lastTrackingValue: one('TrackingValue', { + lastTrackingValue: one("TrackingValue", { compute() { - if (this.thread && this.thread.lastMessage && this.thread.lastMessage.lastTrackingValue) { + if ( + this.thread && + this.thread.lastMessage && + this.thread.lastMessage.lastTrackingValue + ) { return this.thread.lastMessage.lastTrackingValue; } return clear(); @@ -68,21 +73,22 @@ Model({ * Reference of the "mark as read" button. Useful to disable the * top-level click handler when clicking on this specific button. */ - markAsReadRef: attr({ ref: 'markAsRead' }), - messageAuthorPrefixView: one('MessageAuthorPrefixView', { inverse: 'channelPreviewViewOwner', + markAsReadRef: attr({ ref: "markAsRead" }), + messageAuthorPrefixView: one("MessageAuthorPrefixView", { + inverse: "channelPreviewViewOwner", compute() { - if ( - this.thread && - this.thread.lastMessage && - this.thread.lastMessage.author - ) { + if (this.thread && this.thread.lastMessage && this.thread.lastMessage.author) { return {}; } return clear(); }, }), - notificationListViewOwner: one('NotificationListView', { identifying: true, inverse: 'channelPreviewViews' }), - personaImStatusIconView: one('PersonaImStatusIconView', { inverse: 'channelPreviewViewOwner', + notificationListViewOwner: one("NotificationListView", { + identifying: true, + inverse: "channelPreviewViews", + }), + personaImStatusIconView: one("PersonaImStatusIconView", { + inverse: "channelPreviewViewOwner", compute() { if (!this.channel.correspondent) { return clear(); @@ -93,6 +99,6 @@ Model({ return clear(); }, }), - thread: one('Thread', { related: 'channel.thread' }), + thread: one("Thread", { related: "channel.thread" }), }, }); diff --git a/addons/mail/static/src/models/chat_window.js b/addons/mail/static/src/models/chat_window.js index 3d15009181c..81287e27fa7 100644 --- a/addons/mail/static/src/models/chat_window.js +++ b/addons/mail/static/src/models/chat_window.js @@ -1,15 +1,15 @@ /** @odoo-module **/ -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, one, Model } from '@mail/model'; -import { isEventHandled, markEventHandled } from '@mail/utils/utils'; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, one, Model } from "@mail/model"; +import { isEventHandled, markEventHandled } from "@mail/utils/utils"; Model({ - name: 'ChatWindow', - identifyingMode: 'xor', - template: 'mail.ChatWindow', + name: "ChatWindow", + identifyingMode: "xor", + template: "mail.ChatWindow", componentSetup() { - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, recordMethods: { /** @@ -33,7 +33,7 @@ Model({ // server when manually closing the chat window. Delete at destroy // or sync from server value for example should not save the value. if (this.thread && notifyServer && !this.messaging.currentGuest) { - this.thread.notifyFoldStateToServer('closed'); + this.thread.notifyFoldStateToServer("closed"); } if (this.exists()) { this.delete(); @@ -62,8 +62,9 @@ Model({ } }, focusPreviousVisibleUnfoldedChatWindow() { - const previousVisibleUnfoldedChatWindow = - this._getNextVisibleUnfoldedChatWindow({ reverse: true }); + const previousVisibleUnfoldedChatWindow = this._getNextVisibleUnfoldedChatWindow({ + reverse: true, + }); if (previousVisibleUnfoldedChatWindow) { previousVisibleUnfoldedChatWindow.focus(); } @@ -80,7 +81,7 @@ Model({ // Flux specific: manually folding the chat window should save the // new state on the server. if (this.thread && notifyServer && !this.messaging.currentGuest) { - this.thread.notifyFoldStateToServer('folded'); + this.thread.notifyFoldStateToServer("folded"); } }, /** @@ -92,7 +93,11 @@ Model({ makeActive(options) { this.makeVisible(); this.unfold(options); - if ((options && options.focus !== undefined) ? options.focus : !this.messaging.device.isMobileDevice) { + if ( + options && options.focus !== undefined + ? options.focus + : !this.messaging.device.isMobileDevice + ) { this.focus(); } }, @@ -135,16 +140,16 @@ Model({ * @param {function} res */ onAutocompleteSource(req, res) { - this.messaging.models['Partner'].imSearch({ + this.messaging.models["Partner"].imSearch({ callback: (partners) => { - const suggestions = partners.map(partner => { + const suggestions = partners.map((partner) => { return { id: partner.id, value: partner.nameOrDisplayName, label: partner.nameOrDisplayName, }; }); - res(_.sortBy(suggestions, 'label')); + res(_.sortBy(suggestions, "label")); }, keyword: _.escape(req.term), limit: 10, @@ -204,7 +209,7 @@ Model({ * @param {MouseEvent} ev */ onClickHideCallSettingsMenu(ev) { - markEventHandled(ev, 'ChatWindow.onClickCommand'); + markEventHandled(ev, "ChatWindow.onClickCommand"); this.update({ isCallSettingsMenuOpen: false }); }, /** @@ -213,17 +218,17 @@ Model({ * @param {MouseEvent} ev */ onClickHideInviteForm(ev) { - markEventHandled(ev, 'ChatWindow.onClickCommand'); + markEventHandled(ev, "ChatWindow.onClickCommand"); this.update({ channelInvitationForm: clear() }); }, /** * @param {MouseEvent} ev */ onClickHideMemberList(ev) { - markEventHandled(ev, 'ChatWindow.onClickHideMemberList'); + markEventHandled(ev, "ChatWindow.onClickHideMemberList"); this.update({ isMemberListOpened: false }); if (this.threadViewer.threadView) { - this.threadViewer.threadView.addComponentHint('member-list-hidden'); + this.threadViewer.threadView.addComponentHint("member-list-hidden"); } }, /** @@ -242,7 +247,7 @@ Model({ * @param {MouseEvent} ev */ onClickShowInviteForm(ev) { - markEventHandled(ev, 'ChatWindow.onClickCommand'); + markEventHandled(ev, "ChatWindow.onClickCommand"); this.update({ channelInvitationForm: { doFocusOnSearchInput: true, @@ -257,7 +262,7 @@ Model({ * @param {MouseEvent} ev */ onClickShowCallSettingsMenu(ev) { - markEventHandled(ev, 'ChatWindow.onClickCommand'); + markEventHandled(ev, "ChatWindow.onClickCommand"); this.update({ isCallSettingsMenuOpen: true, isMemberListOpened: false, @@ -267,7 +272,7 @@ Model({ * @param {MouseEvent} ev */ onClickShowMemberList(ev) { - markEventHandled(ev, 'ChatWindow.onClickShowMemberList'); + markEventHandled(ev, "ChatWindow.onClickShowMemberList"); this.update({ channelInvitationForm: clear(), isCallSettingsMenuOpen: false, @@ -312,7 +317,7 @@ Model({ return; } switch (ev.key) { - case 'Tab': + case "Tab": ev.preventDefault(); if (ev.shiftKey) { this.focusPreviousVisibleUnfoldedChatWindow(); @@ -320,11 +325,11 @@ Model({ this.focusNextVisibleUnfoldedChatWindow(); } break; - case 'Escape': - if (isEventHandled(ev, 'ComposerTextInput.closeSuggestions')) { + case "Escape": + if (isEventHandled(ev, "ComposerTextInput.closeSuggestions")) { break; } - if (isEventHandled(ev, 'Composer.closeEmojisPopover')) { + if (isEventHandled(ev, "Composer.closeEmojisPopover")) { break; } ev.preventDefault(); @@ -375,7 +380,7 @@ Model({ // Flux specific: manually opening the chat window should save the // new state on the server. if (this.thread && notifyServer && !this.messaging.currentGuest) { - this.thread.notifyFoldStateToServer('open'); + this.thread.notifyFoldStateToServer("open"); } }, /** @@ -399,7 +404,7 @@ Model({ * @param {integer} index * @returns {integer} */ - const _getNextIndex = index => { + const _getNextIndex = (index) => { const directionOffset = reverse ? 1 : -1; let nextIndex = index + directionOffset; if (nextIndex > orderedVisible.length - 1) { @@ -411,7 +416,7 @@ Model({ return nextIndex; }; - const currentIndex = orderedVisible.findIndex(visible => visible === this); + const currentIndex = orderedVisible.findIndex((visible) => visible === this); let nextIndex = _getNextIndex(currentIndex); let nextToFocus = orderedVisible[nextIndex]; while (nextToFocus.isFolded) { @@ -425,7 +430,8 @@ Model({ /** * Model for the component with the controls for RTC related settings. */ - callSettingsMenu: one('CallSettingsMenu', { inverse: 'chatWindowOwner', + callSettingsMenu: one("CallSettingsMenu", { + inverse: "chatWindowOwner", compute() { if (this.isCallSettingsMenuOpen) { return {}; @@ -437,8 +443,9 @@ Model({ * Determines the channel invitation form displayed by this chat window * (if any). Only makes sense if hasInviteFeature is true. */ - channelInvitationForm: one('ChannelInvitationForm', { inverse: 'chatWindow' }), - channelMemberListView: one('ChannelMemberListView', { inverse: 'chatWindowOwner', + channelInvitationForm: one("ChannelInvitationForm", { inverse: "chatWindow" }), + channelMemberListView: one("ChannelMemberListView", { + inverse: "chatWindowOwner", compute() { if (this.thread && this.thread.hasMemberListFeature && this.isMemberListOpened) { return {}; @@ -446,27 +453,35 @@ Model({ return clear(); }, }), - chatWindowHeaderView: one('ChatWindowHeaderView', { default: {}, inverse: 'chatWindowOwner' }), + chatWindowHeaderView: one("ChatWindowHeaderView", { + default: {}, + inverse: "chatWindowOwner", + }), componentStyle: attr({ compute() { const textDirection = this.messaging.locale.textDirection; - const offsetFrom = textDirection === 'rtl' ? 'left' : 'right'; - const oppositeFrom = offsetFrom === 'right' ? 'left' : 'right'; + const offsetFrom = textDirection === "rtl" ? "left" : "right"; + const oppositeFrom = offsetFrom === "right" ? "left" : "right"; return `${offsetFrom}: ${this.visibleOffset}px; ${oppositeFrom}: auto`; }, }), /** * Determines whether the buttons to start a RTC call should be displayed. */ - hasCallButtons: attr({ default: false, + hasCallButtons: attr({ + default: false, compute() { if (!this.thread || !this.thread.channel) { return clear(); } - return this.thread.rtcSessions.length === 0 && ['channel', 'chat', 'group'].includes(this.thread.channel.channel_type); + return ( + this.thread.rtcSessions.length === 0 && + ["channel", "chat", "group"].includes(this.thread.channel.channel_type) + ); }, }), - hasCloseAsBackButton: attr({ default: false, + hasCloseAsBackButton: attr({ + default: false, compute() { if (this.isVisible && this.messaging.device.isSmall) { return true; @@ -480,8 +495,11 @@ Model({ hasInviteFeature: attr({ compute() { return Boolean( - this.thread && this.thread.hasInviteFeature && - this.messaging && this.messaging.device && this.messaging.device.isSmall + this.thread && + this.thread.hasInviteFeature && + this.messaging && + this.messaging.device && + this.messaging.device.isSmall ); }, }), @@ -498,7 +516,14 @@ Model({ */ hasThreadView: attr({ compute() { - return this.isVisible && !this.isFolded && !!this.thread && !this.isMemberListOpened && !this.channelInvitationForm && !this.isCallSettingsMenuOpen; + return ( + this.isVisible && + !this.isFolded && + !!this.thread && + !this.isMemberListOpened && + !this.channelInvitationForm && + !this.isCallSettingsMenuOpen + ); }, }), isCallSettingsMenuOpen: attr({ default: false }), @@ -509,7 +534,8 @@ Model({ * any re-render will programmatically set focus again! */ isDoFocus: attr({ default: false }), - isExpandable: attr({ default: false, + isExpandable: attr({ + default: false, compute() { if (this.isVisible && !this.messaging.device.isSmall && this.thread) { return true; @@ -525,7 +551,8 @@ Model({ * Determines whether `this` is folded. */ isFolded: attr({ default: false }), - isFullscreen: attr({ default: false, + isFullscreen: attr({ + default: false, compute() { if (this.isVisible && this.messaging.device.isSmall) { return true; @@ -551,8 +578,11 @@ Model({ return this.manager.allOrderedVisible.includes(this); }, }), - manager: one('ChatWindowManager', { inverse: 'chatWindows', readonly: true }), - managerAsNewMessage: one('ChatWindowManager', { identifying: true, inverse: 'newMessageChatWindow' }), + manager: one("ChatWindowManager", { inverse: "chatWindows", readonly: true }), + managerAsNewMessage: one("ChatWindowManager", { + identifying: true, + inverse: "newMessageChatWindow", + }), name: attr({ compute() { if (this.thread) { @@ -561,7 +591,8 @@ Model({ return this.env._t("New message"); }, }), - newMessageAutocompleteInputView: one('AutocompleteInputView', { inverse: 'chatWindowOwnerAsNewMessage', + newMessageAutocompleteInputView: one("AutocompleteInputView", { + inverse: "chatWindowOwnerAsNewMessage", compute() { if (this.hasNewMessageForm) { return {}; @@ -582,15 +613,17 @@ Model({ * Determines the `Thread` that should be displayed by `this`. * If no `Thread` is linked, `this` is considered "new message". */ - thread: one('Thread', { identifying: true, inverse: 'chatWindow' }), + thread: one("Thread", { identifying: true, inverse: "chatWindow" }), /** * States the `ThreadView` displaying `this.thread`. */ - threadView: one('ThreadView', { related: 'threadViewer.threadView' }), + threadView: one("ThreadView", { related: "threadViewer.threadView" }), /** * Determines the `ThreadViewer` managing the display of `this.thread`. */ - threadViewer: one('ThreadViewer', { inverse: 'chatWindow', required: true, + threadViewer: one("ThreadViewer", { + inverse: "chatWindow", + required: true, compute() { return { compact: true, @@ -611,7 +644,7 @@ Model({ return clear(); } const visible = this.manager.visual.visible; - const index = visible.findIndex(visible => visible.chatWindow === this); + const index = visible.findIndex((visible) => visible.chatWindow === this); if (index === -1) { return clear(); } @@ -624,7 +657,7 @@ Model({ return 0; } const visible = this.manager.visual.visible; - const index = visible.findIndex(visible => visible.chatWindow === this); + const index = visible.findIndex((visible) => visible.chatWindow === this); if (index === -1) { return 0; } diff --git a/addons/mail/static/src/models/chat_window_header_view.js b/addons/mail/static/src/models/chat_window_header_view.js index 8ffad43295e..bab9d0298f0 100644 --- a/addons/mail/static/src/models/chat_window_header_view.js +++ b/addons/mail/static/src/models/chat_window_header_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { clear, one, Model } from '@mail/model'; -import { isEventHandled } from '@mail/utils/utils'; +import { clear, one, Model } from "@mail/model"; +import { isEventHandled } from "@mail/utils/utils"; Model({ - name: 'ChatWindowHeaderView', - template: 'mail.ChatWindowHeaderView', + name: "ChatWindowHeaderView", + template: "mail.ChatWindowHeaderView", recordMethods: { /** * @param {MouseEvent} ev @@ -15,9 +15,9 @@ Model({ return; } if ( - isEventHandled(ev, 'ChatWindow.onClickCommand') || - isEventHandled(ev, 'ChatWindow.onClickHideMemberList') || - isEventHandled(ev, 'ChatWindow.onClickShowMemberList') + isEventHandled(ev, "ChatWindow.onClickCommand") || + isEventHandled(ev, "ChatWindow.onClickHideMemberList") || + isEventHandled(ev, "ChatWindow.onClickShowMemberList") ) { return; } @@ -29,15 +29,16 @@ Model({ }, }, fields: { - chatWindowOwner: one('ChatWindow', { identifying: true, inverse: 'chatWindowHeaderView' }), - hiddenMenuItem: one('ChatWindowHiddenMenuItemView', { inverse: 'chatWindowHeaderView' }), - threadIconView: one('ThreadIconView', { inverse: 'chatWindowHeaderViewOwner', + chatWindowOwner: one("ChatWindow", { identifying: true, inverse: "chatWindowHeaderView" }), + hiddenMenuItem: one("ChatWindowHiddenMenuItemView", { inverse: "chatWindowHeaderView" }), + threadIconView: one("ThreadIconView", { + inverse: "chatWindowHeaderViewOwner", compute() { if (this.chatWindowOwner.thread && this.chatWindowOwner.thread.channel) { return {}; } return clear(); }, - }) + }), }, }); diff --git a/addons/mail/static/src/models/chat_window_hidden_menu_item_view.js b/addons/mail/static/src/models/chat_window_hidden_menu_item_view.js index ab639c73a46..c416c4166dc 100644 --- a/addons/mail/static/src/models/chat_window_hidden_menu_item_view.js +++ b/addons/mail/static/src/models/chat_window_hidden_menu_item_view.js @@ -1,17 +1,21 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'ChatWindowHiddenMenuItemView', - template: 'mail.ChatWindowHiddenMenuItemView', + name: "ChatWindowHiddenMenuItemView", + template: "mail.ChatWindowHiddenMenuItemView", fields: { - chatWindowHeaderView: one('ChatWindowHeaderView', { identifying: true, inverse: 'hiddenMenuItem' }), - isLast: attr({ default: false, + chatWindowHeaderView: one("ChatWindowHeaderView", { + identifying: true, + inverse: "hiddenMenuItem", + }), + isLast: attr({ + default: false, compute() { return this.owner.lastItem === this; }, }), - owner: one('ChatWindowHiddenMenuView', { identifying: true, inverse: 'items' }), + owner: one("ChatWindowHiddenMenuView", { identifying: true, inverse: "items" }), }, }); diff --git a/addons/mail/static/src/models/chat_window_hidden_menu_view.js b/addons/mail/static/src/models/chat_window_hidden_menu_view.js index e77bc451aed..13fb29f0e97 100644 --- a/addons/mail/static/src/models/chat_window_hidden_menu_view.js +++ b/addons/mail/static/src/models/chat_window_hidden_menu_view.js @@ -1,37 +1,37 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, many, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'ChatWindowHiddenMenuView', - template: 'mail.ChatWindowHiddenMenuView', + name: "ChatWindowHiddenMenuView", + template: "mail.ChatWindowHiddenMenuView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useComponentToModel({ fieldName: "component" }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, lifecycleHooks: { _created() { - document.addEventListener('click', this._onClickCaptureGlobal, true); + document.addEventListener("click", this._onClickCaptureGlobal, true); }, _willDelete() { - document.removeEventListener('click', this._onClickCaptureGlobal, true); + document.removeEventListener("click", this._onClickCaptureGlobal, true); }, }, recordMethods: { applyListHeight() { const device = this.messaging.device; const height = device.globalWindowInnerHeight / 2; - this.listRef.el.style['max-height'] = `${height}px`; + this.listRef.el.style["max-height"] = `${height}px`; }, applyOffset() { const textDirection = this.messaging.locale.textDirection; - const offsetFrom = textDirection === 'rtl' ? 'left' : 'right'; - const oppositeFrom = offsetFrom === 'right' ? 'left' : 'right'; + const offsetFrom = textDirection === "rtl" ? "left" : "right"; + const oppositeFrom = offsetFrom === "right" ? "left" : "right"; const offset = this.owner.visual.hiddenMenuOffset; this.component.root.el.style[offsetFrom] = `${offset}px`; - this.component.root.el.style[oppositeFrom] = 'auto'; + this.component.root.el.style[oppositeFrom] = "auto"; }, onComponentUpdate() { this.applyListHeight(); @@ -45,21 +45,28 @@ Model({ * @param {MouseEvent} ev */ _onClickCaptureGlobal(ev) { - if (!this.component || !this.component.root.el || this.component.root.el.contains(ev.target)) { + if ( + !this.component || + !this.component.root.el || + this.component.root.el.contains(ev.target) + ) { return; } this.owner.closeHiddenMenu(); - } + }, }, fields: { component: attr(), isOpen: attr({ default: false }), - items: many('ChatWindowHiddenMenuItemView', { inverse: 'owner', + items: many("ChatWindowHiddenMenuItemView", { + inverse: "owner", compute() { - return this.owner.hiddenChatWindowHeaderViews.map(chatWindowHeaderView => ({ chatWindowHeaderView })); + return this.owner.hiddenChatWindowHeaderViews.map((chatWindowHeaderView) => ({ + chatWindowHeaderView, + })); }, }), - lastItem: one('ChatWindowHiddenMenuItemView', { + lastItem: one("ChatWindowHiddenMenuItemView", { compute() { if (this.items.length === 0) { return clear(); @@ -71,7 +78,7 @@ Model({ * Reference of the dropup list. Useful to auto-set max height based on * browser screen height. */ - listRef: attr({ ref: 'list' }), - owner: one('ChatWindowManager', { identifying: true, inverse: 'hiddenMenuView' }), + listRef: attr({ ref: "list" }), + owner: one("ChatWindowManager", { identifying: true, inverse: "hiddenMenuView" }), }, }); diff --git a/addons/mail/static/src/models/chat_window_manager.js b/addons/mail/static/src/models/chat_window_manager.js index bb152a34aaf..43edaaeac5d 100644 --- a/addons/mail/static/src/models/chat_window_manager.js +++ b/addons/mail/static/src/models/chat_window_manager.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; export const BASE_VISUAL = { /** @@ -41,8 +41,8 @@ export const BASE_VISUAL = { }; Model({ - name: 'ChatWindowManager', - template: 'mail.ChatWindowManager', + name: "ChatWindowManager", + template: "mail.ChatWindowManager", recordMethods: { /** * Close all chat windows. @@ -98,19 +98,22 @@ Model({ * @param {boolean} [param1.notifyServer] * @param {boolean} [param1.replaceNewMessage=false] */ - openThread(thread, { - focus, - isFolded = false, - makeActive = false, - notifyServer, - replaceNewMessage = false - } = {}) { + openThread( + thread, + { + focus, + isFolded = false, + makeActive = false, + notifyServer, + replaceNewMessage = false, + } = {} + ) { if (notifyServer === undefined) { notifyServer = !this.messaging.device.isSmall; } let chatWindow = thread.chatWindow; if (!chatWindow) { - chatWindow = this.messaging.models['ChatWindow'].insert({ + chatWindow = this.messaging.models["ChatWindow"].insert({ isFolded, manager: this, thread, @@ -129,7 +132,7 @@ Model({ } // Flux specific: notify server of chat window being opened. if (notifyServer && !this.messaging.currentGuest) { - const foldState = chatWindow.isFolded ? 'folded' : 'open'; + const foldState = chatWindow.isFolded ? "folded" : "open"; thread.notifyFoldStateToServer(foldState); } }, @@ -138,8 +141,8 @@ Model({ * @param {ChatWindow} chatWindow2 */ swap(chatWindow1, chatWindow2) { - const index1 = this.chatWindows.findIndex(chatWindow => chatWindow === chatWindow1); - const index2 = this.chatWindows.findIndex(chatWindow => chatWindow === chatWindow2); + const index1 = this.chatWindows.findIndex((chatWindow) => chatWindow === chatWindow1); + const index2 = this.chatWindows.findIndex((chatWindow) => chatWindow === chatWindow2); if (index1 === -1 || index2 === -1) { return; } @@ -149,18 +152,18 @@ Model({ this.update({ chatWindows: _newOrdered }); for (const chatWindow of [chatWindow1, chatWindow2]) { if (chatWindow.threadView) { - chatWindow.threadView.addComponentHint('adjust-scroll'); + chatWindow.threadView.addComponentHint("adjust-scroll"); } } }, }, fields: { - allOrderedHidden: many('ChatWindow', { + allOrderedHidden: many("ChatWindow", { compute() { return this.visual.hiddenChatWindows; }, }), - allOrderedVisible: many('ChatWindow', { + allOrderedVisible: many("ChatWindow", { compute() { return this.visual.visible.map(({ chatWindow }) => chatWindow); }, @@ -168,13 +171,14 @@ Model({ /** * Amount of visible slots available for chat windows. */ - availableVisibleSlots: attr({ default: 0, + availableVisibleSlots: attr({ + default: 0, compute() { return this.visual.availableVisibleSlots; }, }), betweenGapWidth: attr({ default: 5 }), - chatWindows: many('ChatWindow', { inverse: 'manager', isCausal: true }), + chatWindows: many("ChatWindow", { inverse: "manager", isCausal: true }), chatWindowWidth: attr({ default: 340 }), endGapWidth: attr({ compute() { @@ -189,21 +193,26 @@ Model({ return this.allOrderedVisible.length > 0; }, }), - hiddenChatWindowHeaderViews: many('ChatWindowHeaderView', { + hiddenChatWindowHeaderViews: many("ChatWindowHeaderView", { compute() { if (this.allOrderedHidden.length > 0) { - return this.allOrderedHidden.map(chatWindow => ({ chatWindowOwner: chatWindow })); + return this.allOrderedHidden.map((chatWindow) => ({ + chatWindowOwner: chatWindow, + })); } return clear(); }, }), - hiddenMenuView: one('ChatWindowHiddenMenuView', { inverse: 'owner', + hiddenMenuView: one("ChatWindowHiddenMenuView", { + inverse: "owner", compute() { return this.visual.isHiddenMenuVisible ? {} : clear(); }, }), - hiddenMenuWidth: attr({ default: 170 /* max width, including width of dropup list items */ }), - lastVisible: one('ChatWindow', { + hiddenMenuWidth: attr({ + default: 170 /* max width, including width of dropup list items */, + }), + lastVisible: one("ChatWindow", { compute() { const { length: l, [l - 1]: lastVisible } = this.allOrderedVisible; if (!lastVisible) { @@ -212,7 +221,7 @@ Model({ return lastVisible; }, }), - newMessageChatWindow: one('ChatWindow', { inverse: 'managerAsNewMessage' }), + newMessageChatWindow: one("ChatWindow", { inverse: "managerAsNewMessage" }), startGapWidth: attr({ compute() { if (this.messaging.device.isSmall) { @@ -224,7 +233,7 @@ Model({ unreadHiddenConversationAmount: attr({ compute() { const allHiddenWithThread = this.allOrderedHidden.filter( - chatWindow => chatWindow.thread + (chatWindow) => chatWindow.thread ); let amount = 0; for (const chatWindow of allHiddenWithThread) { @@ -235,9 +244,10 @@ Model({ return amount; }, }), - visual: attr({ default: BASE_VISUAL, + visual: attr({ + default: BASE_VISUAL, compute() { - let visual = JSON.parse(JSON.stringify(BASE_VISUAL)); + const visual = JSON.parse(JSON.stringify(BASE_VISUAL)); if (!this.messaging || !this.messaging.device) { return visual; } @@ -247,12 +257,17 @@ Model({ if (!this.chatWindows.length) { return visual; } - const relativeGlobalWindowWidth = this.messaging.device.globalWindowInnerWidth - this.startGapWidth - this.endGapWidth; + const relativeGlobalWindowWidth = + this.messaging.device.globalWindowInnerWidth - + this.startGapWidth - + this.endGapWidth; let maxAmountWithoutHidden = Math.floor( - relativeGlobalWindowWidth / (this.chatWindowWidth + this.betweenGapWidth)); + relativeGlobalWindowWidth / (this.chatWindowWidth + this.betweenGapWidth) + ); let maxAmountWithHidden = Math.floor( (relativeGlobalWindowWidth - this.hiddenMenuWidth - this.betweenGapWidth) / - (this.chatWindowWidth + this.betweenGapWidth)); + (this.chatWindowWidth + this.betweenGapWidth) + ); if (this.messaging.device.isSmall) { maxAmountWithoutHidden = 1; maxAmountWithHidden = 1; @@ -261,7 +276,8 @@ Model({ // all visible for (let i = 0; i < this.chatWindows.length; i++) { const chatWindow = this.chatWindows[i]; - const offset = this.startGapWidth + i * (this.chatWindowWidth + this.betweenGapWidth); + const offset = + this.startGapWidth + i * (this.chatWindowWidth + this.betweenGapWidth); visual.visible.push({ chatWindow, offset }); } visual.availableVisibleSlots = maxAmountWithoutHidden; @@ -269,13 +285,16 @@ Model({ // some visible, some hidden for (let i = 0; i < maxAmountWithHidden; i++) { const chatWindow = this.chatWindows[i]; - const offset = this.startGapWidth + i * (this.chatWindowWidth + this.betweenGapWidth); + const offset = + this.startGapWidth + i * (this.chatWindowWidth + this.betweenGapWidth); visual.visible.push({ chatWindow, offset }); } if (this.chatWindows.length > maxAmountWithHidden) { visual.isHiddenMenuVisible = !this.messaging.device.isSmall; - visual.hiddenMenuOffset = visual.visible[maxAmountWithHidden - 1].offset - + this.chatWindowWidth + this.betweenGapWidth; + visual.hiddenMenuOffset = + visual.visible[maxAmountWithHidden - 1].offset + + this.chatWindowWidth + + this.betweenGapWidth; } for (let j = maxAmountWithHidden; j < this.chatWindows.length; j++) { visual.hiddenChatWindows.push(this.chatWindows[j]); @@ -286,7 +305,7 @@ Model({ visual.isHiddenMenuVisible = !this.messaging.device.isSmall; visual.hiddenMenuOffset = this.startGapWidth; visual.hiddenChatWindows.push(...this.chatWindows); - console.warn('cannot display any visible chat windows (screen is too small)'); + console.warn("cannot display any visible chat windows (screen is too small)"); visual.availableVisibleSlots = 0; } return visual; diff --git a/addons/mail/static/src/models/chatter.js b/addons/mail/static/src/models/chatter.js index 71fadce95f6..fb11bd2e220 100644 --- a/addons/mail/static/src/models/chatter.js +++ b/addons/mail/static/src/models/chatter.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { attr, clear, insert, link, many, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { attr, clear, insert, link, many, one, Model } from "@mail/model"; const getThreadNextTemporaryId = (function () { let tmpId = 0; @@ -20,11 +20,11 @@ const getMessageNextTemporaryId = (function () { })(); Model({ - name: 'Chatter', - template: 'mail.Chatter', + name: "Chatter", + template: "mail.Chatter", isLegacyComponent: true, componentSetup() { - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); }, recordMethods: { focus() { @@ -63,7 +63,7 @@ Model({ * @param {MouseEvent} ev */ onClickChatterTopbarClose(ev) { - this.component.trigger('o-close-chatter'); + this.component.trigger("o-close-chatter"); }, /** * @param {MouseEvent} ev @@ -117,7 +117,7 @@ Model({ return; } this.thread.unfollow(); - this.reloadParentView({ fieldNames: ['message_follower_ids'] }); + this.reloadParentView({ fieldNames: ["message_follower_ids"] }); }, /** * @param {MouseEvent} ev @@ -143,7 +143,11 @@ Model({ * @param {Event} ev */ onScrollScrollPanel(ev) { - if (!this.threadView || !this.threadView.messageListView || !this.threadView.messageListView.component) { + if ( + !this.threadView || + !this.threadView.messageListView || + !this.threadView.messageListView.component + ) { return; } this.threadView.messageListView.onScroll(); @@ -156,36 +160,33 @@ Model({ */ promptAddPartnerFollower() { const action = { - type: 'ir.actions.act_window', - res_model: 'mail.wizard.invite', - view_mode: 'form', - views: [[false, 'form']], + type: "ir.actions.act_window", + res_model: "mail.wizard.invite", + view_mode: "form", + views: [[false, "form"]], name: this.env._t("Invite Follower"), - target: 'new', + target: "new", context: { default_res_model: this.thread.model, default_res_id: this.thread.id, }, }; - this.env.services.action.doAction( - action, - { - onClose: async () => { - if (!this.exists() && !this.thread) { - return; - } - await this.thread.fetchData(['followers']); - if (this.exists() && this.hasParentReloadOnFollowersUpdate) { - this.reloadParentView(); - } - }, - } - ); + this.env.services.action.doAction(action, { + onClose: async () => { + if (!this.exists() && !this.thread) { + return; + } + await this.thread.fetchData(["followers"]); + if (this.exists() && this.hasParentReloadOnFollowersUpdate) { + this.reloadParentView(); + } + }, + }); }, async refresh() { - const requestData = ['activities', 'followers', 'suggestedRecipients']; + const requestData = ["activities", "followers", "suggestedRecipients"]; if (this.hasMessageList) { - requestData.push('attachments', 'messages'); + requestData.push("attachments", "messages"); } this.thread.fetchData(requestData); }, @@ -203,7 +204,7 @@ Model({ if (fieldNames) { options.fieldNames = fieldNames; } - this.component.trigger('reload', options); + this.component.trigger("reload", options); } }, showLogNote() { @@ -239,7 +240,7 @@ Model({ }); } else if (!this.thread || !this.thread.isTemporary) { const currentPartner = this.messaging.currentPartner; - const message = this.messaging.models['Message'].insert({ + const message = this.messaging.models["Message"].insert({ author: currentPartner, body: this.env._t("Creating a new record..."), id: getMessageNextTemporaryId(), @@ -282,12 +283,13 @@ Model({ }, }, fields: { - activityViews: many('ActivityView', { inverse: 'chatterOwner', + activityViews: many("ActivityView", { + inverse: "chatterOwner", compute() { if (!this.thread) { return clear(); } - return this.thread.activities.map(activity => { + return this.thread.activities.map((activity) => { return { activity }; }); }, @@ -295,17 +297,17 @@ Model({ /** * Determines the attachment list that will be used to display the attachments. */ - attachmentList: one('AttachmentList', { inverse: 'chatterOwner', + attachmentList: one("AttachmentList", { + inverse: "chatterOwner", compute() { - return (this.thread && this.thread.allAttachments.length > 0) - ? {} - : clear(); + return this.thread && this.thread.allAttachments.length > 0 ? {} : clear(); }, }), /** * Determines the label on the attachment button of the topbar. */ - attachmentButtonText: attr({ default: "", + attachmentButtonText: attr({ + default: "", compute() { if (!this.thread) { return clear(); @@ -317,7 +319,7 @@ Model({ return attachments.length; }, }), - attachmentsLoaderTimer: one('Timer', { inverse: 'chatterOwnerAsAttachmentsLoader' }), + attachmentsLoaderTimer: one("Timer", { inverse: "chatterOwnerAsAttachmentsLoader" }), /** * States the OWL Chatter component of this chatter. */ @@ -325,9 +327,10 @@ Model({ /** * Determines the composer view used to post in this chatter (if any). */ - composerView: one('ComposerView', { inverse: 'chatter' }), + composerView: one("ComposerView", { inverse: "chatter" }), context: attr({ default: {} }), - dropZoneView: one('DropZoneView', { inverse: 'chatterOwner', + dropZoneView: one("DropZoneView", { + inverse: "chatterOwner", compute() { if (!this.thread) { return clear(); @@ -338,12 +341,14 @@ Model({ return clear(); }, }), - fileUploader: one('FileUploader', { inverse: 'chatterOwner', + fileUploader: one("FileUploader", { + inverse: "chatterOwner", compute() { return this.thread ? {} : clear(); }, }), - followerListMenuView: one('FollowerListMenuView', { inverse: 'chatterOwner', + followerListMenuView: one("FollowerListMenuView", { + inverse: "chatterOwner", compute() { if (this.hasFollowers && this.thread) { return {}; @@ -363,7 +368,8 @@ Model({ * Determines whether `this` should display an activity box. */ hasActivities: attr({ default: true }), - hasActivityBox: attr({ default: false, + hasActivityBox: attr({ + default: false, compute() { if (this.thread && this.thread.hasActivities && this.thread.activities.length > 0) { return true; @@ -373,9 +379,14 @@ Model({ }), hasAttachmentBox: attr({ default: false }), hasExternalBorder: attr({ default: true }), - hasFollowButton: attr({ default: false, + hasFollowButton: attr({ + default: false, compute() { - if (this.hasFollowers && this.thread && (!this.thread.channel || this.thread.channel.channel_type !== 'chat')) { + if ( + this.hasFollowers && + this.thread && + (!this.thread.channel || this.thread.channel.channel_type !== "chat") + ) { return true; } return clear(); @@ -402,7 +413,9 @@ Model({ hasParentReloadOnMessagePosted: attr({ default: false }), hasReadAccess: attr({ compute() { - return Boolean(this.thread && !this.thread.isTemporary && this.thread.hasReadAccess); + return Boolean( + this.thread && !this.thread.isTemporary && this.thread.hasReadAccess + ); }, }), /** @@ -413,14 +426,17 @@ Model({ return Boolean(this.thread && this.hasMessageList); }, }), - hasTopBar: attr({ default: false, + hasTopBar: attr({ + default: false, compute() { return this.thread ? true : clear(); }, }), hasWriteAccess: attr({ compute() { - return Boolean(this.thread && !this.thread.isTemporary && this.thread.hasWriteAccess); + return Boolean( + this.thread && !this.thread.isTemporary && this.thread.hasWriteAccess + ); }, }), hasTopbarCloseButton: attr({ default: false }), @@ -441,14 +457,15 @@ Model({ }, }), isInFormSheetBg: attr({ default: false }), - isPreparingAttachmentsLoading: attr({ default: false, + isPreparingAttachmentsLoading: attr({ + default: false, compute() { return Boolean(this.attachmentsLoaderTimer); }, }), isShowingAttachmentsLoading: attr({ default: false }), isUnfollowButtonHighlighted: attr({ default: false }), - scrollPanelRef: attr({ ref: 'scrollPanel' }), + scrollPanelRef: attr({ ref: "scrollPanel" }), /** * Determines whether the view should reload after file changed in this chatter, * such as from a file upload. @@ -461,7 +478,7 @@ Model({ /** * Determines the `Thread` that should be displayed by `this`. */ - thread: one('Thread'), + thread: one("Thread"), /** * Determines the id of the thread that will be displayed by `this`. */ @@ -473,18 +490,19 @@ Model({ /** * States the `ThreadView` displaying `this.thread`. */ - threadView: one('ThreadView', { related: 'threadViewer.threadView' }), + threadView: one("ThreadView", { related: "threadViewer.threadView" }), /** * Determines the `ThreadViewer` managing the display of `this.thread`. */ - threadViewer: one('ThreadViewer', { inverse: 'chatter', + threadViewer: one("ThreadViewer", { + inverse: "chatter", compute() { if (!this.thread) { return clear(); } return { hasThreadView: this.hasThreadView, - order: 'desc', + order: "desc", thread: this.thread ? this.thread : clear(), }; }, @@ -497,17 +515,22 @@ Model({ return this.env._t("Unfollow"); }, }), - useDragVisibleDropZone: one('UseDragVisibleDropZone', { default: {}, inverse: 'chatterOwner', readonly: true, required: true }), + useDragVisibleDropZone: one("UseDragVisibleDropZone", { + default: {}, + inverse: "chatterOwner", + readonly: true, + required: true, + }), webRecord: attr(), }, onChanges: [ { - dependencies: ['threadId', 'threadModel'], - methodName: '_onThreadIdOrThreadModelChanged', + dependencies: ["threadId", "threadModel"], + methodName: "_onThreadIdOrThreadModelChanged", }, { - dependencies: ['thread.isLoadingAttachments'], - methodName: '_onThreadIsLoadingAttachmentsChanged', + dependencies: ["thread.isLoadingAttachments"], + methodName: "_onThreadIsLoadingAttachmentsChanged", }, ], }); diff --git a/addons/mail/static/src/models/clock.js b/addons/mail/static/src/models/clock.js index a9d2f2b48a2..0dcaf1dd77f 100644 --- a/addons/mail/static/src/models/clock.js +++ b/addons/mail/static/src/models/clock.js @@ -1,12 +1,12 @@ /** @odoo-module **/ -import { attr, many, Model } from '@mail/model'; +import { attr, many, Model } from "@mail/model"; /** * Models a record that provides the current date, updated at a given frequency. */ Model({ - name: 'Clock', + name: "Clock", lifecycleHooks: { _created() { // The date is set here rather than via a default value so that the @@ -55,12 +55,12 @@ Model({ * * The clock self-destructs when there are no more watchers. */ - watchers: many('ClockWatcher', { inverse: 'clock', isCausal: true }), + watchers: many("ClockWatcher", { inverse: "clock", isCausal: true }), }, onChanges: [ { - dependencies: ['watchers'], - methodName: '_onChangeWatchers', + dependencies: ["watchers"], + methodName: "_onChangeWatchers", }, ], }); diff --git a/addons/mail/static/src/models/clock_watcher.js b/addons/mail/static/src/models/clock_watcher.js index 8b5144ab32e..5965cde43ed 100644 --- a/addons/mail/static/src/models/clock_watcher.js +++ b/addons/mail/static/src/models/clock_watcher.js @@ -1,17 +1,20 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; /** * Models a record that makes use of a clock. */ Model({ - name: 'ClockWatcher', - identifyingMode: 'xor', + name: "ClockWatcher", + identifyingMode: "xor", fields: { - activityListViewItemOwner: one('ActivityListViewItem', { identifying: true, inverse: 'clockWatcher' }), - activityViewOwner: one('ActivityView', { identifying: true, inverse: 'clockWatcher' }), - clock: one('Clock', { inverse: 'watchers', required: true }), - messageViewOwner: one('MessageView', { identifying: true, inverse: 'clockWatcher' }), + activityListViewItemOwner: one("ActivityListViewItem", { + identifying: true, + inverse: "clockWatcher", + }), + activityViewOwner: one("ActivityView", { identifying: true, inverse: "clockWatcher" }), + clock: one("Clock", { inverse: "watchers", required: true }), + messageViewOwner: one("MessageView", { identifying: true, inverse: "clockWatcher" }), }, }); diff --git a/addons/mail/static/src/models/composer.js b/addons/mail/static/src/models/composer.js index 5c35c5b2281..922d0d1a51c 100644 --- a/addons/mail/static/src/models/composer.js +++ b/addons/mail/static/src/models/composer.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; -import { sprintf } from '@web/core/utils/strings'; +import { attr, clear, many, one, Model } from "@mail/model"; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'Composer', - identifyingMode: 'xor', + name: "Composer", + identifyingMode: "xor", recordMethods: { /** * @private @@ -26,9 +26,14 @@ Model({ }, }, fields: { - activeThread: one('Thread', { required: true, + activeThread: one("Thread", { + required: true, compute() { - if (this.messageViewInEditing && this.messageViewInEditing.message && this.messageViewInEditing.message.originThread) { + if ( + this.messageViewInEditing && + this.messageViewInEditing.message && + this.messageViewInEditing.message.originThread + ) { return this.messageViewInEditing.message.originThread; } if (this.thread) { @@ -40,8 +45,9 @@ Model({ /** * States which attachments are currently being created in this composer. */ - attachments: many('Attachment', { inverse: 'composer' }), - canPostMessage: attr({ default: false, + attachments: many("Attachment", { inverse: "composer" }), + canPostMessage: attr({ + default: false, compute() { if (this.thread && !this.textInputContent && this.attachments.length === 0) { return false; @@ -49,14 +55,14 @@ Model({ return !this.hasUploadingAttachment && !this.isPostingMessage; }, }), - composerViews: many('ComposerView', { inverse: 'composer', isCausal: true }), + composerViews: many("ComposerView", { inverse: "composer", isCausal: true }), /** * This field determines whether some attachments linked to this * composer are being uploaded. */ hasUploadingAttachment: attr({ compute() { - return this.attachments.some(attachment => attachment.isUploading); + return this.attachments.some((attachment) => attachment.isUploading); }, }), /** @@ -67,7 +73,7 @@ Model({ * Determines whether a post_message request is currently pending. */ isPostingMessage: attr(), - mentionedChannels: many('Thread', { + mentionedChannels: many("Thread", { /** * Detects if mentioned channels are still in the composer text input content * and removes them if not. @@ -78,9 +84,8 @@ Model({ // channels have the same name const namesIndex = {}; for (const channel of this.rawMentionedChannels) { - const fromIndex = namesIndex[channel.name] !== undefined - ? namesIndex[channel.name] + 1 : - 0; + const fromIndex = + namesIndex[channel.name] !== undefined ? namesIndex[channel.name] + 1 : 0; const index = this.textInputContent.indexOf(`#${channel.name}`, fromIndex); if (index === -1) { continue; @@ -91,7 +96,7 @@ Model({ return mentionedChannels; }, }), - mentionedPartners: many('Partner', { + mentionedPartners: many("Partner", { /** * Detects if mentioned partners are still in the composer text input content * and removes them if not. @@ -102,9 +107,8 @@ Model({ // partners have the same name const namesIndex = {}; for (const partner of this.rawMentionedPartners) { - const fromIndex = namesIndex[partner.name] !== undefined - ? namesIndex[partner.name] + 1 : - 0; + const fromIndex = + namesIndex[partner.name] !== undefined ? namesIndex[partner.name] + 1 : 0; const index = this.textInputContent.indexOf(`@${partner.name}`, fromIndex); if (index === -1) { continue; @@ -115,7 +119,10 @@ Model({ return mentionedPartners; }, }), - messageViewInEditing: one('MessageView', { identifying: true, inverse: 'composerForEditing' }), + messageViewInEditing: one("MessageView", { + identifying: true, + inverse: "composerForEditing", + }), /** * Placeholder displayed in the composer textarea when it's empty */ @@ -126,7 +133,10 @@ Model({ } if (this.thread.channel) { if (this.thread.channel.correspondent) { - return sprintf(this.env._t("Message %s..."), this.thread.channel.correspondent.nameOrDisplayName); + return sprintf( + this.env._t("Message %s..."), + this.thread.channel.correspondent.nameOrDisplayName + ); } return sprintf(this.env._t("Message #%s..."), this.thread.displayName); } @@ -136,14 +146,14 @@ Model({ return this.env._t("Send a message to followers..."); }, }), - rawMentionedChannels: many('Thread'), - rawMentionedPartners: many('Partner'), + rawMentionedChannels: many("Thread"), + rawMentionedPartners: many("Partner"), /** * Determines the extra `Partner` (on top of existing followers) * that will receive the message being composed by `this`, and that will * also be added as follower of `this.activeThread`. */ - recipients: many('Partner', { + recipients: many("Partner", { compute() { const recipients = [...this.mentionedPartners]; if (this.activeThread && !this.isLog) { @@ -163,6 +173,6 @@ Model({ /** * States the thread which this composer represents the state (if any). */ - thread: one('Thread', { identifying: true, inverse: 'composer' }), + thread: one("Thread", { identifying: true, inverse: "composer" }), }, }); diff --git a/addons/mail/static/src/models/composer_suggestable.js b/addons/mail/static/src/models/composer_suggestable.js index 161f0afcd0b..24e22913382 100644 --- a/addons/mail/static/src/models/composer_suggestable.js +++ b/addons/mail/static/src/models/composer_suggestable.js @@ -1,16 +1,22 @@ /** @odoo-module **/ -import { many, one, Model } from '@mail/model'; +import { many, one, Model } from "@mail/model"; Model({ - name: 'ComposerSuggestable', - identifyingMode: 'xor', + name: "ComposerSuggestable", + identifyingMode: "xor", fields: { - cannedResponse: one('CannedResponse', { identifying: true, inverse: 'suggestable' }), - channelCommand: one('ChannelCommand', { identifying: true, inverse: 'suggestable' }), - composerSuggestionListViewExtraComposerSuggestionViewItems: many('ComposerSuggestionListViewExtraComposerSuggestionViewItem', { inverse: 'suggestable' }), - composerSuggestionListViewMainComposerSuggestionViewItems: many('ComposerSuggestionListViewMainComposerSuggestionViewItem', { inverse: 'suggestable' }), - partner: one('Partner', { identifying: true, inverse: 'suggestable' }), - thread: one('Thread', { identifying: true, inverse: 'suggestable' }), + cannedResponse: one("CannedResponse", { identifying: true, inverse: "suggestable" }), + channelCommand: one("ChannelCommand", { identifying: true, inverse: "suggestable" }), + composerSuggestionListViewExtraComposerSuggestionViewItems: many( + "ComposerSuggestionListViewExtraComposerSuggestionViewItem", + { inverse: "suggestable" } + ), + composerSuggestionListViewMainComposerSuggestionViewItems: many( + "ComposerSuggestionListViewMainComposerSuggestionViewItem", + { inverse: "suggestable" } + ), + partner: one("Partner", { identifying: true, inverse: "suggestable" }), + thread: one("Thread", { identifying: true, inverse: "suggestable" }), }, }); diff --git a/addons/mail/static/src/models/composer_suggested_recipient_list_view.js b/addons/mail/static/src/models/composer_suggested_recipient_list_view.js index 09c26d02a34..3db7710ada7 100644 --- a/addons/mail/static/src/models/composer_suggested_recipient_list_view.js +++ b/addons/mail/static/src/models/composer_suggested_recipient_list_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'ComposerSuggestedRecipientListView', - template: 'mail.ComposerSuggestedRecipientListView', + name: "ComposerSuggestedRecipientListView", + template: "mail.ComposerSuggestedRecipientListView", recordMethods: { /** * @param {MouseEvent} ev @@ -26,22 +26,30 @@ Model({ }, }, fields: { - composerSuggestedRecipientViews: many('ComposerSuggestedRecipientView', { + composerSuggestedRecipientViews: many("ComposerSuggestedRecipientView", { compute() { if (!this.thread) { return clear(); } if (this.hasShowMoreButton) { - return this.thread.suggestedRecipientInfoList.map(suggestedRecipientInfo => ({ suggestedRecipientInfo })); + return this.thread.suggestedRecipientInfoList.map((suggestedRecipientInfo) => ({ + suggestedRecipientInfo, + })); } else { - return this.thread.suggestedRecipientInfoList.slice(0, 3).map(suggestedRecipientInfo => ({ suggestedRecipientInfo })); + return this.thread.suggestedRecipientInfoList + .slice(0, 3) + .map((suggestedRecipientInfo) => ({ suggestedRecipientInfo })); } }, - inverse: 'composerSuggestedRecipientListViewOwner', + inverse: "composerSuggestedRecipientListViewOwner", + }), + composerViewOwner: one("ComposerView", { + identifying: true, + inverse: "composerSuggestedRecipientListView", }), - composerViewOwner: one('ComposerView', { identifying: true, inverse: 'composerSuggestedRecipientListView' }), hasShowMoreButton: attr({ default: false }), - thread: one('Thread', { required: true, + thread: one("Thread", { + required: true, compute() { return this.composerViewOwner.composer.activeThread; }, diff --git a/addons/mail/static/src/models/composer_suggested_recipient_view.js b/addons/mail/static/src/models/composer_suggested_recipient_view.js index d08972cbfc8..86da4ca66a6 100644 --- a/addons/mail/static/src/models/composer_suggested_recipient_view.js +++ b/addons/mail/static/src/models/composer_suggested_recipient_view.js @@ -1,11 +1,17 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'ComposerSuggestedRecipientView', + name: "ComposerSuggestedRecipientView", fields: { - composerSuggestedRecipientListViewOwner: one('ComposerSuggestedRecipientListView', { identifying: true, inverse: 'composerSuggestedRecipientViews' }), - suggestedRecipientInfo: one('SuggestedRecipientInfo', { identifying: true, inverse: 'composerSuggestedRecipientViews' }), + composerSuggestedRecipientListViewOwner: one("ComposerSuggestedRecipientListView", { + identifying: true, + inverse: "composerSuggestedRecipientViews", + }), + suggestedRecipientInfo: one("SuggestedRecipientInfo", { + identifying: true, + inverse: "composerSuggestedRecipientViews", + }), }, }); diff --git a/addons/mail/static/src/models/composer_suggestion_list_view.js b/addons/mail/static/src/models/composer_suggestion_list_view.js index 925da524bf9..dee0737c3a1 100644 --- a/addons/mail/static/src/models/composer_suggestion_list_view.js +++ b/addons/mail/static/src/models/composer_suggestion_list_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, many, one, Model } from '@mail/model'; +import { attr, many, one, Model } from "@mail/model"; Model({ - name: 'ComposerSuggestionListView', - template: 'mail.ComposerSuggestionListView', + name: "ComposerSuggestionListView", + template: "mail.ComposerSuggestionListView", recordMethods: { /** * Sets the first suggestion as active. Main and extra records are @@ -28,7 +28,7 @@ Model({ */ setNextSuggestionViewActive() { const activeElementIndex = this.suggestionViews.findIndex( - suggestion => suggestion === this.activeSuggestionView + (suggestion) => suggestion === this.activeSuggestionView ); if (activeElementIndex === this.suggestionViews.length - 1) { // loop when reaching the end of the list @@ -44,7 +44,7 @@ Model({ */ setPreviousSuggestionViewActive() { const activeElementIndex = this.suggestionViews.findIndex( - suggestion => suggestion === this.activeSuggestionView + (suggestion) => suggestion === this.activeSuggestionView ); if (activeElementIndex === 0) { // loop when reaching the start of the list @@ -61,7 +61,8 @@ Model({ * is highlighted in the UI and it will be selected when the * suggestion is confirmed by the user. */ - activeSuggestionView: one('ComposerSuggestionView', { inverse: 'composerSuggestionListViewOwnerAsActiveSuggestionView', + activeSuggestionView: one("ComposerSuggestionView", { + inverse: "composerSuggestionListViewOwnerAsActiveSuggestionView", compute() { if (this.suggestionViews.includes(this.rawActiveSuggestionView)) { return this.rawActiveSuggestionView; @@ -70,29 +71,48 @@ Model({ return firstSuggestionView; }, }), - composerSuggestionListViewExtraComposerSuggestionViewItems: many('ComposerSuggestionListViewExtraComposerSuggestionViewItem', { inverse: 'composerSuggestionListViewOwner', - compute() { - return this.composerViewOwner.extraSuggestions.map(suggestable => ({ suggestable })); - }, + composerSuggestionListViewExtraComposerSuggestionViewItems: many( + "ComposerSuggestionListViewExtraComposerSuggestionViewItem", + { + inverse: "composerSuggestionListViewOwner", + compute() { + return this.composerViewOwner.extraSuggestions.map((suggestable) => ({ + suggestable, + })); + }, + } + ), + composerSuggestionListViewMainComposerSuggestionViewItems: many( + "ComposerSuggestionListViewMainComposerSuggestionViewItem", + { + inverse: "composerSuggestionListViewOwner", + compute() { + return this.composerViewOwner.mainSuggestions.map((suggestable) => ({ + suggestable, + })); + }, + } + ), + composerViewOwner: one("ComposerView", { + identifying: true, + inverse: "composerSuggestionListView", }), - composerSuggestionListViewMainComposerSuggestionViewItems: many('ComposerSuggestionListViewMainComposerSuggestionViewItem', { inverse: 'composerSuggestionListViewOwner', - compute() { - return this.composerViewOwner.mainSuggestions.map(suggestable => ({ suggestable })); - }, - }), - composerViewOwner: one('ComposerView', { identifying: true, inverse: 'composerSuggestionListView' }), /** * Determines whether the currently active suggestion should be scrolled * into view. */ hasToScrollToActiveSuggestionView: attr({ default: false }), - rawActiveSuggestionView: one('ComposerSuggestionView'), - suggestionViews: many('ComposerSuggestionView', { + rawActiveSuggestionView: one("ComposerSuggestionView"), + suggestionViews: many("ComposerSuggestionView", { compute() { - const mainSuggestionViews = this.composerSuggestionListViewMainComposerSuggestionViewItems.map(item => item.composerSuggestionView); - const extraSuggestionViews = this.composerSuggestionListViewExtraComposerSuggestionViewItems.map(item => item.composerSuggestionView); + const mainSuggestionViews = this.composerSuggestionListViewMainComposerSuggestionViewItems.map( + (item) => item.composerSuggestionView + ); + const extraSuggestionViews = this.composerSuggestionListViewExtraComposerSuggestionViewItems.map( + (item) => item.composerSuggestionView + ); return mainSuggestionViews.concat(extraSuggestionViews); }, - }) + }), }, }); diff --git a/addons/mail/static/src/models/composer_suggestion_list_view_extra_composer_suggestion_view_item.js b/addons/mail/static/src/models/composer_suggestion_list_view_extra_composer_suggestion_view_item.js index 3a1b67345d8..b8f931c9d6c 100644 --- a/addons/mail/static/src/models/composer_suggestion_list_view_extra_composer_suggestion_view_item.js +++ b/addons/mail/static/src/models/composer_suggestion_list_view_extra_composer_suggestion_view_item.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; /** * Models a relation between a ComposerSuggestionListView and a @@ -8,10 +8,21 @@ import { one, Model } from '@mail/model'; * suggestions. */ Model({ - name: 'ComposerSuggestionListViewExtraComposerSuggestionViewItem', + name: "ComposerSuggestionListViewExtraComposerSuggestionViewItem", fields: { - composerSuggestionListViewOwner: one('ComposerSuggestionListView', { identifying: true, inverse: 'composerSuggestionListViewExtraComposerSuggestionViewItems' }), - composerSuggestionView: one('ComposerSuggestionView', { default: {}, inverse: 'composerSuggestionListViewExtraComposerSuggestionViewItemOwner', readonly: true, required: true }), - suggestable: one('ComposerSuggestable', { identifying: true, inverse: 'composerSuggestionListViewExtraComposerSuggestionViewItems' }), + composerSuggestionListViewOwner: one("ComposerSuggestionListView", { + identifying: true, + inverse: "composerSuggestionListViewExtraComposerSuggestionViewItems", + }), + composerSuggestionView: one("ComposerSuggestionView", { + default: {}, + inverse: "composerSuggestionListViewExtraComposerSuggestionViewItemOwner", + readonly: true, + required: true, + }), + suggestable: one("ComposerSuggestable", { + identifying: true, + inverse: "composerSuggestionListViewExtraComposerSuggestionViewItems", + }), }, }); diff --git a/addons/mail/static/src/models/composer_suggestion_list_view_main_composer_suggestion_view_item.js b/addons/mail/static/src/models/composer_suggestion_list_view_main_composer_suggestion_view_item.js index 40acc855eb9..9cf99a1462e 100644 --- a/addons/mail/static/src/models/composer_suggestion_list_view_main_composer_suggestion_view_item.js +++ b/addons/mail/static/src/models/composer_suggestion_list_view_main_composer_suggestion_view_item.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; /** * Models a relation between a ComposerSuggestionListView and a @@ -8,10 +8,21 @@ import { one, Model } from '@mail/model'; * suggestions. */ Model({ - name: 'ComposerSuggestionListViewMainComposerSuggestionViewItem', + name: "ComposerSuggestionListViewMainComposerSuggestionViewItem", fields: { - composerSuggestionListViewOwner: one('ComposerSuggestionListView', { identifying: true, inverse: 'composerSuggestionListViewMainComposerSuggestionViewItems' }), - composerSuggestionView: one('ComposerSuggestionView', { default: {}, inverse: 'composerSuggestionListViewMainComposerSuggestionViewItemOwner', readonly: true, required: true }), - suggestable: one('ComposerSuggestable', { identifying: true, inverse: 'composerSuggestionListViewMainComposerSuggestionViewItems' }), + composerSuggestionListViewOwner: one("ComposerSuggestionListView", { + identifying: true, + inverse: "composerSuggestionListViewMainComposerSuggestionViewItems", + }), + composerSuggestionView: one("ComposerSuggestionView", { + default: {}, + inverse: "composerSuggestionListViewMainComposerSuggestionViewItemOwner", + readonly: true, + required: true, + }), + suggestable: one("ComposerSuggestable", { + identifying: true, + inverse: "composerSuggestionListViewMainComposerSuggestionViewItems", + }), }, }); diff --git a/addons/mail/static/src/models/composer_suggestion_view.js b/addons/mail/static/src/models/composer_suggestion_view.js index 5a9f3222e4d..982fac8811e 100644 --- a/addons/mail/static/src/models/composer_suggestion_view.js +++ b/addons/mail/static/src/models/composer_suggestion_view.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, one, Model } from '@mail/model'; -import { sprintf } from '@web/core/utils/strings'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, one, Model } from "@mail/model"; +import { sprintf } from "@web/core/utils/strings"; /** * Models a suggestion in the composer suggestion. @@ -12,13 +12,13 @@ import { sprintf } from '@web/core/utils/strings'; * and display suggested partners to mention. */ Model({ - name: 'ComposerSuggestionView', - template: 'mail.ComposerSuggestionView', + name: "ComposerSuggestionView", + template: "mail.ComposerSuggestionView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useComponentToModel({ fieldName: "component" }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, - identifyingMode: 'xor', + identifyingMode: "xor", recordMethods: { /** * @param {Event} ev @@ -37,27 +37,40 @@ Model({ this.composerSuggestionListViewOwner.hasToScrollToActiveSuggestionView && this.composerSuggestionListViewOwnerAsActiveSuggestionView ) { - this.component.root.el.scrollIntoView({ block: 'center' }); - this.composerSuggestionListViewOwner.update({ hasToScrollToActiveSuggestionView: false }); + this.component.root.el.scrollIntoView({ block: "center" }); + this.composerSuggestionListViewOwner.update({ + hasToScrollToActiveSuggestionView: false, + }); } }, }, fields: { component: attr(), - composerSuggestionListViewOwner: one('ComposerSuggestionListView', { required: true, + composerSuggestionListViewOwner: one("ComposerSuggestionListView", { + required: true, compute() { if (this.composerSuggestionListViewExtraComposerSuggestionViewItemOwner) { - return this.composerSuggestionListViewExtraComposerSuggestionViewItemOwner.composerSuggestionListViewOwner; + return this.composerSuggestionListViewExtraComposerSuggestionViewItemOwner + .composerSuggestionListViewOwner; } if (this.composerSuggestionListViewMainComposerSuggestionViewItemOwner) { - return this.composerSuggestionListViewMainComposerSuggestionViewItemOwner.composerSuggestionListViewOwner; + return this.composerSuggestionListViewMainComposerSuggestionViewItemOwner + .composerSuggestionListViewOwner; } return clear(); }, }), - composerSuggestionListViewOwnerAsActiveSuggestionView: one('ComposerSuggestionListView', { inverse: 'activeSuggestionView' }), - composerSuggestionListViewExtraComposerSuggestionViewItemOwner: one('ComposerSuggestionListViewExtraComposerSuggestionViewItem', { identifying: true, inverse: 'composerSuggestionView' }), - composerSuggestionListViewMainComposerSuggestionViewItemOwner: one('ComposerSuggestionListViewMainComposerSuggestionViewItem', { identifying: true, inverse: 'composerSuggestionView' }), + composerSuggestionListViewOwnerAsActiveSuggestionView: one("ComposerSuggestionListView", { + inverse: "activeSuggestionView", + }), + composerSuggestionListViewExtraComposerSuggestionViewItemOwner: one( + "ComposerSuggestionListViewExtraComposerSuggestionViewItem", + { identifying: true, inverse: "composerSuggestionView" } + ), + composerSuggestionListViewMainComposerSuggestionViewItemOwner: one( + "ComposerSuggestionListViewMainComposerSuggestionViewItem", + { identifying: true, inverse: "composerSuggestionView" } + ), /** * The text that identifies this suggestion in a mention. */ @@ -80,18 +93,26 @@ Model({ } }, }), - personaImStatusIconView: one('PersonaImStatusIconView', { inverse: 'composerSuggestionViewOwner', + personaImStatusIconView: one("PersonaImStatusIconView", { + inverse: "composerSuggestionViewOwner", compute() { - return this.suggestable && this.suggestable.partner && this.suggestable.partner.isImStatusSet ? {} : clear(); + return this.suggestable && + this.suggestable.partner && + this.suggestable.partner.isImStatusSet + ? {} + : clear(); }, }), - suggestable: one('ComposerSuggestable', { required: true, + suggestable: one("ComposerSuggestable", { + required: true, compute() { if (this.composerSuggestionListViewExtraComposerSuggestionViewItemOwner) { - return this.composerSuggestionListViewExtraComposerSuggestionViewItemOwner.suggestable; + return this.composerSuggestionListViewExtraComposerSuggestionViewItemOwner + .suggestable; } if (this.composerSuggestionListViewMainComposerSuggestionViewItemOwner) { - return this.composerSuggestionListViewMainComposerSuggestionViewItemOwner.suggestable; + return this.composerSuggestionListViewMainComposerSuggestionViewItemOwner + .suggestable; } return clear(); }, @@ -100,23 +121,36 @@ Model({ * Descriptive title for this suggestion. Useful to be able to * read both parts when they are overflowing the UI. */ - title: attr({ default: "", + title: attr({ + default: "", compute() { if (!this.suggestable) { return clear(); } if (this.suggestable.cannedResponse) { - return sprintf("%s: %s", this.suggestable.cannedResponse.source, this.suggestable.cannedResponse.substitution); + return sprintf( + "%s: %s", + this.suggestable.cannedResponse.source, + this.suggestable.cannedResponse.substitution + ); } if (this.suggestable.thread) { return this.suggestable.thread.name; } if (this.suggestable.channelCommand) { - return sprintf("%s: %s", this.suggestable.channelCommand.name, this.suggestable.channelCommand.help); + return sprintf( + "%s: %s", + this.suggestable.channelCommand.name, + this.suggestable.channelCommand.help + ); } if (this.suggestable.partner) { if (this.suggestable.partner.email) { - return sprintf("%s (%s)", this.suggestable.partner.nameOrDisplayName, this.suggestable.partner.email); + return sprintf( + "%s (%s)", + this.suggestable.partner.nameOrDisplayName, + this.suggestable.partner.email + ); } return this.suggestable.partner.nameOrDisplayName; } diff --git a/addons/mail/static/src/models/composer_text_input_view.js b/addons/mail/static/src/models/composer_text_input_view.js index 972d04dbaf5..0427ef0b0ab 100644 --- a/addons/mail/static/src/models/composer_text_input_view.js +++ b/addons/mail/static/src/models/composer_text_input_view.js @@ -1,19 +1,19 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'ComposerTextInputView', - template: 'mail.ComposerTextInputView', + name: "ComposerTextInputView", + template: "mail.ComposerTextInputView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); /** * Updates the composer text input content when composer is mounted * as textarea content can't be changed from the DOM. */ - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, recordMethods: { onComponentUpdate() { @@ -33,7 +33,7 @@ Model({ this.textareaRef.el.setSelectionRange( this.owner.composer.textInputCursorStart, this.owner.composer.textInputCursorEnd, - this.owner.composer.textInputSelectionDirection, + this.owner.composer.textInputSelectionDirection ); } this.owner.update({ hasToRestoreContent: false }); @@ -48,12 +48,12 @@ Model({ * based on the text content. We need it to downsize the textarea * properly without flicker. */ - mirroredTextareaRef: attr({ ref: 'mirroredTextarea' }), - owner: one('ComposerView', { identifying: true, inverse: 'textInput' }), + mirroredTextareaRef: attr({ ref: "mirroredTextarea" }), + owner: one("ComposerView", { identifying: true, inverse: "textInput" }), /** * Reference of the textarea. Useful to set height, selection and * content. */ - textareaRef: attr({ ref: 'textarea' }), + textareaRef: attr({ ref: "textarea" }), }, }); diff --git a/addons/mail/static/src/models/composer_view.js b/addons/mail/static/src/models/composer_view.js index 6f4d6a99871..ef8b3529c72 100644 --- a/addons/mail/static/src/models/composer_view.js +++ b/addons/mail/static/src/models/composer_view.js @@ -1,26 +1,26 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { attr, clear, link, many, one, Model } from '@mail/model'; -import { addLink, escapeAndCompactTextContent, parseAndTransform } from '@mail/js/utils'; -import { isEventHandled, markEventHandled } from '@mail/utils/utils'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { attr, clear, link, many, one, Model } from "@mail/model"; +import { addLink, escapeAndCompactTextContent, parseAndTransform } from "@mail/js/utils"; +import { isEventHandled, markEventHandled } from "@mail/utils/utils"; -import { escape, sprintf } from '@web/core/utils/strings'; -import { url } from '@web/core/utils/urls'; +import { escape, sprintf } from "@web/core/utils/strings"; +import { url } from "@web/core/utils/urls"; Model({ - name: 'ComposerView', - template: 'mail.ComposerView', + name: "ComposerView", + template: "mail.ComposerView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); }, - identifyingMode: 'xor', + identifyingMode: "xor", lifecycleHooks: { _created() { - document.addEventListener('click', this.onClickCaptureGlobal, true); + document.addEventListener("click", this.onClickCaptureGlobal, true); }, _willDelete() { - document.removeEventListener('click', this.onClickCaptureGlobal, true); + document.removeEventListener("click", this.onClickCaptureGlobal, true); }, }, recordMethods: { @@ -42,7 +42,9 @@ Model({ if (this.emojisPopoverView && this.emojisPopoverView.contains(element)) { return true; } - return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element)); + return Boolean( + this.component && this.component.root.el && this.component.root.el.contains(element) + ); }, /** * Hides the composer, which only makes sense if the composer is @@ -72,12 +74,16 @@ Model({ return; // not supported for non-thread composer (eg. messaging editing) } if ( - this.suggestionModelName === 'ChannelCommand' || + this.suggestionModelName === "ChannelCommand" || this._getCommandFromText(this.composer.textInputContent) ) { return; } - if (this.composer.thread.typingMembers.includes(this.composer.thread.channel.memberOfCurrentUser)) { + if ( + this.composer.thread.typingMembers.includes( + this.composer.thread.channel.memberOfCurrentUser + ) + ) { this.composer.thread.refreshCurrentPartnerIsTyping(); } else { this.composer.thread.registerCurrentPartnerIsTyping(); @@ -89,7 +95,10 @@ Model({ * @param {string} content */ insertIntoTextInput(content) { - const partA = this.composer.textInputContent.slice(0, this.composer.textInputCursorStart); + const partA = this.composer.textInputContent.slice( + 0, + this.composer.textInputCursorStart + ); const partB = this.composer.textInputContent.slice( this.composer.textInputCursorEnd, this.composer.textInputContent.length @@ -124,7 +133,7 @@ Model({ cursorPosition, this.composer.textInputContent.length ); - if (this.suggestionDelimiter === ':') { + if (this.suggestionDelimiter === ":") { textLeft = this.composer.textInputContent.substring( 0, this.suggestionDelimiterPosition @@ -134,9 +143,10 @@ Model({ this.composer.textInputContent.length ); } - const recordReplacement = this.composerSuggestionListView.activeSuggestionView.mentionText; + const recordReplacement = this.composerSuggestionListView.activeSuggestionView + .mentionText; const updateData = { - textInputContent: textLeft + recordReplacement + ' ' + textRight, + textInputContent: textLeft + recordReplacement + " " + textRight, textInputCursorEnd: textLeft.length + recordReplacement.length + 1, textInputCursorStart: textLeft.length + recordReplacement.length + 1, }; @@ -144,10 +154,18 @@ Model({ // the mention will appear in the target channel, or be notified to // the target partner. if (this.composerSuggestionListView.activeSuggestionView.suggestable.thread) { - Object.assign(updateData, { rawMentionedChannels: link(this.composerSuggestionListView.activeSuggestionView.suggestable.thread) }); + Object.assign(updateData, { + rawMentionedChannels: link( + this.composerSuggestionListView.activeSuggestionView.suggestable.thread + ), + }); } if (this.composerSuggestionListView.activeSuggestionView.suggestable.partner) { - Object.assign(updateData, { rawMentionedPartners: link(this.composerSuggestionListView.activeSuggestionView.suggestable.partner) }); + Object.assign(updateData, { + rawMentionedPartners: link( + this.composerSuggestionListView.activeSuggestionView.suggestable.partner + ), + }); } this.composer.update(updateData); for (const composerView of this.composer.composerViews) { @@ -196,7 +214,7 @@ Model({ } // Let event be handled by bubbling handlers first await new Promise(this.messaging.browser.setTimeout); - if (isEventHandled(ev, 'MessageActionList.replyTo')) { + if (isEventHandled(ev, "MessageActionList.replyTo")) { return; } if (this.exists()) { @@ -297,11 +315,11 @@ Model({ * @param {KeyboardEvent} ev */ onKeydown(ev) { - if (ev.key === 'Escape') { - if (isEventHandled(ev, 'ComposerTextInput.closeSuggestions')) { + if (ev.key === "Escape") { + if (isEventHandled(ev, "ComposerTextInput.closeSuggestions")) { return; } - if (isEventHandled(ev, 'Composer.closeEmojisPopover')) { + if (isEventHandled(ev, "Composer.closeEmojisPopover")) { return; } ev.preventDefault(); @@ -313,12 +331,12 @@ Model({ * @param {KeyboardEvent} ev */ onKeydownButtonEmojis(ev) { - if (ev.key === 'Escape' && this.emojisPopoverView) { + if (ev.key === "Escape" && this.emojisPopoverView) { this.update({ doFocus: true, emojisPopoverView: clear(), }); - markEventHandled(ev, 'Composer.closeEmojisPopover'); + markEventHandled(ev, "Composer.closeEmojisPopover"); } }, /** @@ -329,28 +347,28 @@ Model({ return; } switch (ev.key) { - case 'Escape': + case "Escape": if (this.hasSuggestions) { ev.preventDefault(); this.closeSuggestions(); - markEventHandled(ev, 'ComposerTextInput.closeSuggestions'); + markEventHandled(ev, "ComposerTextInput.closeSuggestions"); } break; // UP, DOWN, TAB: prevent moving cursor if navigation in mention suggestions - case 'ArrowUp': - case 'PageUp': - case 'ArrowDown': - case 'PageDown': - case 'Home': - case 'End': - case 'Tab': + case "ArrowUp": + case "PageUp": + case "ArrowDown": + case "PageDown": + case "Home": + case "End": + case "Tab": if (this.hasSuggestions) { // We use preventDefault here to avoid keys native actions but actions are handled in keyUp ev.preventDefault(); } break; // ENTER: submit the message only if the dropdown mention proposition is not displayed - case 'Enter': + case "Enter": this.onKeydownTextareaEnter(ev); break; } @@ -367,7 +385,7 @@ Model({ return; } if ( - this.sendShortcuts.includes('ctrl-enter') && + this.sendShortcuts.includes("ctrl-enter") && !ev.altKey && ev.ctrlKey && !ev.metaKey && @@ -378,7 +396,7 @@ Model({ return; } if ( - this.sendShortcuts.includes('enter') && + this.sendShortcuts.includes("enter") && !ev.altKey && !ev.ctrlKey && !ev.metaKey && @@ -389,7 +407,7 @@ Model({ return; } if ( - this.sendShortcuts.includes('meta-enter') && + this.sendShortcuts.includes("meta-enter") && !ev.altKey && !ev.ctrlKey && ev.metaKey && @@ -401,85 +419,107 @@ Model({ } }, /** - * Key events management is performed in a Keyup to avoid intempestive RPC calls - * - * @param {KeyboardEvent} ev - */ + * Key events management is performed in a Keyup to avoid intempestive RPC calls + * + * @param {KeyboardEvent} ev + */ onKeyupTextarea(ev) { if (!this.exists()) { return; } switch (ev.key) { - case 'Escape': + case "Escape": // already handled in _onKeydownTextarea, break to avoid default break; // ENTER, HOME, END, UP, DOWN, PAGE UP, PAGE DOWN, TAB: check if navigation in mention suggestions - case 'Enter': + case "Enter": if (this.hasSuggestions) { this.insertSuggestion(); this.closeSuggestions(); this.update({ doFocus: true }); } break; - case 'ArrowUp': - case 'PageUp': - if (ev.key === 'ArrowUp' && !this.hasSuggestions && !this.composer.textInputContent && this.threadView) { + case "ArrowUp": + case "PageUp": + if ( + ev.key === "ArrowUp" && + !this.hasSuggestions && + !this.composer.textInputContent && + this.threadView + ) { this.threadView.startEditingLastMessageFromCurrentUser(); break; } if (this.composerSuggestionListView) { this.composerSuggestionListView.setPreviousSuggestionViewActive(); - this.composerSuggestionListView.update({ hasToScrollToActiveSuggestionView: true }); + this.composerSuggestionListView.update({ + hasToScrollToActiveSuggestionView: true, + }); } break; - case 'ArrowDown': - case 'PageDown': - if (ev.key === 'ArrowDown' && !this.hasSuggestions && !this.composer.textInputContent && this.threadView) { + case "ArrowDown": + case "PageDown": + if ( + ev.key === "ArrowDown" && + !this.hasSuggestions && + !this.composer.textInputContent && + this.threadView + ) { this.threadView.startEditingLastMessageFromCurrentUser(); break; } if (this.composerSuggestionListView) { this.composerSuggestionListView.setNextSuggestionViewActive(); - this.composerSuggestionListView.update({ hasToScrollToActiveSuggestionView: true }); + this.composerSuggestionListView.update({ + hasToScrollToActiveSuggestionView: true, + }); } break; - case 'Home': + case "Home": if (this.composerSuggestionListView) { this.composerSuggestionListView.setFirstSuggestionViewActive(); - this.composerSuggestionListView.update({ hasToScrollToActiveSuggestionView: true }); + this.composerSuggestionListView.update({ + hasToScrollToActiveSuggestionView: true, + }); } break; - case 'End': + case "End": if (this.composerSuggestionListView) { this.composerSuggestionListView.setLastSuggestionViewActive(); - this.composerSuggestionListView.update({ hasToScrollToActiveSuggestionView: true }); + this.composerSuggestionListView.update({ + hasToScrollToActiveSuggestionView: true, + }); } break; - case 'Tab': + case "Tab": if (this.composerSuggestionListView) { if (ev.shiftKey) { this.composerSuggestionListView.setPreviousSuggestionViewActive(); - this.composerSuggestionListView.update({ hasToScrollToActiveSuggestionView: true }); + this.composerSuggestionListView.update({ + hasToScrollToActiveSuggestionView: true, + }); } else { this.composerSuggestionListView.setNextSuggestionViewActive(); - this.composerSuggestionListView.update({ hasToScrollToActiveSuggestionView: true }); + this.composerSuggestionListView.update({ + hasToScrollToActiveSuggestionView: true, + }); } } break; - case 'Alt': - case 'AltGraph': - case 'CapsLock': - case 'Control': - case 'Fn': - case 'FnLock': - case 'Hyper': - case 'Meta': - case 'NumLock': - case 'ScrollLock': - case 'Shift': - case 'ShiftSuper': - case 'Symbol': - case 'SymbolLock': + case "Alt": + case "AltGraph": + case "CapsLock": + case "Control": + case "Fn": + case "FnLock": + case "Hyper": + case "Meta": + case "NumLock": + case "ScrollLock": + case "Shift": + case "ShiftSuper": + case "Symbol": + case "SymbolLock": // prevent modifier keys from resetting the suggestion state break; // Otherwise, check if a mention is typed @@ -500,23 +540,23 @@ Model({ * Open the full composer modal. */ async openFullComposer() { - const attachmentIds = this.composer.attachments.map(attachment => attachment.id); + const attachmentIds = this.composer.attachments.map((attachment) => attachment.id); const context = { default_attachment_ids: attachmentIds, default_body: escapeAndCompactTextContent(this.composer.textInputContent), default_is_log: this.composer.isLog, default_model: this.composer.activeThread.model, - default_partner_ids: this.composer.recipients.map(partner => partner.id), + default_partner_ids: this.composer.recipients.map((partner) => partner.id), default_res_id: this.composer.activeThread.id, mail_post_autofollow: this.composer.activeThread.hasWriteAccess, }; const action = { - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - view_mode: 'form', - views: [[false, 'form']], - target: 'new', + type: "ir.actions.act_window", + res_model: "mail.compose.message", + view_mode: "form", + views: [[false, "form"]], + target: "new", context: context, }; const composer = this.composer; @@ -527,7 +567,7 @@ Model({ } composer._reset(); if (composer.activeThread) { - composer.activeThread.fetchData(['messages']); + composer.activeThread.fetchData(["messages"]); } }, }; @@ -540,25 +580,31 @@ Model({ const composer = this.composer; const postData = this._getMessageData(); const params = { - 'post_data': postData, - 'thread_id': composer.thread.id, - 'thread_model': composer.thread.model, + post_data: postData, + thread_id: composer.thread.id, + thread_model: composer.thread.model, }; try { composer.update({ isPostingMessage: true }); - if (composer.thread.model === 'mail.channel') { + if (composer.thread.model === "mail.channel") { Object.assign(postData, { - subtype_xmlid: 'mail.mt_comment', + subtype_xmlid: "mail.mt_comment", }); } else { Object.assign(postData, { - subtype_xmlid: composer.isLog ? 'mail.mt_note' : 'mail.mt_comment', + subtype_xmlid: composer.isLog ? "mail.mt_note" : "mail.mt_comment", }); if (!composer.isLog) { - params.context = { mail_post_autofollow: this.composer.activeThread.hasWriteAccess }; + params.context = { + mail_post_autofollow: this.composer.activeThread.hasWriteAccess, + }; } } - if (this.threadView && this.threadView.replyingToMessageView && this.threadView.thread !== this.messaging.inbox.thread) { + if ( + this.threadView && + this.threadView.replyingToMessageView && + this.threadView.thread !== this.messaging.inbox.thread + ) { postData.parent_id = this.threadView.replyingToMessageView.message.id; } const { threadView = {} } = this; @@ -569,25 +615,31 @@ Model({ // unmounted while awaiting the prc promise. In this // case, this would be undefined. const messaging = this.messaging; - const messageData = await this.messaging.rpc({ route: `/mail/message/post`, params }); + const messageData = await this.messaging.rpc({ + route: `/mail/message/post`, + params, + }); if (!messaging.exists()) { return; } - const message = messaging.models['Message'].insert( - messaging.models['Message'].convertData(messageData) + const message = messaging.models["Message"].insert( + messaging.models["Message"].convertData(messageData) ); if (messaging.hasLinkPreviewFeature && !message.isBodyEmpty) { - messaging.rpc({ - route: `/mail/link_preview`, - params: { - message_id: message.id - } - }, { shadow: true }); + messaging.rpc( + { + route: `/mail/link_preview`, + params: { + message_id: message.id, + }, + }, + { shadow: true } + ); } for (const threadView of message.originThread.threadViews) { // Reset auto scroll to be able to see the newly posted message. threadView.update({ hasAutoScrollOnMessageReceived: true }); - threadView.addComponentHint('message-posted', { message }); + threadView.addComponentHint("message-posted", { message }); } if (chatter && chatter.exists() && chatter.hasParentReloadOnMessagePosted) { chatter.reloadParentView(); @@ -598,14 +650,17 @@ Model({ } if (chatterThread.exists()) { // Load new messages to fetch potential new messages from other users (useful due to lack of auto-sync in chatter). - chatterThread.fetchData(['followers', 'messages', 'suggestedRecipients']); + chatterThread.fetchData(["followers", "messages", "suggestedRecipients"]); } } if (threadViewThread) { if (threadViewThread === messaging.inbox.thread) { messaging.notify({ - message: sprintf(messaging.env._t(`Message posted on "%s"`), message.originThread.displayName), - type: 'info', + message: sprintf( + messaging.env._t(`Message posted on "%s"`), + message.originThread.displayName + ), + type: "info", }); if (this.exists()) { this.delete(); @@ -646,7 +701,7 @@ Model({ if (this.composer.hasUploadingAttachment) { this.messaging.notify({ message: this.env._t("Please wait while the file is uploading."), - type: 'warning', + type: "warning", }); } return; @@ -658,7 +713,10 @@ Model({ if (this.composer.thread.channel) { const command = this._getCommandFromText(this.composer.textInputContent); if (command) { - await command.execute({ channel: this.composer.thread, body: this.composer.textInputContent }); + await command.execute({ + channel: this.composer.thread, + body: this.composer.textInputContent, + }); if (this.composer.exists()) { this.composer._reset(); } @@ -673,7 +731,8 @@ Model({ */ updateTextInputHeight() { this.textInput.mirroredTextareaRef.el.value = this.composer.textInputContent; - this.textInput.textareaRef.el.style.height = this.textInput.mirroredTextareaRef.el.scrollHeight + 'px'; + this.textInput.textareaRef.el.style.height = + this.textInput.mirroredTextareaRef.el.scrollHeight + "px"; }, /** * Update a posted message when the message is ready. @@ -684,9 +743,11 @@ Model({ this.messageViewInEditing.messageActionList.update({ deleteConfirmDialog: {} }); return; } - let data = { + const data = { body: this._generateMessageBody(), - attachment_ids: composer.attachments.concat(this.messageViewInEditing.message.attachments).map(attachment => attachment.id), + attachment_ids: composer.attachments + .concat(this.messageViewInEditing.message.attachments) + .map((attachment) => attachment.id), }; const messageViewInEditing = this.messageViewInEditing; await messageViewInEditing.message.updateContent(data); @@ -731,11 +792,10 @@ Model({ for (const source of emoji.sources) { const escapedSource = String(source).replace( /([.*+?=^!:${}()|[\]/\\])/g, - '\\$1'); - const regexp = new RegExp( - '(\\s|^)(' + escapedSource + ')(?=\\s|$)', - 'g'); - htmlString = htmlString.replace(regexp, '$1' + emoji.codepoints); + "\\$1" + ); + const regexp = new RegExp("(\\s|^)(" + escapedSource + ")(?=\\s|$)", "g"); + htmlString = htmlString.replace(regexp, "$1" + emoji.codepoints); } } return htmlString; @@ -757,9 +817,9 @@ Model({ const placeholder = `@-mention-partner-${partner.id}`; const text = `@${escape(partner.name)}`; mentions.push({ - class: 'o_mail_redirect', + class: "o_mail_redirect", id: partner.id, - model: 'res.partner', + model: "res.partner", placeholder, text, }); @@ -769,15 +829,15 @@ Model({ const placeholder = `#-mention-channel-${channel.id}`; const text = `#${escape(channel.name)}`; mentions.push({ - class: 'o_channel_redirect', + class: "o_channel_redirect", id: channel.id, - model: 'mail.channel', + model: "mail.channel", placeholder, text, }); body = body.replace(text, placeholder); } - const baseHREF = url('/web'); + const baseHREF = url("/web"); for (const mention of mentions) { const href = `href='${baseHREF}#model=${mention.model}&id=${mention.id}'`; const attClass = `class='${mention.class}'`; @@ -790,8 +850,10 @@ Model({ return body; }, _generateMessageBody() { - const escapedAndCompactContent = escapeAndCompactTextContent(this.composer.textInputContent); - let body = escapedAndCompactContent.replace(/ /g, ' ').trim(); + const escapedAndCompactContent = escapeAndCompactTextContent( + this.composer.textInputContent + ); + let body = escapedAndCompactContent.replace(/ /g, " ").trim(); // This message will be received from the mail composer as html content // subtype but the urls will not be linkified. If the mail composer // takes the responsibility to linkify the urls we end up with double @@ -809,14 +871,19 @@ Model({ * @returns {ChannelCommand|undefined} command, if any in the content */ _getCommandFromText(content) { - if (content.startsWith('/')) { + if (content.startsWith("/")) { const firstWord = content.substring(1).split(/\s/)[0]; - return this.messaging.commands.find(command => { + return this.messaging.commands.find((command) => { if (command.name !== firstWord) { return false; } if (command.channel_types) { - return Boolean(this.composer.thread.channel) && command.channel_types.includes(this.composer.thread.channel.channel_type); + return ( + Boolean(this.composer.thread.channel) && + command.channel_types.includes( + this.composer.thread.channel.channel_type + ) + ); } return true; }); @@ -831,10 +898,10 @@ Model({ */ _getMessageData() { return { - attachment_ids: this.composer.attachments.map(attachment => attachment.id), + attachment_ids: this.composer.attachments.map((attachment) => attachment.id), body: this._generateMessageBody(), - message_type: 'comment', - partner_ids: this.composer.recipients.map(partner => partner.id), + message_type: "comment", + partner_ids: this.composer.recipients.map((partner) => partner.id), }; }, /** @@ -868,7 +935,7 @@ Model({ if (this.composer.textInputCursorStart > 0) { candidatePositions.push(this.composer.textInputCursorStart - 1); } - const suggestionDelimiters = ['@', ':', '#', '/']; + const suggestionDelimiters = ["@", ":", "#", "/"]; for (const candidatePosition of candidatePositions) { if ( candidatePosition < 0 || @@ -877,7 +944,7 @@ Model({ continue; } const candidateChar = this.composer.textInputContent[candidatePosition]; - if (candidateChar === '/' && candidatePosition !== 0) { + if (candidateChar === "/" && candidatePosition !== 0) { continue; } if (!suggestionDelimiters.includes(candidateChar)) { @@ -951,21 +1018,26 @@ Model({ return; } const model = this.messaging.models[this.suggestionModelName]; - const [ - mainSuggestedRecords, - extraSuggestedRecords = [], - ] = model.searchSuggestions(this.suggestionSearchTerm, { thread: this.composer.activeThread }); - const sortFunction = model.getSuggestionSortFunction(this.suggestionSearchTerm, { thread: this.composer.activeThread }); + const [mainSuggestedRecords, extraSuggestedRecords = []] = model.searchSuggestions( + this.suggestionSearchTerm, + { thread: this.composer.activeThread } + ); + const sortFunction = model.getSuggestionSortFunction(this.suggestionSearchTerm, { + thread: this.composer.activeThread, + }); mainSuggestedRecords.sort(sortFunction); extraSuggestedRecords.sort(sortFunction); // arbitrary limit to avoid displaying too many elements at once // ideally a load more mechanism should be introduced const limit = 8; mainSuggestedRecords.length = Math.min(mainSuggestedRecords.length, limit); - extraSuggestedRecords.length = Math.min(extraSuggestedRecords.length, limit - mainSuggestedRecords.length); + extraSuggestedRecords.length = Math.min( + extraSuggestedRecords.length, + limit - mainSuggestedRecords.length + ); this.update({ - extraSuggestions: extraSuggestedRecords.map(record => record.suggestable), - mainSuggestions: mainSuggestedRecords.map(record => record.suggestable), + extraSuggestions: extraSuggestedRecords.map((record) => record.suggestable), + mainSuggestions: mainSuggestedRecords.map((record) => record.suggestable), }); if (this.composerSuggestionListView) { this.composerSuggestionListView.update({ hasToScrollToActiveSuggestionView: true }); @@ -987,21 +1059,20 @@ Model({ /** * Determines the attachment list that will be used to display the attachments. */ - attachmentList: one('AttachmentList', { inverse: 'composerViewOwner', + attachmentList: one("AttachmentList", { + inverse: "composerViewOwner", compute() { - return (this.composer && this.composer.attachments.length > 0) - ? {} - : clear(); + return this.composer && this.composer.attachments.length > 0 ? {} : clear(); }, }), /** * States the ref to the html node of the emojis button. */ - buttonEmojisRef: attr({ ref: 'buttonEmojis' }), + buttonEmojisRef: attr({ ref: "buttonEmojis" }), /** * States the chatter which this composer allows editing (if any). */ - chatter: one('Chatter', { identifying: true, inverse: 'composerView' }), + chatter: one("Chatter", { identifying: true, inverse: "composerView" }), /** * States the OWL component of this composer view. */ @@ -1009,7 +1080,9 @@ Model({ /** * States the composer state that is displayed by this composer view. */ - composer: one('Composer', { inverse: 'composerViews', required: true, + composer: one("Composer", { + inverse: "composerViews", + required: true, compute() { if (this.threadView) { // When replying to a message, always use the composer from that message's thread @@ -1029,7 +1102,8 @@ Model({ return clear(); }, }), - composerSuggestedRecipientListView: one('ComposerSuggestedRecipientListView', { inverse: 'composerViewOwner', + composerSuggestedRecipientListView: one("ComposerSuggestedRecipientListView", { + inverse: "composerViewOwner", compute() { if (this.hasHeader && this.hasFollowers && !this.composer.isLog) { return {}; @@ -1037,7 +1111,8 @@ Model({ return clear(); }, }), - composerSuggestionListView: one('ComposerSuggestionListView', { inverse: 'composerViewOwner', + composerSuggestionListView: one("ComposerSuggestionListView", { + inverse: "composerViewOwner", compute() { if (this.hasSuggestions) { return {}; @@ -1051,20 +1126,21 @@ Model({ currentPartnerAvatar: attr({ compute() { if (this.messaging.currentUser) { - return url('/web/image', { - field: 'avatar_128', + return url("/web/image", { + field: "avatar_128", id: this.messaging.currentUser.id, - model: 'res.users', + model: "res.users", }); } - return '/web/static/img/user_menu_avatar.png'; + return "/web/static/img/user_menu_avatar.png"; }, }), /** * Determines whether this composer should be focused at next render. */ doFocus: attr(), - dropZoneView: one('DropZoneView', { inverse: 'composerViewOwner', + dropZoneView: one("DropZoneView", { + inverse: "composerViewOwner", compute() { if (this.useDragVisibleDropZone.isVisible) { return {}; @@ -1075,10 +1151,16 @@ Model({ /** * Determines the emojis popover that is active on this composer view. */ - emojisPopoverView: one('PopoverView', { inverse: 'composerViewOwnerAsEmoji' }), - extraSuggestions: many('ComposerSuggestable'), - fileUploader: one('FileUploader', { default: {}, inverse: 'composerView', readonly: true, required: true }), - hasCurrentPartnerAvatar: attr({ default: true, + emojisPopoverView: one("PopoverView", { inverse: "composerViewOwnerAsEmoji" }), + extraSuggestions: many("ComposerSuggestable"), + fileUploader: one("FileUploader", { + default: {}, + inverse: "composerView", + readonly: true, + required: true, + }), + hasCurrentPartnerAvatar: attr({ + default: true, compute() { if (this.messageViewInEditing) { return false; @@ -1095,7 +1177,8 @@ Model({ return clear(); }, }), - hasDiscardButton: attr({ default: false, + hasDiscardButton: attr({ + default: false, compute() { if (this.messageViewInEditing) { return false; @@ -1107,12 +1190,16 @@ Model({ return clear(); } if (this.threadView.threadViewer.discuss) { - return this.threadView.threadViewer.discuss.activeThread === this.messaging.inbox.thread; + return ( + this.threadView.threadViewer.discuss.activeThread === + this.messaging.inbox.thread + ); } return clear(); }, }), - hasFollowers: attr({ default: false, + hasFollowers: attr({ + default: false, compute() { if (this.chatter) { return true; @@ -1127,9 +1214,9 @@ Model({ compute() { return Boolean( this.hasThreadTyping || - this.composer.attachments.length > 0 || - this.messageViewInEditing || - !this.isCompact + this.composer.attachments.length > 0 || + this.messageViewInEditing || + !this.isCompact ); }, }), @@ -1140,8 +1227,8 @@ Model({ compute() { return Boolean( (this.hasThreadName && this.composer.thread) || - (this.hasFollowers && !this.composer.isLog) || - (this.threadView && this.threadView.replyingToMessageView) + (this.hasFollowers && !this.composer.isLog) || + (this.threadView && this.threadView.replyingToMessageView) ); }, }), @@ -1150,7 +1237,8 @@ Model({ * Useful to queue a new call if there is already one pending. */ hasMentionRpcInProgress: attr({ default: false }), - hasMentionSuggestionsBelowPosition: attr({ default: false, + hasMentionSuggestionsBelowPosition: attr({ + default: false, compute() { if (this.chatter) { return true; @@ -1161,7 +1249,8 @@ Model({ return clear(); }, }), - hasSendButton: attr({ default: true, + hasSendButton: attr({ + default: true, compute() { if (this.messageViewInEditing) { return false; @@ -1176,12 +1265,14 @@ Model({ * States whether there is any result currently found for the current * suggestion delimiter and search term, if applicable. */ - hasSuggestions: attr({ default: false, + hasSuggestions: attr({ + default: false, compute() { return this.mainSuggestions.length > 0 || this.extraSuggestions.length > 0; }, }), - hasThreadName: attr({ default: false, + hasThreadName: attr({ + default: false, compute() { if (this.threadView) { return this.threadView.hasComposerThreadName; @@ -1189,7 +1280,8 @@ Model({ return clear(); }, }), - hasThreadTyping: attr({ default: false, + hasThreadTyping: attr({ + default: false, compute() { if (this.threadView) { return this.threadView.hasComposerThreadTyping; @@ -1205,7 +1297,8 @@ Model({ * it didn't have the focus anymore. */ hasToRestoreContent: attr({ default: false }), - isCompact: attr({ default: true, + isCompact: attr({ + default: true, compute() { if (this.chatter) { return false; @@ -1216,7 +1309,8 @@ Model({ return clear(); }, }), - isExpandable: attr({ default: false, + isExpandable: attr({ + default: false, compute() { if (this.chatter) { return true; @@ -1231,8 +1325,10 @@ Model({ isInDiscuss: attr({ compute() { return Boolean( - (this.threadView && (this.threadView.threadViewer.discuss || this.threadView.threadViewer.discussPublicView)) || - (this.messageViewInEditing && this.messageViewInEditing.isInDiscuss) + (this.threadView && + (this.threadView.threadViewer.discuss || + this.threadView.threadViewer.discussPublicView)) || + (this.messageViewInEditing && this.messageViewInEditing.isInDiscuss) ); }, }), @@ -1243,7 +1339,7 @@ Model({ compute() { return Boolean( (this.threadView && this.threadView.threadViewer.chatWindow) || - (this.messageViewInEditing && this.messageViewInEditing.isInChatWindow) + (this.messageViewInEditing && this.messageViewInEditing.isInChatWindow) ); }, }), @@ -1254,7 +1350,7 @@ Model({ compute() { return Boolean( (this.threadView && this.threadView.threadViewer.chatter) || - (this.messageViewInEditing && this.messageViewInEditing.isInChatter) + (this.messageViewInEditing && this.messageViewInEditing.isInChatter) ); }, }), @@ -1263,11 +1359,14 @@ Model({ * whether the current partner is typing something. */ textareaLastInputValue: attr({ default: "" }), - mainSuggestions: many('ComposerSuggestable'), + mainSuggestions: many("ComposerSuggestable"), /** * States the message view on which this composer allows editing (if any). */ - messageViewInEditing: one('MessageView', { identifying: true, inverse: 'composerViewInEditing' }), + messageViewInEditing: one("MessageView", { + identifying: true, + inverse: "composerViewInEditing", + }), /** * Determines the next function to execute after the current mention * RPC is done, if any. @@ -1282,7 +1381,7 @@ Model({ this.composer && this.composer.isLog && this.composer.activeThread && - this.composer.activeThread.model !== 'mail.channel' + this.composer.activeThread.model !== "mail.channel" ) { return this.env._t("Log"); } @@ -1294,13 +1393,14 @@ Model({ * The format is an array of string that can contain 'enter', * 'ctrl-enter', and/or 'meta-enter'. */ - sendShortcuts: attr({ default: [], + sendShortcuts: attr({ + default: [], compute() { if (this.chatter) { - return ['ctrl-enter', 'meta-enter']; + return ["ctrl-enter", "meta-enter"]; } if (this.messageViewInEditing) { - return ['enter']; + return ["enter"]; } if (this.threadView) { if (!this.messaging.device) { @@ -1312,14 +1412,12 @@ Model({ // the button or using the 'ctrl/meta enter' shortcut. if ( this.messaging.device.isSmall || - ( - this.messaging.discuss.threadView === this.threadView && - this.messaging.discuss.activeThread === this.messaging.inbox.thread - ) + (this.messaging.discuss.threadView === this.threadView && + this.messaging.discuss.activeThread === this.messaging.inbox.thread) ) { - return ['ctrl-enter', 'meta-enter']; + return ["ctrl-enter", "meta-enter"]; } - return ['enter']; + return ["enter"]; } return clear(); }, @@ -1357,14 +1455,14 @@ Model({ suggestionModelName: attr({ compute() { switch (this.suggestionDelimiter) { - case '@': - return 'Partner'; - case ':': - return 'CannedResponse'; - case '/': - return 'ChannelCommand'; - case '#': - return 'Thread'; + case "@": + return "Partner"; + case ":": + return "CannedResponse"; + case "/": + return "ChannelCommand"; + case "#": + return "Thread"; default: return clear(); } @@ -1382,11 +1480,15 @@ Model({ ) { return clear(); } - return this.composer.textInputContent.substring(this.suggestionDelimiterPosition + 1, this.composer.textInputCursorStart); + return this.composer.textInputContent.substring( + this.suggestionDelimiterPosition + 1, + this.composer.textInputCursorStart + ); }, }), - textInput: one('ComposerTextInputView', { default: {}, inverse: 'owner' }), - threadTextualTypingStatusView: one('ThreadTextualTypingStatusView', { inverse: 'owner', + textInput: one("ComposerTextInputView", { default: {}, inverse: "owner" }), + threadTextualTypingStatusView: one("ThreadTextualTypingStatusView", { + inverse: "owner", compute() { if (this.hasThreadTyping) { return {}; @@ -1397,25 +1499,39 @@ Model({ /** * States the thread view on which this composer allows editing (if any). */ - threadView: one('ThreadView', { identifying: true, inverse: 'composerView' }), - useDragVisibleDropZone: one('UseDragVisibleDropZone', { default: {}, inverse: 'composerViewOwner', readonly: true, required: true }), + threadView: one("ThreadView", { identifying: true, inverse: "composerView" }), + useDragVisibleDropZone: one("UseDragVisibleDropZone", { + default: {}, + inverse: "composerViewOwner", + readonly: true, + required: true, + }), }, onChanges: [ { - dependencies: ['composer'], - methodName: '_onChangeComposer', + dependencies: ["composer"], + methodName: "_onChangeComposer", }, { - dependencies: ['composer.textInputContent', 'composer.textInputCursorEnd', 'composer.textInputCursorStart'], - methodName: '_onChangeDetectSuggestionDelimiterPosition', + dependencies: [ + "composer.textInputContent", + "composer.textInputCursorEnd", + "composer.textInputCursorStart", + ], + methodName: "_onChangeDetectSuggestionDelimiterPosition", }, { - dependencies: ['suggestionDelimiterPosition', 'suggestionModelName', 'suggestionSearchTerm', 'composer.activeThread'], - methodName: '_onChangeUpdateSuggestionList', + dependencies: [ + "suggestionDelimiterPosition", + "suggestionModelName", + "suggestionSearchTerm", + "composer.activeThread", + ], + methodName: "_onChangeUpdateSuggestionList", }, { - dependencies: ['suggestionDelimiterPosition'], - methodName: '_onSuggestionDelimiterPositionChanged', + dependencies: ["suggestionDelimiterPosition"], + methodName: "_onSuggestionDelimiterPositionChanged", }, ], }); diff --git a/addons/mail/static/src/models/country.js b/addons/mail/static/src/models/country.js index 869028c6157..40427b25bbd 100644 --- a/addons/mail/static/src/models/country.js +++ b/addons/mail/static/src/models/country.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, Model } from '@mail/model'; +import { attr, clear, Model } from "@mail/model"; Model({ - name: 'Country', + name: "Country", fields: { code: attr(), flagUrl: attr({ diff --git a/addons/mail/static/src/models/delete_message_confirm_view.js b/addons/mail/static/src/models/delete_message_confirm_view.js index 98269afa0b0..fbe6f6f57e4 100644 --- a/addons/mail/static/src/models/delete_message_confirm_view.js +++ b/addons/mail/static/src/models/delete_message_confirm_view.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { attr, clear, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'DeleteMessageConfirmView', - template: 'mail.DeleteMessageConfirmView', + name: "DeleteMessageConfirmView", + template: "mail.DeleteMessageConfirmView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); }, recordMethods: { /** @@ -17,7 +17,9 @@ Model({ * @returns {boolean} */ containsElement(element) { - return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element)); + return Boolean( + this.component && this.component.root.el && this.component.root.el.contains(element) + ); }, onClickCancel() { this.dialogOwner.delete(); @@ -25,23 +27,27 @@ Model({ onClickDelete() { this.message.updateContent({ attachment_ids: [], - body: '', + body: "", }); }, }, fields: { component: attr(), - dialogOwner: one('Dialog', { identifying: true, inverse: 'deleteMessageConfirmView' }), - message: one('Message', { required: true, + dialogOwner: one("Dialog", { identifying: true, inverse: "deleteMessageConfirmView" }), + message: one("Message", { + required: true, compute() { - return this.dialogOwner.messageActionViewOwnerAsDeleteConfirm.messageAction.messageActionListOwner.message; + return this.dialogOwner.messageActionViewOwnerAsDeleteConfirm.messageAction + .messageActionListOwner.message; }, }), /** * Determines the message view that this delete message confirm view * will use to display this message. */ - messageView: one('MessageView', { inverse: 'deleteMessageConfirmViewOwner', required: true, + messageView: one("MessageView", { + inverse: "deleteMessageConfirmViewOwner", + required: true, compute() { return this.message ? { message: this.message } : clear(); }, diff --git a/addons/mail/static/src/models/dialog.js b/addons/mail/static/src/models/dialog.js index 9744f2eb15d..e5e147bb98f 100644 --- a/addons/mail/static/src/models/dialog.js +++ b/addons/mail/static/src/models/dialog.js @@ -1,19 +1,19 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'Dialog', - identifyingMode: 'xor', - template: 'mail.Dialog', + name: "Dialog", + identifyingMode: "xor", + template: "mail.Dialog", lifecycleHooks: { _created() { - document.addEventListener('click', this._onClickGlobal, true); - document.addEventListener('keydown', this._onKeydownGlobal); + document.addEventListener("click", this._onClickGlobal, true); + document.addEventListener("keydown", this._onKeydownGlobal); }, _willDelete() { - document.removeEventListener('click', this._onClickGlobal, true); - document.removeEventListener('keydown', this._onKeydownGlobal); + document.removeEventListener("click", this._onClickGlobal, true); + document.removeEventListener("keydown", this._onKeydownGlobal); }, }, recordMethods: { @@ -45,14 +45,18 @@ Model({ * @param {KeyboardEvent} ev */ _onKeydownGlobal(ev) { - if (ev.key === 'Escape') { + if (ev.key === "Escape") { this.delete(); } }, }, fields: { - attachmentCardOwnerAsAttachmentDeleteConfirm: one('AttachmentCard', { identifying: true, inverse: 'attachmentDeleteConfirmDialog' }), - attachmentDeleteConfirmView: one('AttachmentDeleteConfirmView', { inverse: 'dialogOwner', + attachmentCardOwnerAsAttachmentDeleteConfirm: one("AttachmentCard", { + identifying: true, + inverse: "attachmentDeleteConfirmDialog", + }), + attachmentDeleteConfirmView: one("AttachmentDeleteConfirmView", { + inverse: "dialogOwner", compute() { if (this.attachmentCardOwnerAsAttachmentDeleteConfirm) { return {}; @@ -63,9 +67,16 @@ Model({ return clear(); }, }), - attachmentImageOwnerAsAttachmentDeleteConfirm: one('AttachmentImage', { identifying: true, inverse: 'attachmentDeleteConfirmDialog' }), - attachmentListOwnerAsAttachmentView: one('AttachmentList', { identifying: true, inverse: 'attachmentListViewDialog' }), - attachmentViewer: one('AttachmentViewer', { inverse: 'dialogOwner', + attachmentImageOwnerAsAttachmentDeleteConfirm: one("AttachmentImage", { + identifying: true, + inverse: "attachmentDeleteConfirmDialog", + }), + attachmentListOwnerAsAttachmentView: one("AttachmentList", { + identifying: true, + inverse: "attachmentListViewDialog", + }), + attachmentViewer: one("AttachmentViewer", { + inverse: "dialogOwner", compute() { if (this.attachmentListOwnerAsAttachmentView) { return {}; @@ -84,55 +95,66 @@ Model({ componentClassName: attr({ compute() { if (this.attachmentDeleteConfirmView) { - return 'o_Dialog_componentMediumSize align-self-start mt-5'; + return "o_Dialog_componentMediumSize align-self-start mt-5"; } if (this.deleteMessageConfirmView) { - return 'o_Dialog_componentLargeSize align-self-start mt-5'; + return "o_Dialog_componentLargeSize align-self-start mt-5"; } if (this.linkPreviewDeleteConfirmView) { - return 'o_Dialog_componentMediumSize align-self-start mt-5'; + return "o_Dialog_componentMediumSize align-self-start mt-5"; } - return ''; + return ""; }, }), - componentName: attr({ required: true, + componentName: attr({ + required: true, compute() { if (this.attachmentViewer) { - return 'AttachmentViewer'; + return "AttachmentViewer"; } if (this.attachmentDeleteConfirmView) { - return 'AttachmentDeleteConfirmView'; + return "AttachmentDeleteConfirmView"; } if (this.deleteMessageConfirmView) { - return 'DeleteMessageConfirmView'; + return "DeleteMessageConfirmView"; } if (this.followerSubtypeList) { - return 'FollowerSubtypeList'; + return "FollowerSubtypeList"; } if (this.linkPreviewDeleteConfirmView) { - return 'LinkPreviewDeleteConfirmView'; + return "LinkPreviewDeleteConfirmView"; } return clear(); }, }), - deleteMessageConfirmView: one('DeleteMessageConfirmView', { inverse: 'dialogOwner', + deleteMessageConfirmView: one("DeleteMessageConfirmView", { + inverse: "dialogOwner", compute() { return this.messageActionViewOwnerAsDeleteConfirm ? {} : clear(); }, }), - linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm: one('LinkPreviewAsideView', { inverse: 'linkPreviewDeleteConfirmDialog', readonly: true }), - linkPreviewDeleteConfirmView: one('LinkPreviewDeleteConfirmView', { inverse: 'dialogOwner', + linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm: one("LinkPreviewAsideView", { + inverse: "linkPreviewDeleteConfirmDialog", + readonly: true, + }), + linkPreviewDeleteConfirmView: one("LinkPreviewDeleteConfirmView", { + inverse: "dialogOwner", compute() { return this.linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm ? {} : clear(); }, }), - followerOwnerAsSubtypeList: one('Follower', { identifying: true, inverse: 'followerSubtypeListDialog' }), - followerSubtypeList: one('FollowerSubtypeList', { inverse: 'dialogOwner', + followerOwnerAsSubtypeList: one("Follower", { + identifying: true, + inverse: "followerSubtypeListDialog", + }), + followerSubtypeList: one("FollowerSubtypeList", { + inverse: "dialogOwner", compute() { return this.followerOwnerAsSubtypeList ? {} : clear(); }, }), - isCloseable: attr({ default: true, + isCloseable: attr({ + default: true, compute() { if (this.attachmentViewer) { /** @@ -144,7 +166,8 @@ Model({ return true; }, }), - manager: one('DialogManager', { inverse: 'dialogs', + manager: one("DialogManager", { + inverse: "dialogs", compute() { if (this.messaging.dialogManager) { return this.messaging.dialogManager; @@ -152,13 +175,18 @@ Model({ return clear(); }, }), - messageActionViewOwnerAsDeleteConfirm: one('MessageActionView', { identifying: true, inverse: 'deleteConfirmDialog' }), + messageActionViewOwnerAsDeleteConfirm: one("MessageActionView", { + identifying: true, + inverse: "deleteConfirmDialog", + }), /** * Content of dialog that is directly linked to a record that models * a UI component, such as AttachmentViewer. These records must be * created from @see `DialogManager:open()`. */ - record: one('Record', { isCausal: true, required: true, + record: one("Record", { + isCausal: true, + required: true, compute() { if (this.attachmentViewer) { return this.attachmentViewer; diff --git a/addons/mail/static/src/models/dialog_manager.js b/addons/mail/static/src/models/dialog_manager.js index 4ae6b8ceff1..e4cac6c7ed3 100644 --- a/addons/mail/static/src/models/dialog_manager.js +++ b/addons/mail/static/src/models/dialog_manager.js @@ -1,25 +1,25 @@ /** @odoo-module **/ -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { many, Model } from '@mail/model'; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { many, Model } from "@mail/model"; Model({ - name: 'DialogManager', - template: 'mail.DialogManager', + name: "DialogManager", + template: "mail.DialogManager", componentSetup() { - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, recordMethods: { onComponentUpdate() { if (this.dialogs.length > 0) { - document.body.classList.add('modal-open'); + document.body.classList.add("modal-open"); } else { - document.body.classList.remove('modal-open'); + document.body.classList.remove("modal-open"); } }, }, fields: { // FIXME: dependent on implementation that uses insert order in relations!! - dialogs: many('Dialog', { inverse: 'manager', isCausal: true }), + dialogs: many("Dialog", { inverse: "manager", isCausal: true }), }, }); diff --git a/addons/mail/static/src/models/discuss.js b/addons/mail/static/src/models/discuss.js index cd08fb9c472..c9739144699 100644 --- a/addons/mail/static/src/models/discuss.js +++ b/addons/mail/static/src/models/discuss.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; -import { escape, sprintf } from '@web/core/utils/strings'; +import { attr, clear, one, Model } from "@mail/model"; +import { escape, sprintf } from "@web/core/utils/strings"; Model({ - name: 'Discuss', + name: "Discuss", recordMethods: { /** * Close the discuss app. Should reset its internal state. @@ -30,15 +30,15 @@ Model({ const name = this.discussView.addingChannelValue; this.discussView.clearIsAddingItem(); if (ui.item.create) { - const channel = await this.messaging.models['Thread'].performRpcCreateChannel({ + const channel = await this.messaging.models["Thread"].performRpcCreateChannel({ name, group_id: this.messaging.internalUserGroupId, }); channel.open(); } else { - const channel = this.messaging.models['Thread'].insert({ + const channel = this.messaging.models["Thread"].insert({ id: ui.item.id, - model: 'mail.channel', + model: "mail.channel", }); await channel.join(); // Channel must be pinned immediately to be able to open it before @@ -54,7 +54,10 @@ Model({ */ async handleAddChannelAutocompleteSource(req, res) { this.discussView.update({ addingChannelValue: req.term }); - const threads = await this.messaging.models['Thread'].searchChannelsToOpen({ limit: 10, searchTerm: req.term }); + const threads = await this.messaging.models["Thread"].searchChannelsToOpen({ + limit: 10, + searchTerm: req.term, + }); const items = threads.map((thread) => { const escapedName = escape(thread.name); return { @@ -70,8 +73,8 @@ Model({ create: true, escapedValue, label: sprintf( - `${this.env._t('Create %s')}`, - `${escapedValue}`, + `${this.env._t("Create %s")}`, + `${escapedValue}` ), }); res(items); @@ -93,16 +96,16 @@ Model({ */ handleAddChatAutocompleteSource(req, res) { const value = escape(req.term); - this.messaging.models['Partner'].imSearch({ - callback: partners => { - const suggestions = partners.map(partner => { + this.messaging.models["Partner"].imSearch({ + callback: (partners) => { + const suggestions = partners.map((partner) => { return { id: partner.id, value: partner.nameOrDisplayName, label: partner.nameOrDisplayName, }; }); - res(_.sortBy(suggestions, 'label')); + res(_.sortBy(suggestions, "label")); }, keyword: value, limit: 10, @@ -115,11 +118,12 @@ Model({ * Opens thread from init active id if the thread exists. */ openInitThread() { - const [model, id] = typeof this.initActiveId === 'number' - ? ['mail.channel', this.initActiveId] - : this.initActiveId.split('_'); - const thread = this.messaging.models['Thread'].findFromIdentifyingData({ - id: model !== 'mail.box' ? Number(id) : id, + const [model, id] = + typeof this.initActiveId === "number" + ? ["mail.channel", this.initActiveId] + : this.initActiveId.split("_"); + const thread = this.messaging.models["Thread"].findFromIdentifyingData({ + id: model !== "mail.box" ? Number(id) : id, model, }); if (!thread) { @@ -143,14 +147,11 @@ Model({ this.focus(); } if (!this.discussView) { - this.env.services.action.doAction( - 'mail.action_discuss', - { - active_id: this.threadToActiveId(this), - clearBreadcrumbs: false, - on_reverse_breadcrumb: () => this.close(), // this is useless, close is called by destroy anyway - }, - ); + this.env.services.action.doAction("mail.action_discuss", { + active_id: this.threadToActiveId(this), + clearBreadcrumbs: false, + on_reverse_breadcrumb: () => this.close(), // this is useless, close is called by destroy anyway + }); } }, /** @@ -184,11 +185,11 @@ Model({ /** * Active mobile navbar tab, either 'mailbox', 'chat', or 'channel'. */ - activeMobileNavbarTabId: attr({ default: 'mailbox' }), + activeMobileNavbarTabId: attr({ default: "mailbox" }), /** * Determines the `Thread` that should be displayed by `this`. */ - activeThread: one('Thread', { + activeThread: one("Thread", { /** * Only mailboxes and pinned channels are allowed in Discuss. */ @@ -218,12 +219,15 @@ Model({ /** * Discuss sidebar category for `channel` type channel threads. */ - categoryChannel: one('DiscussSidebarCategory', { default: {}, inverse: 'discussAsChannel' }), + categoryChannel: one("DiscussSidebarCategory", { + default: {}, + inverse: "discussAsChannel", + }), /** * Discuss sidebar category for `chat` type channel threads. */ - categoryChat: one('DiscussSidebarCategory', { default: {}, inverse: 'discussAsChat' }), - discussView: one('DiscussView', { inverse: 'discuss' }), + categoryChat: one("DiscussSidebarCategory", { default: {}, inverse: "discussAsChat" }), + discussView: one("DiscussView", { inverse: "discuss" }), /** * Determines whether `this.thread` should be displayed. */ @@ -234,10 +238,7 @@ Model({ } if ( this.messaging.device.isSmall && - ( - this.activeMobileNavbarTabId !== 'mailbox' || - !this.activeThread.mailbox - ) + (this.activeMobileNavbarTabId !== "mailbox" || !this.activeThread.mailbox) ) { return false; } @@ -252,7 +253,7 @@ Model({ * {string} _ * {int} with default model of 'mail.channel' */ - initActiveId: attr({ default: 'mail.box_inbox' }), + initActiveId: attr({ default: "mail.box_inbox" }), /** * Determines if the logic for opening a thread via the `initActiveId` * has been processed. This is necessary to ensure that this only @@ -264,16 +265,20 @@ Model({ * used to open discuss from elsewhere. */ menu_id: attr({ default: null }), - notificationListView: one('NotificationListView', { inverse: 'discussOwner', + notificationListView: one("NotificationListView", { + inverse: "discussOwner", compute() { - return (this.messaging.device.isSmall && this.activeMobileNavbarTabId !== 'mailbox') ? {} : clear(); + return this.messaging.device.isSmall && this.activeMobileNavbarTabId !== "mailbox" + ? {} + : clear(); }, }), /** * The navbar view on the discuss app when in mobile and when not * replying to a message from inbox. */ - mobileMessagingNavbarView: one('MobileMessagingNavbarView', { inverse: 'discuss', + mobileMessagingNavbarView: one("MobileMessagingNavbarView", { + inverse: "discuss", compute() { if ( this.messaging.device && @@ -290,15 +295,17 @@ Model({ * to filter channels and chats based on this input content. */ sidebarQuickSearchValue: attr({ default: "" }), - thread: one('Thread'), + thread: one("Thread"), /** * States the `ThreadView` displaying `this.thread`. */ - threadView: one('ThreadView', { related: 'threadViewer.threadView' }), + threadView: one("ThreadView", { related: "threadViewer.threadView" }), /** * Determines the `ThreadViewer` managing the display of `this.thread`. */ - threadViewer: one('ThreadViewer', { inverse: 'discuss', required: true, + threadViewer: one("ThreadViewer", { + inverse: "discuss", + required: true, compute() { return { hasMemberList: true, diff --git a/addons/mail/static/src/models/discuss_mobile_mailbox_selection_item_view.js b/addons/mail/static/src/models/discuss_mobile_mailbox_selection_item_view.js index d6390d0ad16..9ec95fccc58 100644 --- a/addons/mail/static/src/models/discuss_mobile_mailbox_selection_item_view.js +++ b/addons/mail/static/src/models/discuss_mobile_mailbox_selection_item_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'DiscussMobileMailboxSelectionItemView', - template: 'mail.DiscussMobileMailboxSelectionItemView', + name: "DiscussMobileMailboxSelectionItemView", + template: "mail.DiscussMobileMailboxSelectionItemView", recordMethods: { onClick() { if (!this.exists()) { @@ -14,7 +14,7 @@ Model({ }, }, fields: { - mailbox: one('Mailbox', { identifying: true, inverse: 'discussMobileSelectionItems' }), - owner: one('DiscussMobileMailboxSelectionView', { identifying: true, inverse: 'items' }), + mailbox: one("Mailbox", { identifying: true, inverse: "discussMobileSelectionItems" }), + owner: one("DiscussMobileMailboxSelectionView", { identifying: true, inverse: "items" }), }, }); diff --git a/addons/mail/static/src/models/discuss_mobile_mailbox_selection_view.js b/addons/mail/static/src/models/discuss_mobile_mailbox_selection_view.js index 644aef0ed63..54cb4bfda5c 100644 --- a/addons/mail/static/src/models/discuss_mobile_mailbox_selection_view.js +++ b/addons/mail/static/src/models/discuss_mobile_mailbox_selection_view.js @@ -1,16 +1,17 @@ /** @odoo-module **/ -import { many, one, Model } from '@mail/model'; +import { many, one, Model } from "@mail/model"; Model({ - name: 'DiscussMobileMailboxSelectionView', - template: 'mail.DiscussMobileMailboxSelectionView', + name: "DiscussMobileMailboxSelectionView", + template: "mail.DiscussMobileMailboxSelectionView", fields: { - items: many('DiscussMobileMailboxSelectionItemView', { inverse: 'owner', + items: many("DiscussMobileMailboxSelectionItemView", { + inverse: "owner", compute() { - return this.owner.orderedMailboxes.map(mailbox => ({ mailbox })); + return this.owner.orderedMailboxes.map((mailbox) => ({ mailbox })); }, }), - owner: one('DiscussView', { identifying: true, inverse: 'mobileMailboxSelectionView' }), + owner: one("DiscussView", { identifying: true, inverse: "mobileMailboxSelectionView" }), }, }); diff --git a/addons/mail/static/src/models/discuss_public_view.js b/addons/mail/static/src/models/discuss_public_view.js index aa112254f12..ac1dbfb8682 100644 --- a/addons/mail/static/src/models/discuss_public_view.js +++ b/addons/mail/static/src/models/discuss_public_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'DiscussPublicView', - template: 'mail.DiscussPublicView', + name: "DiscussPublicView", + template: "mail.DiscussPublicView", recordMethods: { /** * Creates and displays the thread view and clears the welcome view. @@ -12,7 +12,7 @@ Model({ async switchToThreadView() { this.update({ threadViewer: { - extraClass: 'flex-grow-1', + extraClass: "flex-grow-1", hasMemberList: true, hasThreadView: true, hasTopbar: true, @@ -22,9 +22,13 @@ Model({ }); if (this.isChannelTokenSecret) { // Change the URL to avoid leaking the invitation link. - window.history.replaceState(window.history.state, null, `/discuss/channel/${this.channel.id}${window.location.search}`); + window.history.replaceState( + window.history.state, + null, + `/discuss/channel/${this.channel.id}${window.location.search}` + ); } - if (this.channel.defaultDisplayMode === 'video_full_screen') { + if (this.channel.defaultDisplayMode === "video_full_screen") { await this.channel.toggleCall({ startWithVideo: true }); await this.threadView.callView.activateFullScreen(); } @@ -38,8 +42,10 @@ Model({ welcomeView: { channel: this.channel, isDoFocusGuestNameInput: true, - originalGuestName: this.messaging.currentGuest && this.messaging.currentGuest.name, - pendingGuestName: this.messaging.currentGuest && this.messaging.currentGuest.name, + originalGuestName: + this.messaging.currentGuest && this.messaging.currentGuest.name, + pendingGuestName: + this.messaging.currentGuest && this.messaging.currentGuest.name, }, }); if (this.welcomeView.callDemoView) { @@ -52,21 +58,21 @@ Model({ /** * States the channel linked to this discuss public view. */ - channel: one('Thread', { readonly: true, required: true }), + channel: one("Thread", { readonly: true, required: true }), isChannelTokenSecret: attr({ default: true }), shouldAddGuestAsMemberOnJoin: attr({ default: false, readonly: true }), shouldDisplayWelcomeViewInitially: attr({ default: false, readonly: true }), /** * States the thread view linked to this discuss public view. */ - threadView: one('ThreadView', { related: 'threadViewer.threadView' }), + threadView: one("ThreadView", { related: "threadViewer.threadView" }), /** * States the thread viewer linked to this discuss public view. */ - threadViewer: one('ThreadViewer', { inverse: 'discussPublicView' }), + threadViewer: one("ThreadViewer", { inverse: "discussPublicView" }), /** * States the welcome view linked to this discuss public view. */ - welcomeView: one('WelcomeView', { inverse: 'discussPublicView' }), + welcomeView: one("WelcomeView", { inverse: "discussPublicView" }), }, }); diff --git a/addons/mail/static/src/models/discuss_sidebar_category.js b/addons/mail/static/src/models/discuss_sidebar_category.js index 0381863b877..28d8260c6b6 100644 --- a/addons/mail/static/src/models/discuss_sidebar_category.js +++ b/addons/mail/static/src/models/discuss_sidebar_category.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'DiscussSidebarCategory', - template: 'mail.DiscussSidebarCategory', - identifyingMode: 'xor', + name: "DiscussSidebarCategory", + template: "mail.DiscussSidebarCategory", + identifyingMode: "xor", modelMethods: { /** * Performs the `set_res_users_settings` RPC on `res.users.settings`. @@ -17,14 +17,14 @@ Model({ async performRpcSetResUsersSettings(resUsersSettings) { return this.messaging.rpc( { - model: 'res.users.settings', - method: 'set_res_users_settings', + model: "res.users.settings", + method: "set_res_users_settings", args: [[this.messaging.currentUser.res_users_settings_id.id]], kwargs: { new_settings: resUsersSettings, }, }, - { shadow: true }, + { shadow: true } ); }, }, @@ -34,7 +34,7 @@ Model({ */ async close() { this.update({ isPendingOpen: false }); - await this.messaging.models['DiscussSidebarCategory'].performRpcSetResUsersSettings({ + await this.messaging.models["DiscussSidebarCategory"].performRpcSetResUsersSettings({ [this.serverStateKey]: false, }); }, @@ -43,7 +43,7 @@ Model({ */ async open() { this.update({ isPendingOpen: true }); - await this.messaging.models['DiscussSidebarCategory'].performRpcSetResUsersSettings({ + await this.messaging.models["DiscussSidebarCategory"].performRpcSetResUsersSettings({ [this.serverStateKey]: true, }); }, @@ -68,10 +68,10 @@ Model({ */ onAddItemAutocompleteSelect(ev, ui) { switch (this.autocompleteMethod) { - case 'channel': + case "channel": this.messaging.discuss.handleAddChannelAutocompleteSelect(ev, ui); break; - case 'chat': + case "chat": this.messaging.discuss.handleAddChatAutocompleteSelect(ev, ui); break; } @@ -83,10 +83,10 @@ Model({ */ onAddItemAutocompleteSource(req, res) { switch (this.autocompleteMethod) { - case 'channel': + case "channel": this.messaging.discuss.handleAddChannelAutocompleteSource(req, res); break; - case 'chat': + case "chat": this.messaging.discuss.handleAddChatAutocompleteSource(req, res); break; } @@ -107,10 +107,13 @@ Model({ ev.stopPropagation(); return this.env.services.action.doAction({ name: this.env._t("Public Channels"), - type: 'ir.actions.act_window', - res_model: 'mail.channel', - views: [[false, 'kanban'], [false, 'form']], - domain: [['channel_type', '=', 'channel']], + type: "ir.actions.act_window", + res_model: "mail.channel", + views: [ + [false, "kanban"], + [false, "form"], + ], + domain: [["channel_type", "=", "channel"]], }); }, /** @@ -130,9 +133,11 @@ Model({ * The category item which is active and belongs * to the category. */ - activeItem: one('DiscussSidebarCategoryItem', { + activeItem: one("DiscussSidebarCategoryItem", { compute() { - const channel = this.messaging.discuss.activeThread && this.messaging.discuss.activeThread.channel; + const channel = + this.messaging.discuss.activeThread && + this.messaging.discuss.activeThread.channel; if (channel && this.supportedChannelTypes.includes(channel.channel_type)) { return { category: this, @@ -142,7 +147,8 @@ Model({ return clear(); }, }), - addingItemAutocompleteInputView: one('AutocompleteInputView', { inverse: 'discussSidebarCategoryOwnerAsAddingItem', + addingItemAutocompleteInputView: one("AutocompleteInputView", { + inverse: "discussSidebarCategoryOwnerAsAddingItem", compute() { if (this.isOpen && this.isAddingItem) { return {}; @@ -154,13 +160,14 @@ Model({ * Determines how the autocomplete of this category should behave. * Must be one of: 'channel', 'chat'. */ - autocompleteMethod: attr({ default: '', + autocompleteMethod: attr({ + default: "", compute() { if (this.discussAsChannel) { - return 'channel'; + return "channel"; } if (this.discussAsChat) { - return 'chat'; + return "chat"; } return clear(); }, @@ -169,8 +176,8 @@ Model({ * Determines the discuss sidebar category items that are displayed by * this discuss sidebar category. */ - categoryItems: many('DiscussSidebarCategoryItem', { inverse: 'category' }), - categoryItemsOrderedByLastAction: many('DiscussSidebarCategoryItem', { + categoryItems: many("DiscussSidebarCategoryItem", { inverse: "category" }), + categoryItemsOrderedByLastAction: many("DiscussSidebarCategoryItem", { compute() { if (this.discussAsChat) { return this.categoryItems; @@ -180,13 +187,13 @@ Model({ return clear(); }, sort: [ - ['truthy-first', 'thread'], - ['truthy-first', 'thread.lastInterestDateTime'], - ['most-recent-first', 'thread.lastInterestDateTime'], - ['greater-first', 'channel.id'], + ["truthy-first", "thread"], + ["truthy-first", "thread.lastInterestDateTime"], + ["most-recent-first", "thread.lastInterestDateTime"], + ["greater-first", "channel.id"], ], }), - categoryItemsOrderedByName: many('DiscussSidebarCategoryItem', { + categoryItemsOrderedByName: many("DiscussSidebarCategoryItem", { compute() { if (this.discussAsChannel) { return this.categoryItems; @@ -196,16 +203,17 @@ Model({ return clear(); }, sort: [ - ['truthy-first', 'thread'], - ['truthy-first', 'thread.displayName'], - ['case-insensitive-asc', 'thread.displayName'], - ['smaller-first', 'channel.id'], + ["truthy-first", "thread"], + ["truthy-first", "thread.displayName"], + ["case-insensitive-asc", "thread.displayName"], + ["smaller-first", "channel.id"], ], }), /** * The title text in UI for command `add` */ - commandAddTitleText: attr({ default: '', + commandAddTitleText: attr({ + default: "", compute() { if (this.discussAsChannel) { return this.env._t("Add or join a channel"); @@ -220,20 +228,24 @@ Model({ * States the total amount of unread/action-needed threads in this * category. */ - counter: attr({ default: 0, readonly: true, sum: 'categoryItems.categoryCounterContribution' }), - discussAsChannel: one('Discuss', { identifying: true, inverse: 'categoryChannel' }), - discussAsChat: one('Discuss', { identifying: true, inverse: 'categoryChat' }), + counter: attr({ + default: 0, + readonly: true, + sum: "categoryItems.categoryCounterContribution", + }), + discussAsChannel: one("Discuss", { identifying: true, inverse: "categoryChannel" }), + discussAsChat: one("Discuss", { identifying: true, inverse: "categoryChat" }), /** * Determines the filtered and sorted discuss sidebar category items * that are displayed by this discuss sidebar category. */ - filteredCategoryItems: many('DiscussSidebarCategoryItem', { + filteredCategoryItems: many("DiscussSidebarCategoryItem", { compute() { let categoryItems = this.orderedCategoryItems; const searchValue = this.messaging.discuss.sidebarQuickSearchValue; if (searchValue) { const qsVal = searchValue.toLowerCase(); - categoryItems = categoryItems.filter(categoryItem => { + categoryItems = categoryItems.filter((categoryItem) => { const nameVal = categoryItem.channel.displayName.toLowerCase(); return nameVal.includes(qsVal); }); @@ -244,7 +256,8 @@ Model({ /** * Display name of the category. */ - name: attr({ default: '', + name: attr({ + default: "", compute() { if (this.discussAsChannel) { return this.env._t("Channels"); @@ -258,7 +271,8 @@ Model({ /** * Boolean that determines whether this category has a 'add' command. */ - hasAddCommand: attr({ default: false, + hasAddCommand: attr({ + default: false, compute() { if (this.discussAsChannel) { return true; @@ -272,7 +286,8 @@ Model({ /** * Boolean that determines whether this category has a 'view' command. */ - hasViewCommand: attr({ default: false, + hasViewCommand: attr({ + default: false, compute() { if (this.discussAsChannel) { return true; @@ -303,7 +318,8 @@ Model({ /** * Boolean that determines the last open state known by the server. */ - isServerOpen: attr({ default: false, + isServerOpen: attr({ + default: false, compute() { // there is no server state for non-users (guests) if (!this.messaging.currentUser) { @@ -313,10 +329,12 @@ Model({ return clear(); } if (this.discussAsChannel) { - return this.messaging.currentUser.res_users_settings_id.is_discuss_sidebar_category_channel_open; + return this.messaging.currentUser.res_users_settings_id + .is_discuss_sidebar_category_channel_open; } if (this.discussAsChat) { - return this.messaging.currentUser.res_users_settings_id.is_discuss_sidebar_category_chat_open; + return this.messaging.currentUser.res_users_settings_id + .is_discuss_sidebar_category_chat_open; } return clear(); }, @@ -335,7 +353,7 @@ Model({ return clear(); }, }), - orderedCategoryItems: many('DiscussSidebarCategoryItem', { + orderedCategoryItems: many("DiscussSidebarCategoryItem", { compute() { if (this.discussAsChannel) { return this.categoryItemsOrderedByName; @@ -352,10 +370,10 @@ Model({ serverStateKey: attr({ compute() { if (this.discussAsChannel) { - return 'is_discuss_sidebar_category_channel_open'; + return "is_discuss_sidebar_category_channel_open"; } if (this.discussAsChat) { - return 'is_discuss_sidebar_category_chat_open'; + return "is_discuss_sidebar_category_chat_open"; } return clear(); }, @@ -363,13 +381,14 @@ Model({ /** * Channel type which is supported by the category. */ - supportedChannelTypes: attr({ required: true, + supportedChannelTypes: attr({ + required: true, compute() { if (this.discussAsChannel) { - return ['channel']; + return ["channel"]; } if (this.discussAsChat) { - return ['chat', 'group']; + return ["chat", "group"]; } return clear(); }, @@ -377,8 +396,8 @@ Model({ }, onChanges: [ { - dependencies: ['isServerOpen'], - methodName: '_onIsServerOpenChanged', + dependencies: ["isServerOpen"], + methodName: "_onIsServerOpenChanged", }, ], }); diff --git a/addons/mail/static/src/models/discuss_sidebar_category_item.js b/addons/mail/static/src/models/discuss_sidebar_category_item.js index 54cb9f52fed..37f1c7f9c69 100644 --- a/addons/mail/static/src/models/discuss_sidebar_category_item.js +++ b/addons/mail/static/src/models/discuss_sidebar_category_item.js @@ -1,12 +1,12 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; -import Dialog from 'web.Dialog'; +import Dialog from "web.Dialog"; Model({ - name: 'DiscussSidebarCategoryItem', - template: 'mail.DiscussSidebarCategoryItem', + name: "DiscussSidebarCategoryItem", + template: "mail.DiscussSidebarCategoryItem", recordMethods: { /** * @param {MouseEvent} ev @@ -19,10 +19,13 @@ Model({ */ async onClickCommandLeave(ev) { ev.stopPropagation(); - if (this.channel.channel_type !== 'group' && this.thread.creator === this.messaging.currentUser) { + if ( + this.channel.channel_type !== "group" && + this.thread.creator === this.messaging.currentUser + ) { await this._askAdminConfirmation(); } - if (this.channel.channel_type === 'group') { + if (this.channel.channel_type === "group") { await this._askLeaveGroupConfirmation(); } this.thread.leave(); @@ -35,11 +38,11 @@ Model({ onClickCommandSettings(ev) { ev.stopPropagation(); return this.env.services.action.doAction({ - type: 'ir.actions.act_window', + type: "ir.actions.act_window", res_model: this.thread.model, res_id: this.thread.id, - views: [[false, 'form']], - target: 'current', + views: [[false, "form"]], + target: "current", }); }, /** @@ -54,14 +57,17 @@ Model({ * @returns {Promise} */ _askAdminConfirmation() { - return new Promise(resolve => { - Dialog.confirm(this, - this.env._t("You are the administrator of this channel. Are you sure you want to leave?"), + return new Promise((resolve) => { + Dialog.confirm( + this, + this.env._t( + "You are the administrator of this channel. Are you sure you want to leave?" + ), { buttons: [ { text: this.env._t("Leave"), - classes: 'btn-primary', + classes: "btn-primary", close: true, click: resolve, }, @@ -79,22 +85,25 @@ Model({ * @returns {Promise} */ _askLeaveGroupConfirmation() { - return new Promise(resolve => { - Dialog.confirm(this, - this.env._t("You are about to leave this group conversation and will no longer have access to it unless you are invited again. Are you sure you want to continue?"), + return new Promise((resolve) => { + Dialog.confirm( + this, + this.env._t( + "You are about to leave this group conversation and will no longer have access to it unless you are invited again. Are you sure you want to continue?" + ), { buttons: [ { text: this.env._t("Leave"), - classes: 'btn-primary', + classes: "btn-primary", close: true, - click: resolve + click: resolve, }, { text: this.env._t("Discard"), - close: true - } - ] + close: true, + }, + ], } ); }); @@ -107,21 +116,21 @@ Model({ avatarUrl: attr({ compute() { switch (this.channel.channel_type) { - case 'channel': - case 'group': + case "channel": + case "group": return `/web/image/mail.channel/${this.channel.id}/avatar_128?unique=${this.channel.avatarCacheKey}`; - case 'chat': + case "chat": if (this.channel.correspondent) { return this.channel.correspondent.avatarUrl; } } - return '/mail/static/src/img/smiley/avatar.jpg'; + return "/mail/static/src/img/smiley/avatar.jpg"; }, }), /** * Determines the discuss sidebar category displaying this item. */ - category: one('DiscussSidebarCategory', { identifying: true, inverse: 'categoryItems' }), + category: one("DiscussSidebarCategory", { identifying: true, inverse: "categoryItems" }), /** * Determines the contribution of this discuss sidebar category item to * the counter of this category. @@ -132,15 +141,15 @@ Model({ return clear(); } switch (this.channel.channel_type) { - case 'channel': + case "channel": return this.thread.message_needaction_counter > 0 ? 1 : 0; - case 'chat': - case 'group': + case "chat": + case "group": return this.channel.localMessageUnreadCounter > 0 ? 1 : 0; } }, }), - channel: one('Channel', { identifying: true, inverse: 'discussSidebarCategoryItem' }), + channel: one("Channel", { identifying: true, inverse: "discussSidebarCategoryItem" }), /** * Amount of unread/action-needed messages */ @@ -150,10 +159,10 @@ Model({ return clear(); } switch (this.channel.channel_type) { - case 'channel': + case "channel": return this.thread.message_needaction_counter; - case 'chat': - case 'group': + case "chat": + case "group": return this.channel.localMessageUnreadCounter; } }, @@ -167,7 +176,7 @@ Model({ return clear(); } return ( - ['channel', 'group'].includes(this.channel.channel_type) && + ["channel", "group"].includes(this.channel.channel_type) && !this.thread.message_needaction_counter && !this.thread.group_based_subscription ); @@ -178,7 +187,7 @@ Model({ */ hasSettingsCommand: attr({ compute() { - return this.channel.channel_type === 'channel'; + return this.channel.channel_type === "channel"; }, }), /** @@ -186,7 +195,9 @@ Model({ */ hasUnpinCommand: attr({ compute() { - return this.channel.channel_type === 'chat' && !this.channel.localMessageUnreadCounter; + return ( + this.channel.channel_type === "chat" && !this.channel.localMessageUnreadCounter + ); }, }), /** @@ -194,7 +205,9 @@ Model({ */ isActive: attr({ compute() { - return this.messaging.discuss && this.thread === this.messaging.discuss.activeThread; + return ( + this.messaging.discuss && this.thread === this.messaging.discuss.activeThread + ); }, }), /** @@ -208,18 +221,19 @@ Model({ /** * The related thread. */ - thread: one('Thread', { related: 'channel.thread' }), - threadIconView: one('ThreadIconView', { inverse: 'discussSidebarCategoryItemOwner', + thread: one("Thread", { related: "channel.thread" }), + threadIconView: one("ThreadIconView", { + inverse: "discussSidebarCategoryItemOwner", compute() { if (!this.thread) { return clear(); } switch (this.channel.channel_type) { - case 'channel': + case "channel": return this.thread.authorizedGroupFullName ? clear() : {}; - case 'chat': + case "chat": return {}; - case 'group': + case "group": return clear(); } }, diff --git a/addons/mail/static/src/models/discuss_sidebar_mailbox_view.js b/addons/mail/static/src/models/discuss_sidebar_mailbox_view.js index 914cf72f03b..50dd1773c67 100644 --- a/addons/mail/static/src/models/discuss_sidebar_mailbox_view.js +++ b/addons/mail/static/src/models/discuss_sidebar_mailbox_view.js @@ -1,16 +1,23 @@ /** @odoo-module **/ -import { clear, one, Model } from '@mail/model'; +import { clear, one, Model } from "@mail/model"; Model({ - name: 'DiscussSidebarMailboxView', - template: 'mail.DiscussSidebarMailboxView', - identifyingMode: 'xor', + name: "DiscussSidebarMailboxView", + template: "mail.DiscussSidebarMailboxView", + identifyingMode: "xor", fields: { - discussViewOwnerAsHistory: one('DiscussView', { identifying: true, inverse: 'historyView' }), - discussViewOwnerAsInbox: one('DiscussView', { identifying: true, inverse: 'inboxView' }), - discussViewOwnerAsStarred: one('DiscussView', { identifying: true, inverse: 'starredView' }), - mailbox: one('Mailbox', { required: true, + discussViewOwnerAsHistory: one("DiscussView", { + identifying: true, + inverse: "historyView", + }), + discussViewOwnerAsInbox: one("DiscussView", { identifying: true, inverse: "inboxView" }), + discussViewOwnerAsStarred: one("DiscussView", { + identifying: true, + inverse: "starredView", + }), + mailbox: one("Mailbox", { + required: true, compute() { if (this.discussViewOwnerAsHistory) { return this.messaging.history; @@ -24,6 +31,9 @@ Model({ return clear(); }, }), - threadIconView: one('ThreadIconView', { default: {}, inverse: 'discussSidebarMailboxViewOwner' }), + threadIconView: one("ThreadIconView", { + default: {}, + inverse: "discussSidebarMailboxViewOwner", + }), }, }); diff --git a/addons/mail/static/src/models/discuss_sidebar_view.js b/addons/mail/static/src/models/discuss_sidebar_view.js index 7fd63d357bb..290dec3989d 100644 --- a/addons/mail/static/src/models/discuss_sidebar_view.js +++ b/addons/mail/static/src/models/discuss_sidebar_view.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, one, Model } from '@mail/model'; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'DiscussSidebarView', - template: 'mail.DiscussSidebarView', + name: "DiscussSidebarView", + template: "mail.DiscussSidebarView", componentSetup() { - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, recordMethods: { onComponentUpdate() { @@ -17,11 +17,11 @@ Model({ }, }, fields: { - owner: one('DiscussView', { identifying: true, inverse: 'sidebar' }), + owner: one("DiscussView", { identifying: true, inverse: "sidebar" }), /** * Reference to the quick search input. Useful to filter channels and * chats based on the content of the input. */ - quickSearchInputRef: attr({ ref: 'quickSearchInput' }), + quickSearchInputRef: attr({ ref: "quickSearchInput" }), }, }); diff --git a/addons/mail/static/src/models/discuss_view.js b/addons/mail/static/src/models/discuss_view.js index 6200b8359d6..e8c962702f9 100644 --- a/addons/mail/static/src/models/discuss_view.js +++ b/addons/mail/static/src/models/discuss_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'DiscussView', - template: 'mail.DiscussView', + name: "DiscussView", + template: "mail.DiscussView", recordMethods: { clearIsAddingItem() { this.update({ @@ -35,8 +35,8 @@ Model({ * @param {MouseEvent} ev */ async onClickStartAMeetingButton(ev) { - const meetingChannel = await this.messaging.models['Thread'].createGroupChat({ - default_display_mode: 'video_full_screen', + const meetingChannel = await this.messaging.models["Thread"].createGroupChat({ + default_display_mode: "video_full_screen", partners_to: [this.messaging.currentPartner.id], }); meetingChannel.toggleCall({ startWithVideo: true }); @@ -110,9 +110,15 @@ Model({ * Value that is used to create a channel from the sidebar. */ addingChannelValue: attr({ default: "" }), - discuss: one('Discuss', { identifying: true, inverse: 'discussView' }), - historyView: one('DiscussSidebarMailboxView', { default: {}, inverse: 'discussViewOwnerAsHistory' }), - inboxView: one('DiscussSidebarMailboxView', { default: {}, inverse: 'discussViewOwnerAsInbox' }), + discuss: one("Discuss", { identifying: true, inverse: "discussView" }), + historyView: one("DiscussSidebarMailboxView", { + default: {}, + inverse: "discussViewOwnerAsHistory", + }), + inboxView: one("DiscussSidebarMailboxView", { + default: {}, + inverse: "discussViewOwnerAsInbox", + }), /** * Determines whether current user is adding a channel from the sidebar. */ @@ -121,29 +127,33 @@ Model({ * Determines whether current user is adding a chat from the sidebar. */ isAddingChat: attr({ default: false }), - mobileAddItemHeaderAutocompleteInputView: one('AutocompleteInputView', { inverse: 'discussViewOwnerAsMobileAddItemHeader', + mobileAddItemHeaderAutocompleteInputView: one("AutocompleteInputView", { + inverse: "discussViewOwnerAsMobileAddItemHeader", + compute() { + if (this.messaging.device.isSmall && (this.isAddingChannel || this.isAddingChat)) { + return {}; + } + return clear(); + }, + }), + mobileMailboxSelectionView: one("DiscussMobileMailboxSelectionView", { + inverse: "owner", compute() { if ( this.messaging.device.isSmall && - (this.isAddingChannel || this.isAddingChat) + this.discuss.activeMobileNavbarTabId === "mailbox" ) { return {}; } return clear(); }, }), - mobileMailboxSelectionView: one('DiscussMobileMailboxSelectionView', { inverse: 'owner', - compute() { - if (this.messaging.device.isSmall && this.discuss.activeMobileNavbarTabId === 'mailbox') { - return {}; - } - return clear(); - }, + orderedMailboxes: many("Mailbox", { + related: "messaging.allMailboxes", + sort: [["smaller-first", "sequence"]], }), - orderedMailboxes: many('Mailbox', { related: 'messaging.allMailboxes', - sort: [['smaller-first', 'sequence']], - }), - sidebar: one('DiscussSidebarView', { inverse: 'owner', + sidebar: one("DiscussSidebarView", { + inverse: "owner", compute() { if (!this.messaging.device.isSmall) { return {}; @@ -151,12 +161,15 @@ Model({ return clear(); }, }), - starredView: one('DiscussSidebarMailboxView', { default: {}, inverse: 'discussViewOwnerAsStarred' }), + starredView: one("DiscussSidebarMailboxView", { + default: {}, + inverse: "discussViewOwnerAsStarred", + }), }, onChanges: [ { - dependencies: ['discuss.activeThread'], - methodName: '_onDiscussActiveThreadChanged', + dependencies: ["discuss.activeThread"], + methodName: "_onDiscussActiveThreadChanged", }, ], }); diff --git a/addons/mail/static/src/models/drop_zone_view.js b/addons/mail/static/src/models/drop_zone_view.js index ed074d38983..cf6c9d36fc3 100644 --- a/addons/mail/static/src/models/drop_zone_view.js +++ b/addons/mail/static/src/models/drop_zone_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, decrement, increment, one, Model } from '@mail/model'; +import { attr, decrement, increment, one, Model } from "@mail/model"; Model({ - name: 'DropZoneView', - template: 'mail.DropZoneView', - identifyingMode: 'xor', + name: "DropZoneView", + template: "mail.DropZoneView", + identifyingMode: "xor", recordMethods: { /** * Shows a visual drop effect when dragging inside the dropzone. @@ -44,7 +44,7 @@ Model({ */ onDragover(ev) { ev.preventDefault(); - ev.dataTransfer.dropEffect = 'copy'; + ev.dataTransfer.dropEffect = "copy"; }, /** * Removes the visual drop effect. @@ -82,17 +82,17 @@ Model({ _isDragSourceExternalFile(dataTransfer) { const dragDataType = dataTransfer.types; if (dragDataType.constructor === window.DOMStringList) { - return dragDataType.contains('Files'); + return dragDataType.contains("Files"); } if (dragDataType.constructor === Array) { - return dragDataType.includes('Files'); + return dragDataType.includes("Files"); } return false; }, }, fields: { - chatterOwner: one('Chatter', { identifying: true, inverse: 'dropZoneView' }), - composerViewOwner: one('ComposerView', { identifying: true, inverse: 'dropZoneView' }), + chatterOwner: one("Chatter", { identifying: true, inverse: "dropZoneView" }), + composerViewOwner: one("ComposerView", { identifying: true, inverse: "dropZoneView" }), /** * Counts how many drag enter/leave happened on self and children. This * ensures the drop effect stays active when dragging over a child. diff --git a/addons/mail/static/src/models/emoji.js b/addons/mail/static/src/models/emoji.js index cc0755deea0..b37645dea92 100644 --- a/addons/mail/static/src/models/emoji.js +++ b/addons/mail/static/src/models/emoji.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'Emoji', + name: "Emoji", recordMethods: { /** * Compares two strings @@ -30,8 +30,9 @@ Model({ * @returns {boolean} */ _isStringInEmojiKeywords(string) { - for (let index in this.searchData) { - if (this._fuzzySearch(this.searchData[index], string)) { //If at least one correspondence is found, return true. + for (const index in this.searchData) { + if (this._fuzzySearch(this.searchData[index], string)) { + //If at least one correspondence is found, return true. return true; } } @@ -39,13 +40,15 @@ Model({ }, }, fields: { - allEmojiInCategoryOfCurrent: many('EmojiInCategory', { inverse: 'emoji', + allEmojiInCategoryOfCurrent: many("EmojiInCategory", { + inverse: "emoji", compute() { - return this.emojiCategories.map(category => ({ category })); + return this.emojiCategories.map((category) => ({ category })); }, }), codepoints: attr({ identifying: true }), - emojiCategories: many('EmojiCategory', { inverse: 'allEmojis', + emojiCategories: many("EmojiCategory", { + inverse: "allEmojis", compute() { if (!this.emojiRegistry) { return clear(); @@ -53,9 +56,11 @@ Model({ return [this.emojiDataCategory]; }, }), - emojiDataCategory: one('EmojiCategory'), - emojiOrEmojiInCategory: many('EmojiOrEmojiInCategory', { inverse: 'emoji' }), - emojiRegistry: one('EmojiRegistry', { inverse: 'allEmojis', required: true, + emojiDataCategory: one("EmojiCategory"), + emojiOrEmojiInCategory: many("EmojiOrEmojiInCategory", { inverse: "emoji" }), + emojiRegistry: one("EmojiRegistry", { + inverse: "allEmojis", + required: true, compute() { if (!this.messaging) { return clear(); @@ -63,7 +68,7 @@ Model({ return this.messaging.emojiRegistry; }, }), - emojiViews: many('EmojiView', { inverse: 'emoji', readonly: true }), + emojiViews: many("EmojiView", { inverse: "emoji", readonly: true }), emoticons: attr(), keywords: attr(), name: attr({ readonly: true }), diff --git a/addons/mail/static/src/models/emoji_category.js b/addons/mail/static/src/models/emoji_category.js index bb497faf808..be6cad1bc22 100644 --- a/addons/mail/static/src/models/emoji_category.js +++ b/addons/mail/static/src/models/emoji_category.js @@ -1,14 +1,16 @@ /** @odoo-module **/ -import { attr, many, one, Model } from '@mail/model'; +import { attr, many, one, Model } from "@mail/model"; Model({ - name: 'EmojiCategory', + name: "EmojiCategory", fields: { - allEmojiInCategoryOfCurrent: many('EmojiInCategory', { inverse: 'category' }), - allEmojiPickerViewCategory: many('EmojiPickerView.Category', { inverse: 'category' }), - allEmojis: many('Emoji', { inverse: 'emojiCategories' }), - emojiRegistry: one("EmojiRegistry", { inverse: "allCategories", required: true, + allEmojiInCategoryOfCurrent: many("EmojiInCategory", { inverse: "category" }), + allEmojiPickerViewCategory: many("EmojiPickerView.Category", { inverse: "category" }), + allEmojis: many("Emoji", { inverse: "emojiCategories" }), + emojiRegistry: one("EmojiRegistry", { + inverse: "allCategories", + required: true, compute() { return this.messaging.emojiRegistry; }, diff --git a/addons/mail/static/src/models/emoji_category_view.js b/addons/mail/static/src/models/emoji_category_view.js index e4c1d2fc418..45fa9b55376 100644 --- a/addons/mail/static/src/models/emoji_category_view.js +++ b/addons/mail/static/src/models/emoji_category_view.js @@ -1,17 +1,19 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'EmojiCategoryView', - template: 'mail.EmojiCategoryView', + name: "EmojiCategoryView", + template: "mail.EmojiCategoryView", recordMethods: { /** * @param {MouseEvent} ev */ onClick() { this.emojiPickerViewOwner.reset(); - this.emojiPickerViewOwner.emojiGridView.update({ categorySelectedByUser: this.viewCategory }); + this.emojiPickerViewOwner.emojiGridView.update({ + categorySelectedByUser: this.viewCategory, + }); }, /** * @param {MouseEvent} ev @@ -33,14 +35,20 @@ Model({ }, }, fields: { - category: one('EmojiCategory', { related: 'viewCategory.category' }), - emojiPickerViewOwner: one('EmojiPickerView', { identifying: true, inverse: 'emojiCategoryViews' }), + category: one("EmojiCategory", { related: "viewCategory.category" }), + emojiPickerViewOwner: one("EmojiPickerView", { + identifying: true, + inverse: "emojiCategoryViews", + }), isActive: attr({ compute() { return Boolean(this.viewCategory.emojiPickerViewAsActive); }, }), isHovered: attr({ default: false }), - viewCategory: one('EmojiPickerView.Category', { identifying: true, inverse: 'emojiCategoryView' }), + viewCategory: one("EmojiPickerView.Category", { + identifying: true, + inverse: "emojiCategoryView", + }), }, }); diff --git a/addons/mail/static/src/models/emoji_grid_row_view.js b/addons/mail/static/src/models/emoji_grid_row_view.js index 399602423f2..4d37a4df9f2 100644 --- a/addons/mail/static/src/models/emoji_grid_row_view.js +++ b/addons/mail/static/src/models/emoji_grid_row_view.js @@ -1,14 +1,17 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'EmojiGridRowView', - template: 'mail.EmojiGridRowView', + name: "EmojiGridRowView", + template: "mail.EmojiGridRowView", fields: { - category: one('EmojiCategory', { related: 'viewCategory.category' }), - emojiGridViewOwner: one('EmojiGridView', { related: 'emojiGridViewRowRegistryOwner.emojiGridViewOwner' }), - hasSection: attr({ default: false, + category: one("EmojiCategory", { related: "viewCategory.category" }), + emojiGridViewOwner: one("EmojiGridView", { + related: "emojiGridViewRowRegistryOwner.emojiGridViewOwner", + }), + hasSection: attr({ + default: false, compute() { if (this.viewCategory) { return true; @@ -17,8 +20,11 @@ Model({ }, }), index: attr({ identifying: true }), - items: many('EmojiView', { inverse: 'emojiGridRowViewOwner' }), - emojiGridViewRowRegistryOwner: one('EmojiGridViewRowRegistry', { identifying: true, inverse: 'rows' }), - viewCategory: one('EmojiPickerView.Category', { inverse: 'emojiGridRowView' }), + items: many("EmojiView", { inverse: "emojiGridRowViewOwner" }), + emojiGridViewRowRegistryOwner: one("EmojiGridViewRowRegistry", { + identifying: true, + inverse: "rows", + }), + viewCategory: one("EmojiPickerView.Category", { inverse: "emojiGridRowView" }), }, }); diff --git a/addons/mail/static/src/models/emoji_grid_view.js b/addons/mail/static/src/models/emoji_grid_view.js index 67d5a6dca16..b628a9f38f9 100644 --- a/addons/mail/static/src/models/emoji_grid_view.js +++ b/addons/mail/static/src/models/emoji_grid_view.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, increment, many, one, Model } from '@mail/model'; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, increment, many, one, Model } from "@mail/model"; Model({ - name: 'EmojiGridView', - template: 'mail.EmojiGridView', + name: "EmojiGridView", + template: "mail.EmojiGridView", componentSetup() { - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, recordMethods: { doJumpToCategorySelectedByUser() { @@ -20,10 +20,7 @@ Model({ * Handles OWL update on component update. */ onComponentUpdate() { - if ( - this.categorySelectedByUser && - this.emojiPickerViewOwner.currentSearch === "" - ) { + if (this.categorySelectedByUser && this.emojiPickerViewOwner.currentSearch === "") { this.doJumpToCategorySelectedByUser(); } }, @@ -38,9 +35,12 @@ Model({ if ( viewCategory.emojiGridRowView && this.scrollIndex >= viewCategory.emojiGridRowView.index && - (viewCategory.emojiPickerViewOwnerAsLastCategory || this.scrollIndex <= viewCategory.endSectionIndex) + (viewCategory.emojiPickerViewOwnerAsLastCategory || + this.scrollIndex <= viewCategory.endSectionIndex) ) { - this.emojiPickerViewOwner.update({ activeCategoryByGridViewScroll: viewCategory }); + this.emojiPickerViewOwner.update({ + activeCategoryByGridViewScroll: viewCategory, + }); break; } } @@ -51,47 +51,51 @@ Model({ * Filters emoji according to the current search terms. */ _filterEmoji(emoji) { - return (emoji._isStringInEmojiKeywords(this.emojiPickerViewOwner.currentSearch)); + return emoji._isStringInEmojiKeywords(this.emojiPickerViewOwner.currentSearch); }, }, fields: { amountOfItemsPerRow: attr({ default: 9 }), - categorySelectedByUser: one('EmojiPickerView.Category'), - containerRef: attr({ ref: 'containerRef' }), + categorySelectedByUser: one("EmojiPickerView.Category"), + containerRef: attr({ ref: "containerRef" }), /** * Distance of the rendered rows from top. * This is from the PoV of 1st rendered row, including extra rendered rows! */ - distanceFromTop: attr({ default: 0, + distanceFromTop: attr({ + default: 0, compute() { this.scrollRecomputeCount; // observe scroll changes if (!this.listRef || !this.listRef.el) { return clear(); } return Math.max( - (this.scrollPercentage * this.listRef.el.clientHeight - this.extraRenderRowsAmount * this.rowHeight), - 0, + this.scrollPercentage * this.listRef.el.clientHeight - + this.extraRenderRowsAmount * this.rowHeight, + 0 ); }, }), - distanceInRowOffset: attr({ default: 0, + distanceInRowOffset: attr({ + default: 0, compute() { return this.distanceFromTop % this.rowHeight; }, }), - emojiPickerViewOwner: one('EmojiPickerView', { identifying: true, inverse: 'emojiGridView' }), + emojiPickerViewOwner: one("EmojiPickerView", { + identifying: true, + inverse: "emojiGridView", + }), /** * Extra rows above and below the visible part. * 10 means 10 rows above and 10 rows below. */ extraRenderRowsAmount: attr({ default: 10 }), - firstRenderedRowIndex: attr({ default: 0, + firstRenderedRowIndex: attr({ + default: 0, compute() { this.scrollRecomputeCount; // observe scroll changes - return Math.max( - this.scrollIndex - this.extraRenderRowsAmount, - 0, - ); + return Math.max(this.scrollIndex - this.extraRenderRowsAmount, 0); }, }), hasNoSearchResult: attr({ @@ -108,9 +112,10 @@ Model({ return this.rowHeight * 9.5; }, }), - hoveredEmojiView: one('EmojiView', { inverse: 'emojiGridViewAsHovered' }), + hoveredEmojiView: one("EmojiView", { inverse: "emojiGridViewAsHovered" }), itemWidth: attr({ default: 30 }), - lastRenderedRowIndex: attr({ default: 0, + lastRenderedRowIndex: attr({ + default: 0, compute() { this.scrollRecomputeCount; // observe scroll changes let value; @@ -122,24 +127,29 @@ Model({ return Math.ceil(value); }, }), - listHeight: attr({ default: 0, + listHeight: attr({ + default: 0, compute() { return this.rowHeight * this.rows.length; }, }), - listRef: attr({ ref: 'listRef' }), + listRef: attr({ ref: "listRef" }), loadingText: attr({ compute() { return this.env._t("Loading..."); }, }), - nonSearchRowRegistry: one('EmojiGridViewRowRegistry', { default: {}, inverse: 'emojiGridViewOwnerAsNonSearch' }), + nonSearchRowRegistry: one("EmojiGridViewRowRegistry", { + default: {}, + inverse: "emojiGridViewOwnerAsNonSearch", + }), noSearchResultText: attr({ compute() { return this.env._t("No emoji match your search"); }, }), - onScrollThrottle: one('Throttle', { inverse: 'emojiGridViewAsOnScroll', + onScrollThrottle: one("Throttle", { + inverse: "emojiGridViewAsOnScroll", compute() { return { func: () => this.update({ scrollRecomputeCount: increment() }) }; }, @@ -149,7 +159,7 @@ Model({ return this.extraRenderRowsAmount * 2 + Math.ceil(this.visibleMaxAmount); }, }), - renderedRows: many('EmojiGridRowView', { + renderedRows: many("EmojiGridRowView", { compute() { if (this.lastRenderedRowIndex + 1 - this.firstRenderedRowIndex < 0) { return clear(); @@ -157,16 +167,17 @@ Model({ if (this.rows.length === 0) { return clear(); } - return ( - [...Array(this.lastRenderedRowIndex + 1 - this.firstRenderedRowIndex).keys()] - .map(relativeRowIndex => this.rows[this.firstRenderedRowIndex + relativeRowIndex]) - .filter(row => row !== undefined) // some corner cases where very briefly it doesn't sync with rows and it's bigger - ); + return [...Array(this.lastRenderedRowIndex + 1 - this.firstRenderedRowIndex).keys()] + .map( + (relativeRowIndex) => + this.rows[this.firstRenderedRowIndex + relativeRowIndex] + ) + .filter((row) => row !== undefined); // some corner cases where very briefly it doesn't sync with rows and it's bigger }, - sort: [['smaller-first', 'index']], + sort: [["smaller-first", "index"]], }), rowHeight: attr({ default: 30 }), - rows: many('EmojiGridRowView', { + rows: many("EmojiGridRowView", { compute() { if (this.emojiPickerViewOwner.currentSearch !== "") { return this.searchRowRegistry.rows; @@ -177,7 +188,8 @@ Model({ /** * Scroll index of the 1st visible rendered rows (so excluding the extra rendered rendered rows). */ - scrollIndex: attr({ default: 0, + scrollIndex: attr({ + default: 0, compute() { this.scrollRecomputeCount; // observe scroll changes return Math.floor(this.scrollPercentage * this.rows.length); @@ -186,7 +198,8 @@ Model({ /** * Scroll percentage of the 1st visible rendered rows. */ - scrollPercentage: attr({ default: 0, + scrollPercentage: attr({ + default: 0, compute() { this.scrollRecomputeCount; // observe scroll changes if (!this.containerRef || !this.containerRef.el) { @@ -197,8 +210,11 @@ Model({ }), scrollbarThresholdWidth: attr({ default: 15 }), scrollRecomputeCount: attr({ default: 0 }), - searchRowRegistry: one('EmojiGridViewRowRegistry', { default: {}, inverse: 'emojiGridViewOwnerAsSearch' }), - viewBlockRef: attr({ ref: 'viewBlockRef' }), + searchRowRegistry: one("EmojiGridViewRowRegistry", { + default: {}, + inverse: "emojiGridViewOwnerAsSearch", + }), + viewBlockRef: attr({ ref: "viewBlockRef" }), /** * Amount of emoji that are visibly rendered in emoji grid. * Decimal determines the partial visibility of the last emoji. @@ -213,8 +229,8 @@ Model({ }, onChanges: [ { - dependencies: ['scrollRecomputeCount'], - methodName: '_onChangeScrollRecomputeCount', + dependencies: ["scrollRecomputeCount"], + methodName: "_onChangeScrollRecomputeCount", }, ], }); diff --git a/addons/mail/static/src/models/emoji_grid_view_row_registry.js b/addons/mail/static/src/models/emoji_grid_view_row_registry.js index abb80818386..153e2f6c6a8 100644 --- a/addons/mail/static/src/models/emoji_grid_view_row_registry.js +++ b/addons/mail/static/src/models/emoji_grid_view_row_registry.js @@ -1,19 +1,19 @@ /** @odoo-module **/ -import { clear, many, one, Model } from '@mail/model'; +import { clear, many, one, Model } from "@mail/model"; Model({ - name: 'EmojiGridViewRowRegistry', - identifyingMode: 'xor', + name: "EmojiGridViewRowRegistry", + identifyingMode: "xor", recordMethods: { computeNonSearchRows() { const value = []; let index = 0; - for (let viewCategory of this.emojiGridViewOwner.emojiPickerViewOwner.categories) { + for (const viewCategory of this.emojiGridViewOwner.emojiPickerViewOwner.categories) { value.push({ viewCategory, index }); index++; let currentItems = []; - for (let emojiInCategory of viewCategory.category.allEmojiInCategoryOfCurrent) { + for (const emojiInCategory of viewCategory.category.allEmojiInCategoryOfCurrent) { currentItems.push({ emojiOrEmojiInCategory: { emojiInCategory } }); if (currentItems.length === this.emojiGridViewOwner.amountOfItemsPerRow) { value.push({ items: currentItems, index }); @@ -33,11 +33,13 @@ Model({ if (this.emojiGridViewOwner.emojiPickerViewOwner.currentSearch === "") { return clear(); } - const emojis = this.messaging.emojiRegistry.allEmojis.filter(this.emojiGridViewOwner._filterEmoji); + const emojis = this.messaging.emojiRegistry.allEmojis.filter( + this.emojiGridViewOwner._filterEmoji + ); const value = []; let index = 0; let currentItems = []; - for (let emoji of emojis) { + for (const emoji of emojis) { currentItems.push({ emojiOrEmojiInCategory: { emoji } }); if (currentItems.length === this.emojiGridViewOwner.amountOfItemsPerRow) { value.push({ items: currentItems, index }); @@ -54,7 +56,8 @@ Model({ }, }, fields: { - rows: many('EmojiGridRowView', { inverse: 'emojiGridViewRowRegistryOwner', + rows: many("EmojiGridRowView", { + inverse: "emojiGridViewRowRegistryOwner", compute() { if (!this.emojiGridViewOwner) { return clear(); @@ -67,14 +70,20 @@ Model({ } return clear(); }, - sort: [['smaller-first', 'index']], + sort: [["smaller-first", "index"]], }), - emojiGridViewOwner: one('EmojiGridView', { + emojiGridViewOwner: one("EmojiGridView", { compute() { return this.emojiGridViewOwnerAsNonSearch || this.emojiGridViewOwnerAsSearch; }, }), - emojiGridViewOwnerAsNonSearch: one('EmojiGridView', { identifying: true, inverse: 'nonSearchRowRegistry' }), - emojiGridViewOwnerAsSearch: one('EmojiGridView', { identifying: true, inverse: 'searchRowRegistry' }), + emojiGridViewOwnerAsNonSearch: one("EmojiGridView", { + identifying: true, + inverse: "nonSearchRowRegistry", + }), + emojiGridViewOwnerAsSearch: one("EmojiGridView", { + identifying: true, + inverse: "searchRowRegistry", + }), }, }); diff --git a/addons/mail/static/src/models/emoji_in_category.js b/addons/mail/static/src/models/emoji_in_category.js index 6db2e0d59c1..fe95e3b520e 100644 --- a/addons/mail/static/src/models/emoji_in_category.js +++ b/addons/mail/static/src/models/emoji_in_category.js @@ -1,12 +1,15 @@ /** @odoo-module **/ -import { many, one, Model } from '@mail/model'; +import { many, one, Model } from "@mail/model"; Model({ - name: 'EmojiInCategory', + name: "EmojiInCategory", fields: { - category: one('EmojiCategory', { identifying: true, inverse: 'allEmojiInCategoryOfCurrent' }), - emoji: one('Emoji', { identifying: true, inverse: 'allEmojiInCategoryOfCurrent' }), - emojiOrEmojiInCategory: many('EmojiOrEmojiInCategory', { inverse: 'emojiInCategory' }), + category: one("EmojiCategory", { + identifying: true, + inverse: "allEmojiInCategoryOfCurrent", + }), + emoji: one("Emoji", { identifying: true, inverse: "allEmojiInCategoryOfCurrent" }), + emojiOrEmojiInCategory: many("EmojiOrEmojiInCategory", { inverse: "emojiInCategory" }), }, }); diff --git a/addons/mail/static/src/models/emoji_or_emoji_in_category.js b/addons/mail/static/src/models/emoji_or_emoji_in_category.js index 1e03cdabe7b..e48bf26240d 100644 --- a/addons/mail/static/src/models/emoji_or_emoji_in_category.js +++ b/addons/mail/static/src/models/emoji_or_emoji_in_category.js @@ -1,13 +1,16 @@ /** @odoo-module **/ -import { many, one, Model } from '@mail/model'; +import { many, one, Model } from "@mail/model"; Model({ - name: 'EmojiOrEmojiInCategory', - identifyingMode: 'xor', + name: "EmojiOrEmojiInCategory", + identifyingMode: "xor", fields: { - emoji: one('Emoji', { identifying: true, inverse: 'emojiOrEmojiInCategory' }), - emojiInCategory: one('EmojiInCategory', { identifying: true, inverse: 'emojiOrEmojiInCategory' }), - emojiViews: many('EmojiView', { inverse: 'emojiOrEmojiInCategory' }), + emoji: one("Emoji", { identifying: true, inverse: "emojiOrEmojiInCategory" }), + emojiInCategory: one("EmojiInCategory", { + identifying: true, + inverse: "emojiOrEmojiInCategory", + }), + emojiViews: many("EmojiView", { inverse: "emojiOrEmojiInCategory" }), }, }); diff --git a/addons/mail/static/src/models/emoji_picker_header_action_view.js b/addons/mail/static/src/models/emoji_picker_header_action_view.js index 2ba82a06bf3..1d569a9818e 100644 --- a/addons/mail/static/src/models/emoji_picker_header_action_view.js +++ b/addons/mail/static/src/models/emoji_picker_header_action_view.js @@ -1,25 +1,29 @@ /** @odoo-module **/ -import { attr, one, clear, Model } from '@mail/model'; +import { attr, one, clear, Model } from "@mail/model"; Model({ - name: 'EmojiPickerHeaderActionView', - template: 'mail.EmojiPickerHeaderActionView', - identifyingMode: 'xor', + name: "EmojiPickerHeaderActionView", + template: "mail.EmojiPickerHeaderActionView", + identifyingMode: "xor", fields: { // dummy identifying field, so that it works without defining one initially in mail - __ownerAsDummy: one('EmojiPickerView', { identifying: true, inverse: '__dummyActionView', }), - content: one('Record', { required: true, + __ownerAsDummy: one("EmojiPickerView", { identifying: true, inverse: "__dummyActionView" }), + content: one("Record", { + required: true, compute() { return clear(); }, }), - contentComponentName: attr({ required: true, + contentComponentName: attr({ + required: true, compute() { return clear(); }, }), - owner: one('EmojiPickerView', { inverse: 'actionViews', required: true, + owner: one("EmojiPickerView", { + inverse: "actionViews", + required: true, compute() { return clear(); }, diff --git a/addons/mail/static/src/models/emoji_picker_view.js b/addons/mail/static/src/models/emoji_picker_view.js index 8c2701cc841..421b3c63201 100644 --- a/addons/mail/static/src/models/emoji_picker_view.js +++ b/addons/mail/static/src/models/emoji_picker_view.js @@ -1,15 +1,15 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, many, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'EmojiPickerView', - template: 'mail.EmojiPickerView', + name: "EmojiPickerView", + template: "mail.EmojiPickerView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); - useUpdateToModel({ methodName: 'onComponentUpdate', modelName: 'EmojiPickerView' }); + useComponentToModel({ fieldName: "component" }); + useUpdateToModel({ methodName: "onComponentUpdate", modelName: "EmojiPickerView" }); }, lifecycleHooks: { _created() { @@ -71,13 +71,13 @@ Model({ }, }, fields: { - __dummyActionView: one('EmojiPickerHeaderActionView', { inverse: '__ownerAsDummy' }), - actionViews: many('EmojiPickerHeaderActionView', { - inverse: 'owner', - sort: [['smaller-first', 'sequence']], + __dummyActionView: one("EmojiPickerHeaderActionView", { inverse: "__ownerAsDummy" }), + actionViews: many("EmojiPickerHeaderActionView", { + inverse: "owner", + sort: [["smaller-first", "sequence"]], }), - activeCategoryByGridViewScroll: one('EmojiPickerView.Category'), - activeCategory: one('EmojiPickerView.Category', { + activeCategoryByGridViewScroll: one("EmojiPickerView.Category"), + activeCategory: one("EmojiPickerView.Category", { compute() { if (this.currentSearch !== "") { return clear(); @@ -90,16 +90,17 @@ Model({ } return clear(); }, - inverse: 'emojiPickerViewAsActive', + inverse: "emojiPickerViewAsActive", }), - categories: many('EmojiPickerView.Category', { inverse: 'emojiPickerViewOwner', + categories: many("EmojiPickerView.Category", { + inverse: "emojiPickerViewOwner", compute() { - return this.messaging.emojiRegistry.allCategories.map(category => ({ category })); + return this.messaging.emojiRegistry.allCategories.map((category) => ({ category })); }, }), component: attr(), currentSearch: attr({ default: "" }), - defaultActiveCategory: one('EmojiPickerView.Category', { + defaultActiveCategory: one("EmojiPickerView.Category", { compute() { if (this.categories.length === 0) { return clear(); @@ -107,20 +108,27 @@ Model({ return this.categories[0]; }, }), - emojiCategoryViews: many('EmojiCategoryView', { inverse: 'emojiPickerViewOwner', + emojiCategoryViews: many("EmojiCategoryView", { + inverse: "emojiPickerViewOwner", compute() { - return this.categories.map(category => ({ viewCategory: category })); + return this.categories.map((category) => ({ viewCategory: category })); }, }), - emojiGridView: one('EmojiGridView', { default: {}, inverse: 'emojiPickerViewOwner', readonly: true, required: true }), - inputRef: attr({ ref: 'input' }), + emojiGridView: one("EmojiGridView", { + default: {}, + inverse: "emojiPickerViewOwner", + readonly: true, + required: true, + }), + inputRef: attr({ ref: "input" }), isDoFocus: attr({ default: false }), isFocused: attr({ default: false }), - placeholder: attr({ required: true, + placeholder: attr({ + required: true, compute() { return this.env._t("Search an emoji"); }, }), - popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'emojiPickerView' }), + popoverViewOwner: one("PopoverView", { identifying: true, inverse: "emojiPickerView" }), }, }); diff --git a/addons/mail/static/src/models/emoji_picker_view__category.js b/addons/mail/static/src/models/emoji_picker_view__category.js index 6523554f3a2..ecccf601221 100644 --- a/addons/mail/static/src/models/emoji_picker_view__category.js +++ b/addons/mail/static/src/models/emoji_picker_view__category.js @@ -1,27 +1,35 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; /** * Emoji category info of a specific emoji picker view */ Model({ - name: 'EmojiPickerView.Category', + name: "EmojiPickerView.Category", fields: { - category: one('EmojiCategory', { identifying: true, inverse: 'allEmojiPickerViewCategory' }), - emojiPickerViewOwner: one('EmojiPickerView', { identifying: true, inverse: 'categories' }), - emojiPickerViewAsActive: one('EmojiPickerView', { inverse: 'activeCategory' }), - emojiCategoryView: one('EmojiCategoryView', { inverse: 'viewCategory' }), - emojiGridRowView: one('EmojiGridRowView', { inverse: 'viewCategory' }), - emojiPickerViewOwnerAsLastCategory: one('EmojiPickerView', { + category: one("EmojiCategory", { + identifying: true, + inverse: "allEmojiPickerViewCategory", + }), + emojiPickerViewOwner: one("EmojiPickerView", { identifying: true, inverse: "categories" }), + emojiPickerViewAsActive: one("EmojiPickerView", { inverse: "activeCategory" }), + emojiCategoryView: one("EmojiCategoryView", { inverse: "viewCategory" }), + emojiGridRowView: one("EmojiGridRowView", { inverse: "viewCategory" }), + emojiPickerViewOwnerAsLastCategory: one("EmojiPickerView", { compute() { - if (this.emojiPickerViewOwner.categories[this.emojiPickerViewOwner.categories.length - 1] === this) { + if ( + this.emojiPickerViewOwner.categories[ + this.emojiPickerViewOwner.categories.length - 1 + ] === this + ) { return this.emojiPickerViewOwner; } return clear(); }, }), - endSectionIndex: attr({ default: 0, + endSectionIndex: attr({ + default: 0, compute() { if (!this.nextViewCategory || !this.nextViewCategory.emojiGridRowView) { return clear(); @@ -29,9 +37,11 @@ Model({ return this.nextViewCategory.emojiGridRowView.index - 1; }, }), - nextViewCategory: one('EmojiPickerView.Category', { + nextViewCategory: one("EmojiPickerView.Category", { compute() { - const index = this.emojiPickerViewOwner.categories.findIndex(category => category === this); + const index = this.emojiPickerViewOwner.categories.findIndex( + (category) => category === this + ); if (index === -1) { return clear(); } diff --git a/addons/mail/static/src/models/emoji_registry.js b/addons/mail/static/src/models/emoji_registry.js index 72098b390c6..29d2bc96530 100644 --- a/addons/mail/static/src/models/emoji_registry.js +++ b/addons/mail/static/src/models/emoji_registry.js @@ -1,22 +1,24 @@ /** @odoo-module **/ -import { attr, insert, many, Model } from '@mail/model'; -import { getBundle, loadBundle } from '@web/core/assets'; +import { attr, insert, many, Model } from "@mail/model"; +import { getBundle, loadBundle } from "@web/core/assets"; Model({ - name: 'EmojiRegistry', + name: "EmojiRegistry", recordMethods: { async loadEmojiData() { this.update({ isLoading: true }); - await getBundle('mail.assets_model_data').then(loadBundle); - const { emojiCategoriesData, emojisData } = await odoo.runtimeImport("@mail/models_data/emoji_data"); + await getBundle("mail.assets_model_data").then(loadBundle); + const { emojiCategoriesData, emojisData } = await odoo.runtimeImport( + "@mail/models_data/emoji_data" + ); if (!this.exists()) { return; } this._populateFromEmojiData(emojiCategoriesData, emojisData); }, async _populateFromEmojiData(dataCategories, dataEmojis) { - dataCategories.map(category => { + dataCategories.map((category) => { this.update({ dataCategories: insert({ name: category.name, @@ -25,14 +27,16 @@ Model({ }), }); }); - this.models['Emoji'].insert(dataEmojis.map(emojiData => ({ - codepoints: emojiData.codepoints, - shortcodes: emojiData.shortcodes, - emoticons: emojiData.emoticons, - name: emojiData.name, - keywords: emojiData.keywords, - emojiDataCategory: { name: emojiData.category }, - }))); + this.models["Emoji"].insert( + dataEmojis.map((emojiData) => ({ + codepoints: emojiData.codepoints, + shortcodes: emojiData.shortcodes, + emoticons: emojiData.emoticons, + name: emojiData.name, + keywords: emojiData.keywords, + emojiDataCategory: { name: emojiData.category }, + })) + ); this.update({ isLoaded: true, isLoading: false, @@ -40,16 +44,18 @@ Model({ }, }, fields: { - allCategories: many('EmojiCategory', { inverse: 'emojiRegistry', + allCategories: many("EmojiCategory", { + inverse: "emojiRegistry", compute() { return this.dataCategories; }, - sort: [['smaller-first', 'sortId']], + sort: [["smaller-first", "sortId"]], }), - allEmojis: many('Emoji', { inverse: 'emojiRegistry', - sort: [['smaller-first', 'codepoints']], + allEmojis: many("Emoji", { + inverse: "emojiRegistry", + sort: [["smaller-first", "codepoints"]], }), - dataCategories: many('EmojiCategory'), + dataCategories: many("EmojiCategory"), isLoaded: attr({ default: false }), isLoading: attr({ default: false }), }, diff --git a/addons/mail/static/src/models/emoji_view.js b/addons/mail/static/src/models/emoji_view.js index 79eb8730b69..ba0980991ee 100644 --- a/addons/mail/static/src/models/emoji_view.js +++ b/addons/mail/static/src/models/emoji_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'EmojiView', - template: 'mail.EmojiView', + name: "EmojiView", + template: "mail.EmojiView", recordMethods: { /** * @param {MouseEvent} ev @@ -14,11 +14,15 @@ Model({ return; } if (this.emojiPickerViewOwner.popoverViewOwner.messageActionViewOwnerAsReaction) { - this.emojiPickerViewOwner.popoverViewOwner.messageActionViewOwnerAsReaction.onClickReaction(ev); + this.emojiPickerViewOwner.popoverViewOwner.messageActionViewOwnerAsReaction.onClickReaction( + ev + ); return; } if (this.emojiPickerViewOwner.popoverViewOwner.composerViewOwnerAsEmoji) { - this.emojiPickerViewOwner.popoverViewOwner.composerViewOwnerAsEmoji.onClickEmoji(ev); + this.emojiPickerViewOwner.popoverViewOwner.composerViewOwnerAsEmoji.onClickEmoji( + ev + ); return; } }, @@ -42,7 +46,8 @@ Model({ }, }, fields: { - emoji: one('Emoji', { inverse: 'emojiViews', + emoji: one("Emoji", { + inverse: "emojiViews", compute() { if (this.emojiOrEmojiInCategory.emoji) { return this.emojiOrEmojiInCategory.emoji; @@ -53,15 +58,19 @@ Model({ return clear(); }, }), - emojiGridViewAsHovered: one('EmojiGridView', { inverse: 'hoveredEmojiView' }), - emojiOrEmojiInCategory: one('EmojiOrEmojiInCategory', { identifying: true, inverse: 'emojiViews' }), - emojiGridRowViewOwner: one('EmojiGridRowView', { identifying: true, inverse: 'items' }), - emojiPickerViewOwner: one('EmojiPickerView', { + emojiGridViewAsHovered: one("EmojiGridView", { inverse: "hoveredEmojiView" }), + emojiOrEmojiInCategory: one("EmojiOrEmojiInCategory", { + identifying: true, + inverse: "emojiViews", + }), + emojiGridRowViewOwner: one("EmojiGridRowView", { identifying: true, inverse: "items" }), + emojiPickerViewOwner: one("EmojiPickerView", { compute() { return this.emojiGridRowViewOwner.emojiGridViewOwner.emojiPickerViewOwner; - } + }, }), - width: attr({ default: 0, + width: attr({ + default: 0, compute() { if (!this.emojiGridRowViewOwner.emojiGridViewOwner) { return clear(); diff --git a/addons/mail/static/src/models/file_uploader.js b/addons/mail/static/src/models/file_uploader.js index 66a7bbcd615..d8db5348c41 100644 --- a/addons/mail/static/src/models/file_uploader.js +++ b/addons/mail/static/src/models/file_uploader.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; -import core from 'web.core'; +import core from "web.core"; const getAttachmentNextTemporaryId = (function () { let tmpId = 0; @@ -13,8 +13,8 @@ const getAttachmentNextTemporaryId = (function () { })(); Model({ - name: 'FileUploader', - identifyingMode: 'xor', + name: "FileUploader", + identifyingMode: "xor", recordMethods: { openBrowserFileUploader() { this.fileInput.click(); @@ -41,9 +41,9 @@ Model({ if (this.chatterOwner && !this.chatterOwner.hasAttachmentBox) { this.chatterOwner.openAttachmentBox(); } - this.messaging.messagingBus.trigger('o-file-uploader-upload', { files }); + this.messaging.messagingBus.trigger("o-file-uploader-upload", { files }); // clear at the end because side-effect of emptying `files` - this.fileInput.value = ''; + this.fileInput.value = ""; }, /** * @private @@ -55,11 +55,11 @@ Model({ */ _createFormData({ composer, file, thread }) { const formData = new window.FormData(); - formData.append('csrf_token', core.csrf_token); - formData.append('is_pending', Boolean(composer)); - formData.append('thread_id', thread && thread.id); - formData.append('thread_model', thread && thread.model); - formData.append('ufile', file, file.name); + formData.append("csrf_token", core.csrf_token); + formData.append("is_pending", Boolean(composer)); + formData.append("thread_id", thread && thread.id); + formData.append("thread_model", thread && thread.model); + formData.append("ufile", file, file.name); return formData; }, /** @@ -73,14 +73,14 @@ Model({ _onAttachmentUploaded({ attachmentData, composer, thread }) { if (attachmentData.error || !attachmentData.id) { this.messaging.notify({ - type: 'danger', + type: "danger", message: attachmentData.error, }); return; } - return (composer || thread).messaging.models['Attachment'].insert({ + return (composer || thread).messaging.models["Attachment"].insert({ composer: composer, - originThread: (!composer && thread) ? thread : undefined, + originThread: !composer && thread ? thread : undefined, ...attachmentData, }); }, @@ -91,29 +91,31 @@ Model({ * @returns {Promise} */ async _performUpload({ files }) { - const reloadFunc = this.activityListViewItemOwner && this.activityListViewItemOwner.reloadFunc; - const webRecord = this.activityListViewItemOwner && this.activityListViewItemOwner.webRecord; + const reloadFunc = + this.activityListViewItemOwner && this.activityListViewItemOwner.reloadFunc; + const webRecord = + this.activityListViewItemOwner && this.activityListViewItemOwner.webRecord; const composer = this.composerView && this.composerView.composer; // save before async const thread = this.thread; // save before async - const chatter = ( - (this.chatterOwner) || - (this.activityView && this.activityView.chatterOwner) - ); // save before async - const activity = ( - this.activityView && this.activityView.activity || - this.activityListViewItemOwner && this.activityListViewItemOwner.activity - ); // save before async + const chatter = + this.chatterOwner || (this.activityView && this.activityView.chatterOwner); // save before async + const activity = + (this.activityView && this.activityView.activity) || + (this.activityListViewItemOwner && this.activityListViewItemOwner.activity); // save before async const uploadingAttachments = new Map(); for (const file of files) { - uploadingAttachments.set(file, this.messaging.models['Attachment'].insert({ - composer, - filename: file.name, - id: getAttachmentNextTemporaryId(), - isUploading: true, - mimetype: file.type, - name: file.name, - originThread: (!composer && thread) ? thread : undefined, - })); + uploadingAttachments.set( + file, + this.messaging.models["Attachment"].insert({ + composer, + filename: file.name, + id: getAttachmentNextTemporaryId(), + isUploading: true, + mimetype: file.type, + name: file.name, + originThread: !composer && thread ? thread : undefined, + }) + ); } const attachments = []; for (const file of files) { @@ -126,11 +128,14 @@ Model({ return; } try { - const response = await (composer || thread).messaging.browser.fetch('/mail/attachment/upload', { - method: 'POST', - body: this._createFormData({ composer, file, thread }), - signal: uploadingAttachment.uploadingAbortController.signal, - }); + const response = await (composer || thread).messaging.browser.fetch( + "/mail/attachment/upload", + { + method: "POST", + body: this._createFormData({ composer, file, thread }), + signal: uploadingAttachment.uploadingAbortController.signal, + } + ); const attachmentData = await response.json(); if (uploadingAttachment.exists()) { uploadingAttachment.delete(); @@ -138,10 +143,14 @@ Model({ if ((composer && !composer.exists()) || (thread && !thread.exists())) { return; } - const attachment = this._onAttachmentUploaded({ attachmentData, composer, thread }); + const attachment = this._onAttachmentUploaded({ + attachmentData, + composer, + thread, + }); attachments.push(attachment); } catch (e) { - if (e.name !== 'AbortError') { + if (e.name !== "AbortError") { throw e; } } @@ -161,10 +170,13 @@ Model({ }, }, fields: { - activityListViewItemOwner: one('ActivityListViewItem', { identifying: true, inverse: 'fileUploader' }), - activityView: one('ActivityView', { identifying: true, inverse: 'fileUploader' }), - chatterOwner: one('Chatter', { identifying: true, inverse: 'fileUploader' }), - composerView: one('ComposerView', { identifying: true, inverse: 'fileUploader' }), + activityListViewItemOwner: one("ActivityListViewItem", { + identifying: true, + inverse: "fileUploader", + }), + activityView: one("ActivityView", { identifying: true, inverse: "fileUploader" }), + chatterOwner: one("Chatter", { identifying: true, inverse: "fileUploader" }), + composerView: one("ComposerView", { identifying: true, inverse: "fileUploader" }), fileInput: attr({ /** * Create an HTML element that will serve as file input. @@ -172,14 +184,15 @@ Model({ * use to trigger the file browser and start the upload process. */ compute() { - const fileInput = document.createElement('input'); - fileInput.type = 'file'; + const fileInput = document.createElement("input"); + fileInput.type = "file"; fileInput.multiple = true; fileInput.onchange = this.onChangeAttachment; return fileInput; }, }), - thread: one('Thread', { required: true, + thread: one("Thread", { + required: true, compute() { if (this.activityView) { return this.activityView.activity.thread; @@ -195,6 +208,6 @@ Model({ } return clear(); }, - }) + }), }, }); diff --git a/addons/mail/static/src/models/follower.js b/addons/mail/static/src/models/follower.js index b4ec7089e4f..0bccdcaca3f 100644 --- a/addons/mail/static/src/models/follower.js +++ b/addons/mail/static/src/models/follower.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, insert, link, many, one, Model, unlink } from '@mail/model'; +import { attr, clear, insert, link, many, one, Model, unlink } from "@mail/model"; Model({ - name: 'Follower', + name: "Follower", modelMethods: { /** * @param {Object} data @@ -11,13 +11,13 @@ Model({ */ convertData(data) { const data2 = {}; - if ('id' in data) { + if ("id" in data) { data2.id = data.id; } - if ('is_active' in data) { + if ("is_active" in data) { data2.isActive = data.is_active; } - if ('partner_id' in data) { + if ("partner_id" in data) { if (!data.partner_id) { data2.partner = clear(); } else { @@ -58,11 +58,11 @@ Model({ const followedThread = this.followedThread; await this.messaging.rpc({ model: this.followedThread.model, - method: 'message_unsubscribe', - args: [[this.followedThread.id], partner_ids] + method: "message_unsubscribe", + args: [[this.followedThread.id], partner_ids], }); if (followedThread.exists()) { - followedThread.fetchData(['suggestedRecipients']); + followedThread.fetchData(["suggestedRecipients"]); } if (!this.exists()) { return; @@ -82,7 +82,7 @@ Model({ */ async showSubtypes() { const subtypesData = await this.messaging.rpc({ - route: '/mail/read_subscription_data', + route: "/mail/read_subscription_data", params: { follower_id: this.id }, }); if (!this.exists()) { @@ -90,8 +90,8 @@ Model({ } this.update({ subtypes: clear() }); for (const data of subtypesData) { - const subtype = this.messaging.models['FollowerSubtype'].insert( - this.messaging.models['FollowerSubtype'].convertData(data) + const subtype = this.messaging.models["FollowerSubtype"].insert( + this.messaging.models["FollowerSubtype"].convertData(data) ); this.update({ subtypes: link(subtype) }); if (data.followed) { @@ -118,14 +118,14 @@ Model({ this.remove(); } else { const kwargs = { - subtype_ids: this.selectedSubtypes.map(subtype => subtype.id), + subtype_ids: this.selectedSubtypes.map((subtype) => subtype.id), }; if (this.partner) { kwargs.partner_ids = [this.partner.id]; } await this.messaging.rpc({ model: this.followedThread.model, - method: 'message_subscribe', + method: "message_subscribe", args: [[this.followedThread.id]], kwargs, }); @@ -133,7 +133,7 @@ Model({ return; } this.messaging.notify({ - type: 'success', + type: "success", message: this.env._t("The subscription preferences were successfully applied."), }); } @@ -141,8 +141,9 @@ Model({ }, }, fields: { - followedThread: one('Thread', { inverse: 'followers' }), - followedThreadAsFollowerOfCurrentPartner: one('Thread', { inverse: 'followerOfCurrentPartner', + followedThread: one("Thread", { inverse: "followers" }), + followedThreadAsFollowerOfCurrentPartner: one("Thread", { + inverse: "followerOfCurrentPartner", compute() { if (!this.followedThread) { return clear(); @@ -156,8 +157,8 @@ Model({ return clear(); }, }), - followerSubtypeListDialog: one('Dialog', { inverse: 'followerOwnerAsSubtypeList' }), - followerViews: many('FollowerView', { inverse: 'follower' }), + followerSubtypeListDialog: one("Dialog", { inverse: "followerOwnerAsSubtypeList" }), + followerViews: many("FollowerView", { inverse: "follower" }), id: attr({ identifying: true }), isActive: attr({ default: true }), /** @@ -165,12 +166,16 @@ Model({ */ isEditable: attr({ compute() { - const hasWriteAccess = this.followedThread ? this.followedThread.hasWriteAccess : false; - return this.messaging.currentPartner === this.partner ? this.followedThread.hasReadAccess : hasWriteAccess; + const hasWriteAccess = this.followedThread + ? this.followedThread.hasWriteAccess + : false; + return this.messaging.currentPartner === this.partner + ? this.followedThread.hasReadAccess + : hasWriteAccess; }, }), - partner: one('Partner', { required: true }), - selectedSubtypes: many('FollowerSubtype'), - subtypes: many('FollowerSubtype'), + partner: one("Partner", { required: true }), + selectedSubtypes: many("FollowerSubtype"), + subtypes: many("FollowerSubtype"), }, }); diff --git a/addons/mail/static/src/models/follower_list_menu_view.js b/addons/mail/static/src/models/follower_list_menu_view.js index 6f9ed520310..6e9da08a468 100644 --- a/addons/mail/static/src/models/follower_list_menu_view.js +++ b/addons/mail/static/src/models/follower_list_menu_view.js @@ -1,16 +1,16 @@ /** @odoo-module **/ -import { attr, many, one, Model } from '@mail/model'; +import { attr, many, one, Model } from "@mail/model"; Model({ - name: 'FollowerListMenuView', - template: 'mail.FollowerListMenuView', + name: "FollowerListMenuView", + template: "mail.FollowerListMenuView", lifecycleHooks: { _created() { - document.addEventListener('click', this._onClickCaptureGlobal, true); + document.addEventListener("click", this._onClickCaptureGlobal, true); }, _willDelete() { - document.removeEventListener('click', this._onClickCaptureGlobal, true); + document.removeEventListener("click", this._onClickCaptureGlobal, true); }, }, recordMethods: { @@ -37,7 +37,7 @@ Model({ onKeydown(ev) { ev.stopPropagation(); switch (ev.key) { - case 'Escape': + case "Escape": ev.preventDefault(); this.hide(); break; @@ -54,23 +54,28 @@ Model({ return; } // since dropdown is conditionally shown based on state, dropdownRef can be null - if (this.dropdownRef && this.dropdownRef.el && !this.dropdownRef.el.contains(ev.target)) { + if ( + this.dropdownRef && + this.dropdownRef.el && + !this.dropdownRef.el.contains(ev.target) + ) { this.hide(); } }, }, fields: { - chatterOwner: one('Chatter', { identifying: true, inverse: 'followerListMenuView' }), - dropdownRef: attr({ ref: 'dropdown' }), - followerViews: many('FollowerView', { inverse: 'followerListMenuViewOwner', + chatterOwner: one("Chatter", { identifying: true, inverse: "followerListMenuView" }), + dropdownRef: attr({ ref: "dropdown" }), + followerViews: many("FollowerView", { + inverse: "followerListMenuViewOwner", compute() { - return this.chatterOwner.thread.followers.map(follower => ({ follower })); + return this.chatterOwner.thread.followers.map((follower) => ({ follower })); }, }), isDisabled: attr({ compute() { return !this.chatterOwner.hasReadAccess; - } + }, }), isDropdownOpen: attr({ default: false }), }, diff --git a/addons/mail/static/src/models/follower_subtype.js b/addons/mail/static/src/models/follower_subtype.js index fa9a6caf995..bd84fecf162 100644 --- a/addons/mail/static/src/models/follower_subtype.js +++ b/addons/mail/static/src/models/follower_subtype.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, many, Model } from '@mail/model'; +import { attr, many, Model } from "@mail/model"; Model({ - name: 'FollowerSubtype', + name: "FollowerSubtype", modelMethods: { /** * @param {Object} data @@ -11,32 +11,32 @@ Model({ */ convertData(data) { const data2 = {}; - if ('default' in data) { + if ("default" in data) { data2.isDefault = data.default; } - if ('id' in data) { + if ("id" in data) { data2.id = data.id; } - if ('internal' in data) { + if ("internal" in data) { data2.isInternal = data.internal; } - if ('name' in data) { + if ("name" in data) { data2.name = data.name; } - if ('parent_model' in data) { + if ("parent_model" in data) { data2.parentModel = data.parent_model; } - if ('res_model' in data) { + if ("res_model" in data) { data2.resModel = data.res_model; } - if ('sequence' in data) { + if ("sequence" in data) { data2.sequence = data.sequence; } return data2; }, }, fields: { - followerSubtypeViews: many('FollowerSubtypeView', { inverse: 'subtype' }), + followerSubtypeViews: many("FollowerSubtypeView", { inverse: "subtype" }), id: attr({ identifying: true }), isDefault: attr({ default: false }), isInternal: attr({ default: false }), diff --git a/addons/mail/static/src/models/follower_subtype_list.js b/addons/mail/static/src/models/follower_subtype_list.js index 4c46e17758a..4cd05f57229 100644 --- a/addons/mail/static/src/models/follower_subtype_list.js +++ b/addons/mail/static/src/models/follower_subtype_list.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { attr, clear, many, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'FollowerSubtypeList', - template: 'mail.FollowerSubtypeList', + name: "FollowerSubtypeList", + template: "mail.FollowerSubtypeList", componentSetup() { - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); }, recordMethods: { /** @@ -17,7 +17,9 @@ Model({ * @returns {boolean} */ containsElement(element) { - return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element)); + return Boolean( + this.component && this.component.root.el && this.component.root.el.contains(element) + ); }, /** * Called when clicking on apply button. @@ -44,23 +46,31 @@ Model({ /** * States the dialog displaying this follower subtype list. */ - dialogOwner: one('Dialog', { identifying: true, inverse: 'followerSubtypeList', isCausal: true }), - follower: one('Follower', { related: 'dialogOwner.followerOwnerAsSubtypeList', required: true }), - followerSubtypeViews: many('FollowerSubtypeView', { inverse: 'followerSubtypeListOwner', + dialogOwner: one("Dialog", { + identifying: true, + inverse: "followerSubtypeList", + isCausal: true, + }), + follower: one("Follower", { + related: "dialogOwner.followerOwnerAsSubtypeList", + required: true, + }), + followerSubtypeViews: many("FollowerSubtypeView", { + inverse: "followerSubtypeListOwner", compute() { if (this.follower.subtypes.length === 0) { return clear(); } - return this.follower.subtypes.map(subtype => ({ subtype })); + return this.follower.subtypes.map((subtype) => ({ subtype })); }, sort: [ - ['falsy-first', 'subtype.parentModel'], - ['case-insensitive-asc', 'subtype.parentModel'], - ['falsy-first', 'subtype.resModel'], - ['case-insensitive-asc', 'subtype.resModel'], - ['smaller-first', 'subtype.isInternal'], - ['smaller-first', 'subtype.sequence'], - ['smaller-first', 'subtype.id'], + ["falsy-first", "subtype.parentModel"], + ["case-insensitive-asc", "subtype.parentModel"], + ["falsy-first", "subtype.resModel"], + ["case-insensitive-asc", "subtype.resModel"], + ["smaller-first", "subtype.isInternal"], + ["smaller-first", "subtype.sequence"], + ["smaller-first", "subtype.id"], ], }), }, diff --git a/addons/mail/static/src/models/follower_subtype_view.js b/addons/mail/static/src/models/follower_subtype_view.js index 4bbfff25836..368a21a2405 100644 --- a/addons/mail/static/src/models/follower_subtype_view.js +++ b/addons/mail/static/src/models/follower_subtype_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'FollowerSubtypeView', - template: 'mail.FollowerSubtypeView', + name: "FollowerSubtypeView", + template: "mail.FollowerSubtypeView", recordMethods: { /** * Called when clicking on cancel button. @@ -20,8 +20,11 @@ Model({ }, }, fields: { - follower: one('Follower', { related: 'followerSubtypeListOwner.follower' }), - followerSubtypeListOwner: one('FollowerSubtypeList', { identifying: true, inverse: 'followerSubtypeViews' }), - subtype: one('FollowerSubtype', { identifying: true, inverse: 'followerSubtypeViews' }), + follower: one("Follower", { related: "followerSubtypeListOwner.follower" }), + followerSubtypeListOwner: one("FollowerSubtypeList", { + identifying: true, + inverse: "followerSubtypeViews", + }), + subtype: one("FollowerSubtype", { identifying: true, inverse: "followerSubtypeViews" }), }, }); diff --git a/addons/mail/static/src/models/follower_view.js b/addons/mail/static/src/models/follower_view.js index 5faa2d7114b..ec55de1e8da 100644 --- a/addons/mail/static/src/models/follower_view.js +++ b/addons/mail/static/src/models/follower_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'FollowerView', - template: 'mail.FollowerView', + name: "FollowerView", + template: "mail.FollowerView", recordMethods: { /** * @param {MouseEvent} ev @@ -30,13 +30,18 @@ Model({ const followerListMenuView = this.followerListMenuViewOwner; this.follower.remove(); if (followerListMenuView.chatterOwner) { - followerListMenuView.chatterOwner.reloadParentView({ fieldNames: ['message_follower_ids'] }); + followerListMenuView.chatterOwner.reloadParentView({ + fieldNames: ["message_follower_ids"], + }); } followerListMenuView.hide(); }, }, fields: { - follower: one('Follower', { identifying: true, inverse: 'followerViews' }), - followerListMenuViewOwner: one('FollowerListMenuView', { identifying: true, inverse: 'followerViews' }), + follower: one("Follower", { identifying: true, inverse: "followerViews" }), + followerListMenuViewOwner: one("FollowerListMenuView", { + identifying: true, + inverse: "followerViews", + }), }, }); diff --git a/addons/mail/static/src/models/guest.js b/addons/mail/static/src/models/guest.js index 888cea23e01..a711eaf87d0 100644 --- a/addons/mail/static/src/models/guest.js +++ b/addons/mail/static/src/models/guest.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, many, one, Model } from '@mail/model'; +import { attr, many, one, Model } from "@mail/model"; Model({ - name: 'Guest', + name: "Guest", modelMethods: { /** * @param {Object} param0 @@ -12,7 +12,7 @@ Model({ */ async performRpcGuestUpdateName({ id, name }) { await this.messaging.rpc({ - route: '/mail/guest/update_name', + route: "/mail/guest/update_name", params: { guest_id: id, name, @@ -21,7 +21,7 @@ Model({ }, }, fields: { - authoredMessages: many('Message', { inverse: 'guestAuthor' }), + authoredMessages: many("Message", { inverse: "guestAuthor" }), avatarUrl: attr({ compute() { return `/web/image/mail.guest/${this.id}/avatar_128?unique=${this.name}`; @@ -31,11 +31,11 @@ Model({ im_status: attr(), isOnline: attr({ compute() { - return ['online', 'away'].includes(this.im_status); + return ["online", "away"].includes(this.im_status); }, }), name: attr(), - persona: one('Persona', { default: {}, inverse: 'guest', readonly: true, required: true }), - volumeSetting: one('res.users.settings.volumes', { inverse: 'guest_id' }), + persona: one("Persona", { default: {}, inverse: "guest", readonly: true, required: true }), + volumeSetting: one("res.users.settings.volumes", { inverse: "guest_id" }), }, }); diff --git a/addons/mail/static/src/models/ir_model.js b/addons/mail/static/src/models/ir_model.js index 9f2ff89bca2..296eab21e71 100644 --- a/addons/mail/static/src/models/ir_model.js +++ b/addons/mail/static/src/models/ir_model.js @@ -1,19 +1,19 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'ir.model', + name: "ir.model", fields: { /** * Determines the name of the views that are available for this model. */ availableWebViews: attr({ compute() { - return ['kanban', 'list', 'form', 'activity']; + return ["kanban", "list", "form", "activity"]; }, }), - activityGroup: one('ActivityGroup', { inverse: 'irModel' }), + activityGroup: one("ActivityGroup", { inverse: "irModel" }), iconUrl: attr(), id: attr({ identifying: true }), model: attr({ required: true }), diff --git a/addons/mail/static/src/models/kanban_field_activity_view.js b/addons/mail/static/src/models/kanban_field_activity_view.js index dc8428ffa85..c1ce901eb90 100644 --- a/addons/mail/static/src/models/kanban_field_activity_view.js +++ b/addons/mail/static/src/models/kanban_field_activity_view.js @@ -1,14 +1,18 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'KanbanFieldActivityView', - template: 'mail.KanbanFieldActivityView', + name: "KanbanFieldActivityView", + template: "mail.KanbanFieldActivityView", fields: { - activityButtonView: one('ActivityButtonView', { default: {}, inverse: 'kanbanFieldActivityViewOwner', required: true }), + activityButtonView: one("ActivityButtonView", { + default: {}, + inverse: "kanbanFieldActivityViewOwner", + required: true, + }), id: attr({ identifying: true }), - thread: one('Thread', { required: true }), + thread: one("Thread", { required: true }), webRecord: attr({ required: true }), }, }); diff --git a/addons/mail/static/src/models/link_preview.js b/addons/mail/static/src/models/link_preview.js index 704958e6e08..0db717e4e5f 100644 --- a/addons/mail/static/src/models/link_preview.js +++ b/addons/mail/static/src/models/link_preview.js @@ -1,15 +1,18 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'LinkPreview', + name: "LinkPreview", recordMethods: { async remove() { - await this.messaging.rpc({ - route: '/mail/link_preview/delete', - params: { link_preview_id: this.id }, - }, { shadow: true }); + await this.messaging.rpc( + { + route: "/mail/link_preview/delete", + params: { link_preview_id: this.id }, + }, + { shadow: true } + ); }, }, fields: { @@ -26,14 +29,16 @@ Model({ return false; } return ( - (this.message.author && this.message.author === this.messaging.currentPartner) || - (this.message.guestAuthor && this.message.guestAuthor === this.messaging.currentGuest) + (this.message.author && + this.message.author === this.messaging.currentPartner) || + (this.message.guestAuthor && + this.message.guestAuthor === this.messaging.currentGuest) ); }, }), isImage: attr({ compute() { - return Boolean(this.image_mimetype || this.og_mimetype === 'image/gif'); + return Boolean(this.image_mimetype || this.og_mimetype === "image/gif"); }, }), isVideo: attr({ @@ -41,13 +46,13 @@ Model({ if (!this.og_type) { return clear(); } - return this.og_type.startsWith('video') && !this.isImage; + return this.og_type.startsWith("video") && !this.isImage; }, }), - linkPreviewCardView: many('LinkPreviewCardView', { inverse: 'linkPreview' }), - linkPreviewImageView: many('LinkPreviewImageView', { inverse: 'linkPreview' }), - linkPreviewVideoView: many('LinkPreviewVideoView', { inverse: 'linkPreview' }), - message: one('Message', { inverse: 'linkPreviews' }), + linkPreviewCardView: many("LinkPreviewCardView", { inverse: "linkPreview" }), + linkPreviewImageView: many("LinkPreviewImageView", { inverse: "linkPreview" }), + linkPreviewVideoView: many("LinkPreviewVideoView", { inverse: "linkPreview" }), + message: one("Message", { inverse: "linkPreviews" }), og_description: attr(), og_image: attr(), og_mimetype: attr(), diff --git a/addons/mail/static/src/models/link_preview_aside_view.js b/addons/mail/static/src/models/link_preview_aside_view.js index 47d5c4bfa77..4cc2de2f690 100644 --- a/addons/mail/static/src/models/link_preview_aside_view.js +++ b/addons/mail/static/src/models/link_preview_aside_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { clear, one, Model } from '@mail/model'; +import { clear, one, Model } from "@mail/model"; Model({ - name: 'LinkPreviewAsideView', - template: 'mail.LinkPreviewAsideView', - identifyingMode: 'xor', + name: "LinkPreviewAsideView", + template: "mail.LinkPreviewAsideView", + identifyingMode: "xor", recordMethods: { /** * Handles the click on delete link preview and open the confirm dialog. @@ -15,7 +15,7 @@ Model({ }, }, fields: { - linkPreview: one('LinkPreview', { + linkPreview: one("LinkPreview", { compute() { if (this.linkPreviewCardView) { return this.linkPreviewCardView.linkPreview; @@ -30,9 +30,21 @@ Model({ }, required: true, }), - linkPreviewCardView: one('LinkPreviewCardView', { identifying: true, inverse: 'linkPreviewAsideView' }), - linkPreviewDeleteConfirmDialog: one('Dialog', { inverse: 'linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm', isCausal: true }), - linkPreviewImageView: one('LinkPreviewImageView', { identifying: true, inverse: 'linkPreviewAsideView' }), - linkPreviewVideoView: one('LinkPreviewVideoView', { identifying: true, inverse: 'linkPreviewAsideView' }), + linkPreviewCardView: one("LinkPreviewCardView", { + identifying: true, + inverse: "linkPreviewAsideView", + }), + linkPreviewDeleteConfirmDialog: one("Dialog", { + inverse: "linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm", + isCausal: true, + }), + linkPreviewImageView: one("LinkPreviewImageView", { + identifying: true, + inverse: "linkPreviewAsideView", + }), + linkPreviewVideoView: one("LinkPreviewVideoView", { + identifying: true, + inverse: "linkPreviewAsideView", + }), }, }); diff --git a/addons/mail/static/src/models/link_preview_card_view.js b/addons/mail/static/src/models/link_preview_card_view.js index 10e22363ec6..aeb3a359d3b 100644 --- a/addons/mail/static/src/models/link_preview_card_view.js +++ b/addons/mail/static/src/models/link_preview_card_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'LinkPreviewCardView', - template: 'mail.LinkPreviewCardView', + name: "LinkPreviewCardView", + template: "mail.LinkPreviewCardView", recordMethods: { /** * Handles mouse enter event for the container of this element. @@ -27,8 +27,8 @@ Model({ }, fields: { isHovered: attr({ default: false }), - linkPreview: one('LinkPreview', { identifying: true, inverse: 'linkPreviewCardView' }), - linkPreviewAsideView: one('LinkPreviewAsideView', { + linkPreview: one("LinkPreview", { identifying: true, inverse: "linkPreviewCardView" }), + linkPreviewAsideView: one("LinkPreviewAsideView", { compute() { if (!this.linkPreview.isDeletable) { return clear(); @@ -36,13 +36,20 @@ Model({ if (this.messaging.device.isMobileDevice) { return {}; } - if (this.isHovered || (this.linkPreviewAsideView && this.linkPreviewAsideView.linkPreviewDeleteConfirmDialog)) { + if ( + this.isHovered || + (this.linkPreviewAsideView && + this.linkPreviewAsideView.linkPreviewDeleteConfirmDialog) + ) { return {}; } return clear(); }, - inverse: 'linkPreviewCardView', + inverse: "linkPreviewCardView", + }), + linkPreviewListViewOwner: one("LinkPreviewListView", { + identifying: true, + inverse: "linkPreviewAsCardViews", }), - linkPreviewListViewOwner: one('LinkPreviewListView', { identifying: true, inverse: 'linkPreviewAsCardViews' }), }, }); diff --git a/addons/mail/static/src/models/link_preview_delete_confirm_view.js b/addons/mail/static/src/models/link_preview_delete_confirm_view.js index 0fa9e962640..9e7d3d2cf9c 100644 --- a/addons/mail/static/src/models/link_preview_delete_confirm_view.js +++ b/addons/mail/static/src/models/link_preview_delete_confirm_view.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { attr, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'LinkPreviewDeleteConfirmView', - template: 'mail.LinkPreviewDeleteConfirmView', + name: "LinkPreviewDeleteConfirmView", + template: "mail.LinkPreviewDeleteConfirmView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); }, recordMethods: { /** @@ -17,7 +17,9 @@ Model({ * @returns {boolean} */ containsElement(element) { - return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element)); + return Boolean( + this.component && this.component.root.el && this.component.root.el.contains(element) + ); }, onClickCancel() { this.dialogOwner.delete(); @@ -28,10 +30,12 @@ Model({ }, fields: { component: attr(), - dialogOwner: one('Dialog', { identifying: true, inverse: 'linkPreviewDeleteConfirmView' }), - linkPreview: one('LinkPreview', { required: true, + dialogOwner: one("Dialog", { identifying: true, inverse: "linkPreviewDeleteConfirmView" }), + linkPreview: one("LinkPreview", { + required: true, compute() { - return this.dialogOwner.linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm.linkPreview; + return this.dialogOwner.linkPreviewAsideViewOwnerAsLinkPreviewDeleteConfirm + .linkPreview; }, }), }, diff --git a/addons/mail/static/src/models/link_preview_image_view.js b/addons/mail/static/src/models/link_preview_image_view.js index 5c13402d772..b98ca9fb7e9 100644 --- a/addons/mail/static/src/models/link_preview_image_view.js +++ b/addons/mail/static/src/models/link_preview_image_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'LinkPreviewImageView', - template: 'mail.LinkPreviewImageView', + name: "LinkPreviewImageView", + template: "mail.LinkPreviewImageView", recordMethods: { /** * Handles mouse enter event for the container of this element. @@ -28,12 +28,15 @@ Model({ fields: { imageUrl: attr({ compute() { - return this.linkPreview.og_image ? this.linkPreview.og_image : this.linkPreview.source_url; + return this.linkPreview.og_image + ? this.linkPreview.og_image + : this.linkPreview.source_url; }, }), isHovered: attr({ default: false }), - linkPreview: one('LinkPreview', { identifying: true, inverse: 'linkPreviewImageView' }), - linkPreviewAsideView: one('LinkPreviewAsideView', { inverse: 'linkPreviewImageView', + linkPreview: one("LinkPreview", { identifying: true, inverse: "linkPreviewImageView" }), + linkPreviewAsideView: one("LinkPreviewAsideView", { + inverse: "linkPreviewImageView", compute() { if (!this.linkPreview.isDeletable) { return clear(); @@ -41,12 +44,19 @@ Model({ if (this.messaging.device.isMobileDevice) { return {}; } - if (this.isHovered || (this.linkPreviewAsideView && this.linkPreviewAsideView.linkPreviewDeleteConfirmDialog)) { + if ( + this.isHovered || + (this.linkPreviewAsideView && + this.linkPreviewAsideView.linkPreviewDeleteConfirmDialog) + ) { return {}; } return clear(); }, }), - linkPreviewListViewOwner: one('LinkPreviewListView', { identifying: true, inverse: 'linkPreviewAsImageViews' }), + linkPreviewListViewOwner: one("LinkPreviewListView", { + identifying: true, + inverse: "linkPreviewAsImageViews", + }), }, }); diff --git a/addons/mail/static/src/models/link_preview_list_view.js b/addons/mail/static/src/models/link_preview_list_view.js index 2969470d93e..6408f5cfc02 100644 --- a/addons/mail/static/src/models/link_preview_list_view.js +++ b/addons/mail/static/src/models/link_preview_list_view.js @@ -1,29 +1,38 @@ /** @odoo-module **/ -import { many, one, Model } from '@mail/model'; +import { many, one, Model } from "@mail/model"; Model({ - name: 'LinkPreviewListView', - template: 'mail.LinkPreviewListView', + name: "LinkPreviewListView", + template: "mail.LinkPreviewListView", fields: { /** * Determines if we are in the ChatWindow view AND if the message is left aligned */ - linkPreviewAsCardViews: many('LinkPreviewCardView', { inverse: 'linkPreviewListViewOwner', + linkPreviewAsCardViews: many("LinkPreviewCardView", { + inverse: "linkPreviewListViewOwner", compute() { - return this.messageViewOwner.message.linkPreviews.filter(linkPreview => linkPreview.isCard).map(linkPreview => ({ linkPreview })); + return this.messageViewOwner.message.linkPreviews + .filter((linkPreview) => linkPreview.isCard) + .map((linkPreview) => ({ linkPreview })); }, }), - linkPreviewAsImageViews: many('LinkPreviewImageView', { inverse: 'linkPreviewListViewOwner', + linkPreviewAsImageViews: many("LinkPreviewImageView", { + inverse: "linkPreviewListViewOwner", compute() { - return this.messageViewOwner.message.linkPreviews.filter(linkPreview => linkPreview.isImage).map(linkPreview => ({ linkPreview })); + return this.messageViewOwner.message.linkPreviews + .filter((linkPreview) => linkPreview.isImage) + .map((linkPreview) => ({ linkPreview })); }, }), - linkPreviewAsVideoViews: many('LinkPreviewVideoView', { inverse: 'linkPreviewListViewOwner', + linkPreviewAsVideoViews: many("LinkPreviewVideoView", { + inverse: "linkPreviewListViewOwner", compute() { - return this.messageViewOwner.message.linkPreviews.filter(linkPreview => linkPreview.isVideo).map(linkPreview => ({ linkPreview })); + return this.messageViewOwner.message.linkPreviews + .filter((linkPreview) => linkPreview.isVideo) + .map((linkPreview) => ({ linkPreview })); }, }), - messageViewOwner: one('MessageView', { identifying: true, inverse: 'linkPreviewListView' }), + messageViewOwner: one("MessageView", { identifying: true, inverse: "linkPreviewListView" }), }, }); diff --git a/addons/mail/static/src/models/link_preview_video_view.js b/addons/mail/static/src/models/link_preview_video_view.js index 0f0316e8c05..0e5ace84679 100644 --- a/addons/mail/static/src/models/link_preview_video_view.js +++ b/addons/mail/static/src/models/link_preview_video_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'LinkPreviewVideoView', - template: 'mail.LinkPreviewVideoView', + name: "LinkPreviewVideoView", + template: "mail.LinkPreviewVideoView", recordMethods: { /** * Handles mouse enter event for the container of this element. @@ -27,8 +27,9 @@ Model({ }, fields: { isHovered: attr({ default: false }), - linkPreview: one('LinkPreview', { identifying: true, inverse: 'linkPreviewVideoView' }), - linkPreviewAsideView: one('LinkPreviewAsideView', { inverse: 'linkPreviewVideoView', + linkPreview: one("LinkPreview", { identifying: true, inverse: "linkPreviewVideoView" }), + linkPreviewAsideView: one("LinkPreviewAsideView", { + inverse: "linkPreviewVideoView", compute() { if (!this.linkPreview.isDeletable) { return clear(); @@ -36,12 +37,19 @@ Model({ if (this.messaging.device.isMobileDevice) { return {}; } - if (this.isHovered || (this.linkPreviewAsideView && this.linkPreviewAsideView.linkPreviewDeleteConfirmDialog)) { + if ( + this.isHovered || + (this.linkPreviewAsideView && + this.linkPreviewAsideView.linkPreviewDeleteConfirmDialog) + ) { return {}; } return clear(); }, }), - linkPreviewListViewOwner: one('LinkPreviewListView', { identifying: true, inverse: 'linkPreviewAsVideoViews' }), + linkPreviewListViewOwner: one("LinkPreviewListView", { + identifying: true, + inverse: "linkPreviewAsVideoViews", + }), }, }); diff --git a/addons/mail/static/src/models/list_field_activity_view.js b/addons/mail/static/src/models/list_field_activity_view.js index 34151bc89eb..20701528a39 100644 --- a/addons/mail/static/src/models/list_field_activity_view.js +++ b/addons/mail/static/src/models/list_field_activity_view.js @@ -1,14 +1,19 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'ListFieldActivityView', - template: 'mail.ListFieldActivityView', + name: "ListFieldActivityView", + template: "mail.ListFieldActivityView", fields: { - activityButtonView: one('ActivityButtonView', { default: {}, inverse: 'listFieldActivityViewOwner', required: true }), + activityButtonView: one("ActivityButtonView", { + default: {}, + inverse: "listFieldActivityViewOwner", + required: true, + }), id: attr({ identifying: true }), - summaryText: attr({ default: '', + summaryText: attr({ + default: "", compute() { if (this.thread.activities.length === 0) { return clear(); @@ -25,7 +30,7 @@ Model({ return clear(); }, }), - thread: one('Thread', { required: true }), + thread: one("Thread", { required: true }), webRecord: attr({ required: true }), }, }); diff --git a/addons/mail/static/src/models/mail_template.js b/addons/mail/static/src/models/mail_template.js index 05a21fff7f9..0168b6c9603 100644 --- a/addons/mail/static/src/models/mail_template.js +++ b/addons/mail/static/src/models/mail_template.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, many, Model } from '@mail/model'; +import { attr, many, Model } from "@mail/model"; Model({ - name: 'MailTemplate', + name: "MailTemplate", recordMethods: { /** * @param {Activity} activity @@ -11,10 +11,10 @@ Model({ preview(activity) { const action = { name: this.env._t("Compose Email"), - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - views: [[false, 'form']], - target: 'new', + type: "ir.actions.act_window", + res_model: "mail.compose.message", + views: [[false, "form"]], + target: "new", context: { default_res_id: activity.thread.id, default_model: activity.thread.model, @@ -23,10 +23,9 @@ Model({ force_email: true, }, }; - this.env.services.action.doAction( - action, - { onClose: () => activity.thread.fetchData(['attachments', 'messages']) } - ); + this.env.services.action.doAction(action, { + onClose: () => activity.thread.fetchData(["attachments", "messages"]), + }); }, /** * @param {Activity} activity @@ -35,16 +34,16 @@ Model({ const thread = activity.thread; await this.messaging.rpc({ model: activity.thread.model, - method: 'activity_send_mail', + method: "activity_send_mail", args: [[activity.thread.id], this.id], }); if (thread.exists()) { - thread.fetchData(['attachments', 'messages']); + thread.fetchData(["attachments", "messages"]); } }, }, fields: { - activities: many('Activity', { inverse: 'mailTemplates' }), + activities: many("Activity", { inverse: "mailTemplates" }), id: attr({ identifying: true }), name: attr(), }, diff --git a/addons/mail/static/src/models/mail_template_view.js b/addons/mail/static/src/models/mail_template_view.js index 872b9ffa285..7537004c2cd 100644 --- a/addons/mail/static/src/models/mail_template_view.js +++ b/addons/mail/static/src/models/mail_template_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'MailTemplateView', - template: 'mail.MailTemplateView', + name: "MailTemplateView", + template: "mail.MailTemplateView", recordMethods: { /** * @param {MouseEvent} ev @@ -30,7 +30,7 @@ Model({ }, }, fields: { - activity: one('Activity', { + activity: one("Activity", { compute() { if (this.activityViewOwner) { return this.activityViewOwner.activity; @@ -38,10 +38,10 @@ Model({ if (this.activityListViewItemOwner) { return this.activityListViewItemOwner.activity; } - } + }, }), - activityListViewItemOwner: one('ActivityListViewItem', { inverse: 'mailTemplateViews' }), - activityViewOwner: one('ActivityView', { inverse: 'mailTemplateViews' }), - mailTemplate: one('MailTemplate', { identifying: true }), + activityListViewItemOwner: one("ActivityListViewItem", { inverse: "mailTemplateViews" }), + activityViewOwner: one("ActivityView", { inverse: "mailTemplateViews" }), + mailTemplate: one("MailTemplate", { identifying: true }), }, }); diff --git a/addons/mail/static/src/models/mailbox.js b/addons/mail/static/src/models/mailbox.js index 511e7d0b243..5558b708c5d 100644 --- a/addons/mail/static/src/models/mailbox.js +++ b/addons/mail/static/src/models/mailbox.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'Mailbox', - identifyingMode: 'xor', + name: "Mailbox", + identifyingMode: "xor", recordMethods: { _onChangeCounter() { if (this !== this.messaging.inbox) { @@ -12,11 +12,12 @@ Model({ } if ( this.thread.threadViews.length > 0 && - this.previousValueOfInboxCounter > 0 && this.counter === 0 + this.previousValueOfInboxCounter > 0 && + this.counter === 0 ) { this.env.services.effect.add({ message: this.env._t("Congratulations, your inbox is empty!"), - type: 'rainbow_man', + type: "rainbow_man", }); } this.update({ previousValueOfInboxCounter: this.counter }); @@ -24,16 +25,18 @@ Model({ }, fields: { counter: attr({ default: 0 }), - discussMobileSelectionItems: many('DiscussMobileMailboxSelectionItemView', { inverse: 'mailbox' }), + discussMobileSelectionItems: many("DiscussMobileMailboxSelectionItemView", { + inverse: "mailbox", + }), fetchMessagesUrl: attr({ compute() { switch (this) { case this.messaging.history: - return '/mail/history/messages'; + return "/mail/history/messages"; case this.messaging.inbox: - return '/mail/inbox/messages'; + return "/mail/inbox/messages"; case this.messaging.starred: - return '/mail/starred/messages'; + return "/mail/starred/messages"; default: return clear(); } @@ -42,7 +45,8 @@ Model({ /** * Useful to fill its inverse `Messaging/allMailboxes`. */ - messagingAsAnyMailbox: one('Messaging', { inverse: 'allMailboxes', + messagingAsAnyMailbox: one("Messaging", { + inverse: "allMailboxes", compute() { if (!this.messaging) { return clear(); @@ -50,9 +54,9 @@ Model({ return this.messaging; }, }), - messagingAsHistory: one('Messaging', { identifying: true, inverse: 'history' }), - messagingAsInbox: one('Messaging', { identifying: true, inverse: 'inbox' }), - messagingAsStarred: one('Messaging', { identifying: true, inverse: 'starred' }), + messagingAsHistory: one("Messaging", { identifying: true, inverse: "history" }), + messagingAsInbox: one("Messaging", { identifying: true, inverse: "inbox" }), + messagingAsStarred: one("Messaging", { identifying: true, inverse: "starred" }), name: attr({ compute() { switch (this) { @@ -85,16 +89,17 @@ Model({ } }, }), - thread: one('Thread', { inverse: 'mailbox', + thread: one("Thread", { + inverse: "mailbox", compute() { const threadId = (() => { switch (this) { case this.messaging.history: - return 'history'; + return "history"; case this.messaging.inbox: - return 'inbox'; + return "inbox"; case this.messaging.starred: - return 'starred'; + return "starred"; } })(); if (!threadId) { @@ -102,15 +107,15 @@ Model({ } return { id: threadId, - model: 'mail.box', + model: "mail.box", }; }, }), }, onChanges: [ { - dependencies: ['counter'], - methodName: '_onChangeCounter', + dependencies: ["counter"], + methodName: "_onChangeCounter", }, ], }); diff --git a/addons/mail/static/src/models/media_stream.js b/addons/mail/static/src/models/media_stream.js index 950606ca9ea..20844dc5c7e 100644 --- a/addons/mail/static/src/models/media_stream.js +++ b/addons/mail/static/src/models/media_stream.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, Model } from '@mail/model'; +import { attr, Model } from "@mail/model"; Model({ - name: 'MediaStream', + name: "MediaStream", lifecycleHooks: { _willDelete() { for (const track of this.webMediaStream.getTracks()) { diff --git a/addons/mail/static/src/models/message.js b/addons/mail/static/src/models/message.js index 04df512350e..e8f42c0ac45 100644 --- a/addons/mail/static/src/models/message.js +++ b/addons/mail/static/src/models/message.js @@ -1,16 +1,16 @@ /** @odoo-module **/ -import { attr, clear, insert, many, one, Model } from '@mail/model'; -import { addLink, htmlToTextContentInline, parseAndTransform } from '@mail/js/utils'; +import { attr, clear, insert, many, one, Model } from "@mail/model"; +import { addLink, htmlToTextContentInline, parseAndTransform } from "@mail/js/utils"; -import { session } from '@web/session'; +import { session } from "@web/session"; -import { getLangDatetimeFormat, str_to_datetime } from 'web.time'; +import { getLangDatetimeFormat, str_to_datetime } from "web.time"; -import { markup } from '@odoo/owl'; +import { markup } from "@odoo/owl"; Model({ - name: 'Message', + name: "Message", modelMethods: { /** * @param {Object} data @@ -19,90 +19,98 @@ Model({ convertData(data) { const data2 = {}; data2.attachments = data.attachment_ids; - if ('author' in data) { + if ("author" in data) { data2.author = data.author; } - if ('body' in data) { + if ("body" in data) { data2.body = data.body; } - if ('date' in data && data.date) { + if ("date" in data && data.date) { data2.date = data.date; } - if ('email_from' in data) { + if ("email_from" in data) { data2.email_from = data.email_from; } - if ('guestAuthor' in data) { + if ("guestAuthor" in data) { data2.guestAuthor = data.guestAuthor; } - if ('history_partner_ids' in data && this.messaging.currentPartner) { - data2.isHistory = data.history_partner_ids.includes(this.messaging.currentPartner.id); + if ("history_partner_ids" in data && this.messaging.currentPartner) { + data2.isHistory = data.history_partner_ids.includes( + this.messaging.currentPartner.id + ); } - if ('id' in data) { + if ("id" in data) { data2.id = data.id; } - if ('is_discussion' in data) { + if ("is_discussion" in data) { data2.is_discussion = data.is_discussion; } - if ('is_note' in data) { + if ("is_note" in data) { data2.is_note = data.is_note; } - if ('is_notification' in data) { + if ("is_notification" in data) { data2.is_notification = data.is_notification; } data2.linkPreviews = data.linkPreviews; - if ('messageReactionGroups' in data) { + if ("messageReactionGroups" in data) { data2.messageReactionGroups = data.messageReactionGroups; } - if ('message_type' in data) { + if ("message_type" in data) { data2.message_type = data.message_type; } - if ('model' in data && 'res_id' in data && data.model && data.res_id) { + if ("model" in data && "res_id" in data && data.model && data.res_id) { const originThreadData = { id: data.res_id, model: data.model, }; - if ('record_name' in data && data.record_name) { + if ("record_name" in data && data.record_name) { originThreadData.name = data.record_name; } - if ('res_model_name' in data && data.res_model_name) { + if ("res_model_name" in data && data.res_model_name) { originThreadData.model_name = data.res_model_name; } - if ('module_icon' in data) { + if ("module_icon" in data) { originThreadData.moduleIcon = data.module_icon; } data2.originThread = originThreadData; } - if ('needaction_partner_ids' in data && this.messaging.currentPartner) { - data2.isNeedaction = data.needaction_partner_ids.includes(this.messaging.currentPartner.id); + if ("needaction_partner_ids" in data && this.messaging.currentPartner) { + data2.isNeedaction = data.needaction_partner_ids.includes( + this.messaging.currentPartner.id + ); } - if ('notifications' in data) { - data2.notifications = insert(data.notifications.map(notificationData => - this.messaging.models['Notification'].convertData(notificationData) - )); + if ("notifications" in data) { + data2.notifications = insert( + data.notifications.map((notificationData) => + this.messaging.models["Notification"].convertData(notificationData) + ) + ); } - if ('parentMessage' in data) { + if ("parentMessage" in data) { if (!data.parentMessage) { data2.parentMessage = clear(); } else { data2.parentMessage = this.convertData(data.parentMessage); } } - if ('recipients' in data) { + if ("recipients" in data) { data2.recipients = data.recipients; } - if ('starred_partner_ids' in data && this.messaging.currentPartner) { - data2.isStarred = data.starred_partner_ids.includes(this.messaging.currentPartner.id); + if ("starred_partner_ids" in data && this.messaging.currentPartner) { + data2.isStarred = data.starred_partner_ids.includes( + this.messaging.currentPartner.id + ); } - if ('subject' in data) { + if ("subject" in data) { data2.subject = data.subject; } - if ('subtype_description' in data) { + if ("subtype_description" in data) { data2.subtype_description = data.subtype_description; } - if ('subtype_id' in data) { + if ("subtype_id" in data) { data2.subtype_id = data.subtype_id; } - if ('trackingValues' in data) { + if ("trackingValues" in data) { data2.trackingValues = data.trackingValues; } @@ -114,11 +122,14 @@ Model({ * @param {Array[]} domain */ async markAllAsRead(domain) { - await this.messaging.rpc({ - model: 'mail.message', - method: 'mark_all_as_read', - kwargs: { domain }, - }, { shadow: true }); + await this.messaging.rpc( + { + model: "mail.message", + method: "mark_all_as_read", + kwargs: { domain }, + }, + { shadow: true } + ); }, /** * Mark provided messages as read. Messages that have been marked as @@ -133,9 +144,9 @@ Model({ */ async markAsRead(messages) { await this.messaging.rpc({ - model: 'mail.message', - method: 'set_message_done', - args: [messages.map(message => message.id)] + model: "mail.message", + method: "set_message_done", + args: [messages.map((message) => message.id)], }); }, /** @@ -150,18 +161,20 @@ Model({ if (!this.messaging) { return; } - const messages = this.messaging.models['Message'].insert(messagesData.map( - messageData => this.messaging.models['Message'].convertData(messageData) - )); + const messages = this.messaging.models["Message"].insert( + messagesData.map((messageData) => + this.messaging.models["Message"].convertData(messageData) + ) + ); // compute seen indicators (if applicable) for (const message of messages) { for (const thread of message.threads) { - if (!thread.channel || thread.channel.channel_type === 'channel') { + if (!thread.channel || thread.channel.channel_type === "channel") { // disabled on non-channel threads and // on `channel` channels for performance reasons continue; } - this.messaging.models['MessageSeenIndicator'].insert({ + this.messaging.models["MessageSeenIndicator"].insert({ thread, message, }); @@ -174,8 +187,8 @@ Model({ */ async unstarAll() { await this.messaging.rpc({ - model: 'mail.message', - method: 'unstar_all', + model: "mail.message", + method: "unstar_all", }); }, }, @@ -187,7 +200,7 @@ Model({ */ async addReaction(content) { const messageData = await this.messaging.rpc({ - route: '/mail/message/add_reaction', + route: "/mail/message/add_reaction", params: { content, message_id: this.id }, }); if (!this.exists()) { @@ -201,23 +214,20 @@ Model({ */ async markAsRead() { await this.messaging.rpc({ - model: 'mail.message', - method: 'set_message_done', - args: [[this.id]] + model: "mail.message", + method: "set_message_done", + args: [[this.id]], }); }, /** * Opens the view that allows to resend the message in case of failure. */ openResendAction() { - this.env.services.action.doAction( - 'mail.mail_resend_message_action', - { - additionalContext: { - mail_message_to_resend: this.id, - }, - } - ); + this.env.services.action.doAction("mail.mail_resend_message_action", { + additionalContext: { + mail_message_to_resend: this.id, + }, + }); }, /** * Removes the given reaction from this message. @@ -226,7 +236,7 @@ Model({ */ async removeReaction(content) { const messageData = await this.messaging.rpc({ - route: '/mail/message/remove_reaction', + route: "/mail/message/remove_reaction", params: { content, message_id: this.id }, }); if (!this.exists()) { @@ -239,9 +249,9 @@ Model({ */ async toggleStar() { await this.messaging.rpc({ - model: 'mail.message', - method: 'toggle_message_starred', - args: [[this.id]] + model: "mail.message", + method: "toggle_message_starred", + args: [[this.id]], }); }, /** @@ -252,7 +262,7 @@ Model({ */ async updateContent({ body, attachment_ids }) { const messageData = await this.messaging.rpc({ - route: '/mail/message/update_content', + route: "/mail/message/update_content", params: { body, attachment_ids, @@ -262,7 +272,7 @@ Model({ if (!this.messaging) { return; } - this.messaging.models['Message'].insert(messageData); + this.messaging.models["Message"].insert(messageData); }, }, fields: { @@ -280,25 +290,39 @@ Model({ return this.env._t("Anonymous"); }, }), - attachments: many('Attachment', { inverse: 'messages' }), - author: one('Partner'), + attachments: many("Attachment", { inverse: "messages" }), + author: one("Partner"), avatarUrl: attr({ compute() { - if (this.author && (!this.originThread || this.originThread.model !== 'mail.channel')) { + if ( + this.author && + (!this.originThread || this.originThread.model !== "mail.channel") + ) { // TODO FIXME for public user this might not be accessible. task-2223236 // we should probably use the correspondig attachment id + access token // or create a dedicated route to get message image, checking the access right of the message return this.author.avatarUrl; - } else if (this.author && this.originThread && this.originThread.model === 'mail.channel') { + } else if ( + this.author && + this.originThread && + this.originThread.model === "mail.channel" + ) { return `/mail/channel/${this.originThread.id}/partner/${this.author.id}/avatar_128`; - } else if (this.guestAuthor && (!this.originThread || this.originThread.model !== 'mail.channel')) { + } else if ( + this.guestAuthor && + (!this.originThread || this.originThread.model !== "mail.channel") + ) { return this.guestAuthor.avatarUrl; - } else if (this.guestAuthor && this.originThread && this.originThread.model === 'mail.channel') { + } else if ( + this.guestAuthor && + this.originThread && + this.originThread.model === "mail.channel" + ) { return `/mail/channel/${this.originThread.id}/guest/${this.guestAuthor.id}/avatar_128?unique=${this.guestAuthor.name}`; - } else if (this.message_type === 'email') { - return '/mail/static/src/img/email_icon.png'; + } else if (this.message_type === "email") { + return "/mail/static/src/img/email_icon.png"; } - return '/mail/static/src/img/smiley/avatar.jpg'; + return "/mail/static/src/img/smiley/avatar.jpg"; }, }), /** @@ -322,10 +346,10 @@ Model({ if (this.trackingValues.length > 0) { return false; } - if (this.message_type !== 'comment') { + if (this.message_type !== "comment") { return false; } - if (this.originThread.model === 'mail.channel') { + if (this.originThread.model === "mail.channel") { return true; } return this.is_note; @@ -352,7 +376,7 @@ Model({ return moment(str_to_datetime(this.date)); } return this.date; - } + }, }), /** * States the date of this message as a string (either a relative period @@ -365,17 +389,13 @@ Model({ // mainly done to avoid flicker inside the UI. return this.env._t("Today"); } - const date = this.momentDate.format('YYYY-MM-DD'); - if (date === moment().format('YYYY-MM-DD')) { + const date = this.momentDate.format("YYYY-MM-DD"); + if (date === moment().format("YYYY-MM-DD")) { return this.env._t("Today"); - } else if ( - date === moment() - .subtract(1, 'days') - .format('YYYY-MM-DD') - ) { + } else if (date === moment().subtract(1, "days").format("YYYY-MM-DD")) { return this.env._t("Yesterday"); } - return this.momentDate.format('LL'); + return this.momentDate.format("LL"); }, }), /** @@ -390,12 +410,12 @@ Model({ }, }), email_from: attr(), - failureNotifications: many('Notification', { + failureNotifications: many("Notification", { compute() { - return this.notifications.filter(notifications => notifications.isFailure); + return this.notifications.filter((notifications) => notifications.isFailure); }, }), - guestAuthor: one('Guest', { inverse: 'authoredMessages' }), + guestAuthor: one("Guest", { inverse: "authoredMessages" }), /** * States whether the message has some attachments. */ @@ -413,16 +433,20 @@ Model({ }, }), id: attr({ identifying: true }), - isCurrentUserOrGuestAuthor: attr({ default: false, + isCurrentUserOrGuestAuthor: attr({ + default: false, compute() { - return !!( - this.author && - this.messaging.currentPartner && - this.messaging.currentPartner === this.author - ) || !!( - this.guestAuthor && - this.messaging.currentGuest && - this.messaging.currentGuest === this.guestAuthor + return ( + !!( + this.author && + this.messaging.currentPartner && + this.messaging.currentPartner === this.author + ) || + !!( + this.guestAuthor && + this.messaging.currentGuest && + this.messaging.currentGuest === this.guestAuthor + ) ); }, }), @@ -435,12 +459,9 @@ Model({ compute() { return ( !this.body || - [ - '', - '

', - '


', - '


', - ].includes(this.body.replace(/\s/g, '')) + ["", "

", "


", "


"].includes( + this.body.replace(/\s/g, "") + ) ); }, }), @@ -464,7 +485,8 @@ Model({ * body created by a user with a different language. * - Their content might be mostly but not exactly the same. */ - isBodyEqualSubtypeDescription: attr({ default: false, + isBodyEqualSubtypeDescription: attr({ + default: false, compute() { if (!this.body || !this.subtype_description) { return false; @@ -473,7 +495,8 @@ Model({ return inlineBody.toLowerCase() === this.subtype_description.toLowerCase(); }, }), - isDiscussionOrNotification: attr({ default: false, + isDiscussionOrNotification: attr({ + default: false, compute() { if (this.is_discussion || this.is_notification) { return true; @@ -520,15 +543,11 @@ Model({ */ isSubjectSimilarToOriginThreadName: attr({ compute() { - if ( - !this.subject || - !this.originThread || - !this.originThread.name - ) { + if (!this.subject || !this.originThread || !this.originThread.name) { return false; } const threadName = this.originThread.name.toLowerCase().trim(); - const prefixList = ['re:', 'fw:', 'fwd:']; + const prefixList = ["re:", "fw:", "fwd:"]; let cleanedSubject = this.subject.toLowerCase(); let wasSubjectCleaned = true; while (wasSubjectCleaned) { @@ -538,7 +557,7 @@ Model({ } for (const prefix of prefixList) { if (cleanedSubject.startsWith(prefix)) { - cleanedSubject = cleanedSubject.replace(prefix, '').trim(); + cleanedSubject = cleanedSubject.replace(prefix, "").trim(); wasSubjectCleaned = true; break; } @@ -565,7 +584,8 @@ Model({ /** * Determine whether the current partner is mentioned. */ - isCurrentPartnerMentioned: attr({ default: false, + isCurrentPartnerMentioned: attr({ + default: false, compute() { return this.recipients.includes(this.messaging.currentPartner); }, @@ -578,7 +598,7 @@ Model({ return ( this.isCurrentPartnerMentioned && this.originThread && - this.originThread.model === 'mail.channel' + this.originThread.model === "mail.channel" ); }, }), @@ -590,27 +610,24 @@ Model({ /** * Last tracking value of the message. */ - lastTrackingValue: one('TrackingValue', { + lastTrackingValue: one("TrackingValue", { compute() { - const { - length: l, - [l - 1]: lastTrackingValue, - } = this.trackingValues; + const { length: l, [l - 1]: lastTrackingValue } = this.trackingValues; if (lastTrackingValue) { return lastTrackingValue; } return clear(); }, }), - linkPreviews: many('LinkPreview', { inverse: 'message' }), + linkPreviews: many("LinkPreview", { inverse: "message" }), /** * Groups of reactions per content allowing to know the number of * reactions for each. */ - messageReactionGroups: many('MessageReactionGroup', { inverse: 'message' }), + messageReactionGroups: many("MessageReactionGroup", { inverse: "message" }), messageTypeText: attr({ compute() { - if (this.message_type === 'notification') { + if (this.message_type === "notification") { return this.env._t("System notification"); } if (!this.is_discussion && !this.is_notification) { @@ -620,31 +637,35 @@ Model({ }, }), message_type: attr(), - notificationMessageViews: many('NotificationMessageView', { inverse: 'message', isCausal: true }), + notificationMessageViews: many("NotificationMessageView", { + inverse: "message", + isCausal: true, + }), /** * States the views that are displaying this message. */ - messageViews: many('MessageView', { inverse: 'message', isCausal: true }), - messageListViewItems: many('MessageListViewItem', { inverse: 'message' }), - notifications: many('Notification', { inverse: 'message', isCausal: true }), + messageViews: many("MessageView", { inverse: "message", isCausal: true }), + messageListViewItems: many("MessageListViewItem", { inverse: "message" }), + notifications: many("Notification", { inverse: "message", isCausal: true }), /** * Origin thread of this message (if any). */ - originThread: one('Thread', { inverse: 'messagesAsOriginThread' }), + originThread: one("Thread", { inverse: "messagesAsOriginThread" }), /** * States the message that this message replies to (if any). Only makes * sense on channels. Other types of threads might have a parent message * (parent_id in python) that should be ignored for the purpose of this * feature. */ - parentMessage: one('Message'), + parentMessage: one("Message"), /** * This value is meant to be based on field body which is * returned by the server (and has been sanitized before stored into db). * Do not use this value in a 't-raw' if the message has been created * directly from user input and not from server data as it's not escaped. */ - prettyBody: attr({ default: "", + prettyBody: attr({ + default: "", /** * This value is meant to be based on field body which is * returned by the server (and has been sanitized before stored into db). @@ -665,13 +686,13 @@ Model({ return markup(this.prettyBody); }, }), - recipients: many('Partner'), + recipients: many("Partner"), shortTime: attr({ compute() { if (!this.momentDate) { return clear(); } - return this.momentDate.format('hh:mm'); + return this.momentDate.format("hh:mm"); }, }), subject: attr(), @@ -680,7 +701,8 @@ Model({ /** * All threads that this message is linked to. This field is read-only. */ - threads: many('Thread', { inverse: 'messages', + threads: many("Thread", { + inverse: "messages", compute() { const threads = []; if (this.isHistory && this.messaging.history) { @@ -698,8 +720,10 @@ Model({ return threads; }, }), - trackingValues: many('TrackingValue', { inverse: 'messageOwner', isCausal: true, - sort: [['smaller-first', 'id']], + trackingValues: many("TrackingValue", { + inverse: "messageOwner", + isCausal: true, + sort: [["smaller-first", "id"]], }), }, }); diff --git a/addons/mail/static/src/models/message_action.js b/addons/mail/static/src/models/message_action.js index dcdda3aa891..2dc61eea7ca 100644 --- a/addons/mail/static/src/models/message_action.js +++ b/addons/mail/static/src/models/message_action.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'MessageAction', - identifyingMode: 'xor', + name: "MessageAction", + identifyingMode: "xor", fields: { isNonCompactActionContribution: attr({ compute() { @@ -14,7 +14,9 @@ Model({ return 1; }, }), - messageActionListOwner: one('MessageActionList', { inverse: 'messageActions', required: true, + messageActionListOwner: one("MessageActionList", { + inverse: "messageActions", + required: true, compute() { if (this.messageActionListOwnerAsDelete) { return this.messageActionListOwnerAsDelete; @@ -34,14 +36,36 @@ Model({ return clear(); }, }), - messageActionListOwnerAsDelete: one('MessageActionList', { identifying: true, inverse: 'actionDelete' }), - messageActionListOwnerAsEdit: one('MessageActionList', { identifying: true, inverse: 'actionEdit' }), - messageActionListOwnerAsMarkAsRead: one('MessageActionList', { identifying: true, inverse: 'actionMarkAsRead' }), - messageActionListOwnerAsReaction: one('MessageActionList', { identifying: true, inverse: 'actionReaction' }), - messageActionListOwnerAsReplyTo: one('MessageActionList', { identifying: true, inverse: 'actionReplyTo' }), - messageActionListOwnerAsToggleCompact: one('MessageActionList', { identifying: true, inverse: 'actionToggleCompact' }), - messageActionListOwnerAsToggleStar: one('MessageActionList', { identifying: true, inverse: 'actionToggleStar' }), - messageActionView: one('MessageActionView', { inverse: 'messageAction', + messageActionListOwnerAsDelete: one("MessageActionList", { + identifying: true, + inverse: "actionDelete", + }), + messageActionListOwnerAsEdit: one("MessageActionList", { + identifying: true, + inverse: "actionEdit", + }), + messageActionListOwnerAsMarkAsRead: one("MessageActionList", { + identifying: true, + inverse: "actionMarkAsRead", + }), + messageActionListOwnerAsReaction: one("MessageActionList", { + identifying: true, + inverse: "actionReaction", + }), + messageActionListOwnerAsReplyTo: one("MessageActionList", { + identifying: true, + inverse: "actionReplyTo", + }), + messageActionListOwnerAsToggleCompact: one("MessageActionList", { + identifying: true, + inverse: "actionToggleCompact", + }), + messageActionListOwnerAsToggleStar: one("MessageActionList", { + identifying: true, + inverse: "actionToggleStar", + }), + messageActionView: one("MessageActionView", { + inverse: "messageAction", compute() { /** * Case 0: Always display Reaction and ToggleCompact if they are existing. @@ -53,8 +77,11 @@ Model({ if ( this.messageActionListOwnerAsReaction || this.messageActionListOwnerAsToggleCompact || - (this.messageActionListOwner && !this.messageActionListOwner.actionToggleCompact) || - (this.messageActionListOwner && this.messageActionListOwner.actionToggleCompact && !this.messageActionListOwner.isCompact) + (this.messageActionListOwner && + !this.messageActionListOwner.actionToggleCompact) || + (this.messageActionListOwner && + this.messageActionListOwner.actionToggleCompact && + !this.messageActionListOwner.isCompact) ) { return {}; } diff --git a/addons/mail/static/src/models/message_action_list.js b/addons/mail/static/src/models/message_action_list.js index 7ae9172a7d8..e6b00317ec1 100644 --- a/addons/mail/static/src/models/message_action_list.js +++ b/addons/mail/static/src/models/message_action_list.js @@ -1,14 +1,23 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'MessageActionList', + name: "MessageActionList", template: "mail.MessageActionList", fields: { - actionViewsCount: attr({ default: 0, readonly: true, sum: 'messageActionViews.actionViewCounterContribution' }), - actionsWithoutCompactCount: attr({ default: 0, readonly: true, sum: 'messageActions.isNonCompactActionContribution' }), - actionDelete: one('MessageAction', { inverse: 'messageActionListOwnerAsDelete', + actionViewsCount: attr({ + default: 0, + readonly: true, + sum: "messageActionViews.actionViewCounterContribution", + }), + actionsWithoutCompactCount: attr({ + default: 0, + readonly: true, + sum: "messageActions.isNonCompactActionContribution", + }), + actionDelete: one("MessageAction", { + inverse: "messageActionListOwnerAsDelete", compute() { if (this.message && this.message.canBeDeleted) { return {}; @@ -16,7 +25,8 @@ Model({ return clear(); }, }), - actionEdit: one('MessageAction', { inverse: 'messageActionListOwnerAsEdit', + actionEdit: one("MessageAction", { + inverse: "messageActionListOwnerAsEdit", compute() { if (this.message && this.message.canBeDeleted) { return {}; @@ -24,19 +34,26 @@ Model({ return clear(); }, }), - actionMarkAsRead: one('MessageAction', { inverse: 'messageActionListOwnerAsMarkAsRead', + actionMarkAsRead: one("MessageAction", { + inverse: "messageActionListOwnerAsMarkAsRead", compute() { if ( - this.messaging && this.messaging.inbox && - this.messageView && this.messageView.messageListViewItemOwner && this.messageView.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread && - this.messageView.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread === this.messaging.inbox.thread + this.messaging && + this.messaging.inbox && + this.messageView && + this.messageView.messageListViewItemOwner && + this.messageView.messageListViewItemOwner.messageListViewOwner.threadViewOwner + .thread && + this.messageView.messageListViewItemOwner.messageListViewOwner.threadViewOwner + .thread === this.messaging.inbox.thread ) { return {}; } return clear(); }, }), - actionReaction: one('MessageAction', { inverse: 'messageActionListOwnerAsReaction', + actionReaction: one("MessageAction", { + inverse: "messageActionListOwnerAsReaction", compute() { if (this.message && this.message.hasReactionIcon) { return {}; @@ -44,30 +61,43 @@ Model({ return clear(); }, }), - actionReplyTo: one('MessageAction', { inverse: 'messageActionListOwnerAsReplyTo', + actionReplyTo: one("MessageAction", { + inverse: "messageActionListOwnerAsReplyTo", compute() { if ( - this.messaging && this.messaging.inbox && - this.message && !this.message.isTemporary && !this.message.isTransient && - this.messageView && this.messageView.messageListViewItemOwner && this.messageView.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread && ( - this.messageView.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread === this.messaging.inbox.thread || - this.messageView.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread.channel - ) + this.messaging && + this.messaging.inbox && + this.message && + !this.message.isTemporary && + !this.message.isTransient && + this.messageView && + this.messageView.messageListViewItemOwner && + this.messageView.messageListViewItemOwner.messageListViewOwner.threadViewOwner + .thread && + (this.messageView.messageListViewItemOwner.messageListViewOwner.threadViewOwner + .thread === this.messaging.inbox.thread || + this.messageView.messageListViewItemOwner.messageListViewOwner + .threadViewOwner.thread.channel) ) { return {}; } return clear(); }, }), - actionToggleCompact: one('MessageAction', { inverse: 'messageActionListOwnerAsToggleCompact', + actionToggleCompact: one("MessageAction", { + inverse: "messageActionListOwnerAsToggleCompact", compute() { - if (this.messageView.isInChatWindow && (this.actionsWithoutCompactCount > this.compactThreshold)) { + if ( + this.messageView.isInChatWindow && + this.actionsWithoutCompactCount > this.compactThreshold + ) { return {}; } return clear(); }, }), - actionToggleStar: one('MessageAction', { inverse: 'messageActionListOwnerAsToggleStar', + actionToggleStar: one("MessageAction", { + inverse: "messageActionListOwnerAsToggleStar", compute() { if (this.message && this.message.canStarBeToggled) { return {}; @@ -76,7 +106,7 @@ Model({ }, }), compactThreshold: attr({ default: 2, readonly: true }), - firstActionView: one('MessageActionView', { + firstActionView: one("MessageActionView", { compute() { if (this.actionViewsCount === 0) { return clear(); @@ -85,7 +115,7 @@ Model({ }, }), isCompact: attr({ default: true }), - lastActionView: one('MessageActionView', { + lastActionView: one("MessageActionView", { compute() { if (this.actionViewsCount === 0) { return clear(); @@ -96,14 +126,18 @@ Model({ /** * States the message on which this action message list operates. */ - message: one('Message', { related: 'messageView.message' }), - messageActions: many('MessageAction', { inverse: 'messageActionListOwner', isCausal: true }), - messageActionViews: many('MessageActionView', { related: 'messageActions.messageActionView', - sort: [['smaller-first', 'messageAction.sequence']], + message: one("Message", { related: "messageView.message" }), + messageActions: many("MessageAction", { + inverse: "messageActionListOwner", + isCausal: true, + }), + messageActionViews: many("MessageActionView", { + related: "messageActions.messageActionView", + sort: [["smaller-first", "messageAction.sequence"]], }), /** * States the message view that controls this message action list. */ - messageView: one('MessageView', { identifying: true, inverse: 'messageActionList' }), + messageView: one("MessageView", { identifying: true, inverse: "messageActionList" }), }, }); diff --git a/addons/mail/static/src/models/message_action_view.js b/addons/mail/static/src/models/message_action_view.js index 1e2f483f849..ba71ab3740d 100644 --- a/addons/mail/static/src/models/message_action_view.js +++ b/addons/mail/static/src/models/message_action_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; -import { markEventHandled } from '@mail/utils/utils'; +import { attr, clear, one, Model } from "@mail/model"; +import { markEventHandled } from "@mail/utils/utils"; Model({ - name: 'MessageActionView', + name: "MessageActionView", template: "mail.MessageActionView", recordMethods: { /** @@ -30,11 +30,13 @@ Model({ } break; case this.messageAction.messageActionListOwnerAsReplyTo: - markEventHandled(ev, 'MessageActionList.replyTo'); + markEventHandled(ev, "MessageActionList.replyTo"); this.messageAction.messageActionListOwner.messageView.replyTo(); break; case this.messageAction.messageActionListOwnerAsToggleCompact: - this.messageAction.messageActionListOwner.update({ isCompact: !this.messageAction.messageActionListOwner.isCompact }); + this.messageAction.messageActionListOwner.update({ + isCompact: !this.messageAction.messageActionListOwner.isCompact, + }); break; case this.messageAction.messageActionListOwnerAsToggleStar: this.messageAction.messageActionListOwner.message.toggleStar(); @@ -46,7 +48,9 @@ Model({ * @param {MouseEvent} ev */ onClickReaction(ev) { - this.messageAction.messageActionListOwner.message.addReaction(ev.currentTarget.dataset.codepoints); + this.messageAction.messageActionListOwner.message.addReaction( + ev.currentTarget.dataset.codepoints + ); this.update({ reactionPopoverView: clear() }); }, }, @@ -54,7 +58,7 @@ Model({ /** * States the reference to the action in the component. */ - actionRef: attr({ ref: 'action' }), + actionRef: attr({ ref: "action" }), actionViewCounterContribution: attr({ default: 1, readonly: true }), ariaPressedState: attr({ compute() { @@ -64,80 +68,80 @@ Model({ return clear(); }, }), - classNames: attr({ default: '', + classNames: attr({ + default: "", compute() { const classNames = []; classNames.push(this.paddingClassNames); switch (this.messageAction.messageActionListOwner) { case this.messageAction.messageActionListOwnerAsDelete: - classNames.push('fa fa-lg fa-trash o_MessageActionView_actionDelete'); + classNames.push("fa fa-lg fa-trash o_MessageActionView_actionDelete"); break; case this.messageAction.messageActionListOwnerAsEdit: - classNames.push('fa fa-lg fa-pencil o_MessageActionView_actionEdit'); + classNames.push("fa fa-lg fa-pencil o_MessageActionView_actionEdit"); break; case this.messageAction.messageActionListOwnerAsMarkAsRead: - classNames.push('fa fa-lg fa-check o_MessageActionView_actionMarkAsRead'); + classNames.push("fa fa-lg fa-check o_MessageActionView_actionMarkAsRead"); break; case this.messageAction.messageActionListOwnerAsReaction: - classNames.push('fa fa-lg fa-smile-o o_MessageActionView_actionReaction'); + classNames.push("fa fa-lg fa-smile-o o_MessageActionView_actionReaction"); break; case this.messageAction.messageActionListOwnerAsReplyTo: - classNames.push('fa fa-lg fa-reply o_MessageActionView_actionReplyTo'); + classNames.push("fa fa-lg fa-reply o_MessageActionView_actionReplyTo"); break; case this.messageAction.messageActionListOwnerAsToggleCompact: - classNames.push('fa fa-lg fa-ellipsis-h o_MessageAction_actionToggleCompact'); + classNames.push( + "fa fa-lg fa-ellipsis-h o_MessageAction_actionToggleCompact" + ); break; case this.messageAction.messageActionListOwnerAsToggleStar: - classNames.push('fa fa-lg o_MessageActionView_actionToggleStar'); + classNames.push("fa fa-lg o_MessageActionView_actionToggleStar"); if (this.messageAction.messageActionListOwner.message.isStarred) { classNames.push(`fa-star o_MessageActionView_actionToggleStar_active`); } else { - classNames.push('fa-star-o'); + classNames.push("fa-star-o"); } break; } - return classNames.join(' '); + return classNames.join(" "); }, }), - deleteConfirmDialog: one('Dialog', { inverse: 'messageActionViewOwnerAsDeleteConfirm' }), - messageAction: one('MessageAction', { identifying: true, inverse: 'messageActionView' }), - paddingClassNames: attr({ default: '', + deleteConfirmDialog: one("Dialog", { inverse: "messageActionViewOwnerAsDeleteConfirm" }), + messageAction: one("MessageAction", { identifying: true, inverse: "messageActionView" }), + paddingClassNames: attr({ + default: "", compute() { const isDeviceSmall = this.messaging.device.isSmall; const paddingClassNames = []; if ( - ( - this.messageAction.messageActionListOwner.firstActionView === this && - !this.messageAction.messageActionListOwner.messageView.isInChatWindowAndIsAlignedRight - ) || - ( - this.messageAction.messageActionListOwner.lastActionView === this && - this.messageAction.messageActionListOwner.messageView.isInChatWindowAndIsAlignedRight - ) + (this.messageAction.messageActionListOwner.firstActionView === this && + !this.messageAction.messageActionListOwner.messageView + .isInChatWindowAndIsAlignedRight) || + (this.messageAction.messageActionListOwner.lastActionView === this && + this.messageAction.messageActionListOwner.messageView + .isInChatWindowAndIsAlignedRight) ) { - paddingClassNames.push(isDeviceSmall ? 'ps-3' : 'ps-2'); + paddingClassNames.push(isDeviceSmall ? "ps-3" : "ps-2"); } else { - paddingClassNames.push(isDeviceSmall ? 'ps-2' : 'ps-1'); + paddingClassNames.push(isDeviceSmall ? "ps-2" : "ps-1"); } if ( - ( - this.messageAction.messageActionListOwner.lastActionView === this && - !this.messageAction.messageActionListOwner.messageView.isInChatWindowAndIsAlignedRight - ) || - ( - this.messageAction.messageActionListOwner.firstActionView === this && - this.messageAction.messageActionListOwner.messageView.isInChatWindowAndIsAlignedRight - ) + (this.messageAction.messageActionListOwner.lastActionView === this && + !this.messageAction.messageActionListOwner.messageView + .isInChatWindowAndIsAlignedRight) || + (this.messageAction.messageActionListOwner.firstActionView === this && + this.messageAction.messageActionListOwner.messageView + .isInChatWindowAndIsAlignedRight) ) { - paddingClassNames.push(isDeviceSmall ? 'pe-3' : 'pe-2'); + paddingClassNames.push(isDeviceSmall ? "pe-3" : "pe-2"); } else { - paddingClassNames.push(isDeviceSmall ? 'pe-2' : 'pe-1'); + paddingClassNames.push(isDeviceSmall ? "pe-2" : "pe-1"); } - paddingClassNames.push(isDeviceSmall ? 'py-3' : 'py-2'); - return paddingClassNames.join(' '); + paddingClassNames.push(isDeviceSmall ? "py-3" : "py-2"); + return paddingClassNames.join(" "); }, }), - reactionPopoverView: one('PopoverView', { inverse: 'messageActionViewOwnerAsReaction' }), + reactionPopoverView: one("PopoverView", { inverse: "messageActionViewOwnerAsReaction" }), tabindex: attr({ compute() { if (this.messageAction.messageActionListOwnerAsReaction) { @@ -146,7 +150,8 @@ Model({ return 0; }, }), - title: attr({ default: '', + title: attr({ + default: "", compute() { switch (this.messageAction.messageActionListOwner) { case this.messageAction.messageActionListOwnerAsDelete: diff --git a/addons/mail/static/src/models/message_author_prefix_view.js b/addons/mail/static/src/models/message_author_prefix_view.js index df1fd9e6378..fa281b96278 100644 --- a/addons/mail/static/src/models/message_author_prefix_view.js +++ b/addons/mail/static/src/models/message_author_prefix_view.js @@ -1,25 +1,29 @@ /** @odoo-module **/ -import { clear, one, Model } from '@mail/model'; +import { clear, one, Model } from "@mail/model"; Model({ - name: 'MessageAuthorPrefixView', - template: 'mail.MessageAuthorPrefixView', - identifyingMode: 'xor', + name: "MessageAuthorPrefixView", + template: "mail.MessageAuthorPrefixView", + identifyingMode: "xor", fields: { - channelPreviewViewOwner: one('ChannelPreviewView', { identifying: true, inverse: 'messageAuthorPrefixView' }), - message: one('Message', { + channelPreviewViewOwner: one("ChannelPreviewView", { + identifying: true, + inverse: "messageAuthorPrefixView", + }), + message: one("Message", { compute() { if (this.channelPreviewViewOwner) { return this.channelPreviewViewOwner.thread.lastMessage; } if (this.threadNeedactionPreviewViewOwner) { - return this.threadNeedactionPreviewViewOwner.thread.lastNeedactionMessageAsOriginThread; + return this.threadNeedactionPreviewViewOwner.thread + .lastNeedactionMessageAsOriginThread; } return clear(); }, }), - thread: one('Thread', { + thread: one("Thread", { compute() { if (this.channelPreviewViewOwner) { return this.channelPreviewViewOwner.thread; @@ -30,6 +34,9 @@ Model({ return clear(); }, }), - threadNeedactionPreviewViewOwner: one('ThreadNeedactionPreviewView', { identifying: true, inverse: 'messageAuthorPrefixView' }), + threadNeedactionPreviewViewOwner: one("ThreadNeedactionPreviewView", { + identifying: true, + inverse: "messageAuthorPrefixView", + }), }, }); diff --git a/addons/mail/static/src/models/message_in_reply_to_view.js b/addons/mail/static/src/models/message_in_reply_to_view.js index 7b9bc403ff9..567eea919ed 100644 --- a/addons/mail/static/src/models/message_in_reply_to_view.js +++ b/addons/mail/static/src/models/message_in_reply_to_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; -import { markEventHandled } from '@mail/utils/utils'; +import { attr, one, Model } from "@mail/model"; +import { markEventHandled } from "@mail/utils/utils"; Model({ - name: 'MessageInReplyToView', + name: "MessageInReplyToView", template: "mail.MessageInReplyToView", recordMethods: { /** @@ -12,13 +12,16 @@ Model({ * @param {MouseEvent} ev */ onClickReply(ev) { - markEventHandled(ev, 'MessageInReplyToView.ClickMessageInReplyTo'); - const messageListViewItem = this.messageView && this.messageView.messageListViewItemOwner; + markEventHandled(ev, "MessageInReplyToView.ClickMessageInReplyTo"); + const messageListViewItem = + this.messageView && this.messageView.messageListViewItemOwner; const parentMessage = this.messageView.message.parentMessage; if (!messageListViewItem || !parentMessage) { return; } - const parentMessageListViewItem = this.messaging.models['MessageListViewItem'].findFromIdentifyingData({ + const parentMessageListViewItem = this.messaging.models[ + "MessageListViewItem" + ].findFromIdentifyingData({ message: parentMessage, messageListViewOwner: messageListViewItem.messageListViewOwner, }); @@ -47,6 +50,6 @@ Model({ return !this.messageView.message.parentMessage.isBodyEmpty; }, }), - messageView: one('MessageView', { identifying: true, inverse: 'messageInReplyToView' }), + messageView: one("MessageView", { identifying: true, inverse: "messageInReplyToView" }), }, }); diff --git a/addons/mail/static/src/models/message_list_view.js b/addons/mail/static/src/models/message_list_view.js index 103477b7048..ea6c94e9da8 100644 --- a/addons/mail/static/src/models/message_list_view.js +++ b/addons/mail/static/src/models/message_list_view.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'MessageListView', + name: "MessageListView", recordMethods: { /** * Update the scroll position of the message list. @@ -36,22 +36,22 @@ Model({ adjustFromComponentHints() { for (const hint of this.threadViewOwner.componentHintList) { switch (hint.type) { - case 'change-of-thread-cache': - case 'member-list-hidden': - case 'adjust-scroll': + case "change-of-thread-cache": + case "member-list-hidden": + case "adjust-scroll": // thread just became visible, the goal is to restore its // saved position if it exists or scroll to the end this.adjustScrollFromModel(); break; - case 'message-posted': - case 'message-received': - case 'messages-loaded': - case 'new-messages-loaded': + case "message-posted": + case "message-received": + case "messages-loaded": + case "new-messages-loaded": // messages have been added at the end, either scroll to the // end or keep the current position this.adjustScrollForExtraMessagesAtTheEnd(); break; - case 'more-messages-loaded': + case "more-messages-loaded": // messages have been added at the start, keep the current // position this.adjustScrollForExtraMessagesAtTheStart(); @@ -66,9 +66,11 @@ Model({ return; } if (!this.threadViewOwner.hasAutoScrollOnMessageReceived) { - if (this.threadViewOwner.order === 'desc' && this.component._willPatchSnapshot) { + if (this.threadViewOwner.order === "desc" && this.component._willPatchSnapshot) { const { scrollHeight, scrollTop } = this.component._willPatchSnapshot; - this.setScrollTop(this.getScrollableElement().scrollHeight - scrollHeight + scrollTop); + this.setScrollTop( + this.getScrollableElement().scrollHeight - scrollHeight + scrollTop + ); } return; } @@ -79,7 +81,7 @@ Model({ !this.getScrollableElement() || !this.hasScrollAdjust || !this.component._willPatchSnapshot || - this.threadViewOwner.order === 'desc' + this.threadViewOwner.order === "desc" ) { return; } @@ -92,7 +94,8 @@ Model({ } if ( this.threadViewOwner.threadCacheInitialScrollPosition !== undefined && - this.getScrollableElement().scrollHeight === this.threadViewOwner.threadCacheInitialScrollHeight + this.getScrollableElement().scrollHeight === + this.threadViewOwner.threadCacheInitialScrollHeight ) { this.setScrollTop(this.threadViewOwner.threadCacheInitialScrollPosition); return; @@ -108,7 +111,7 @@ Model({ this.threadViewOwner.lastMessageListViewItem.isPartiallyVisible() ) { this.threadViewOwner.handleVisibleMessage( - this.threadViewOwner.lastMessageListViewItem.message, + this.threadViewOwner.lastMessageListViewItem.message ); } }, @@ -131,7 +134,8 @@ Model({ } const loadMoreRect = loadMore.getBoundingClientRect(); const elRect = this.getScrollableElement().getBoundingClientRect(); - const isInvisible = loadMoreRect.top > elRect.bottom || loadMoreRect.bottom < elRect.top; + const isInvisible = + loadMoreRect.top > elRect.bottom || loadMoreRect.bottom < elRect.top; return !isInvisible; }, onClickRetryLoadMoreMessages() { @@ -164,7 +168,12 @@ Model({ this.scrollThrottle.do(); }, scrollToEnd() { - this.setScrollTop(this.threadViewOwner.order === 'asc' ? this.getScrollableElement().scrollHeight - this.getScrollableElement().clientHeight : 0); + this.setScrollTop( + this.threadViewOwner.order === "asc" + ? this.getScrollableElement().scrollHeight - + this.getScrollableElement().clientHeight + : 0 + ); }, /** * @param {integer} value @@ -188,7 +197,7 @@ Model({ return; } const scrollTop = this.getScrollableElement().scrollTop; - this.messaging.messagingBus.trigger('o-component-message-list-scrolled', { + this.messaging.messagingBus.trigger("o-component-message-list-scrolled", { orderedMessages: this.threadViewOwner.threadCache.orderedMessages, scrollTop, thread: this.threadViewOwner.thread, @@ -207,11 +216,11 @@ Model({ } this.threadViewOwner.threadViewer.saveThreadCacheScrollHeightAsInitial( this.getScrollableElement().scrollHeight, - this.threadViewOwner.threadCache, + this.threadViewOwner.threadCache ); this.threadViewOwner.threadViewer.saveThreadCacheScrollPositionsAsInitial( scrollTop, - this.threadViewOwner.threadCache, + this.threadViewOwner.threadCache ); if ( !this.isLastScrollProgrammatic && @@ -222,7 +231,7 @@ Model({ } this.checkMostRecentMessageIsVisible(); this.update({ isLastScrollProgrammatic: false }); - } + }, }, fields: { clientHeight: attr(), @@ -230,7 +239,8 @@ Model({ * States the OWL component of this message list view */ component: attr(), - hasScrollAdjust: attr({ default: true, + hasScrollAdjust: attr({ + default: true, compute() { if (this.threadViewOwner.threadViewer.chatter) { return this.threadViewOwner.threadViewer.chatter.hasMessageListScrollAdjust; @@ -250,7 +260,7 @@ Model({ * the threadView. */ const endThreshold = 30; - if (this.threadViewOwner.order === 'asc') { + if (this.threadViewOwner.order === "asc") { return this.scrollTop >= this.scrollHeight - this.clientHeight - endThreshold; } return this.scrollTop <= endThreshold; @@ -272,20 +282,24 @@ Model({ /** * States the message views used to display this thread view owner's messages. */ - messageListViewItems: many('MessageListViewItem', { inverse: 'messageListViewOwner', + messageListViewItems: many("MessageListViewItem", { + inverse: "messageListViewOwner", compute() { if (!this.threadViewOwner.threadCache) { return clear(); } const orderedMessages = this.threadViewOwner.threadCache.orderedNonEmptyMessages; - if (this.threadViewOwner.order === 'desc') { + if (this.threadViewOwner.order === "desc") { orderedMessages.reverse(); } const messageViewsData = []; let prevMessage; for (const message of orderedMessages) { messageViewsData.push({ - isSquashed: this.threadViewOwner._shouldMessageBeSquashed(prevMessage, message), + isSquashed: this.threadViewOwner._shouldMessageBeSquashed( + prevMessage, + message + ), message, }); prevMessage = message; @@ -294,13 +308,14 @@ Model({ }, }), scrollHeight: attr(), - scrollThrottle: one('Throttle', { inverse: 'messageListViewAsScroll', + scrollThrottle: one("Throttle", { + inverse: "messageListViewAsScroll", compute() { return { func: () => this._onThrottledScroll() }; }, }), scrollTop: attr(), - thread: one('Thread', { related: 'threadViewOwner.thread' }), - threadViewOwner: one('ThreadView', { identifying: true, inverse: 'messageListView' }), + thread: one("Thread", { related: "threadViewOwner.thread" }), + threadViewOwner: one("ThreadView", { identifying: true, inverse: "messageListView" }), }, }); diff --git a/addons/mail/static/src/models/message_list_view_item.js b/addons/mail/static/src/models/message_list_view_item.js index 33a2f715971..ed1e6c2be04 100644 --- a/addons/mail/static/src/models/message_list_view_item.js +++ b/addons/mail/static/src/models/message_list_view_item.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; /** * Models a relation between a message list view and a message view where * message is used as iterating field. */ Model({ - name: 'MessageListViewItem', + name: "MessageListViewItem", recordMethods: { /** * Tell whether the item is partially visible on browser window or not. @@ -25,27 +25,35 @@ Model({ } const parentRect = itemView.component.root.el.parentNode.getBoundingClientRect(); // intersection with 5px offset - return ( - elRect.top < parentRect.bottom + 5 && - parentRect.top < elRect.bottom + 5 - ); + return elRect.top < parentRect.bottom + 5 && parentRect.top < elRect.bottom + 5; }, }, fields: { isSquashed: attr({ required: true }), - message: one('Message', { identifying: true, inverse: 'messageListViewItems' }), - messageListViewOwner: one('MessageListView', { identifying: true, inverse: 'messageListViewItems' }), - notificationMessageView: one('NotificationMessageView', { inverse: 'messageListViewItemOwner', + message: one("Message", { identifying: true, inverse: "messageListViewItems" }), + messageListViewOwner: one("MessageListView", { + identifying: true, + inverse: "messageListViewItems", + }), + notificationMessageView: one("NotificationMessageView", { + inverse: "messageListViewItemOwner", compute() { - if (this.message.message_type === 'notification' && this.message.originThread.channel) { + if ( + this.message.message_type === "notification" && + this.message.originThread.channel + ) { return {}; } return clear(); }, }), - messageView: one('MessageView', { inverse: 'messageListViewItemOwner', + messageView: one("MessageView", { + inverse: "messageListViewItemOwner", compute() { - if (this.message.message_type !== 'notification' || !this.message.originThread.channel) { + if ( + this.message.message_type !== "notification" || + !this.message.originThread.channel + ) { return {}; } return clear(); @@ -55,6 +63,9 @@ Model({ * States whether this message list view item is the last one of its thread view. * Computed from inverse relation. */ - threadViewOwnerAsLastMessageListViewItem: one('ThreadView', { inverse: 'lastMessageListViewItem', readonly: true }), + threadViewOwnerAsLastMessageListViewItem: one("ThreadView", { + inverse: "lastMessageListViewItem", + readonly: true, + }), }, }); diff --git a/addons/mail/static/src/models/message_notification_popover_content_view.js b/addons/mail/static/src/models/message_notification_popover_content_view.js index 4cce732e289..aa5f43cd880 100644 --- a/addons/mail/static/src/models/message_notification_popover_content_view.js +++ b/addons/mail/static/src/models/message_notification_popover_content_view.js @@ -1,12 +1,17 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'MessageNotificationPopoverContentView', - template: 'mail.MessageNotificationPopoverContentView', + name: "MessageNotificationPopoverContentView", + template: "mail.MessageNotificationPopoverContentView", fields: { - messageView: one('MessageView', { related: 'popoverViewOwner.messageViewOwnerAsNotificationContent' }), - popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'messageNotificationPopoverContentView' }), + messageView: one("MessageView", { + related: "popoverViewOwner.messageViewOwnerAsNotificationContent", + }), + popoverViewOwner: one("PopoverView", { + identifying: true, + inverse: "messageNotificationPopoverContentView", + }), }, }); diff --git a/addons/mail/static/src/models/message_reaction_group.js b/addons/mail/static/src/models/message_reaction_group.js index af33da301dd..7f09e31be92 100644 --- a/addons/mail/static/src/models/message_reaction_group.js +++ b/addons/mail/static/src/models/message_reaction_group.js @@ -1,12 +1,12 @@ /** @odoo-module **/ -import { attr, many, one, Model } from '@mail/model'; -import { markEventHandled } from '@mail/utils/utils'; -import { sprintf } from '@web/core/utils/strings'; +import { attr, many, one, Model } from "@mail/model"; +import { markEventHandled } from "@mail/utils/utils"; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'MessageReactionGroup', - template: 'mail.MessageReactionGroup', + name: "MessageReactionGroup", + template: "mail.MessageReactionGroup", recordMethods: { /** * Handles click on the reaction group. @@ -14,7 +14,7 @@ Model({ * @param {MouseEvent} ev */ onClick(ev) { - markEventHandled(ev, 'MessageReactionGroup.Click'); + markEventHandled(ev, "MessageReactionGroup.Click"); if (this.hasUserReacted) { this.message.removeReaction(this.content); } else { @@ -28,47 +28,73 @@ Model({ /** * States the guests that have used this reaction on this message. */ - guests: many('Guest'), - hasUserReacted: attr({ default: false, + guests: many("Guest"), + hasUserReacted: attr({ + default: false, compute() { return Boolean( - (this.messaging.currentPartner && this.partners.includes(this.messaging.currentPartner)) || - (this.messaging.currentGuest && this.guests.includes(this.messaging.currentGuest)) + (this.messaging.currentPartner && + this.partners.includes(this.messaging.currentPartner)) || + (this.messaging.currentGuest && + this.guests.includes(this.messaging.currentGuest)) ); }, }), - message: one('Message', { identifying: true, inverse: 'messageReactionGroups' }), + message: one("Message", { identifying: true, inverse: "messageReactionGroups" }), /** * States the partners that have used this reaction on this message. */ - partners: many('Partner'), + partners: many("Partner"), summary: attr({ compute() { - const { - length, - 0: firstUser, - 1: secondUser, - 2: thirdUser, - } = [ - ...this.partners.map(partner => { + const { length, 0: firstUser, 1: secondUser, 2: thirdUser } = [ + ...this.partners.map((partner) => { if (this.message.originThread) { return this.message.originThread.getMemberName(partner.persona); } return partner.nameOrDisplayName; }), - ...this.guests.map(guest => guest.name), + ...this.guests.map((guest) => guest.name), ]; switch (length) { case 1: - return sprintf(this.env._t('%s has reacted with %s'), firstUser, this.content); + return sprintf( + this.env._t("%s has reacted with %s"), + firstUser, + this.content + ); case 2: - return sprintf(this.env._t('%s and %s have reacted with %s'), firstUser, secondUser, this.content); + return sprintf( + this.env._t("%s and %s have reacted with %s"), + firstUser, + secondUser, + this.content + ); case 3: - return sprintf(this.env._t('%s, %s, %s have reacted with %s'), firstUser, secondUser, thirdUser, this.content); + return sprintf( + this.env._t("%s, %s, %s have reacted with %s"), + firstUser, + secondUser, + thirdUser, + this.content + ); case 4: - return sprintf(this.env._t('%s, %s, %s and 1 other person have reacted with %s'), firstUser, secondUser, thirdUser, this.content); + return sprintf( + this.env._t("%s, %s, %s and 1 other person have reacted with %s"), + firstUser, + secondUser, + thirdUser, + this.content + ); default: - return sprintf(this.env._t('%s, %s, %s and %s other persons have reacted with %s'), firstUser, secondUser, thirdUser, length - 3, this.content); + return sprintf( + this.env._t("%s, %s, %s and %s other persons have reacted with %s"), + firstUser, + secondUser, + thirdUser, + length - 3, + this.content + ); } }, }), diff --git a/addons/mail/static/src/models/message_seen_indicator.js b/addons/mail/static/src/models/message_seen_indicator.js index a6f1dcd9ab8..b62bb0bf184 100644 --- a/addons/mail/static/src/models/message_seen_indicator.js +++ b/addons/mail/static/src/models/message_seen_indicator.js @@ -1,50 +1,49 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; -import { sprintf } from '@web/core/utils/strings'; +import { attr, clear, many, one, Model } from "@mail/model"; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'MessageSeenIndicator', + name: "MessageSeenIndicator", fields: { - hasEveryoneFetched: attr({ default: false, + hasEveryoneFetched: attr({ + default: false, compute() { if (!this.message || !this.thread || !this.thread.partnerSeenInfos) { return false; } - const otherPartnerSeenInfosDidNotFetch = - this.thread.partnerSeenInfos.filter(partnerSeenInfo => + const otherPartnerSeenInfosDidNotFetch = this.thread.partnerSeenInfos.filter( + (partnerSeenInfo) => partnerSeenInfo.partner !== this.message.author && - ( - !partnerSeenInfo.lastFetchedMessage || - partnerSeenInfo.lastFetchedMessage.id < this.message.id - ) + (!partnerSeenInfo.lastFetchedMessage || + partnerSeenInfo.lastFetchedMessage.id < this.message.id) ); return otherPartnerSeenInfosDidNotFetch.length === 0; }, }), - hasEveryoneSeen: attr({ default: false, + hasEveryoneSeen: attr({ + default: false, compute() { if (!this.message || !this.thread || !this.thread.partnerSeenInfos) { return false; } - const otherPartnerSeenInfosDidNotSee = - this.thread.partnerSeenInfos.filter(partnerSeenInfo => + const otherPartnerSeenInfosDidNotSee = this.thread.partnerSeenInfos.filter( + (partnerSeenInfo) => partnerSeenInfo.partner !== this.message.author && - ( - !partnerSeenInfo.lastSeenMessage || - partnerSeenInfo.lastSeenMessage.id < this.message.id - ) + (!partnerSeenInfo.lastSeenMessage || + partnerSeenInfo.lastSeenMessage.id < this.message.id) ); return otherPartnerSeenInfosDidNotSee.length === 0; }, }), - hasSomeoneFetched: attr({ default: false, + hasSomeoneFetched: attr({ + default: false, compute() { if (!this.message || !this.thread || !this.thread.partnerSeenInfos) { return false; } - const otherPartnerSeenInfosFetched = - this.thread.partnerSeenInfos.filter(partnerSeenInfo => + const otherPartnerSeenInfosFetched = this.thread.partnerSeenInfos.filter( + (partnerSeenInfo) => partnerSeenInfo.partner !== this.message.author && partnerSeenInfo.lastFetchedMessage && partnerSeenInfo.lastFetchedMessage.id >= this.message.id @@ -52,13 +51,14 @@ Model({ return otherPartnerSeenInfosFetched.length > 0; }, }), - hasSomeoneSeen: attr({ default: false, + hasSomeoneSeen: attr({ + default: false, compute() { if (!this.message || !this.thread || !this.thread.partnerSeenInfos) { return false; } - const otherPartnerSeenInfosSeen = - this.thread.partnerSeenInfos.filter(partnerSeenInfo => + const otherPartnerSeenInfosSeen = this.thread.partnerSeenInfos.filter( + (partnerSeenInfo) => partnerSeenInfo.partner !== this.message.author && partnerSeenInfo.lastSeenMessage && partnerSeenInfo.lastSeenMessage.id >= this.message.id @@ -67,7 +67,8 @@ Model({ }, }), id: attr(), - isMessagePreviousToLastCurrentPartnerMessageSeenByEveryone: attr({ default: false, + isMessagePreviousToLastCurrentPartnerMessageSeenByEveryone: attr({ + default: false, compute() { if ( !this.message || @@ -82,71 +83,72 @@ Model({ /** * The message concerned by this seen indicator. */ - message: one('Message', { identifying: true }), - partnersThatHaveFetched: many('Partner', { + message: one("Message", { identifying: true }), + partnersThatHaveFetched: many("Partner", { compute() { if (!this.message || !this.thread || !this.thread.partnerSeenInfos) { return clear(); } const otherPartnersThatHaveFetched = this.thread.partnerSeenInfos - .filter(partnerSeenInfo => - /** - * Relation may not be set yet immediately - * @see ThreadPartnerSeenInfo:partnerId field - * FIXME task-2278551 - */ - partnerSeenInfo.partner && - partnerSeenInfo.partner !== this.message.author && - partnerSeenInfo.lastFetchedMessage && - partnerSeenInfo.lastFetchedMessage.id >= this.message.id + .filter( + (partnerSeenInfo) => + /** + * Relation may not be set yet immediately + * @see ThreadPartnerSeenInfo:partnerId field + * FIXME task-2278551 + */ + partnerSeenInfo.partner && + partnerSeenInfo.partner !== this.message.author && + partnerSeenInfo.lastFetchedMessage && + partnerSeenInfo.lastFetchedMessage.id >= this.message.id ) - .map(partnerSeenInfo => partnerSeenInfo.partner); + .map((partnerSeenInfo) => partnerSeenInfo.partner); if (otherPartnersThatHaveFetched.length === 0) { return clear(); } return otherPartnersThatHaveFetched; }, }), - partnersThatHaveSeen: many('Partner', { + partnersThatHaveSeen: many("Partner", { compute() { if (!this.message || !this.thread || !this.thread.partnerSeenInfos) { return clear(); } const otherPartnersThatHaveSeen = this.thread.partnerSeenInfos - .filter(partnerSeenInfo => - /** - * Relation may not be set yet immediately - * @see ThreadPartnerSeenInfo:partnerId field - * FIXME task-2278551 - */ - partnerSeenInfo.partner && - partnerSeenInfo.partner !== this.message.author && - partnerSeenInfo.lastSeenMessage && - partnerSeenInfo.lastSeenMessage.id >= this.message.id) - .map(partnerSeenInfo => partnerSeenInfo.partner); + .filter( + (partnerSeenInfo) => + /** + * Relation may not be set yet immediately + * @see ThreadPartnerSeenInfo:partnerId field + * FIXME task-2278551 + */ + partnerSeenInfo.partner && + partnerSeenInfo.partner !== this.message.author && + partnerSeenInfo.lastSeenMessage && + partnerSeenInfo.lastSeenMessage.id >= this.message.id + ) + .map((partnerSeenInfo) => partnerSeenInfo.partner); if (otherPartnersThatHaveSeen.length === 0) { return clear(); } return otherPartnersThatHaveSeen; }, }), - text: attr({ default: '', + text: attr({ + default: "", compute() { if (this.hasEveryoneSeen) { return this.env._t("Seen by Everyone"); } if (this.hasSomeoneSeen) { - const partnersThatHaveSeen = this.partnersThatHaveSeen.map(partner => { + const partnersThatHaveSeen = this.partnersThatHaveSeen.map((partner) => { if (this.message.originThread) { return this.message.originThread.getMemberName(partner.persona); } return partner.nameOrDisplayName; }); if (partnersThatHaveSeen.length === 1) { - return sprintf( - this.env._t("Seen by %s"), - partnersThatHaveSeen[0] - ); + return sprintf(this.env._t("Seen by %s"), partnersThatHaveSeen[0]); } if (partnersThatHaveSeen.length === 2) { return sprintf( @@ -165,17 +167,14 @@ Model({ return this.env._t("Received by Everyone"); } if (this.hasSomeoneFetched) { - const partnersThatHaveFetched = this.partnersThatHaveFetched.map(partner => { + const partnersThatHaveFetched = this.partnersThatHaveFetched.map((partner) => { if (this.message.originThread) { return this.message.originThread.getMemberName(partner.persona); } return partner.nameOrDisplayName; }); if (partnersThatHaveFetched.length === 1) { - return sprintf( - this.env._t("Received by %s"), - partnersThatHaveFetched[0] - ); + return sprintf(this.env._t("Received by %s"), partnersThatHaveFetched[0]); } if (partnersThatHaveFetched.length === 2) { return sprintf( @@ -196,6 +195,6 @@ Model({ /** * The thread concerned by this seen indicator. */ - thread: one('Thread', { identifying: true, inverse: 'messageSeenIndicators' }), + thread: one("Thread", { identifying: true, inverse: "messageSeenIndicators" }), }, }); diff --git a/addons/mail/static/src/models/message_seen_indicator_view.js b/addons/mail/static/src/models/message_seen_indicator_view.js index 85700bf57f6..a10e70fd6a4 100644 --- a/addons/mail/static/src/models/message_seen_indicator_view.js +++ b/addons/mail/static/src/models/message_seen_indicator_view.js @@ -1,18 +1,26 @@ /** @odoo-module **/ -import { clear, one, Model } from '@mail/model'; +import { clear, one, Model } from "@mail/model"; Model({ - name: 'MessageSeenIndicatorView', - template: 'mail.MessageSeenIndicatorView', + name: "MessageSeenIndicatorView", + template: "mail.MessageSeenIndicatorView", fields: { - messageViewOwner: one('MessageView', { identifying: true, inverse: 'messageSeenIndicatorView' }), - messageSeenIndicator: one('MessageSeenIndicator', { + messageViewOwner: one("MessageView", { + identifying: true, + inverse: "messageSeenIndicatorView", + }), + messageSeenIndicator: one("MessageSeenIndicator", { compute() { - if (this.messageViewOwner.messageListViewItemOwner && this.messageViewOwner.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread) { + if ( + this.messageViewOwner.messageListViewItemOwner && + this.messageViewOwner.messageListViewItemOwner.messageListViewOwner + .threadViewOwner.thread + ) { return { message: this.messageViewOwner.message, - thread: this.messageViewOwner.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread, + thread: this.messageViewOwner.messageListViewItemOwner.messageListViewOwner + .threadViewOwner.thread, }; } return clear(); diff --git a/addons/mail/static/src/models/message_view.js b/addons/mail/static/src/models/message_view.js index cc910408f4a..bef47e9ea42 100644 --- a/addons/mail/static/src/models/message_view.js +++ b/addons/mail/static/src/models/message_view.js @@ -1,18 +1,18 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, increment, one, Model } from '@mail/model'; -import { isEventHandled, markEventHandled } from '@mail/utils/utils'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, increment, one, Model } from "@mail/model"; +import { isEventHandled, markEventHandled } from "@mail/utils/utils"; Model({ - name: 'MessageView', - template: 'mail.MessageView', + name: "MessageView", + template: "mail.MessageView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useComponentToModel({ fieldName: "component" }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, - identifyingMode: 'xor', + identifyingMode: "xor", recordMethods: { /** * Briefly highlights the message. @@ -27,12 +27,12 @@ Model({ * @param {MouseEvent} ev */ async onClick(ev) { - if (ev.target.closest('.o_channel_redirect')) { + if (ev.target.closest(".o_channel_redirect")) { // avoid following dummy href ev.preventDefault(); - const channel = this.messaging.models['Thread'].insert({ + const channel = this.messaging.models["Thread"].insert({ id: Number(ev.target.dataset.oeId), - model: 'mail.channel', + model: "mail.channel", }); if (!channel.isPinned) { await channel.join(); @@ -40,14 +40,14 @@ Model({ } channel.open(); return; - } else if (ev.target.closest('.o_mail_redirect')) { + } else if (ev.target.closest(".o_mail_redirect")) { ev.preventDefault(); this.messaging.openChat({ - partnerId: Number(ev.target.dataset.oeId) + partnerId: Number(ev.target.dataset.oeId), }); return; } - if (ev.target.tagName === 'A') { + if (ev.target.tagName === "A") { if (ev.target.dataset.oeId && ev.target.dataset.oeModel) { // avoid following dummy href ev.preventDefault(); @@ -59,13 +59,13 @@ Model({ return; } if ( - !isEventHandled(ev, 'Message.ClickAuthorAvatar') && - !isEventHandled(ev, 'Message.ClickAuthorName') && - !isEventHandled(ev, 'Message.ClickFailure') && - !isEventHandled(ev, 'MessageActionList.Click') && - !isEventHandled(ev, 'MessageReactionGroup.Click') && - !isEventHandled(ev, 'MessageInReplyToView.ClickMessageInReplyTo') && - !isEventHandled(ev, 'PersonaImStatusIcon.Click') + !isEventHandled(ev, "Message.ClickAuthorAvatar") && + !isEventHandled(ev, "Message.ClickAuthorName") && + !isEventHandled(ev, "Message.ClickFailure") && + !isEventHandled(ev, "MessageActionList.Click") && + !isEventHandled(ev, "MessageReactionGroup.Click") && + !isEventHandled(ev, "MessageInReplyToView.ClickMessageInReplyTo") && + !isEventHandled(ev, "PersonaImStatusIcon.Click") ) { if (this.messagingAsClickedMessageView) { this.messaging.update({ clickedMessageView: clear() }); @@ -78,7 +78,7 @@ Model({ * @param {MouseEvent} ev */ onClickAuthorAvatar(ev) { - markEventHandled(ev, 'Message.ClickAuthorAvatar'); + markEventHandled(ev, "Message.ClickAuthorAvatar"); if (!this.hasAuthorOpenChat) { return; } @@ -88,7 +88,7 @@ Model({ * @param {MouseEvent} ev */ onClickAuthorName(ev) { - markEventHandled(ev, 'Message.ClickAuthorName'); + markEventHandled(ev, "Message.ClickAuthorName"); if (!this.message.author) { return; } @@ -98,7 +98,7 @@ Model({ * @param {MouseEvent} ev */ onClickFailure(ev) { - markEventHandled(ev, 'Message.ClickFailure'); + markEventHandled(ev, "Message.ClickFailure"); this.message.openResendAction(); }, onClickNotificationIcon() { @@ -116,12 +116,18 @@ Model({ return; } if (this.doHighlight && this.component && this.component.root.el) { - this.component.root.el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + this.component.root.el.scrollIntoView({ behavior: "smooth", block: "center" }); this.highlight(); this.update({ doHighlight: clear() }); } - if (this.messageListViewItemOwner && this.messageListViewItemOwner.threadViewOwnerAsLastMessageListViewItem && this.messageListViewItemOwner.isPartiallyVisible()) { - this.messageListViewItemOwner.threadViewOwnerAsLastMessageListViewItem.handleVisibleMessage(this.message); + if ( + this.messageListViewItemOwner && + this.messageListViewItemOwner.threadViewOwnerAsLastMessageListViewItem && + this.messageListViewItemOwner.isPartiallyVisible() + ) { + this.messageListViewItemOwner.threadViewOwnerAsLastMessageListViewItem.handleVisibleMessage( + this.message + ); } if (this.prettyBodyRef.el && this.message.prettyBody !== this.lastPrettyBody) { this.prettyBodyRef.el.innerHTML = this.message.prettyBody; @@ -134,12 +140,14 @@ Model({ // This is needed because insertReadMoreLess is working with direct DOM mutations // which are not sync with Owl. if (this.contentRef.el) { - for (const el of this.contentRef.el.querySelectorAll(':scope .o_MessageView_readMoreLess')) { + for (const el of this.contentRef.el.querySelectorAll( + ":scope .o_MessageView_readMoreLess" + )) { el.remove(); } this.update({ lastReadMoreIndex: clear() }); this.insertReadMoreLess($(this.contentRef.el)); - this.messaging.messagingBus.trigger('o-component-message-read-more-less-inserted', { + this.messaging.messagingBus.trigger("o-component-message-read-more-less-inserted", { message: this.message, }); } @@ -182,34 +190,32 @@ Model({ // nodeType 1: element_node // nodeType 3: text_node - const $children = $element.contents() - .filter((index, content) => - content.nodeType === 1 || (content.nodeType === 3 && content.nodeValue.trim()) + const $children = $element + .contents() + .filter( + (index, content) => + content.nodeType === 1 || + (content.nodeType === 3 && content.nodeValue.trim()) ); for (const child of $children) { let $child = $(child); // Hide Text nodes if "stopSpelling" - if ( - child.nodeType === 3 && - $child.prevAll('[id*="stopSpelling"]').length > 0 - ) { + if (child.nodeType === 3 && $child.prevAll('[id*="stopSpelling"]').length > 0) { // Convert Text nodes to Element nodes - $child = $('', { + $child = $("", { text: child.textContent, - 'data-o-mail-quote': '1', + "data-o-mail-quote": "1", }); child.parentNode.replaceChild($child[0], child); } // Create array for each 'read more' with nodes to toggle if ( - $child.attr('data-o-mail-quote') || - ( - $child.get(0).nodeName === 'BR' && - $child.prev('[data-o-mail-quote="1"]').length > 0 - ) + $child.attr("data-o-mail-quote") || + ($child.get(0).nodeName === "BR" && + $child.prev('[data-o-mail-quote="1"]').length > 0) ) { if (!readMoreNodes) { readMoreNodes = []; @@ -226,9 +232,9 @@ Model({ for (const group of groups) { const index = this.update({ lastReadMoreIndex: increment() }); // Insert link just before the first node - const $readMoreLess = $('', { - class: 'o_MessageView_readMoreLess d-block', - href: '#', + const $readMoreLess = $("", { + class: "o_MessageView_readMoreLess d-block", + href: "#", text: this.readMoreText, }).insertBefore(group[0]); @@ -244,7 +250,7 @@ Model({ } $readMoreLess.text(isReadMore ? this.readMoreText : this.readLessText); }; - $readMoreLess.click(e => { + $readMoreLess.click((e) => { e.preventDefault(); this.isReadMoreByIndex.set(index, !this.isReadMoreByIndex.get(index)); updateFromState(); @@ -257,7 +263,10 @@ Model({ */ replyTo() { // When already replying to this messageView, discard the reply. - if (this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.replyingToMessageView === this) { + if ( + this.messageListViewItemOwner.messageListViewOwner.threadViewOwner + .replyingToMessageView === this + ) { this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.composerView.discard(); return; } @@ -278,7 +287,10 @@ Model({ */ startEditing() { const parser = new DOMParser(); - const htmlDoc = parser.parseFromString(this.message.body.replaceAll('
', '\n').replaceAll('
', '\n'), "text/html"); + const htmlDoc = parser.parseFromString( + this.message.body.replaceAll("
", "\n").replaceAll("
", "\n"), + "text/html" + ); const textInputContent = htmlDoc.body.textContent; this.update({ composerForEditing: { @@ -286,7 +298,7 @@ Model({ textInputContent, textInputCursorEnd: textInputContent.length, textInputCursorStart: textInputContent.length, - textInputSelectionDirection: 'none', + textInputSelectionDirection: "none", }, composerViewInEditing: { doFocus: true, @@ -298,8 +310,14 @@ Model({ * Stops editing this message. */ stopEditing() { - if (this.messageListViewItemOwner && this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.composerView && !this.messaging.device.isMobileDevice) { - this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.composerView.update({ doFocus: true }); + if ( + this.messageListViewItemOwner && + this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.composerView && + !this.messaging.device.isMobileDevice + ) { + this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.composerView.update( + { doFocus: true } + ); } this.update({ composerForEditing: clear(), @@ -312,32 +330,34 @@ Model({ * Determines the attachment list displaying the attachments of this * message (if any). */ - attachmentList: one('AttachmentList', { inverse: 'messageViewOwner', + attachmentList: one("AttachmentList", { + inverse: "messageViewOwner", compute() { - return (this.message && this.message.attachments.length > 0) - ? {} - : clear(); + return this.message && this.message.attachments.length > 0 ? {} : clear(); }, }), authorTitleText: attr({ compute() { - return this.hasAuthorOpenChat ? this.env._t("Open chat") : ''; + return this.hasAuthorOpenChat ? this.env._t("Open chat") : ""; }, }), - clockWatcher: one('ClockWatcher', { default: { clock: { frequency: 60 * 1000 } }, inverse: 'messageViewOwner' }), + clockWatcher: one("ClockWatcher", { + default: { clock: { frequency: 60 * 1000 } }, + inverse: "messageViewOwner", + }), /** * States the component displaying this message view (if any). */ component: attr(), - composerForEditing: one('Composer', { inverse: 'messageViewInEditing' }), + composerForEditing: one("Composer", { inverse: "messageViewInEditing" }), /** - * Determines the composer that is used to edit this message (if any). - */ - composerViewInEditing: one('ComposerView', { inverse: 'messageViewInEditing' }), + * Determines the composer that is used to edit this message (if any). + */ + composerViewInEditing: one("ComposerView", { inverse: "messageViewInEditing" }), /** * Reference to the content of the message. */ - contentRef: attr({ ref: 'content' }), + contentRef: attr({ ref: "content" }), /** * States the time elapsed since date up to now. */ @@ -353,7 +373,7 @@ Model({ return clear(); } const now = moment(this.clockWatcher.clock.date.getTime()); - if (now.diff(this.message.momentDate, 'seconds') < 45) { + if (now.diff(this.message.momentDate, "seconds") < 45) { return this.env._t("now"); } return this.message.momentDate.fromNow(); @@ -363,7 +383,10 @@ Model({ * States the delete message confirm view that is displaying this * message view. */ - deleteMessageConfirmViewOwner: one('DeleteMessageConfirmView', { identifying: true, inverse: 'messageView' }), + deleteMessageConfirmViewOwner: one("DeleteMessageConfirmView", { + identifying: true, + inverse: "messageView", + }), /** * Determines whether this message view should be highlighted at next * render. Scrolls into view and briefly highlights it. @@ -372,20 +395,23 @@ Model({ /** * Determines which extra class this message view component should have. */ - extraClass: attr({ default: '', + extraClass: attr({ + default: "", compute() { if (this.messageListViewItemOwner) { - return 'o_MessageListView_item o_MessageListView_message'; + return "o_MessageListView_item o_MessageListView_message"; } return clear(); }, }), - failureNotificationIconClassName: attr({ default: 'fa fa-envelope', + failureNotificationIconClassName: attr({ + default: "fa fa-envelope", compute() { return clear(); }, }), - failureNotificationIconLabel: attr({ default: '', + failureNotificationIconLabel: attr({ + default: "", compute() { return clear(); }, @@ -407,8 +433,10 @@ Model({ if ( this.messageListViewItemOwner && this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread && - this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread.channel && - this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread.channel.correspondent === this.message.author + this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread + .channel && + this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread + .channel.correspondent === this.message.author ) { return false; } @@ -418,7 +446,7 @@ Model({ /** * Current timer that will reset isHighlighted to false. */ - highlightTimer: one('Timer', { inverse: 'messageViewOwnerAsHighlight' }), + highlightTimer: one("Timer", { inverse: "messageViewOwnerAsHighlight" }), /** * Whether the message is "active", ie: hovered or clicked, and should * display additional things (date in sidebar, message actions, etc.) @@ -427,9 +455,17 @@ Model({ compute() { return Boolean( this.isHovered || - this.messagingAsClickedMessageView || - (this.messageActionList && this.messageActionList.actionReaction && this.messageActionList.actionReaction.messageActionView && this.messageActionList.actionReaction.messageActionView.reactionPopoverView) || - (this.messageActionList && this.messageActionList.actionDelete && this.messageActionList.actionDelete.messageActionView && this.messageActionList.actionDelete.messageActionView.deleteConfirmDialog) + this.messagingAsClickedMessageView || + (this.messageActionList && + this.messageActionList.actionReaction && + this.messageActionList.actionReaction.messageActionView && + this.messageActionList.actionReaction.messageActionView + .reactionPopoverView) || + (this.messageActionList && + this.messageActionList.actionDelete && + this.messageActionList.actionDelete.messageActionView && + this.messageActionList.actionDelete.messageActionView + .deleteConfirmDialog) ); }, }), @@ -450,10 +486,10 @@ Model({ compute() { return Boolean( this.messageListViewItemOwner && - ( - this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.threadViewer.discuss || - this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.threadViewer.discussPublicView - ) + (this.messageListViewItemOwner.messageListViewOwner.threadViewOwner + .threadViewer.discuss || + this.messageListViewItemOwner.messageListViewOwner.threadViewOwner + .threadViewer.discussPublicView) ); }, }), @@ -464,7 +500,8 @@ Model({ compute() { return Boolean( this.messageListViewItemOwner && - this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.threadViewer.chatWindow + this.messageListViewItemOwner.messageListViewOwner.threadViewOwner + .threadViewer.chatWindow ); }, }), @@ -475,7 +512,8 @@ Model({ compute() { return Boolean( this.messageListViewItemOwner && - this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.threadViewer.chatter + this.messageListViewItemOwner.messageListViewOwner.threadViewOwner + .threadViewer.chatter ); }, }), @@ -485,9 +523,7 @@ Model({ isInChatWindowAndIsAlignedRight: attr({ compute() { return Boolean( - this.isInChatWindow && - this.message && - this.message.isCurrentUserOrGuestAuthor + this.isInChatWindow && this.message && this.message.isCurrentUserOrGuestAuthor ); }, }), @@ -497,9 +533,7 @@ Model({ isInChatWindowAndIsAlignedLeft: attr({ compute() { return Boolean( - this.isInChatWindow && - this.message && - !this.message.isCurrentUserOrGuestAuthor + this.isInChatWindow && this.message && !this.message.isCurrentUserOrGuestAuthor ); }, }), @@ -524,17 +558,11 @@ Model({ return Boolean( !( this.isInChatWindow && - ( - ( - this.message && - this.message.isCurrentUserOrGuestAuthor - ) || - ( - this.messageListViewItemOwner && + ((this.message && this.message.isCurrentUserOrGuestAuthor) || + (this.messageListViewItemOwner && this.messageListViewItemOwner.messageListViewOwner.thread.channel && - this.messageListViewItemOwner.messageListViewOwner.thread.channel.channel_type === 'chat' - ) - ) + this.messageListViewItemOwner.messageListViewOwner.thread.channel + .channel_type === "chat")) ) ); }, @@ -542,18 +570,21 @@ Model({ /** * Tells whether the message is selected in the current thread viewer. */ - isSelected: attr({ default: false, + isSelected: attr({ + default: false, compute() { return Boolean( this.messageListViewItemOwner && - this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.replyingToMessageView === this + this.messageListViewItemOwner.messageListViewOwner.threadViewOwner + .replyingToMessageView === this ); }, }), /** * Determines whether this message view should be squashed visually. */ - isSquashed: attr({ default: false, + isSquashed: attr({ + default: false, compute() { if (this.messageListViewItemOwner) { return this.messageListViewItemOwner.isSquashed; @@ -572,15 +603,17 @@ Model({ * is re-rendered. */ lastReadMoreIndex: attr({ default: 0 }), - linkPreviewListView: one('LinkPreviewListView', { inverse: 'messageViewOwner', + linkPreviewListView: one("LinkPreviewListView", { + inverse: "messageViewOwner", compute() { - return (this.message && this.message.linkPreviews.length > 0) ? {} : clear(); + return this.message && this.message.linkPreviews.length > 0 ? {} : clear(); }, }), /** * Determines the message action list of this message view (if any). */ - messageActionList: one('MessageActionList', { inverse: 'messageView', + messageActionList: one("MessageActionList", { + inverse: "messageView", compute() { return this.deleteMessageConfirmViewOwner ? clear() : {}; }, @@ -588,7 +621,9 @@ Model({ /** * Determines the message that is displayed by this message view. */ - message: one('Message', { inverse: 'messageViews', required: true, + message: one("Message", { + inverse: "messageViews", + required: true, compute() { if (this.messageListViewItemOwner) { return this.messageListViewItemOwner.message; @@ -603,44 +638,55 @@ Model({ * States the message in reply to view that displays the message of * which this message is a reply to (if any). */ - messageInReplyToView: one('MessageInReplyToView', { inverse: 'messageView', + messageInReplyToView: one("MessageInReplyToView", { + inverse: "messageView", compute() { - return ( - this.message && + return this.message && this.message.originThread && - this.message.originThread.model === 'mail.channel' && + this.message.originThread.model === "mail.channel" && this.message.parentMessage - ) ? {} : clear(); + ? {} + : clear(); }, }), - messageListViewItemOwner: one('MessageListViewItem', { identifying: true, inverse: 'messageView' }), - messageSeenIndicatorView: one('MessageSeenIndicatorView', { inverse: 'messageViewOwner', + messageListViewItemOwner: one("MessageListViewItem", { + identifying: true, + inverse: "messageView", + }), + messageSeenIndicatorView: one("MessageSeenIndicatorView", { + inverse: "messageViewOwner", compute() { if ( this.message.isCurrentUserOrGuestAuthor && this.messageListViewItemOwner && this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread && - this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread.hasSeenIndicators + this.messageListViewItemOwner.messageListViewOwner.threadViewOwner.thread + .hasSeenIndicators ) { return {}; } return clear(); }, }), - messagingAsClickedMessageView: one('Messaging', { inverse: 'clickedMessageView' }), - notificationIconClassName: attr({ default: 'fa fa-envelope-o', + messagingAsClickedMessageView: one("Messaging", { inverse: "clickedMessageView" }), + notificationIconClassName: attr({ + default: "fa fa-envelope-o", compute() { return clear(); }, }), - notificationIconLabel: attr({ default: '', + notificationIconLabel: attr({ + default: "", compute() { return clear(); }, }), - notificationIconRef: attr({ ref: 'notificationIcon' }), - notificationPopoverView: one('PopoverView', { inverse: 'messageViewOwnerAsNotificationContent' }), - personaImStatusIconView: one('PersonaImStatusIconView', { inverse: 'messageViewOwner', + notificationIconRef: attr({ ref: "notificationIcon" }), + notificationPopoverView: one("PopoverView", { + inverse: "messageViewOwnerAsNotificationContent", + }), + personaImStatusIconView: one("PersonaImStatusIconView", { + inverse: "messageViewOwner", compute() { if (this.message.guestAuthor && this.message.guestAuthor.im_status) { return {}; @@ -652,7 +698,7 @@ Model({ * Reference to element containing the prettyBody. Useful to be able to * replace prettyBody with new value in JS (which is faster than t-raw). */ - prettyBodyRef: attr({ ref: 'prettyBody' }), + prettyBodyRef: attr({ ref: "prettyBody" }), readLessText: attr({ compute() { return this.env._t("Read Less"); diff --git a/addons/mail/static/src/models/messaging.js b/addons/mail/static/src/models/messaging.js index 2311182e065..0879918d2d5 100644 --- a/addons/mail/static/src/models/messaging.js +++ b/addons/mail/static/src/models/messaging.js @@ -1,18 +1,18 @@ /** @odoo-module **/ -import { attr, clear, many, one, Patch } from '@mail/model'; +import { attr, clear, many, one, Patch } from "@mail/model"; Patch({ - name: 'Messaging', + name: "Messaging", lifecycleHooks: { _created() { this._super(); this.refreshIsNotificationPermissionDefault(); }, _willDelete() { - this.env.services['im_status'].unregisterFromImStatus('res.partner'); - this.env.services['im_status'].unregisterFromImStatus('mail.guest'); - this.env.bus.removeEventListener('window_focus', this._handleGlobalWindowFocus); + this.env.services["im_status"].unregisterFromImStatus("res.partner"); + this.env.services["im_status"].unregisterFromImStatus("mail.guest"); + this.env.bus.removeEventListener("window_focus", this._handleGlobalWindowFocus); this._super(); }, }, @@ -28,7 +28,9 @@ Patch({ let date = new Date(); for (const func of functions) { if (new Date() - date > 100) { - await new Promise(resolve => setTimeout(resolve, this.isInQUnitTest ? 0 : 50)); + await new Promise((resolve) => + setTimeout(resolve, this.isInQUnitTest ? 0 : 50) + ); date = new Date(); } await func(); @@ -48,11 +50,11 @@ Patch({ */ async getChat({ partnerId, userId }) { if (userId) { - const user = this.messaging.models['User'].insert({ id: userId }); + const user = this.messaging.models["User"].insert({ id: userId }); return user.getChat(); } if (partnerId) { - const partner = this.messaging.models['Partner'].insert({ id: partnerId }); + const partner = this.messaging.models["Partner"].insert({ id: partnerId }); return partner.getChat(); } }, @@ -64,9 +66,11 @@ Patch({ */ handleResponseNotificationPermission(value) { this.refreshIsNotificationPermissionDefault(); - if (value !== 'granted') { + if (value !== "granted") { this.userNotificationManager.sendNotification({ - message: this.env._t("Odoo will not have the permission to send native notifications on this device."), + message: this.env._t( + "Odoo will not have the permission to send native notifications on this device." + ), title: this.env._t("Permission denied"), }); } @@ -100,7 +104,7 @@ Patch({ * @returns {Promise} resolved when the form is closed */ async openActivityForm({ activity, defaultActivityTypeId, thread }) { - const targetThread = activity && activity.thread || thread; + const targetThread = (activity && activity.thread) || thread; const context = { default_res_id: targetThread.id, default_res_model: targetThread.model, @@ -109,16 +113,16 @@ Patch({ context.default_activity_type_id = defaultActivityTypeId; } const action = { - type: 'ir.actions.act_window', + type: "ir.actions.act_window", name: this.env._t("Schedule Activity"), - res_model: 'mail.activity', - view_mode: 'form', - views: [[false, 'form']], - target: 'new', + res_model: "mail.activity", + view_mode: "form", + views: [[false, "form"]], + target: "new", context, res_id: activity ? activity.id : false, }; - return new Promise(resolve => { + return new Promise((resolve) => { this.env.services.action.doAction(action, { onClose: resolve, }); @@ -151,9 +155,9 @@ Patch({ */ async openDocument({ id, model }) { this.env.services.action.doAction({ - type: 'ir.actions.act_window', + type: "ir.actions.act_window", res_model: model, - views: [[false, 'form']], + views: [[false, "form"]], res_id: id, }); if (this.messaging.device.isSmall) { @@ -173,18 +177,23 @@ Patch({ * @param {string} param0.model */ async openProfile({ id, model }) { - if (model === 'res.partner') { - const partner = this.messaging.models['Partner'].insert({ id }); + if (model === "res.partner") { + const partner = this.messaging.models["Partner"].insert({ id }); return partner.openProfile(); } - if (model === 'res.users') { - const user = this.messaging.models['User'].insert({ id }); + if (model === "res.users") { + const user = this.messaging.models["User"].insert({ id }); return user.openProfile(); } - if (model === 'mail.channel') { - let channel = this.messaging.models['Thread'].findFromIdentifyingData({ id, model: 'mail.channel' }); + if (model === "mail.channel") { + let channel = this.messaging.models["Thread"].findFromIdentifyingData({ + id, + model: "mail.channel", + }); if (!channel) { - const res = await this.messaging.models['Thread'].performRpcChannelInfo({ ids: [id] }); + const res = await this.messaging.models["Thread"].performRpcChannelInfo({ + ids: [id], + }); if (!this.exists()) { return; } @@ -193,7 +202,7 @@ Patch({ if (!channel) { this.messaging.notify({ message: this.env._t("You can only open the profile of existing channels."), - type: 'warning', + type: "warning", }); return; } @@ -210,14 +219,17 @@ Patch({ refreshIsNotificationPermissionDefault() { const browserNotification = this.messaging.browser.Notification; this.update({ - isNotificationPermissionDefault: Boolean(browserNotification) && browserNotification.permission === 'default', + isNotificationPermissionDefault: + Boolean(browserNotification) && browserNotification.permission === "default", }); }, requestNotificationPermission() { const windowNotification = this.messaging.browser.Notification; const def = windowNotification && windowNotification.requestPermission(); if (def) { - def.then((permission) => this.messaging.handleResponseNotificationPermission(permission)); + def.then((permission) => + this.messaging.handleResponseNotificationPermission(permission) + ); } }, /** @@ -226,7 +238,7 @@ Patch({ * @override */ async start() { - this.env.bus.addEventListener('window_focus', this._handleGlobalWindowFocus); + this.env.bus.addEventListener("window_focus", this._handleGlobalWindowFocus); await this.initializer.start(); if (!this.exists()) { return; @@ -238,25 +250,25 @@ Patch({ }, updateImStatusRegistration() { const partnerIds = []; - for (const partner of this.models['Partner'].all()) { - if (partner.im_status !== 'im_partner' && !partner.is_public) { + for (const partner of this.models["Partner"].all()) { + if (partner.im_status !== "im_partner" && !partner.is_public) { partnerIds.push(partner.id); } } const guestIds = []; - for (const guest of this.models['Guest'].all()) { + for (const guest of this.models["Guest"].all()) { guestIds.push(guest.id); } - this.env.services['im_status'].registerToImStatus('res.partner', partnerIds); - this.env.services['im_status'].registerToImStatus('mail.guest', guestIds); + this.env.services["im_status"].registerToImStatus("res.partner", partnerIds); + this.env.services["im_status"].registerToImStatus("mail.guest", guestIds); }, /** * @private */ _handleGlobalWindowFocus() { this.update({ outOfFocusUnreadMessageCounter: 0 }); - this.env.bus.trigger('set_title_part', { - part: '_chat', + this.env.bus.trigger("set_title_part", { + part: "_chat", }); }, /** @@ -290,39 +302,44 @@ Patch({ /** * This field contains all current client channels. */ - allCurrentClientThreads: many('Thread', { inverse: 'messagingAsAllCurrentClientThreads' }), - allMailboxes: many('Mailbox', { inverse: 'messagingAsAnyMailbox' }), - allPersonas: many('Persona', { inverse: 'messagingAsAnyPersona' }), - callInviteRequestPopups: many('CallInviteRequestPopup', { isCausal: true, + allCurrentClientThreads: many("Thread", { inverse: "messagingAsAllCurrentClientThreads" }), + allMailboxes: many("Mailbox", { inverse: "messagingAsAnyMailbox" }), + allPersonas: many("Persona", { inverse: "messagingAsAnyPersona" }), + callInviteRequestPopups: many("CallInviteRequestPopup", { + isCausal: true, compute() { if (this.ringingThreads.length === 0) { return clear(); } - return this.ringingThreads.map(thread => thread.callInviteRequestPopup); + return this.ringingThreads.map((thread) => thread.callInviteRequestPopup); }, }), - cannedResponses: many('CannedResponse'), - chatWindowManager: one('ChatWindowManager', { default: {}, isCausal: true, readonly: true }), + cannedResponses: many("CannedResponse"), + chatWindowManager: one("ChatWindowManager", { + default: {}, + isCausal: true, + readonly: true, + }), /** * Determines which message view is currently clicked, if any. */ - clickedMessageView: one('MessageView', { inverse: 'messagingAsClickedMessageView' }), - commands: many('ChannelCommand'), + clickedMessageView: one("MessageView", { inverse: "messagingAsClickedMessageView" }), + commands: many("ChannelCommand"), companyName: attr(), - currentGuest: one('Guest'), - currentPartner: one('Partner'), - currentUser: one('User'), - dialogManager: one('DialogManager', { default: {}, isCausal: true, readonly: true }), + currentGuest: one("Guest"), + currentPartner: one("Partner"), + currentUser: one("User"), + dialogManager: one("DialogManager", { default: {}, isCausal: true, readonly: true }), /** * Determines whether animations should be disabled. */ disableAnimation: attr({ default: false }), - discuss: one('Discuss', { default: {}, isCausal: true, readonly: true }), - emojiRegistry: one('EmojiRegistry', { default: {}, isCausal: true, readonly: true }), + discuss: one("Discuss", { default: {}, isCausal: true, readonly: true }), + emojiRegistry: one("EmojiRegistry", { default: {}, isCausal: true, readonly: true }), hasLinkPreviewFeature: attr(), - history: one('Mailbox', { default: {}, inverse: 'messagingAsHistory' }), - inbox: one('Mailbox', { default: {}, inverse: 'messagingAsInbox' }), - initializer: one('MessagingInitializer', { default: {}, isCausal: true, readonly: true }), + history: one("Mailbox", { default: {}, inverse: "messagingAsHistory" }), + inbox: one("Mailbox", { default: {}, inverse: "messagingAsInbox" }), + initializer: one("MessagingInitializer", { default: {}, isCausal: true, readonly: true }), internalUserGroupId: attr(), isCurrentUserGuest: attr({ compute() { @@ -335,7 +352,7 @@ Patch({ const windowNotification = this.browser.Notification; return ( windowNotification && - windowNotification.permission !== 'granted' && + windowNotification.permission !== "granted" && !this.isNotificationPermissionDefault ); }, @@ -351,43 +368,49 @@ Patch({ * shown. Useful to avoid flicker for almost instant loading. */ loadingBaseDelayDuration: attr({ default: 400 }), - messagingMenu: one('MessagingMenu', { default: {}, isCausal: true }), - notificationHandler: one('MessagingNotificationHandler', { isCausal: true, + messagingMenu: one("MessagingMenu", { default: {}, isCausal: true }), + notificationHandler: one("MessagingNotificationHandler", { + isCausal: true, compute() { return {}; }, }), outOfFocusUnreadMessageCounter: attr({ default: 0 }), - partnerRoot: one('Partner'), - popoverManager: one('PopoverManager', { default: {}, isCausal: true, readonly: true }), + partnerRoot: one("Partner"), + popoverManager: one("PopoverManager", { default: {}, isCausal: true, readonly: true }), /** * Threads for which the current partner has a pending invitation. * It is computed from the inverse relation for performance reasons. */ - ringingThreads: many('Thread', { inverse: 'messagingAsRingingThread' }), - rtc: one('Rtc', { default: {}, isCausal: true, readonly: true }), - soundEffects: one('SoundEffects', { default: {}, isCausal: true, readonly: true }), - starred: one('Mailbox', { default: {}, inverse: 'messagingAsStarred' }), - updateImStatusRegisterThrottle: one('Throttle', { inverse: 'messagingAsUpdateImStatusRegister', + ringingThreads: many("Thread", { inverse: "messagingAsRingingThread" }), + rtc: one("Rtc", { default: {}, isCausal: true, readonly: true }), + soundEffects: one("SoundEffects", { default: {}, isCausal: true, readonly: true }), + starred: one("Mailbox", { default: {}, inverse: "messagingAsStarred" }), + updateImStatusRegisterThrottle: one("Throttle", { + inverse: "messagingAsUpdateImStatusRegister", compute() { return { func: this.updateImStatusRegistration }; }, }), - userNotificationManager: one('UserNotificationManager', { default: {}, isCausal: true, readonly: true }), - userSetting: one('UserSetting', { default: {}, isCausal: true }), + userNotificationManager: one("UserNotificationManager", { + default: {}, + isCausal: true, + readonly: true, + }), + userSetting: one("UserSetting", { default: {}, isCausal: true }), }, onChanges: [ { - dependencies: ['ringingThreads'], - methodName: '_onChangeRingingThreads', + dependencies: ["ringingThreads"], + methodName: "_onChangeRingingThreads", }, { - dependencies: ['allCurrentClientThreads'], - methodName: '_onChangeAllCurrentClientThreads', + dependencies: ["allCurrentClientThreads"], + methodName: "_onChangeAllCurrentClientThreads", }, { - dependencies: ['allPersonas'], - methodName: '_onChangeAllPersonas', + dependencies: ["allPersonas"], + methodName: "_onChangeAllPersonas", }, ], }); diff --git a/addons/mail/static/src/models/messaging_initializer.js b/addons/mail/static/src/models/messaging_initializer.js index d2215894d05..6ca2c140133 100644 --- a/addons/mail/static/src/models/messaging_initializer.js +++ b/addons/mail/static/src/models/messaging_initializer.js @@ -1,17 +1,20 @@ /** @odoo-module **/ -import { insert, Model } from '@mail/model'; +import { insert, Model } from "@mail/model"; Model({ - name: 'MessagingInitializer', + name: "MessagingInitializer", recordMethods: { /** * @returns {Object} */ async performInitRpc() { - return await this.messaging.rpc({ - route: '/mail/init_messaging', - }, { shadow: true }); + return await this.messaging.rpc( + { + route: "/mail/init_messaging", + }, + { shadow: true } + ); }, /** * Fetch messaging data initially to populate the store specifically for @@ -83,7 +86,7 @@ Model({ }); // init mail user settings if (current_user_settings) { - this.messaging.models['res.users.settings'].insert(current_user_settings); + this.messaging.models["res.users.settings"].insert(current_user_settings); } // various suggestions in no particular order this._initCannedResponses(shortcodes); @@ -116,20 +119,22 @@ Model({ * @param {Object[]} channelsData */ async _initChannels(channelsData) { - return this.messaging.executeGracefully(channelsData.map(channelData => () => { - if (!this.exists()) { - return; - } - const convertedData = this.messaging.models['Thread'].convertData(channelData); - const channel = this.messaging.models['Thread'].insert( - Object.assign({ model: 'mail.channel' }, convertedData) - ); - // flux specific: channels received at init have to be - // considered pinned. task-2284357 - if (!channel.isPinned) { - channel.pin(); - } - })); + return this.messaging.executeGracefully( + channelsData.map((channelData) => () => { + if (!this.exists()) { + return; + } + const convertedData = this.messaging.models["Thread"].convertData(channelData); + const channel = this.messaging.models["Thread"].insert( + Object.assign({ model: "mail.channel" }, convertedData) + ); + // flux specific: channels received at init have to be + // considered pinned. task-2284357 + if (!channel.isPinned) { + channel.pin(); + } + }) + ); }, /** * @private @@ -139,20 +144,20 @@ Model({ commands: insert([ { help: this.env._t("Show a helper message"), - methodName: 'execute_command_help', + methodName: "execute_command_help", name: "help", }, { help: this.env._t("Leave this channel"), - methodName: 'execute_command_leave', + methodName: "execute_command_leave", name: "leave", }, { - channel_types: ['channel', 'chat', 'group'], + channel_types: ["channel", "chat", "group"], help: this.env._t("List users in the current channel"), - methodName: 'execute_command_who', + methodName: "execute_command_who", name: "who", - } + }, ]), }); }, @@ -162,10 +167,7 @@ Model({ * @param {integer} param0.needaction_inbox_counter * @param {integer} param0.starred_counter */ - _initMailboxes({ - needaction_inbox_counter, - starred_counter, - }) { + _initMailboxes({ needaction_inbox_counter, starred_counter }) { this.messaging.inbox.update({ counter: needaction_inbox_counter }); this.messaging.starred.update({ counter: starred_counter }); }, @@ -174,19 +176,21 @@ Model({ * @param {Object[]} mailFailuresData */ async _initMailFailures(mailFailuresData) { - await this.messaging.executeGracefully(mailFailuresData.map(messageData => () => { - if (!this.exists()) { - return; - } - const message = this.messaging.models['Message'].insert( - this.messaging.models['Message'].convertData(messageData) - ); - // implicit: failures are sent by the server at initialization - // only if the current partner is author of the message - if (!message.author && this.messaging.currentPartner) { - message.update({ author: this.messaging.currentPartner }); - } - })); + await this.messaging.executeGracefully( + mailFailuresData.map((messageData) => () => { + if (!this.exists()) { + return; + } + const message = this.messaging.models["Message"].insert( + this.messaging.models["Message"].convertData(messageData) + ); + // implicit: failures are sent by the server at initialization + // only if the current partner is author of the message + if (!message.author && this.messaging.currentPartner) { + message.update({ author: this.messaging.currentPartner }); + } + }) + ); }, /** * @private @@ -220,9 +224,12 @@ Model({ * @private */ async _loadMessageFailures() { - const data = await this.messaging.rpc({ - route: '/mail/load_message_failures', - }, { shadow: true }); + const data = await this.messaging.rpc( + { + route: "/mail/load_message_failures", + }, + { shadow: true } + ); this._initMailFailures(data); }, }, diff --git a/addons/mail/static/src/models/messaging_menu.js b/addons/mail/static/src/models/messaging_menu.js index 3cf2d719087..f336f8c47b8 100644 --- a/addons/mail/static/src/models/messaging_menu.js +++ b/addons/mail/static/src/models/messaging_menu.js @@ -1,20 +1,20 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { attr, clear, many, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'MessagingMenu', - template: 'mail.MessagingMenu', + name: "MessagingMenu", + template: "mail.MessagingMenu", componentSetup() { - useComponentToModel({ fieldName: 'component' }); + useComponentToModel({ fieldName: "component" }); }, lifecycleHooks: { _created() { - document.addEventListener('click', this._onClickCaptureGlobal, true); + document.addEventListener("click", this._onClickCaptureGlobal, true); }, _willDelete() { - document.removeEventListener('click', this._onClickCaptureGlobal, true); + document.removeEventListener("click", this._onClickCaptureGlobal, true); }, }, recordMethods: { @@ -66,16 +66,16 @@ Model({ */ onMobileNewMessageInputSource(req, res) { const value = _.escape(req.term); - this.messaging.models['Partner'].imSearch({ - callback: partners => { - const suggestions = partners.map(partner => { + this.messaging.models["Partner"].imSearch({ + callback: (partners) => { + const suggestions = partners.map((partner) => { return { id: partner.id, value: partner.nameOrDisplayName, label: partner.nameOrDisplayName, }; }); - res(_.sortBy(suggestions, 'label')); + res(_.sortBy(suggestions, "label")); }, keyword: value, limit: 10, @@ -121,14 +121,14 @@ Model({ }, }, fields: { - activeTab: one('MessagingMenuTabView', { + activeTab: one("MessagingMenuTabView", { compute() { switch (this.activeTabId) { - case 'all': + case "all": return this.allTab; - case 'channel': + case "channel": return this.channelTab; - case 'chat': + case "chat": return this.chatTab; } }, @@ -137,8 +137,9 @@ Model({ * Tab selected in the messaging menu. * Either 'all', 'chat' or 'channel'. */ - activeTabId: attr({ default: 'all' }), - allTab: one('MessagingMenuTabView', { inverse: 'ownerAsAll', + activeTabId: attr({ default: "all" }), + allTab: one("MessagingMenuTabView", { + inverse: "ownerAsAll", compute() { if (this.isOpen && this.messaging.isInitialized && !this.messaging.device.isSmall) { return {}; @@ -146,7 +147,8 @@ Model({ return clear(); }, }), - channelTab: one('MessagingMenuTabView', { inverse: 'ownerAsChannel', + channelTab: one("MessagingMenuTabView", { + inverse: "ownerAsChannel", compute() { if (this.isOpen && this.messaging.isInitialized && !this.messaging.device.isSmall) { return {}; @@ -154,7 +156,8 @@ Model({ return clear(); }, }), - chatTab: one('MessagingMenuTabView', { inverse: 'ownerAsChat', + chatTab: one("MessagingMenuTabView", { + inverse: "ownerAsChat", compute() { if (this.isOpen && this.messaging.isInitialized && !this.messaging.device.isSmall) { return {}; @@ -175,12 +178,18 @@ Model({ } const inboxCounter = this.messaging.inbox ? this.messaging.inbox.counter : 0; const unreadChannelsCounter = this.pinnedAndUnreadChannels.length; - const notificationGroupsCounter = this.messaging.models['NotificationGroup'].all().reduce( - (total, group) => total + group.notifications.length, - 0 + const notificationGroupsCounter = this.messaging.models["NotificationGroup"] + .all() + .reduce((total, group) => total + group.notifications.length, 0); + const notificationPemissionCounter = this.messaging.isNotificationPermissionDefault + ? 1 + : 0; + return ( + inboxCounter + + unreadChannelsCounter + + notificationGroupsCounter + + notificationPemissionCounter ); - const notificationPemissionCounter = this.messaging.isNotificationPermissionDefault ? 1 : 0; - return inboxCounter + unreadChannelsCounter + notificationGroupsCounter + notificationPemissionCounter; }, }), /** @@ -191,7 +200,8 @@ Model({ * Determine whether the messaging menu dropdown is open or not. */ isOpen: attr({ default: false }), - notificationListView: one('NotificationListView', { inverse: 'messagingMenuOwner', + notificationListView: one("NotificationListView", { + inverse: "messagingMenuOwner", compute() { return this.isOpen ? {} : clear(); }, @@ -199,7 +209,8 @@ Model({ /** * The navbar view on the messaging menu when in mobile. */ - mobileMessagingNavbarView: one('MobileMessagingNavbarView', { inverse: 'messagingMenu', + mobileMessagingNavbarView: one("MobileMessagingNavbarView", { + inverse: "messagingMenu", compute() { if (this.messaging.device && this.messaging.device.isSmall) { return {}; @@ -207,9 +218,15 @@ Model({ return clear(); }, }), - mobileNewMessageAutocompleteInputView: one('AutocompleteInputView', { inverse: 'messagingMenuOwnerAsMobileNewMessageInput', + mobileNewMessageAutocompleteInputView: one("AutocompleteInputView", { + inverse: "messagingMenuOwnerAsMobileNewMessageInput", compute() { - if (this.isOpen && this.messaging.isInitialized && this.messaging.device.isSmall && this.isMobileNewMessageToggled) { + if ( + this.isOpen && + this.messaging.isInitialized && + this.messaging.device.isSmall && + this.isMobileNewMessageToggled + ) { return {}; } return clear(); @@ -223,7 +240,10 @@ Model({ /** * States all the pinned channels that have unread messages. */ - pinnedAndUnreadChannels: many('Thread', { inverse: 'messagingMenuAsPinnedAndUnreadChannel', readonly: true }), + pinnedAndUnreadChannels: many("Thread", { + inverse: "messagingMenuAsPinnedAndUnreadChannel", + readonly: true, + }), /** * global JS generated ID for this record view. Useful to provide a * custom class to autocomplete input, so that click in an autocomplete @@ -231,7 +251,7 @@ Model({ */ viewId: attr({ compute() { - return _.uniqueId('o_messagingMenu_'); + return _.uniqueId("o_messagingMenu_"); }, }), }, diff --git a/addons/mail/static/src/models/messaging_menu_tab_view.js b/addons/mail/static/src/models/messaging_menu_tab_view.js index e51bc4fcb1d..bf16f3ec1b3 100644 --- a/addons/mail/static/src/models/messaging_menu_tab_view.js +++ b/addons/mail/static/src/models/messaging_menu_tab_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'MessagingMenuTabView', - template: 'mail.MessagingMenuTabView', - identifyingMode: 'xor', + name: "MessagingMenuTabView", + template: "mail.MessagingMenuTabView", + identifyingMode: "xor", recordMethods: { /** * @param {MouseEvent} ev @@ -19,16 +19,17 @@ Model({ * Note: when possible, better use the relations with MessagingMenu * rather than these hardcoded IDs. */ - id: attr({ required: true, + id: attr({ + required: true, compute() { if (this.ownerAsAll) { - return 'all'; + return "all"; } if (this.ownerAsChannel) { - return 'channel'; + return "channel"; } if (this.ownerAsChat) { - return 'chat'; + return "chat"; } }, }), @@ -37,7 +38,8 @@ Model({ return this.messaging.messagingMenu.activeTab === this; }, }), - name: attr({ required: true, + name: attr({ + required: true, compute() { if (this.ownerAsAll) { return this.env._t("All"); @@ -50,8 +52,8 @@ Model({ } }, }), - ownerAsAll: one('MessagingMenu', { identifying: true, inverse: 'allTab' }), - ownerAsChannel: one('MessagingMenu', { identifying: true, inverse: 'channelTab' }), - ownerAsChat: one('MessagingMenu', { identifying: true, inverse: 'chatTab' }), + ownerAsAll: one("MessagingMenu", { identifying: true, inverse: "allTab" }), + ownerAsChannel: one("MessagingMenu", { identifying: true, inverse: "channelTab" }), + ownerAsChat: one("MessagingMenu", { identifying: true, inverse: "chatTab" }), }, }); diff --git a/addons/mail/static/src/models/messaging_notification_handler.js b/addons/mail/static/src/models/messaging_notification_handler.js index 3b0b2091025..92fcad609ef 100644 --- a/addons/mail/static/src/models/messaging_notification_handler.js +++ b/addons/mail/static/src/models/messaging_notification_handler.js @@ -1,19 +1,22 @@ /** @odoo-module **/ -import { decrement, increment, insert, Model } from '@mail/model'; -import { htmlToTextContentInline } from '@mail/js/utils'; +import { decrement, increment, insert, Model } from "@mail/model"; +import { htmlToTextContentInline } from "@mail/js/utils"; -import { escape, sprintf } from '@web/core/utils/strings'; -import { str_to_datetime } from 'web.time'; -import { Markup } from 'web.utils'; +import { escape, sprintf } from "@web/core/utils/strings"; +import { str_to_datetime } from "web.time"; +import { Markup } from "web.utils"; const PREVIEW_MSG_MAX_SIZE = 350; // optimal for native English speakers Model({ - name: 'MessagingNotificationHandler', + name: "MessagingNotificationHandler", lifecycleHooks: { _willDelete() { - this.env.services['bus_service'].removeEventListener('notification', this._handleNotifications); + this.env.services["bus_service"].removeEventListener( + "notification", + this._handleNotifications + ); }, }, recordMethods: { @@ -22,7 +25,10 @@ Model({ * the current users. This includes pinned channels for instance. */ start() { - this.env.services['bus_service'].addEventListener('notification', this._handleNotifications); + this.env.services["bus_service"].addEventListener( + "notification", + this._handleNotifications + ); }, /** * @private @@ -40,24 +46,26 @@ Model({ async _handleNotifications({ detail: notifications }) { const channelsLeft = new Set( notifications - .filter(notification => notification.type === 'mail.channel/leave') - .map(notification => notification.payload.id) + .filter((notification) => notification.type === "mail.channel/leave") + .map((notification) => notification.payload.id) ); - const proms = notifications.map(message => { - if (typeof message === 'object') { + const proms = notifications.map((message) => { + if (typeof message === "object") { switch (message.type) { - case 'mail.record/insert': + case "mail.record/insert": this.messaging.modelManager.multiModelInsert(message.payload); break; - case 'ir.attachment/delete': + case "ir.attachment/delete": return this._handleNotificationAttachmentDelete(message.payload); - case 'mail.channel.member/seen': + case "mail.channel.member/seen": return this._handleNotificationChannelMemberSeen(message.payload); - case 'mail.channel.member/fetched': + case "mail.channel.member/fetched": return this._handleNotificationChannelMemberFetched(message.payload); - case 'mail.channel.member/typing_status': - return this._handleNotificationChannelMemberTypingStatus(message.payload); - case 'mail.channel/new_message': + case "mail.channel.member/typing_status": + return this._handleNotificationChannelMemberTypingStatus( + message.payload + ); + case "mail.channel/new_message": if (channelsLeft.has(message.payload.id)) { /** * `_handleNotificationChannelMessage` tries to pin the channel, @@ -70,33 +78,39 @@ Model({ return; } return this._handleNotificationChannelMessage(message.payload); - case 'mail.link.preview/delete': { - const linkPreview = this.messaging.models['LinkPreview'].findFromIdentifyingData(message.payload); + case "mail.link.preview/delete": { + const linkPreview = this.messaging.models[ + "LinkPreview" + ].findFromIdentifyingData(message.payload); if (linkPreview) { linkPreview.delete(); } return; } - case 'mail.message/delete': + case "mail.message/delete": return this._handleNotificationMessageDelete(message.payload); - case 'mail.message/inbox': + case "mail.message/inbox": return this._handleNotificationNeedaction(message.payload); - case 'mail.message/mark_as_read': + case "mail.message/mark_as_read": return this._handleNotificationPartnerMarkAsRead(message.payload); - case 'mail.message/notification_update': - return this._handleNotificationPartnerMessageNotificationUpdate(message.payload); - case 'simple_notification': + case "mail.message/notification_update": + return this._handleNotificationPartnerMessageNotificationUpdate( + message.payload + ); + case "simple_notification": return this._handleNotificationSimpleNotification(message.payload); - case 'mail.message/toggle_star': + case "mail.message/toggle_star": return this._handleNotificationPartnerToggleStar(message.payload); - case 'mail.channel/transient_message': + case "mail.channel/transient_message": return this._handleNotificationPartnerTransientMessage(message.payload); - case 'mail.channel/leave': + case "mail.channel/leave": return this._handleNotificationChannelLeave(message.payload); - case 'res.users/connection': + case "res.users/connection": return this._handleNotificationPartnerUserConnection(message.payload); - case 'mail.activity/updated': { - for (const activityMenuView of this.messaging.models['ActivityMenuView'].all()) { + case "mail.activity/updated": { + for (const activityMenuView of this.messaging.models[ + "ActivityMenuView" + ].all()) { if (message.payload.activity_created) { activityMenuView.update({ extraCount: increment() }); } @@ -106,19 +120,26 @@ Model({ } return; } - case 'mail.channel/unpin': + case "mail.channel/unpin": return this._handleNotificationChannelUnpin(message.payload); - case 'mail.channel/joined': + case "mail.channel/joined": return this._handleNotificationChannelJoined(message.payload); - case 'mail.channel/last_interest_dt_changed': - return this._handleNotificationChannelLastInterestDateTimeChanged(message.payload); - case 'mail.channel/legacy_insert': - return this.messaging.models['Thread'].insert(this.messaging.models['Thread'].convertData({ model: 'mail.channel', ...message.payload })); - case 'mail.channel.rtc.session/peer_notification': + case "mail.channel/last_interest_dt_changed": + return this._handleNotificationChannelLastInterestDateTimeChanged( + message.payload + ); + case "mail.channel/legacy_insert": + return this.messaging.models["Thread"].insert( + this.messaging.models["Thread"].convertData({ + model: "mail.channel", + ...message.payload, + }) + ); + case "mail.channel.rtc.session/peer_notification": return this._handleNotificationRtcPeerToPeer(message.payload); - case 'mail.channel/rtc_sessions_update': + case "mail.channel/rtc_sessions_update": return this._handleNotificationRtcSessionUpdate(message.payload); - case 'mail.channel.rtc.session/ended': + case "mail.channel.rtc.session/ended": return this._handleNotificationRtcSessionEnded(message.payload); default: return this._handleNotification(message); @@ -145,9 +166,9 @@ Model({ * @param {integer} [payload.id] */ _handleNotificationAttachmentDelete(payload) { - const attachment = this.messaging.models['Attachment'].findFromIdentifyingData(payload); + const attachment = this.messaging.models["Attachment"].findFromIdentifyingData(payload); if (attachment) { - this.messaging.messagingBus.trigger('o-attachment-deleted', { attachment }); + this.messaging.messagingBus.trigger("o-attachment-deleted", { attachment }); attachment.delete(); } }, @@ -163,22 +184,24 @@ Model({ last_message_id, partner_id, }) { - const channel = this.messaging.models['Channel'].findFromIdentifyingData({ id: channelId }); + const channel = this.messaging.models["Channel"].findFromIdentifyingData({ + id: channelId, + }); if (!channel) { // for example seen from another browser, the current one has no // knowledge of the channel return; } - if (channel.channel_type === 'channel') { + if (channel.channel_type === "channel") { // disabled on `channel` channels for performance reasons return; } - this.messaging.models['ThreadPartnerSeenInfo'].insert({ + this.messaging.models["ThreadPartnerSeenInfo"].insert({ lastFetchedMessage: insert({ id: last_message_id }), partner: { id: partner_id }, thread: channel.thread, }); - this.messaging.models['MessageSeenIndicator'].insert({ + this.messaging.models["MessageSeenIndicator"].insert({ message: { id: last_message_id }, thread: channel.thread, }); @@ -190,8 +213,14 @@ Model({ * @param {integer} [payload.invited_by_user_id] * @param {boolean} [payload.open_chat_window] if true, will pin the channel */ - _handleNotificationChannelJoined({ channel: channelData, invited_by_user_id: invitedByUserId, open_chat_window: openChatWindow }) { - const thread = this.messaging.models['Thread'].insert(this.messaging.models['Thread'].convertData(channelData)); + _handleNotificationChannelJoined({ + channel: channelData, + invited_by_user_id: invitedByUserId, + open_chat_window: openChatWindow, + }) { + const thread = this.messaging.models["Thread"].insert( + this.messaging.models["Thread"].convertData(channelData) + ); if (this.messaging.currentUser && invitedByUserId !== this.messaging.currentUser.id) { // Current user was invited by someone else. this.messaging.notify({ @@ -199,13 +228,17 @@ Model({ this.env._t("You have been invited to #%s"), thread.displayName ), - type: 'info', + type: "info", }); } if (openChatWindow) { // open chat upon being invited (if it was not already opened or folded) - if (thread.channel.channel_type !== 'channel' && !this.messaging.device.isSmall && !thread.chatWindow) { + if ( + thread.channel.channel_type !== "channel" && + !this.messaging.device.isSmall && + !thread.chatWindow + ) { this.messaging.chatWindowManager.openThread(thread); } } @@ -217,10 +250,14 @@ Model({ * @param {boolean} payload.isServerPinned * @param {string} payload.last_interest_dt */ - _handleNotificationChannelLastInterestDateTimeChanged({ id, isServerPinned, last_interest_dt }) { - const channel = this.messaging.models['Thread'].findFromIdentifyingData({ + _handleNotificationChannelLastInterestDateTimeChanged({ + id, + isServerPinned, + last_interest_dt, + }) { + const channel = this.messaging.models["Thread"].findFromIdentifyingData({ id: id, - model: 'mail.channel', + model: "mail.channel", }); if (channel) { channel.update({ @@ -236,11 +273,13 @@ Model({ * @param {Object} payload.messageData */ async _handleNotificationChannelMessage({ id: channelId, message: messageData }) { - let channel = this.messaging.models['Channel'].findFromIdentifyingData({ id: channelId }); + let channel = this.messaging.models["Channel"].findFromIdentifyingData({ + id: channelId, + }); if (!channel && this.messaging.isCurrentUserGuest) { return; // guests should not receive messages for channels they don't know, and they can't make the channel_info RPC } - const convertedData = this.messaging.models['Message'].convertData(messageData); + const convertedData = this.messaging.models["Message"].convertData(messageData); // Fetch missing info from channel before going further. Inserting // a channel with incomplete info can lead to issues. This is in @@ -248,7 +287,9 @@ Model({ // "required" by the rest of the code and is necessary for some // features such as chat windows. if (!channel) { - const res = await this.messaging.models['Thread'].performRpcChannelInfo({ ids: [channelId] }); + const res = await this.messaging.models["Thread"].performRpcChannelInfo({ + ids: [channelId], + }); if (!this.exists()) { return; } @@ -258,7 +299,7 @@ Model({ channel.thread.pin(); } - const message = this.messaging.models['Message'].insert(convertedData); + const message = this.messaging.models["Message"].insert(convertedData); this._notifyThreadViewsMessageReceived(message); // If the current partner is author, do nothing else. @@ -268,12 +309,10 @@ Model({ // Chat from OdooBot is considered disturbing and should only be // shown on the menu, but no notification and no thread open. - const isChatWithOdooBot = ( - channel.correspondent && - channel.correspondent === this.messaging.partnerRoot - ); + const isChatWithOdooBot = + channel.correspondent && channel.correspondent === this.messaging.partnerRoot; if (!isChatWithOdooBot) { - const isOdooFocused = this.env.services['presence'].isOdooFocused(); + const isOdooFocused = this.env.services["presence"].isOdooFocused(); // Notify if out of focus if (!isOdooFocused && channel.thread.isChatChannel) { this._notifyNewChannelMessageWhileOutOfFocus({ @@ -281,13 +320,17 @@ Model({ message, }); } - if (channel.channel_type !== 'channel' && !this.messaging.currentGuest) { + if (channel.channel_type !== "channel" && !this.messaging.currentGuest) { // disabled on non-channel threads and // on `channel` channels for performance reasons channel.thread.markAsFetched(); } // open chat on receiving new message if it was not already opened or folded - if (channel.channel_type !== 'channel' && !this.messaging.device.isSmall && !channel.thread.chatWindow) { + if ( + channel.channel_type !== "channel" && + !this.messaging.device.isSmall && + !channel.thread.chatWindow + ) { this.messaging.chatWindowManager.openThread(channel.thread); } } @@ -307,23 +350,25 @@ Model({ last_message_id, partner_id, }) { - const channel = this.messaging.models['Channel'].findFromIdentifyingData({ id: channelId }); + const channel = this.messaging.models["Channel"].findFromIdentifyingData({ + id: channelId, + }); if (!channel) { // for example seen from another browser, the current one has no // knowledge of the channel return; } - const lastMessage = this.messaging.models['Message'].insert({ id: last_message_id }); + const lastMessage = this.messaging.models["Message"].insert({ id: last_message_id }); // restrict computation of seen indicator for "non-channel" channels // for performance reasons - const shouldComputeSeenIndicators = channel.channel_type !== 'channel'; + const shouldComputeSeenIndicators = channel.channel_type !== "channel"; if (shouldComputeSeenIndicators) { - this.messaging.models['ThreadPartnerSeenInfo'].insert({ + this.messaging.models["ThreadPartnerSeenInfo"].insert({ lastSeenMessage: lastMessage, partner: { id: partner_id }, thread: channel.thread, }); - this.messaging.models['MessageSeenIndicator'].insert({ + this.messaging.models["MessageSeenIndicator"].insert({ message: lastMessage, thread: channel.thread, }); @@ -340,7 +385,7 @@ Model({ * @param {Object} channelMemberData */ _handleNotificationChannelMemberTypingStatus(channelMemberData) { - const member = this.messaging.models['ChannelMember'].insert(channelMemberData); + const member = this.messaging.models["ChannelMember"].insert(channelMemberData); if (member.isMemberOfCurrentUser) { // Ignore management of current persona is typing notification. return; @@ -365,8 +410,8 @@ Model({ * @param {Object} data */ _handleNotificationNeedaction(data) { - const message = this.messaging.models['Message'].insert( - this.messaging.models['Message'].convertData(data) + const message = this.messaging.models["Message"].insert( + this.messaging.models["Message"].convertData(data) ); this.messaging.inbox.update({ counter: increment() }); const originThread = message.originThread; @@ -394,12 +439,18 @@ Model({ * @param {string} param1.title * @param {boolean} param1.warning */ - _handleNotificationSimpleNotification({ message, message_is_html, sticky, title, warning }) { + _handleNotificationSimpleNotification({ + message, + message_is_html, + sticky, + title, + warning, + }) { this.messaging.notify({ message: message_is_html ? Markup(message) : message, sticky, title, - type: warning ? 'warning' : 'danger', + type: warning ? "warning" : "danger", }); }, /** @@ -413,7 +464,7 @@ Model({ this.messaging.rtc.channel.endCall(); this.messaging.notify({ message: this.env._t("Disconnected from the RTC call by the server"), - type: 'warning', + type: "warning", }); } }, @@ -424,7 +475,10 @@ Model({ * @param {Object} [data.rtcSessions] */ async _handleNotificationRtcSessionUpdate({ id, rtcSessions }) { - const channel = this.messaging.models['Thread'].findFromIdentifyingData({ id, model: 'mail.channel' }); + const channel = this.messaging.models["Thread"].findFromIdentifyingData({ + id, + model: "mail.channel", + }); if (!channel) { return; } @@ -439,7 +493,7 @@ Model({ let deletedInboxMsg = 0; let deletedStarredMsg = 0; for (const id of message_ids) { - const message = this.messaging.models['Message'].findFromIdentifyingData({ id }); + const message = this.messaging.models["Message"].findFromIdentifyingData({ id }); if (message) { if (message.isNeedaction) { deletedInboxMsg++; @@ -463,8 +517,8 @@ Model({ */ _handleNotificationPartnerMessageNotificationUpdate({ elements }) { for (const messageData of elements) { - const message = this.messaging.models['Message'].insert( - this.messaging.models['Message'].convertData(messageData) + const message = this.messaging.models["Message"].insert( + this.messaging.models["Message"].convertData(messageData) ); // implicit: failures are sent by the server as notification // only if the current partner is author of the message @@ -486,7 +540,9 @@ Model({ // Furthermore, server should not send back all message_ids marked as read // but something like last read message_id or something like that. // (just imagine you mark 1000 messages as read ... ) - const message = this.messaging.models['Message'].findFromIdentifyingData({ id: message_id }); + const message = this.messaging.models["Message"].findFromIdentifyingData({ + id: message_id, + }); if (!message) { continue; } @@ -524,7 +580,7 @@ Model({ _handleNotificationPartnerToggleStar({ message_ids = [], starred }) { const starredMailbox = this.messaging.starred; for (const messageId of message_ids) { - const message = this.messaging.models['Message'].findFromIdentifyingData({ + const message = this.messaging.models["Message"].findFromIdentifyingData({ id: messageId, }); if (!message) { @@ -545,17 +601,18 @@ Model({ * @param {Object} data */ _handleNotificationPartnerTransientMessage(data) { - const convertedData = this.messaging.models['Message'].convertData(data); - const lastMessageId = this.messaging.models['Message'].all().reduce( - (lastMessageId, message) => Math.max(lastMessageId, message.id), - 0 - ); + const convertedData = this.messaging.models["Message"].convertData(data); + const lastMessageId = this.messaging.models["Message"] + .all() + .reduce((lastMessageId, message) => Math.max(lastMessageId, message.id), 0); const partnerRoot = this.messaging.partnerRoot; - const message = this.messaging.models['Message'].insert(Object.assign(convertedData, { - author: partnerRoot, - id: lastMessageId + 0.01, - isTransient: true, - })); + const message = this.messaging.models["Message"].insert( + Object.assign(convertedData, { + author: partnerRoot, + id: lastMessageId + 0.01, + isTransient: true, + }) + ); this._notifyThreadViewsMessageReceived(message); }, /** @@ -564,15 +621,15 @@ Model({ * @param {integer} payload.id */ _handleNotificationChannelLeave({ id }) { - const thread = this.messaging.models['Thread'].findFromIdentifyingData({ + const thread = this.messaging.models["Thread"].findFromIdentifyingData({ id, - model: 'mail.channel', + model: "mail.channel", }); if (!thread) { return; } const message = sprintf(this.env._t("You unsubscribed from %s."), thread.displayName); - this.messaging.notify({ message, type: 'info' }); + this.messaging.notify({ message, type: "info" }); // We assume that arriving here the server has effectively // unpinned the channel thread.update({ @@ -588,15 +645,18 @@ Model({ * @param {integer} payload.id */ _handleNotificationChannelUnpin({ id }) { - const thread = this.messaging.models['Thread'].findFromIdentifyingData({ + const thread = this.messaging.models["Thread"].findFromIdentifyingData({ id, - model: 'mail.channel', + model: "mail.channel", }); if (!thread) { return; } - const message = sprintf(this.env._t("You unpinned your conversation with %s."), thread.displayName); - this.messaging.notify({ message, type: 'info' }); + const message = sprintf( + this.env._t("You unpinned your conversation with %s."), + thread.displayName + ); + this.messaging.notify({ message, type: "info" }); // We assume that arriving here the server has effectively // unpinned the channel thread.update({ @@ -616,9 +676,13 @@ Model({ // If the current user invited a new user, and the new user is // connecting for the first time while the current user is present // then open a chat for the current user with the new user. - const message = sprintf(this.env._t('%s connected'), username); + const message = sprintf(this.env._t("%s connected"), username); const title = this.env._t("This is their first connection. Wish them luck."); - this.messaging.userNotificationManager.sendNotification({ message, title, type: 'info' }); + this.messaging.userNotificationManager.sendNotification({ + message, + title, + type: "info", + }); const chat = await this.messaging.getChat({ partnerId }); if (!this.exists() || !chat || this.messaging.device.isSmall) { return; @@ -638,12 +702,12 @@ Model({ if (!author) { notificationTitle = this.env._t("New message"); } else { - if (channel.channel_type === 'channel') { + if (channel.channel_type === "channel") { notificationTitle = sprintf( this.env._t("%(author name)s from %(channel name)s"), { - 'author name': author.nameOrDisplayName, - 'channel name': channel.displayName, + "author name": author.nameOrDisplayName, + "channel name": channel.displayName, } ); } else { @@ -656,14 +720,15 @@ Model({ this.messaging.userNotificationManager.sendNotification({ message: notificationContent, title: notificationTitle, - type: 'info', + type: "info", }); messaging.update({ outOfFocusUnreadMessageCounter: increment() }); - const titlePattern = messaging.outOfFocusUnreadMessageCounter === 1 - ? this.env._t("%s Message") - : this.env._t("%s Messages"); - this.env.bus.trigger('set_title_part', { - part: '_chat', + const titlePattern = + messaging.outOfFocusUnreadMessageCounter === 1 + ? this.env._t("%s Message") + : this.env._t("%s Messages"); + this.env.bus.trigger("set_title_part", { + part: "_chat", title: sprintf(titlePattern, messaging.outOfFocusUnreadMessageCounter), }); }, @@ -677,7 +742,7 @@ Model({ _notifyThreadViewsMessageReceived(message) { for (const thread of message.threads) { for (const threadView of thread.threadViews) { - threadView.addComponentHint('message-received', { message }); + threadView.addComponentHint("message-received", { message }); } } }, diff --git a/addons/mail/static/src/models/mobile_messaging_navbar_view.js b/addons/mail/static/src/models/mobile_messaging_navbar_view.js index 3be67d01b2f..b7d4891cfd2 100644 --- a/addons/mail/static/src/models/mobile_messaging_navbar_view.js +++ b/addons/mail/static/src/models/mobile_messaging_navbar_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'MobileMessagingNavbarView', - template: 'mail.MobileMessagingNavbarView', - identifyingMode: 'xor', + name: "MobileMessagingNavbarView", + template: "mail.MobileMessagingNavbarView", + identifyingMode: "xor", recordMethods: { /** * @param {string} tabId @@ -17,18 +17,18 @@ Model({ } this.discuss.update({ activeMobileNavbarTabId: tabId }); if ( - this.discuss.activeMobileNavbarTabId === 'mailbox' && + this.discuss.activeMobileNavbarTabId === "mailbox" && (!this.discuss.activeThread || !this.discuss.activeThread.mailbox) ) { this.discuss.update({ thread: this.messaging.inbox.thread }); } - if (this.discuss.activeMobileNavbarTabId !== 'mailbox') { + if (this.discuss.activeMobileNavbarTabId !== "mailbox") { this.discuss.update({ thread: clear() }); } - if (this.discuss.activeMobileNavbarTabId !== 'chat') { + if (this.discuss.activeMobileNavbarTabId !== "chat") { this.discuss.discussView.update({ isAddingChat: false }); } - if (this.discuss.activeMobileNavbarTabId !== 'channel') { + if (this.discuss.activeMobileNavbarTabId !== "channel") { this.discuss.discussView.update({ isAddingChannel: false }); } } @@ -53,8 +53,11 @@ Model({ return clear(); }, }), - discuss: one('Discuss', { identifying: true, inverse: 'mobileMessagingNavbarView' }), - messagingMenu: one('MessagingMenu', { identifying: true, inverse: 'mobileMessagingNavbarView' }), + discuss: one("Discuss", { identifying: true, inverse: "mobileMessagingNavbarView" }), + messagingMenu: one("MessagingMenu", { + identifying: true, + inverse: "mobileMessagingNavbarView", + }), /** * Ordered list of tabs that this navbar has. * Format of tab: @@ -67,34 +70,42 @@ Model({ tabs: attr({ compute() { if (this.discuss) { - return [{ - icon: 'fa fa-inbox', - id: 'mailbox', - label: this.env._t("Mailboxes"), - }, { - icon: 'fa fa-user', - id: 'chat', - label: this.env._t("Chat"), - }, { - icon: 'fa fa-users', - id: 'channel', - label: this.env._t("Channel"), - }]; + return [ + { + icon: "fa fa-inbox", + id: "mailbox", + label: this.env._t("Mailboxes"), + }, + { + icon: "fa fa-user", + id: "chat", + label: this.env._t("Chat"), + }, + { + icon: "fa fa-users", + id: "channel", + label: this.env._t("Channel"), + }, + ]; } if (this.messagingMenu) { - return [{ - icon: 'fa fa-envelope', - id: 'all', - label: this.env._t("All"), - }, { - icon: 'fa fa-user', - id: 'chat', - label: this.env._t("Chat"), - }, { - icon: 'fa fa-users', - id: 'channel', - label: this.env._t("Channel"), - }]; + return [ + { + icon: "fa fa-envelope", + id: "all", + label: this.env._t("All"), + }, + { + icon: "fa fa-user", + id: "chat", + label: this.env._t("Chat"), + }, + { + icon: "fa fa-users", + id: "channel", + label: this.env._t("Channel"), + }, + ]; } return []; }, diff --git a/addons/mail/static/src/models/notification.js b/addons/mail/static/src/models/notification.js index 33ad08c66d9..68351c89384 100644 --- a/addons/mail/static/src/models/notification.js +++ b/addons/mail/static/src/models/notification.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, insert, one, Model } from '@mail/model'; +import { attr, clear, insert, one, Model } from "@mail/model"; Model({ - name: 'Notification', + name: "Notification", modelMethods: { /** * @param {Object} data @@ -11,19 +11,19 @@ Model({ */ convertData(data) { const data2 = {}; - if ('failure_type' in data) { + if ("failure_type" in data) { data2.failure_type = data.failure_type; } - if ('id' in data) { + if ("id" in data) { data2.id = data.id; } - if ('notification_status' in data) { + if ("notification_status" in data) { data2.notification_status = data.notification_status; } - if ('notification_type' in data) { + if ("notification_type" in data) { data2.notification_type = data.notification_type; } - if ('res_partner_id' in data) { + if ("res_partner_id" in data) { if (!data.res_partner_id) { data2.partner = clear(); } else { @@ -44,18 +44,18 @@ Model({ iconClass: attr({ compute() { switch (this.notification_status) { - case 'sent': - return 'fa fa-check'; - case 'bounce': - return 'fa fa-exclamation'; - case 'exception': - return 'fa fa-exclamation'; - case 'ready': - return 'fa fa-send-o'; - case 'canceled': - return 'fa fa-trash-o'; + case "sent": + return "fa fa-check"; + case "bounce": + return "fa fa-exclamation"; + case "exception": + return "fa fa-exclamation"; + case "ready": + return "fa fa-send-o"; + case "canceled": + return "fa fa-trash-o"; } - return ''; + return ""; }, }), /** @@ -64,36 +64,41 @@ Model({ iconTitle: attr({ compute() { switch (this.notification_status) { - case 'sent': + case "sent": return this.env._t("Sent"); - case 'bounce': + case "bounce": return this.env._t("Bounced"); - case 'exception': + case "exception": return this.env._t("Error"); - case 'ready': + case "ready": return this.env._t("Ready"); - case 'canceled': + case "canceled": return this.env._t("Canceled"); } - return ''; + return ""; }, }), id: attr({ identifying: true }), isFailure: attr({ compute() { - return ['exception', 'bounce'].includes(this.notification_status); + return ["exception", "bounce"].includes(this.notification_status); }, }), isFromCurrentUser: attr({ compute() { - if (!this.messaging || !this.messaging.currentPartner || !this.message || !this.message.author) { + if ( + !this.messaging || + !this.messaging.currentPartner || + !this.message || + !this.message.author + ) { return clear(); } return this.messaging.currentPartner === this.message.author; }, }), - message: one('Message', { inverse: 'notifications' }), - notificationGroup: one('NotificationGroup', { + message: one("Message", { inverse: "notifications" }), + notificationGroup: one("NotificationGroup", { compute() { if (!this.isFailure || !this.isFromCurrentUser) { return clear(); @@ -105,15 +110,15 @@ Model({ // and not its kanban/list/form view. return { notification_type: this.notification_type, - res_id: thread.model === 'mail.channel' ? thread.id : null, + res_id: thread.model === "mail.channel" ? thread.id : null, res_model: thread.model, res_model_name: thread.model_name, }; }, - inverse: 'notifications', + inverse: "notifications", }), notification_status: attr(), notification_type: attr(), - partner: one('Partner'), + partner: one("Partner"), }, }); diff --git a/addons/mail/static/src/models/notification_group.js b/addons/mail/static/src/models/notification_group.js index 482477d0ac6..f0e76634b47 100644 --- a/addons/mail/static/src/models/notification_group.js +++ b/addons/mail/static/src/models/notification_group.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, insert, many, one, Model } from '@mail/model'; +import { attr, clear, insert, many, one, Model } from "@mail/model"; Model({ - name: 'NotificationGroup', + name: "NotificationGroup", recordMethods: { /** * Cancel notifications of the group. @@ -11,7 +11,7 @@ Model({ notifyCancel() { this.messaging.rpc({ model: this.res_model, - method: 'notify_cancel_by_type', + method: "notify_cancel_by_type", kwargs: { notification_type: this.notification_type }, }); }, @@ -40,17 +40,21 @@ Model({ * @private */ _openDocuments() { - if (this.notification_type !== 'email') { + if (this.notification_type !== "email") { return; } this.env.services.action.doAction({ name: this.env._t("Mail Failures"), - type: 'ir.actions.act_window', - view_mode: 'kanban,list,form', - views: [[false, 'kanban'], [false, 'list'], [false, 'form']], - target: 'current', + type: "ir.actions.act_window", + view_mode: "kanban,list,form", + views: [ + [false, "kanban"], + [false, "list"], + [false, "form"], + ], + target: "current", res_model: this.res_model, - domain: [['message_has_error', '=', true]], + domain: [["message_has_error", "=", true]], context: { create: false }, }); if (this.messaging.device.isSmall) { @@ -70,8 +74,10 @@ Model({ */ compute() { const dates = this.notifications - .filter(notification => notification.message && notification.message.momentDate) - .map(notification => notification.message.momentDate); + .filter( + (notification) => notification.message && notification.message.momentDate + ) + .map((notification) => notification.message.momentDate); if (dates.length === 0) { return clear(); } @@ -79,30 +85,35 @@ Model({ }, }), notification_type: attr({ identifying: true }), - notifications: many('Notification', { inverse: 'notificationGroup' }), - notificationGroupViews: many('NotificationGroupView', { inverse: 'notificationGroup' }), + notifications: many("Notification", { inverse: "notificationGroup" }), + notificationGroupViews: many("NotificationGroupView", { inverse: "notificationGroup" }), res_id: attr({ identifying: true }), res_model: attr({ identifying: true }), res_model_name: attr(), /** * States the position of the group inside the notification list. */ - sequence: attr({ default: 0, + sequence: attr({ + default: 0, /** * Compute the position of the group inside the notification list. */ compute() { - return -Math.max(...this.notifications.map(notification => notification.message.id)); + return -Math.max( + ...this.notifications.map((notification) => notification.message.id) + ); }, }), /** * Related thread when the notification group concerns a single thread. */ - thread: one('Thread', { + thread: one("Thread", { compute() { const notificationsThreadIds = this.notifications - .filter(notification => notification.message && notification.message.originThread) - .map(notification => notification.message.originThread.id); + .filter( + (notification) => notification.message && notification.message.originThread + ) + .map((notification) => notification.message.originThread.id); const threadIds = new Set(notificationsThreadIds); if (threadIds.size !== 1) { return clear(); @@ -116,8 +127,8 @@ Model({ }, onChanges: [ { - dependencies: ['notifications'], - methodName: '_onChangeNotifications', + dependencies: ["notifications"], + methodName: "_onChangeNotifications", }, ], }); diff --git a/addons/mail/static/src/models/notification_group_view.js b/addons/mail/static/src/models/notification_group_view.js index c9f46277a2f..6d19c0f5453 100644 --- a/addons/mail/static/src/models/notification_group_view.js +++ b/addons/mail/static/src/models/notification_group_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'NotificationGroupView', - template: 'mail.NotificationGroupView', + name: "NotificationGroupView", + template: "mail.NotificationGroupView", recordMethods: { /** * @param {MouseEvent} ev @@ -34,8 +34,8 @@ Model({ fields: { imageSrc: attr({ compute() { - if (this.notificationGroup.notification_type === 'email') { - return '/mail/static/src/img/smiley/mailfailure.jpg'; + if (this.notificationGroup.notification_type === "email") { + return "/mail/static/src/img/smiley/mailfailure.jpg"; } return clear(); }, @@ -44,8 +44,14 @@ Model({ * Reference of the "mark as read" button. Useful to disable the * top-level click handler when clicking on this specific button. */ - markAsReadRef: attr({ ref: 'markAsRead' }), - notificationGroup: one('NotificationGroup', { identifying: true, inverse: 'notificationGroupViews' }), - notificationListViewOwner: one('NotificationListView', { identifying: true, inverse: 'notificationGroupViews' }), + markAsReadRef: attr({ ref: "markAsRead" }), + notificationGroup: one("NotificationGroup", { + identifying: true, + inverse: "notificationGroupViews", + }), + notificationListViewOwner: one("NotificationListView", { + identifying: true, + inverse: "notificationGroupViews", + }), }, }); diff --git a/addons/mail/static/src/models/notification_list_view.js b/addons/mail/static/src/models/notification_list_view.js index 21ec31ba8c1..72c070eeeb3 100644 --- a/addons/mail/static/src/models/notification_list_view.js +++ b/addons/mail/static/src/models/notification_list_view.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'NotificationListView', - template: 'mail.NotificationListView', - identifyingMode: 'xor', + name: "NotificationListView", + template: "mail.NotificationListView", + identifyingMode: "xor", lifecycleHooks: { _created() { this._loadPreviews(); @@ -19,20 +19,28 @@ Model({ * @private */ async _loadPreviews() { - const threads = this.channelPreviewViews - .map(channelPreviewView => channelPreviewView.thread); - this.messaging.models['Thread'].loadPreviews(threads); + const threads = this.channelPreviewViews.map( + (channelPreviewView) => channelPreviewView.thread + ); + this.messaging.models["Thread"].loadPreviews(threads); }, }, fields: { - channelPreviewViews: many('ChannelPreviewView', { inverse: 'notificationListViewOwner', + channelPreviewViews: many("ChannelPreviewView", { + inverse: "notificationListViewOwner", compute() { return this.filteredChannels .sort((c1, c2) => { - if (c1.localMessageUnreadCounter > 0 && c2.localMessageUnreadCounter === 0) { + if ( + c1.localMessageUnreadCounter > 0 && + c2.localMessageUnreadCounter === 0 + ) { return -1; } - if (c1.localMessageUnreadCounter === 0 && c2.localMessageUnreadCounter > 0) { + if ( + c1.localMessageUnreadCounter === 0 && + c2.localMessageUnreadCounter > 0 + ) { return 1; } if (c1.thread.lastMessage && c2.thread.lastMessage) { @@ -46,10 +54,10 @@ Model({ } return c1.id < c2.id ? -1 : 1; }) - .map(channel => ({ channel })); + .map((channel) => ({ channel })); }, }), - discussOwner: one('Discuss', { identifying: true, inverse: 'notificationListView' }), + discussOwner: one("Discuss", { identifying: true, inverse: "notificationListView" }), filter: attr({ compute() { if (this.discussOwner) { @@ -61,53 +69,60 @@ Model({ return clear(); }, }), - filteredChannels: many('Channel', { + filteredChannels: many("Channel", { compute() { switch (this.filter) { - case 'channel': { - return this.messaging.models['Channel'] - .all(channel => - channel.channel_type === 'channel' && - channel.thread.isPinned + case "channel": { + return this.messaging.models["Channel"] + .all( + (channel) => + channel.channel_type === "channel" && channel.thread.isPinned ) - .sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1); + .sort((c1, c2) => (c1.displayName < c2.displayName ? -1 : 1)); } - case 'chat': { - return this.messaging.models['Channel'] - .all(channel => - channel.thread.isChatChannel && - channel.thread.isPinned + case "chat": { + return this.messaging.models["Channel"] + .all( + (channel) => channel.thread.isChatChannel && channel.thread.isPinned ) - .sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1); + .sort((c1, c2) => (c1.displayName < c2.displayName ? -1 : 1)); } - case 'all': { + case "all": { // "All" filter is for channels and chats - return this.messaging.models['Channel'] - .all(channel => channel.thread.isPinned) - .sort((c1, c2) => c1.displayName < c2.displayName ? -1 : 1); + return this.messaging.models["Channel"] + .all((channel) => channel.thread.isPinned) + .sort((c1, c2) => (c1.displayName < c2.displayName ? -1 : 1)); } } return clear(); }, }), - messagingMenuOwner: one('MessagingMenu', { identifying: true, inverse: 'notificationListView' }), - notificationGroupViews: many('NotificationGroupView', { inverse: 'notificationListViewOwner', + messagingMenuOwner: one("MessagingMenu", { + identifying: true, + inverse: "notificationListView", + }), + notificationGroupViews: many("NotificationGroupView", { + inverse: "notificationListViewOwner", compute() { - if (this.filter !== 'all') { + if (this.filter !== "all") { return clear(); } - return this.models['NotificationGroup'] + return this.models["NotificationGroup"] .all() .sort((group1, group2) => group1.sequence - group2.sequence) - .map(notificationGroup => ({ notificationGroup })); + .map((notificationGroup) => ({ notificationGroup })); }, }), - notificationRequestView: one('NotificationRequestView', { inverse: 'notificationListViewOwner', + notificationRequestView: one("NotificationRequestView", { + inverse: "notificationListViewOwner", compute() { - return (this.filter === 'all' && this.messaging.isNotificationPermissionDefault) ? {} : clear(); + return this.filter === "all" && this.messaging.isNotificationPermissionDefault + ? {} + : clear(); }, }), - notificationViews: many('Record', { isCausal: true, + notificationViews: many("Record", { + isCausal: true, compute() { const notifications = []; if (this.notificationRequestView) { @@ -119,22 +134,35 @@ Model({ return notifications; }, }), - threadNeedactionPreviewViews: many('ThreadNeedactionPreviewView', { inverse: 'notificationListViewOwner', + threadNeedactionPreviewViews: many("ThreadNeedactionPreviewView", { + inverse: "notificationListViewOwner", compute() { - if (this.filter !== 'all') { + if (this.filter !== "all") { return clear(); } - return this.messaging.models['Thread'] - .all(t => !t.mailbox && t.needactionMessagesAsOriginThread.length > 0) + return this.messaging.models["Thread"] + .all((t) => !t.mailbox && t.needactionMessagesAsOriginThread.length > 0) .sort((t1, t2) => { - if (t1.needactionMessagesAsOriginThread.length > 0 && t2.needactionMessagesAsOriginThread.length === 0) { + if ( + t1.needactionMessagesAsOriginThread.length > 0 && + t2.needactionMessagesAsOriginThread.length === 0 + ) { return -1; } - if (t1.needactionMessagesAsOriginThread.length === 0 && t2.needactionMessagesAsOriginThread.length > 0) { + if ( + t1.needactionMessagesAsOriginThread.length === 0 && + t2.needactionMessagesAsOriginThread.length > 0 + ) { return 1; } - if (t1.lastNeedactionMessageAsOriginThread && t2.lastNeedactionMessageAsOriginThread) { - return t1.lastNeedactionMessageAsOriginThread.id < t2.lastNeedactionMessageAsOriginThread.id ? 1 : -1; + if ( + t1.lastNeedactionMessageAsOriginThread && + t2.lastNeedactionMessageAsOriginThread + ) { + return t1.lastNeedactionMessageAsOriginThread.id < + t2.lastNeedactionMessageAsOriginThread.id + ? 1 + : -1; } if (t1.lastNeedactionMessageAsOriginThread) { return -1; @@ -144,7 +172,7 @@ Model({ } return t1.id < t2.id ? -1 : 1; }) - .map(thread => ({ thread })); + .map((thread) => ({ thread })); }, }), }, diff --git a/addons/mail/static/src/models/notification_message_view.js b/addons/mail/static/src/models/notification_message_view.js index 55e7a218fb2..63a5e2e2c45 100644 --- a/addons/mail/static/src/models/notification_message_view.js +++ b/addons/mail/static/src/models/notification_message_view.js @@ -1,29 +1,41 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'NotificationMessageView', - template: 'mail.NotificationMessageView', + name: "NotificationMessageView", + template: "mail.NotificationMessageView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useComponentToModel({ fieldName: "component" }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, recordMethods: { onComponentUpdate() { if (!this.exists()) { return; } - if (this.messageListViewItemOwner.threadViewOwnerAsLastMessageListViewItem && this.messageListViewItemOwner.isPartiallyVisible()) { - this.messageListViewItemOwner.threadViewOwnerAsLastMessageListViewItem.handleVisibleMessage(this.message); + if ( + this.messageListViewItemOwner.threadViewOwnerAsLastMessageListViewItem && + this.messageListViewItemOwner.isPartiallyVisible() + ) { + this.messageListViewItemOwner.threadViewOwnerAsLastMessageListViewItem.handleVisibleMessage( + this.message + ); } }, }, fields: { component: attr(), - message: one('Message', { inverse: 'notificationMessageViews', related: 'messageListViewItemOwner.message', required: true }), - messageListViewItemOwner: one('MessageListViewItem', { identifying: true, inverse: 'notificationMessageView' }), + message: one("Message", { + inverse: "notificationMessageViews", + related: "messageListViewItemOwner.message", + required: true, + }), + messageListViewItemOwner: one("MessageListViewItem", { + identifying: true, + inverse: "notificationMessageView", + }), }, }); diff --git a/addons/mail/static/src/models/notification_request_view.js b/addons/mail/static/src/models/notification_request_view.js index e9f8ab5e1c7..d0d8c1fd4fc 100644 --- a/addons/mail/static/src/models/notification_request_view.js +++ b/addons/mail/static/src/models/notification_request_view.js @@ -1,12 +1,12 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; -import { sprintf } from '@web/core/utils/strings'; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'NotificationRequestView', - template: 'mail.NotificationRequestView', + name: "NotificationRequestView", + template: "mail.NotificationRequestView", recordMethods: { onClick() { this.messaging.requestNotificationPermission(); @@ -21,16 +21,21 @@ Model({ if (!this.messaging.partnerRoot) { return clear(); } - return sprintf( - this.env._t("%(odoobotName)s has a request"), - { odoobotName: this.messaging.partnerRoot.nameOrDisplayName }, - ); + return sprintf(this.env._t("%(odoobotName)s has a request"), { + odoobotName: this.messaging.partnerRoot.nameOrDisplayName, + }); }, }), - notificationListViewOwner: one('NotificationListView', { identifying: true, inverse: 'notificationRequestView' }), - personaImStatusIconView: one('PersonaImStatusIconView', { inverse: 'notificationRequestViewOwner', + notificationListViewOwner: one("NotificationListView", { + identifying: true, + inverse: "notificationRequestView", + }), + personaImStatusIconView: one("PersonaImStatusIconView", { + inverse: "notificationRequestViewOwner", compute() { - return this.messaging.partnerRoot && this.messaging.partnerRoot.isImStatusSet ? {} : clear(); + return this.messaging.partnerRoot && this.messaging.partnerRoot.isImStatusSet + ? {} + : clear(); }, }), }, diff --git a/addons/mail/static/src/models/other_member_long_typing_in_thread_timer.js b/addons/mail/static/src/models/other_member_long_typing_in_thread_timer.js index 88893970681..b4f85e38ab4 100644 --- a/addons/mail/static/src/models/other_member_long_typing_in_thread_timer.js +++ b/addons/mail/static/src/models/other_member_long_typing_in_thread_timer.js @@ -1,17 +1,24 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'OtherMemberLongTypingInThreadTimer', + name: "OtherMemberLongTypingInThreadTimer", recordMethods: { onOtherMemberLongTypingTimeout() { this.thread.unregisterOtherMemberTypingMember(this.member); }, }, fields: { - member: one('ChannelMember', { identifying: true, inverse: 'otherMemberLongTypingInThreadTimers' }), - thread: one('Thread', { identifying: true, inverse: 'otherMembersLongTypingTimers' }), - timer: one('Timer', { default: {}, inverse: 'otherMemberLongTypingInThreadTimerOwner', required: true }), + member: one("ChannelMember", { + identifying: true, + inverse: "otherMemberLongTypingInThreadTimers", + }), + thread: one("Thread", { identifying: true, inverse: "otherMembersLongTypingTimers" }), + timer: one("Timer", { + default: {}, + inverse: "otherMemberLongTypingInThreadTimerOwner", + required: true, + }), }, }); diff --git a/addons/mail/static/src/models/partner.js b/addons/mail/static/src/models/partner.js index ac676d4e5bb..b63673d1805 100644 --- a/addons/mail/static/src/models/partner.js +++ b/addons/mail/static/src/models/partner.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, insert, many, one, Model } from '@mail/model'; -import { cleanSearchTerm } from '@mail/utils/utils'; +import { attr, clear, insert, many, one, Model } from "@mail/model"; +import { cleanSearchTerm } from "@mail/utils/utils"; Model({ - name: 'Partner', + name: "Partner", modelMethods: { /** * Fetches partners matching the given search term to extend the @@ -17,19 +17,22 @@ Model({ */ async fetchSuggestions(searchTerm, { thread } = {}) { const kwargs = { search: searchTerm }; - const isNonPublicChannel = thread && thread.model === 'mail.channel' && (thread.authorizedGroupFullName || thread.channel.channel_type !== 'channel'); + const isNonPublicChannel = + thread && + thread.model === "mail.channel" && + (thread.authorizedGroupFullName || thread.channel.channel_type !== "channel"); if (isNonPublicChannel) { kwargs.channel_id = thread.id; } const suggestedPartners = await this.messaging.rpc( { - model: 'res.partner', - method: 'get_mention_suggestions', + model: "res.partner", + method: "get_mention_suggestions", kwargs, }, - { shadow: true }, + { shadow: true } ); - this.messaging.models['Partner'].insert(suggestedPartners); + this.messaging.models["Partner"].insert(suggestedPartners); }, /** * Returns a sort function to determine the order of display of partners @@ -72,12 +75,18 @@ Model({ return 1; } } - const cleanedAName = cleanSearchTerm(a.name || ''); - const cleanedBName = cleanSearchTerm(b.name || ''); - if (cleanedAName.startsWith(cleanedSearchTerm) && !cleanedBName.startsWith(cleanedSearchTerm)) { + const cleanedAName = cleanSearchTerm(a.name || ""); + const cleanedBName = cleanSearchTerm(b.name || ""); + if ( + cleanedAName.startsWith(cleanedSearchTerm) && + !cleanedBName.startsWith(cleanedSearchTerm) + ) { return -1; } - if (!cleanedAName.startsWith(cleanedSearchTerm) && cleanedBName.startsWith(cleanedSearchTerm)) { + if ( + !cleanedAName.startsWith(cleanedSearchTerm) && + cleanedBName.startsWith(cleanedSearchTerm) + ) { return 1; } if (cleanedAName < cleanedBName) { @@ -86,12 +95,18 @@ Model({ if (cleanedAName > cleanedBName) { return 1; } - const cleanedAEmail = cleanSearchTerm(a.email || ''); - const cleanedBEmail = cleanSearchTerm(b.email || ''); - if (cleanedAEmail.startsWith(cleanedSearchTerm) && !cleanedAEmail.startsWith(cleanedSearchTerm)) { + const cleanedAEmail = cleanSearchTerm(a.email || ""); + const cleanedBEmail = cleanSearchTerm(b.email || ""); + if ( + cleanedAEmail.startsWith(cleanedSearchTerm) && + !cleanedAEmail.startsWith(cleanedSearchTerm) + ) { return -1; } - if (!cleanedBEmail.startsWith(cleanedSearchTerm) && cleanedBEmail.startsWith(cleanedSearchTerm)) { + if ( + !cleanedBEmail.startsWith(cleanedSearchTerm) && + cleanedBEmail.startsWith(cleanedSearchTerm) + ) { return 1; } if (cleanedAEmail < cleanedBEmail) { @@ -113,9 +128,9 @@ Model({ */ async imSearch({ callback, keyword, limit = 10 }) { // prefetched partners - let partners = []; + const partners = []; const cleanedSearchTerm = cleanSearchTerm(keyword); - for (const partner of this.all(partner => partner.active)) { + for (const partner of this.all((partner) => partner.active)) { if (partners.length < limit) { if ( partner.name && @@ -129,9 +144,9 @@ Model({ if (!partners.length) { const partnersData = await this.messaging.rpc( { - model: 'res.partner', - method: 'im_search', - args: [keyword, limit] + model: "res.partner", + method: "im_search", + args: [keyword, limit], }, { shadow: true } ); @@ -151,16 +166,21 @@ Model({ */ searchSuggestions(searchTerm, { thread } = {}) { let partners; - const isNonPublicChannel = thread && thread.channel && (thread.authorizedGroupFullName || thread.channel.channel_type !== 'channel'); + const isNonPublicChannel = + thread && + thread.channel && + (thread.authorizedGroupFullName || thread.channel.channel_type !== "channel"); if (isNonPublicChannel) { // Only return the channel members when in the context of a // group restricted channel. Indeed, the message with the mention // would be notified to the mentioned partner, so this prevents // from inadvertently leaking the private message to the // mentioned partner. - partners = thread.channel.channelMembers.filter(member => member.persona && member.persona.partner).map(member => member.persona.partner); + partners = thread.channel.channelMembers + .filter((member) => member.persona && member.persona.partner) + .map((member) => member.persona.partner); } else { - partners = this.messaging.models['Partner'].all(); + partners = this.messaging.models["Partner"].all(); } const cleanedSearchTerm = cleanSearchTerm(searchTerm); const mainSuggestionList = []; @@ -177,7 +197,7 @@ Model({ continue; } if ( - (cleanSearchTerm(partner.name).includes(cleanedSearchTerm)) || + cleanSearchTerm(partner.name).includes(cleanedSearchTerm) || (partner.email && cleanSearchTerm(partner.email).includes(cleanedSearchTerm)) ) { if (partner.user) { @@ -196,14 +216,17 @@ Model({ * applicable. */ async checkIsUser() { - const userIds = await this.messaging.rpc({ - model: 'res.users', - method: 'search', - args: [[['partner_id', '=', this.id]]], - kwargs: { - context: { active_test: false }, + const userIds = await this.messaging.rpc( + { + model: "res.users", + method: "search", + args: [[["partner_id", "=", this.id]]], + kwargs: { + context: { active_test: false }, + }, }, - }, { shadow: true }); + { shadow: true } + ); if (!this.exists()) { return; } @@ -229,8 +252,10 @@ Model({ // prevent chatting with non-users if (!this.user) { this.messaging.notify({ - message: this.env._t("You can only chat with partners that have a dedicated user."), - type: 'info', + message: this.env._t( + "You can only chat with partners that have a dedicated user." + ), + type: "info", }); return; } @@ -260,7 +285,7 @@ Model({ async openProfile() { return this.messaging.openDocument({ id: this.id, - model: 'res.partner', + model: "res.partner", }); }, }, @@ -271,9 +296,15 @@ Model({ return `/web/image/res.partner/${this.id}/avatar_128`; }, }), - channelInvitationFormSelectablePartnerViews: many('ChannelInvitationFormSelectablePartnerView', { inverse: 'partner' }), - channelInvitationFormSelectedPartnerViews: many('ChannelInvitationFormSelectedPartnerView', { inverse: 'partner' }), - country: one('Country'), + channelInvitationFormSelectablePartnerViews: many( + "ChannelInvitationFormSelectablePartnerView", + { inverse: "partner" } + ), + channelInvitationFormSelectedPartnerViews: many( + "ChannelInvitationFormSelectedPartnerView", + { inverse: "partner" } + ), + country: one("Country"), /** * Deprecated. * States the `display_name` of this partner, as returned by the server. @@ -284,7 +315,8 @@ Model({ * relevant fields instead. */ display_name: attr(), - displayName: attr({ default: "", + displayName: attr({ + default: "", compute() { if (this.display_name) { return this.display_name; @@ -295,7 +327,7 @@ Model({ return clear(); }, }), - dmChatWithCurrentPartner: one('Channel', { inverse: 'correspondentOfDmChat' }), + dmChatWithCurrentPartner: one("Channel", { inverse: "correspondentOfDmChat" }), email: attr(), /** * Whether an attempt was already made to fetch the user corresponding @@ -306,7 +338,7 @@ Model({ im_status: attr(), isImStatusSet: attr({ compute() { - return Boolean(this.im_status && this.im_status !== 'im_partner'); + return Boolean(this.im_status && this.im_status !== "im_partner"); }, }), /** @@ -314,20 +346,30 @@ Model({ */ isOnline: attr({ compute() { - return ['online', 'away'].includes(this.im_status); + return ["online", "away"].includes(this.im_status); }, }), is_public: attr(), - model: attr({ default: 'res.partner' }), + model: attr({ default: "res.partner" }), name: attr(), nameOrDisplayName: attr({ compute() { return this.name || this.displayName; }, }), - persona: one('Persona', { default: {}, inverse: 'partner', readonly: true, required: true }), - suggestable: one('ComposerSuggestable', { default: {}, inverse: 'partner', readonly: true, required: true }), - user: one('User', { inverse: 'partner' }), - volumeSetting: one('res.users.settings.volumes', { inverse: 'partner_id' }), + persona: one("Persona", { + default: {}, + inverse: "partner", + readonly: true, + required: true, + }), + suggestable: one("ComposerSuggestable", { + default: {}, + inverse: "partner", + readonly: true, + required: true, + }), + user: one("User", { inverse: "partner" }), + volumeSetting: one("res.users.settings.volumes", { inverse: "partner_id" }), }, }); diff --git a/addons/mail/static/src/models/persona.js b/addons/mail/static/src/models/persona.js index 805c497932a..2f5ec25df9c 100644 --- a/addons/mail/static/src/models/persona.js +++ b/addons/mail/static/src/models/persona.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'Persona', - identifyingMode: 'xor', + name: "Persona", + identifyingMode: "xor", fields: { - channelMembers: many('ChannelMember', { inverse: 'persona', isCausal: true }), - guest: one('Guest', { identifying: true, inverse: 'persona' }), + channelMembers: many("ChannelMember", { inverse: "persona", isCausal: true }), + guest: one("Guest", { identifying: true, inverse: "persona" }), im_status: attr({ compute() { if (this.guest) { @@ -19,7 +19,7 @@ Model({ return clear(); }, }), - messagingAsAnyPersona: one('Messaging', { default: {}, inverse: 'allPersonas' }), + messagingAsAnyPersona: one("Messaging", { default: {}, inverse: "allPersonas" }), name: attr({ compute() { if (this.guest) { @@ -31,8 +31,8 @@ Model({ return clear(); }, }), - partner: one('Partner', { identifying: true, inverse: 'persona' }), - volumeSetting: one('res.users.settings.volumes', { + partner: one("Partner", { identifying: true, inverse: "persona" }), + volumeSetting: one("res.users.settings.volumes", { compute() { if (this.guest) { return this.guest.volumeSetting || clear(); diff --git a/addons/mail/static/src/models/persona_im_status_icon_view.js b/addons/mail/static/src/models/persona_im_status_icon_view.js index d046bbba4ac..97de8d4911d 100644 --- a/addons/mail/static/src/models/persona_im_status_icon_view.js +++ b/addons/mail/static/src/models/persona_im_status_icon_view.js @@ -1,18 +1,18 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; -import { markEventHandled } from '@mail/utils/utils'; +import { attr, clear, one, Model } from "@mail/model"; +import { markEventHandled } from "@mail/utils/utils"; Model({ - name: 'PersonaImStatusIconView', - template: 'mail.PersonaImStatusIconView', - identifyingMode: 'xor', + name: "PersonaImStatusIconView", + template: "mail.PersonaImStatusIconView", + identifyingMode: "xor", recordMethods: { /** * @param {MouseEvent} ev */ onClick(ev) { - markEventHandled(ev, 'PersonaImStatusIcon.Click'); + markEventHandled(ev, "PersonaImStatusIcon.Click"); if (!this.hasOpenChat || !this.persona.partner) { return; } @@ -20,11 +20,24 @@ Model({ }, }, fields: { - channelInvitationFormSelectablePartnerViewOwner: one('ChannelInvitationFormSelectablePartnerView', { identifying: true, inverse: 'personaImStatusIconView' }), - channelMemberViewOwner: one('ChannelMemberView', { identifying: true, inverse: 'personaImStatusIconView' }), - channelPreviewViewOwner: one('ChannelPreviewView', { identifying: true, inverse: 'personaImStatusIconView' }), - composerSuggestionViewOwner: one('ComposerSuggestionView', { identifying: true, inverse: 'personaImStatusIconView' }), - hasBackground: attr({ default: true, + channelInvitationFormSelectablePartnerViewOwner: one( + "ChannelInvitationFormSelectablePartnerView", + { identifying: true, inverse: "personaImStatusIconView" } + ), + channelMemberViewOwner: one("ChannelMemberView", { + identifying: true, + inverse: "personaImStatusIconView", + }), + channelPreviewViewOwner: one("ChannelPreviewView", { + identifying: true, + inverse: "personaImStatusIconView", + }), + composerSuggestionViewOwner: one("ComposerSuggestionView", { + identifying: true, + inverse: "personaImStatusIconView", + }), + hasBackground: attr({ + default: true, compute() { if (this.composerSuggestionViewOwner) { return false; @@ -36,7 +49,8 @@ Model({ * Determines whether a click on this view should open a chat with the * corresponding persona. */ - hasOpenChat: attr({ default: false, + hasOpenChat: attr({ + default: false, compute() { if (this.channelMemberViewOwner) { return this.channelMemberViewOwner.hasOpenChat; @@ -47,9 +61,16 @@ Model({ return clear(); }, }), - messageViewOwner: one('MessageView', { identifying: true, inverse: 'personaImStatusIconView' }), - notificationRequestViewOwner: one('NotificationRequestView', { identifying: true, inverse: 'personaImStatusIconView' }), - persona: one('Persona', { required: true, + messageViewOwner: one("MessageView", { + identifying: true, + inverse: "personaImStatusIconView", + }), + notificationRequestViewOwner: one("NotificationRequestView", { + identifying: true, + inverse: "personaImStatusIconView", + }), + persona: one("Persona", { + required: true, compute() { if (this.channelInvitationFormSelectablePartnerViewOwner) { return this.channelInvitationFormSelectablePartnerViewOwner.partner.persona; @@ -75,11 +96,15 @@ Model({ return this.messaging.partnerRoot.persona; } if (this.threadNeedactionPreviewViewOwner) { - return this.threadNeedactionPreviewViewOwner.thread.channel.correspondent.persona; + return this.threadNeedactionPreviewViewOwner.thread.channel.correspondent + .persona; } return clear(); }, }), - threadNeedactionPreviewViewOwner: one('ThreadNeedactionPreviewView', { identifying: true, inverse: 'personaImStatusIconView' }), + threadNeedactionPreviewViewOwner: one("ThreadNeedactionPreviewView", { + identifying: true, + inverse: "personaImStatusIconView", + }), }, }); diff --git a/addons/mail/static/src/models/popover_manager.js b/addons/mail/static/src/models/popover_manager.js index ccb2424d60f..40fe2e7cf6a 100644 --- a/addons/mail/static/src/models/popover_manager.js +++ b/addons/mail/static/src/models/popover_manager.js @@ -1,12 +1,12 @@ /** @odoo-module **/ -import { many, Model } from '@mail/model'; +import { many, Model } from "@mail/model"; Model({ - name: 'PopoverManager', - template: 'mail.PopoverManager', + name: "PopoverManager", + template: "mail.PopoverManager", fields: { // FIXME: dependent on implementation that uses insert order in relations!! - popoverViews: many('PopoverView', { inverse: 'manager', isCausal: true }), + popoverViews: many("PopoverView", { inverse: "manager", isCausal: true }), }, }); diff --git a/addons/mail/static/src/models/popover_view.js b/addons/mail/static/src/models/popover_view.js index 3d5d5619b2b..0209a3d98ed 100644 --- a/addons/mail/static/src/models/popover_view.js +++ b/addons/mail/static/src/models/popover_view.js @@ -1,31 +1,28 @@ /** @odoo-module **/ -import { useComponentToModel } from '@mail/component_hooks/use_component_to_model'; -import { attr, clear, one, Model } from '@mail/model'; +import { useComponentToModel } from "@mail/component_hooks/use_component_to_model"; +import { attr, clear, one, Model } from "@mail/model"; -import { usePosition } from '@web/core/position_hook'; +import { usePosition } from "@web/core/position_hook"; Model({ - name: 'PopoverView', - template: 'mail.PopoverView', + name: "PopoverView", + template: "mail.PopoverView", componentSetup() { - useComponentToModel({ fieldName: 'component' }); - usePosition( - () => this.anchorRef && this.anchorRef.el, - { - popper: "root", - margin: 16, - position: this.position, - } - ); + useComponentToModel({ fieldName: "component" }); + usePosition(() => this.anchorRef && this.anchorRef.el, { + popper: "root", + margin: 16, + position: this.position, + }); }, - identifyingMode: 'xor', + identifyingMode: "xor", lifecycleHooks: { _created() { - document.addEventListener('click', this._onClickCaptureGlobal, true); + document.addEventListener("click", this._onClickCaptureGlobal, true); }, _willDelete() { - document.removeEventListener('click', this._onClickCaptureGlobal, true); + document.removeEventListener("click", this._onClickCaptureGlobal, true); }, }, recordMethods: { @@ -37,7 +34,9 @@ Model({ * @returns {boolean} */ contains(element) { - return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element)); + return Boolean( + this.component && this.component.root.el && this.component.root.el.contains(element) + ); }, /** * Closes the popover when clicking outside, if appropriate. @@ -59,14 +58,25 @@ Model({ }, }, fields: { - activityButtonViewOwnerAsActivityList: one('ActivityButtonView', { identifying: true, inverse: 'activityListPopoverView' }), - activityCellViewOwnerAsActivityList: one('ActivityCellView', { identifying: true, inverse: 'activityListPopoverView' }), - activityListView: one('ActivityListView', { inverse: 'popoverViewOwner', + activityButtonViewOwnerAsActivityList: one("ActivityButtonView", { + identifying: true, + inverse: "activityListPopoverView", + }), + activityCellViewOwnerAsActivityList: one("ActivityCellView", { + identifying: true, + inverse: "activityListPopoverView", + }), + activityListView: one("ActivityListView", { + inverse: "popoverViewOwner", compute() { - return this.activityButtonViewOwnerAsActivityList || this.activityCellViewOwnerAsActivityList ? {} : clear(); + return this.activityButtonViewOwnerAsActivityList || + this.activityCellViewOwnerAsActivityList + ? {} + : clear(); }, }), - activityMarkDonePopoverContentView: one('ActivityMarkDonePopoverContentView', { inverse: 'popoverViewOwner', + activityMarkDonePopoverContentView: one("ActivityMarkDonePopoverContentView", { + inverse: "popoverViewOwner", compute() { if (this.activityViewOwnerAsMarkDone) { return {}; @@ -74,11 +84,15 @@ Model({ return clear(); }, }), - activityViewOwnerAsMarkDone: one('ActivityView', { identifying: true, inverse: 'markDonePopoverView' }), + activityViewOwnerAsMarkDone: one("ActivityView", { + identifying: true, + inverse: "markDonePopoverView", + }), /** * HTML element that is used as anchor position for this popover view. */ - anchorRef: attr({ required: true, + anchorRef: attr({ + required: true, compute() { if (this.activityViewOwnerAsMarkDone) { return this.activityViewOwnerAsMarkDone.markDoneButtonRef; @@ -110,8 +124,12 @@ Model({ return clear(); }, }), - callActionListViewOwnerAsMoreMenu: one('CallActionListView', { identifying: true, inverse: 'moreMenuPopoverView' }), - callOptionMenuView: one('CallOptionMenu', { inverse: 'popoverViewOwner', + callActionListViewOwnerAsMoreMenu: one("CallActionListView", { + identifying: true, + inverse: "moreMenuPopoverView", + }), + callOptionMenuView: one("CallOptionMenu", { + inverse: "popoverViewOwner", compute() { if (this.callActionListViewOwnerAsMoreMenu) { return {}; @@ -119,8 +137,12 @@ Model({ return clear(); }, }), - callParticipantCardOwner: one('CallParticipantCard', { identifying: true, inverse: 'callParticipantCardPopoverView' }), - callParticipantCardPopoverContentView: one('CallParticipantCardPopoverContentView', { inverse: 'popoverViewOwner', + callParticipantCardOwner: one("CallParticipantCard", { + identifying: true, + inverse: "callParticipantCardPopoverView", + }), + callParticipantCardPopoverContentView: one("CallParticipantCardPopoverContentView", { + inverse: "popoverViewOwner", compute() { if (this.callParticipantCardOwner) { return {}; @@ -131,7 +153,8 @@ Model({ /** * The record that represents the content inside the popover view. */ - channelInvitationForm: one('ChannelInvitationForm', { inverse: 'popoverViewOwner', + channelInvitationForm: one("ChannelInvitationForm", { + inverse: "popoverViewOwner", compute() { if (this.threadViewTopbarOwnerAsInvite) { return {}; @@ -146,11 +169,15 @@ Model({ /** * If set, this popover view is owned by a composer view. */ - composerViewOwnerAsEmoji: one('ComposerView', { identifying: true, inverse: 'emojisPopoverView' }), + composerViewOwnerAsEmoji: one("ComposerView", { + identifying: true, + inverse: "emojisPopoverView", + }), /** * Determines the record that is content of this popover view. */ - content: one('Record', { required: true, + content: one("Record", { + required: true, compute() { if (this.activityMarkDonePopoverContentView) { return this.activityMarkDonePopoverContentView; @@ -167,7 +194,10 @@ Model({ if (this.emojiPickerView) { return this.emojiPickerView; } - if (this.activityButtonViewOwnerAsActivityList || this.activityCellViewOwnerAsActivityList) { + if ( + this.activityButtonViewOwnerAsActivityList || + this.activityCellViewOwnerAsActivityList + ) { return this.activityListView; } if (this.messageNotificationPopoverContentView) { @@ -180,13 +210,14 @@ Model({ * Determines the class name for the component * that is content of this popover view. */ - contentClassName: attr({ default: '', + contentClassName: attr({ + default: "", compute() { if (this.channelInvitationForm) { - return 'o_PopoverView_channelInvitationForm'; + return "o_PopoverView_channelInvitationForm"; } if (this.emojiPickerView) { - return 'o_PopoverView_emojiPickerView'; + return "o_PopoverView_emojiPickerView"; } return clear(); }, @@ -194,28 +225,33 @@ Model({ /** * Determines the component name of the content. */ - contentComponentName: attr({ default: '', required: true, + contentComponentName: attr({ + default: "", + required: true, compute() { if (this.activityMarkDonePopoverContentView) { - return 'ActivityMarkDonePopoverContentView'; + return "ActivityMarkDonePopoverContentView"; } if (this.callOptionMenuView) { - return 'CallOptionMenu'; + return "CallOptionMenu"; } if (this.callParticipantCardPopoverContentView) { - return 'CallParticipantCardPopoverContentView'; + return "CallParticipantCardPopoverContentView"; } if (this.channelInvitationForm) { - return 'ChannelInvitationForm'; + return "ChannelInvitationForm"; } if (this.emojiPickerView) { - return 'EmojiPickerView'; + return "EmojiPickerView"; } - if (this.activityButtonViewOwnerAsActivityList || this.activityCellViewOwnerAsActivityList) { - return 'ActivityListView'; + if ( + this.activityButtonViewOwnerAsActivityList || + this.activityCellViewOwnerAsActivityList + ) { + return "ActivityListView"; } if (this.messageNotificationPopoverContentView) { - return 'MessageNotificationPopoverContentView'; + return "MessageNotificationPopoverContentView"; } return clear(); }, @@ -223,7 +259,8 @@ Model({ /** * If set, the content of this popover view is a list of emojis. */ - emojiPickerView: one('EmojiPickerView', { inverse: 'popoverViewOwner', + emojiPickerView: one("EmojiPickerView", { + inverse: "popoverViewOwner", compute() { if (this.composerViewOwnerAsEmoji) { return {}; @@ -234,7 +271,8 @@ Model({ return clear(); }, }), - manager: one('PopoverManager', { inverse: 'popoverViews', + manager: one("PopoverManager", { + inverse: "popoverViews", compute() { if (this.messaging.popoverManager) { return this.messaging.popoverManager; @@ -245,8 +283,12 @@ Model({ /** * If set, this popover view is owned by a message action view. */ - messageActionViewOwnerAsReaction: one('MessageActionView', { identifying: true, inverse: 'reactionPopoverView' }), - messageNotificationPopoverContentView: one('MessageNotificationPopoverContentView', { inverse: 'popoverViewOwner', + messageActionViewOwnerAsReaction: one("MessageActionView", { + identifying: true, + inverse: "reactionPopoverView", + }), + messageNotificationPopoverContentView: one("MessageNotificationPopoverContentView", { + inverse: "popoverViewOwner", compute() { if (this.messageViewOwnerAsNotificationContent) { return {}; @@ -254,36 +296,40 @@ Model({ return clear(); }, }), - messageViewOwnerAsNotificationContent: one('MessageView', { identifying: true, inverse: 'notificationPopoverView' }), + messageViewOwnerAsNotificationContent: one("MessageView", { + identifying: true, + inverse: "notificationPopoverView", + }), /** * Position of the popover view relative to its anchor point. * Valid values: 'top', 'right', 'bottom', 'left' */ - position: attr({ default: 'top', + position: attr({ + default: "top", compute() { if (this.activityViewOwnerAsMarkDone) { - return 'right'; + return "right"; } if (this.callActionListViewOwnerAsMoreMenu) { - return 'top'; + return "top"; } if (this.callParticipantCardOwner) { - return 'bottom'; + return "bottom"; } if (this.threadViewTopbarOwnerAsInvite) { - return 'bottom'; + return "bottom"; } if (this.composerViewOwnerAsEmoji) { - return 'top'; + return "top"; } if (this.activityButtonViewOwnerAsActivityList) { - return 'bottom-start'; + return "bottom-start"; } if (this.activityCellViewOwnerAsActivityList) { - return 'bottom-start'; + return "bottom-start"; } if (this.messageActionViewOwnerAsReaction) { - return 'top'; + return "top"; } return clear(); }, @@ -291,6 +337,9 @@ Model({ /** * If set, this popover view is owned by a thread view topbar record. */ - threadViewTopbarOwnerAsInvite: one('ThreadViewTopbar', { identifying: true, inverse: 'invitePopoverView' }), + threadViewTopbarOwnerAsInvite: one("ThreadViewTopbar", { + identifying: true, + inverse: "invitePopoverView", + }), }, }); diff --git a/addons/mail/static/src/models/res_users_settings.js b/addons/mail/static/src/models/res_users_settings.js index 05ceaf10b4f..17613099b7f 100644 --- a/addons/mail/static/src/models/res_users_settings.js +++ b/addons/mail/static/src/models/res_users_settings.js @@ -1,23 +1,26 @@ /** @odoo-module **/ -import { attr, many, one, Model } from '@mail/model'; +import { attr, many, one, Model } from "@mail/model"; /** * Mirrors the fields of the python model res.users.settings. */ Model({ - name: 'res.users.settings', + name: "res.users.settings", fields: { id: attr({ identifying: true }), is_discuss_sidebar_category_channel_open: attr({ default: true }), is_discuss_sidebar_category_chat_open: attr({ default: true }), push_to_talk_key: attr(), use_push_to_talk: attr({ default: false }), - user_id: one('User', { inverse: 'res_users_settings_id', required: true }), + user_id: one("User", { inverse: "res_users_settings_id", required: true }), voice_active_duration: attr(), /** * States the volume chosen by the current user for each other user. */ - volume_settings_ids: many('res.users.settings.volumes', { inverse: 'user_setting_id', isCausal: true }), + volume_settings_ids: many("res.users.settings.volumes", { + inverse: "user_setting_id", + isCausal: true, + }), }, }); diff --git a/addons/mail/static/src/models/res_users_settings_volumes.js b/addons/mail/static/src/models/res_users_settings_volumes.js index 92a474df6db..9536e9cfdb7 100644 --- a/addons/mail/static/src/models/res_users_settings_volumes.js +++ b/addons/mail/static/src/models/res_users_settings_volumes.js @@ -1,12 +1,12 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; /** * Mirrors the fields of the python model res.users.settings.volumes. */ Model({ - name: 'res.users.settings.volumes', + name: "res.users.settings.volumes", recordMethods: { /** * @private @@ -24,16 +24,20 @@ Model({ }, }, fields: { - guest_id: one('Guest', { inverse: 'volumeSetting' }), + guest_id: one("Guest", { inverse: "volumeSetting" }), id: attr({ identifying: true }), - partner_id: one('Partner', { inverse: 'volumeSetting' }), - user_setting_id: one('res.users.settings', { inverse: 'volume_settings_ids', readonly: true, required: true }), + partner_id: one("Partner", { inverse: "volumeSetting" }), + user_setting_id: one("res.users.settings", { + inverse: "volume_settings_ids", + readonly: true, + required: true, + }), volume: attr({ default: 0.5 }), }, onChanges: [ { - dependencies: ['volume'], - methodName: '_onChangeVolume', + dependencies: ["volume"], + methodName: "_onChangeVolume", }, ], }); diff --git a/addons/mail/static/src/models/rtc.js b/addons/mail/static/src/models/rtc.js index 7f5d83452bd..e9f08694c11 100644 --- a/addons/mail/static/src/models/rtc.js +++ b/addons/mail/static/src/models/rtc.js @@ -2,9 +2,9 @@ import { browser } from "@web/core/browser/browser"; -import { attr, clear, insert, many, one, Model, unlink } from '@mail/model'; -import { monitorAudio } from '@mail/utils/media_monitoring'; -import { sprintf } from '@web/core/utils/strings'; +import { attr, clear, insert, many, one, Model, unlink } from "@mail/model"; +import { monitorAudio } from "@mail/utils/media_monitoring"; +import { sprintf } from "@web/core/utils/strings"; const getRTCPeerNotificationNextTemporaryId = (function () { let tmpId = 0; @@ -15,18 +15,18 @@ const getRTCPeerNotificationNextTemporaryId = (function () { })(); Model({ - name: 'Rtc', + name: "Rtc", lifecycleHooks: { _created() { - browser.addEventListener('keydown', this._onKeyDown); - browser.addEventListener('keyup', this._onKeyUp); + browser.addEventListener("keydown", this._onKeyDown); + browser.addEventListener("keyup", this._onKeyUp); // Disconnects the RTC session if the page is closed or reloaded. - browser.addEventListener('beforeunload', this._onBeforeUnload); + browser.addEventListener("beforeunload", this._onBeforeUnload); }, async _willDelete() { - browser.removeEventListener('beforeunload', this._onBeforeUnload); - browser.removeEventListener('keydown', this._onKeyDown); - browser.removeEventListener('keyup', this._onKeyUp); + browser.removeEventListener("beforeunload", this._onBeforeUnload); + browser.removeEventListener("keydown", this._onKeyDown); + browser.removeEventListener("keyup", this._onKeyUp); this.messaging.browser.clearInterval(this.pingInterval); }, }, @@ -41,14 +41,18 @@ Model({ * @param {RtcSession[]} currentSessions list of sessions of this call. */ async filterCallees(currentSessions) { - const currentSessionsTokens = new Set(currentSessions.map(session => session.id)); + const currentSessionsTokens = new Set(currentSessions.map((session) => session.id)); for (const rtcSession of this.connectedRtcSessions) { if (!currentSessionsTokens.has(rtcSession.id)) { - this._addLogEntry(rtcSession.id, 'session removed from the server'); + this._addLogEntry(rtcSession.id, "session removed from the server"); this._removePeer(rtcSession.id); } } - if (this.channel && this.currentRtcSession && !currentSessionsTokens.has(this.currentRtcSession.id)) { + if ( + this.channel && + this.currentRtcSession && + !currentSessionsTokens.has(this.currentRtcSession.id) + ) { // if the current RTC session is not in the channel sessions, this call is no longer valid. this.channel.endCall(); } @@ -59,7 +63,9 @@ Model({ */ async handleNotification(sender, content) { const { event, channelId, payload } = JSON.parse(content); - const rtcSession = this.messaging.models['RtcSession'].findFromIdentifyingData({ id: sender }); + const rtcSession = this.messaging.models["RtcSession"].findFromIdentifyingData({ + id: sender, + }); if (!rtcSession || rtcSession.channel !== this.channel) { // does handle notifications targeting a different session return; @@ -67,26 +73,35 @@ Model({ if (!this.messaging.device.hasRtcSupport) { return; } - if (!rtcSession.rtcPeerConnection && (!channelId || !this.channel || channelId !== this.channel.id)) { + if ( + !rtcSession.rtcPeerConnection && + (!channelId || !this.channel || channelId !== this.channel.id) + ) { return; } switch (event) { case "offer": - this._addLogEntry(sender, `received notification: ${event}`, { step: 'received offer' }); + this._addLogEntry(sender, `received notification: ${event}`, { + step: "received offer", + }); await this._handleRtcTransactionOffer(rtcSession, payload); break; case "answer": - this._addLogEntry(sender, `received notification: ${event}`, { step: 'received answer' }); + this._addLogEntry(sender, `received notification: ${event}`, { + step: "received answer", + }); await this._handleRtcTransactionAnswer(rtcSession, payload); break; case "ice-candidate": await this._handleRtcTransactionICECandidate(rtcSession, payload); break; case "disconnect": - this._addLogEntry(sender, `received notification: ${event}`, { step: ' peer cleanly disconnected ' }); + this._addLogEntry(sender, `received notification: ${event}`, { + step: " peer cleanly disconnected ", + }); this._removePeer(rtcSession.id); break; - case 'trackChange': + case "trackChange": this._handleTrackChange(rtcSession, payload); break; } @@ -100,7 +115,13 @@ Model({ * @param {boolean} [param0.startWithVideo] * @param {'user-video'|'display'} [param0.videoType] 'user-video' or 'display' */ - async initSession({ currentSessionId, iceServers, startWithAudio, startWithVideo, videoType = 'user-video' }) { + async initSession({ + currentSessionId, + iceServers, + startWithAudio, + startWithVideo, + videoType = "user-video", + }) { // Initializing a new session implies closing the current session. this.reset(); this.update({ @@ -126,11 +147,11 @@ Model({ * @param {String} [param1.type] 'server' or 'peerToPeer', * 'peerToPeer' requires an active RTCPeerConnection */ - async notifyPeers(targetTokens, { event, payload, type = 'server' }) { + async notifyPeers(targetTokens, { event, payload, type = "server" }) { if (!targetTokens.length || !this.channel || !this.currentRtcSession) { return; } - if (type === 'server') { + if (type === "server") { this.update({ peerNotificationsToSend: insert({ channelId: this.channel.id, @@ -138,26 +159,30 @@ Model({ id: getRTCPeerNotificationNextTemporaryId(), payload, senderId: this.currentRtcSession.id, - targetTokens + targetTokens, }), }); await this._sendPeerNotifications(); } - if (type === 'peerToPeer') { + if (type === "peerToPeer") { for (const token of targetTokens) { - const rtcSession = this.messaging.models['RtcSession'].findFromIdentifyingData({ id: token }); + const rtcSession = this.messaging.models["RtcSession"].findFromIdentifyingData({ + id: token, + }); if (!rtcSession) { continue; } const rtcDataChannel = rtcSession.rtcDataChannel; - if (!rtcDataChannel || rtcDataChannel.dataChannel.readyState !== 'open') { + if (!rtcDataChannel || rtcDataChannel.dataChannel.readyState !== "open") { continue; } - rtcDataChannel.dataChannel.send(JSON.stringify({ - event, - channelId: this.channel.id, - payload, - })); + rtcDataChannel.dataChannel.send( + JSON.stringify({ + event, + channelId: this.channel.id, + payload, + }) + ); } } }, @@ -178,12 +203,12 @@ Model({ } if (this.disconnectAudioMonitor) { - this.disconnectAudioMonitor() + this.disconnectAudioMonitor(); } this.audioTrack && this.audioTrack.stop(); this.videoTrack && this.videoTrack.stop(); - for (const rtcSession of this.messaging.models['RtcSession'].all()) { + for (const rtcSession of this.messaging.models["RtcSession"].all()) { this.messaging.browser.clearTimeout(rtcSession.connectionRecoveryTimeout); rtcSession.update({ connectionRecoveryTimeout: clear(), @@ -218,13 +243,13 @@ Model({ * toggles screen broadcasting to peers. */ async toggleScreenShare() { - this._toggleVideoBroadcast({ type: 'display' }); + this._toggleVideoBroadcast({ type: "display" }); }, /** * Toggles user video (eg: webcam) broadcasting to peers. */ async toggleUserVideo({ force } = {}) { - this._toggleVideoBroadcast({ type: 'user-video', force }); + this._toggleVideoBroadcast({ type: "user-video", force }); }, async undeafen() { await this._setDeafState(false); @@ -253,15 +278,17 @@ Model({ if (audio) { let audioTrack; try { - const audioStream = await browser.navigator.mediaDevices.getUserMedia({ audio: this.messaging.userSetting.getAudioConstraints() }); + const audioStream = await browser.navigator.mediaDevices.getUserMedia({ + audio: this.messaging.userSetting.getAudioConstraints(), + }); audioTrack = audioStream.getAudioTracks()[0]; } catch { this.messaging.notify({ message: sprintf( this.env._t(`"%s" requires microphone access`), - window.location.host, + window.location.host ), - type: 'warning', + type: "warning", }); if (this.currentRtcSession) { this.currentRtcSession.updateAndBroadcast({ isSelfMuted: true }); @@ -274,7 +301,7 @@ Model({ audioTrack.stop(); return; } - audioTrack.addEventListener('ended', async () => { + audioTrack.addEventListener("ended", async () => { // this mostly happens when the user retracts microphone permission. await this.updateLocalAudioTrack(false); if (!this.exists()) { @@ -284,14 +311,15 @@ Model({ await this._updateLocalAudioTrackEnabledState(); }); this.currentRtcSession.updateAndBroadcast({ isSelfMuted: false }); - audioTrack.enabled = !this.currentRtcSession.isMute && this.currentRtcSession.isTalking; + audioTrack.enabled = + !this.currentRtcSession.isMute && this.currentRtcSession.isTalking; this.update({ audioTrack }); await this.updateVoiceActivation(); if (!this.exists()) { return; } for (const rtcSession of this.connectedRtcSessions) { - await rtcSession.updateRemoteTrack('audio'); + await rtcSession.updateRemoteTrack("audio"); } } }, @@ -321,13 +349,10 @@ Model({ } try { this.update({ - disconnectAudioMonitor: await monitorAudio( - this.audioTrack, - { - onThreshold: this._onThresholdAudioMonitor, - volumeThreshold: this.messaging.userSetting.voiceActivationThreshold, - }, - ), + disconnectAudioMonitor: await monitorAudio(this.audioTrack, { + onThreshold: this._onThresholdAudioMonitor, + volumeThreshold: this.messaging.userSetting.voiceActivationThreshold, + }), }); } catch { /** @@ -337,7 +362,7 @@ Model({ */ this.messaging.notify({ message: this.env._t("Your browser does not support voice activation"), - type: 'warning', + type: "warning", }); this.currentRtcSession.update({ isTalking: true }); } @@ -357,17 +382,17 @@ Model({ return; } if (!(token in this.logs)) { - this.logs[token] = { step: '', state: '', logs: [] }; + this.logs[token] = { step: "", state: "", logs: [] }; } - const trace = window.Error().stack || ''; + const trace = window.Error().stack || ""; this.logs[token].logs.push({ - event: `${window.moment().format('h:mm:ss')}: ${entry}`, + event: `${window.moment().format("h:mm:ss")}: ${entry}`, error: error && { name: error.name, message: error.message, - stack: error.stack && error.stack.split('\n'), + stack: error.stack && error.stack.split("\n"), }, - trace: trace.split('\n'), + trace: trace.split("\n"), }); if (step) { this.logs[token].step = step; @@ -404,9 +429,9 @@ Model({ continue; } rtcSession.update({ - connectionState: 'Not connected: sending initial RTC offer', + connectionState: "Not connected: sending initial RTC offer", }); - this._addLogEntry(rtcSession.id, 'init call', { step: 'init call' }); + this._addLogEntry(rtcSession.id, "init call", { step: "init call" }); this._callPeer(rtcSession); } }, @@ -420,13 +445,15 @@ Model({ */ _createPeerConnection(rtcSession) { const peerConnection = new window.RTCPeerConnection({ iceServers: this.iceServers }); - this._addLogEntry(rtcSession.id, `RTCPeerConnection created`, { step: 'peer connection created' }); + this._addLogEntry(rtcSession.id, `RTCPeerConnection created`, { + step: "peer connection created", + }); peerConnection.onicecandidate = async (event) => { if (!event.candidate) { return; } await this.notifyPeers([rtcSession.id], { - event: 'ice-candidate', + event: "ice-candidate", payload: { candidate: event.candidate }, }); }; @@ -438,7 +465,10 @@ Model({ }; peerConnection.onicecandidateerror = async (error) => { this._addLogEntry(rtcSession.id, `ice candidate error`); - this._recoverConnection(rtcSession, { delay: this.recoveryTimeout, reason: 'ice candidate error' }); + this._recoverConnection(rtcSession, { + delay: this.recoveryTimeout, + reason: "ice candidate error", + }); }; peerConnection.onnegotiationneeded = async (event) => { const offer = await peerConnection.createOffer(); @@ -449,9 +479,11 @@ Model({ this._addLogEntry(rtcSession.id, `couldn't setLocalDescription`, { error: e }); return; } - this._addLogEntry(rtcSession.id, `sending notification: offer`, { step: 'sending offer' }); + this._addLogEntry(rtcSession.id, `sending notification: offer`, { + step: "sending offer", + }); await this.notifyPeers([rtcSession.id], { - event: 'offer', + event: "offer", payload: { sdp: peerConnection.localDescription }, }); }; @@ -459,7 +491,10 @@ Model({ this._addLogEntry(rtcSession.id, `received ${track.kind} track`); rtcSession.updateStream(track); }; - const dataChannel = peerConnection.createDataChannel("notifications", { negotiated: true, id: 1 }); + const dataChannel = peerConnection.createDataChannel("notifications", { + negotiated: true, + id: 1, + }); dataChannel.onmessage = (event) => { this.handleNotification(rtcSession.id, event.data); }; @@ -470,10 +505,10 @@ Model({ */ try { await this.notifyPeers([rtcSession.id], { - event: 'trackChange', - type: 'peerToPeer', + event: "trackChange", + type: "peerToPeer", payload: { - type: 'audio', + type: "audio", state: { isTalking: this.audioTrack && this.audioTrack.enabled, isSelfMuted: this.currentRtcSession.isSelfMuted, @@ -484,11 +519,17 @@ Model({ if (!(e instanceof DOMException) || e.name !== "OperationError") { throw e; } - this._addLogEntry(rtcSession.id, `failed to send on datachannel; dataChannelInfo: ${this._serializeRTCDataChannel(dataChannel)}`, { error: e }); + this._addLogEntry( + rtcSession.id, + `failed to send on datachannel; dataChannelInfo: ${this._serializeRTCDataChannel( + dataChannel + )}`, + { error: e } + ); } }; rtcSession.update({ rtcPeerConnection: { peerConnection } }); - this.messaging.models['RtcDataChannel'].insert({ + this.messaging.models["RtcDataChannel"].insert({ dataChannel, rtcSession, }); @@ -504,12 +545,14 @@ Model({ const rtcPeerConnection = rtcSession.rtcPeerConnection; if ( !rtcPeerConnection || - this.invalidIceConnectionStates.has(rtcPeerConnection.peerConnection.iceConnectionState) || - rtcPeerConnection.peerConnection.signalingState === 'stable' + this.invalidIceConnectionStates.has( + rtcPeerConnection.peerConnection.iceConnectionState + ) || + rtcPeerConnection.peerConnection.signalingState === "stable" ) { return; } - if (rtcPeerConnection.peerConnection.signalingState === 'have-remote-offer') { + if (rtcPeerConnection.peerConnection.signalingState === "have-remote-offer") { // we already have an offer return; } @@ -517,7 +560,11 @@ Model({ try { await rtcPeerConnection.peerConnection.setRemoteDescription(rtcSessionDescription); } catch (e) { - this._addLogEntry(rtcSession.id, 'answer handling: Failed at setting remoteDescription', { error: e }); + this._addLogEntry( + rtcSession.id, + "answer handling: Failed at setting remoteDescription", + { error: e } + ); // ignored the transaction may have been resolved by another concurrent offer. } }, @@ -529,15 +576,27 @@ Model({ */ async _handleRtcTransactionICECandidate(rtcSession, { candidate }) { const rtcPeerConnection = rtcSession.rtcPeerConnection; - if (!rtcPeerConnection || this.invalidIceConnectionStates.has(rtcPeerConnection.peerConnection.iceConnectionState)) { + if ( + !rtcPeerConnection || + this.invalidIceConnectionStates.has( + rtcPeerConnection.peerConnection.iceConnectionState + ) + ) { return; } const rtcIceCandidate = new window.RTCIceCandidate(candidate); try { await rtcPeerConnection.peerConnection.addIceCandidate(rtcIceCandidate); } catch (error) { - this._addLogEntry(rtcSession.id, 'ICE candidate handling: failed at adding the candidate to the connection', { error }); - this._recoverConnection(rtcSession, { delay: this.recoveryTimeout, reason: 'failed at adding ice candidate' }); + this._addLogEntry( + rtcSession.id, + "ICE candidate handling: failed at adding the candidate to the connection", + { error } + ); + this._recoverConnection(rtcSession, { + delay: this.recoveryTimeout, + reason: "failed at adding ice candidate", + }); } }, /** @@ -548,45 +607,69 @@ Model({ */ async _handleRtcTransactionOffer(rtcSession, { sdp }) { if (!rtcSession.rtcPeerConnection) { - this._createPeerConnection(rtcSession) + this._createPeerConnection(rtcSession); } - if (!rtcSession.rtcPeerConnection || this.invalidIceConnectionStates.has(rtcSession.rtcPeerConnection.peerConnection.iceConnectionState)) { + if ( + !rtcSession.rtcPeerConnection || + this.invalidIceConnectionStates.has( + rtcSession.rtcPeerConnection.peerConnection.iceConnectionState + ) + ) { return; } - if (rtcSession.rtcPeerConnection.peerConnection.signalingState === 'have-remote-offer') { + if ( + rtcSession.rtcPeerConnection.peerConnection.signalingState === "have-remote-offer" + ) { // we already have an offer return; } const rtcSessionDescription = new window.RTCSessionDescription(sdp); try { - await rtcSession.rtcPeerConnection.peerConnection.setRemoteDescription(rtcSessionDescription); + await rtcSession.rtcPeerConnection.peerConnection.setRemoteDescription( + rtcSessionDescription + ); } catch (e) { - this._addLogEntry(rtcSession.id, 'offer handling: failed at setting remoteDescription', { error: e }); + this._addLogEntry( + rtcSession.id, + "offer handling: failed at setting remoteDescription", + { error: e } + ); return; } - await rtcSession.updateRemoteTrack('audio'); - await rtcSession.updateRemoteTrack('video'); + await rtcSession.updateRemoteTrack("audio"); + await rtcSession.updateRemoteTrack("video"); let answer; try { answer = await rtcSession.rtcPeerConnection.peerConnection.createAnswer(); } catch (e) { - this._addLogEntry(rtcSession.id, 'offer handling: failed at creating answer', { error: e }); + this._addLogEntry(rtcSession.id, "offer handling: failed at creating answer", { + error: e, + }); return; } try { await rtcSession.rtcPeerConnection.peerConnection.setLocalDescription(answer); } catch (e) { - this._addLogEntry(rtcSession.id, 'offer handling: failed at setting localDescription', { error: e }); + this._addLogEntry( + rtcSession.id, + "offer handling: failed at setting localDescription", + { error: e } + ); return; } - this._addLogEntry(rtcSession.id, `sending notification: answer`, { step: 'sending answer' }); + this._addLogEntry(rtcSession.id, `sending notification: answer`, { + step: "sending answer", + }); await this.notifyPeers([rtcSession.id], { - event: 'answer', + event: "answer", payload: { sdp: rtcSession.rtcPeerConnection.peerConnection.localDescription }, }); - this._recoverConnection(rtcSession, { delay: this.recoveryTimeout, reason: 'standard answer timeout' }); + this._recoverConnection(rtcSession, { + delay: this.recoveryTimeout, + reason: "standard answer timeout", + }); }, /** * @private @@ -597,7 +680,7 @@ Model({ */ _handleTrackChange(rtcSession, { type, state }) { const { isSelfMuted, isTalking, isSendingVideo, isDeaf } = state; - if (type === 'audio') { + if (type === "audio") { if (!rtcSession.audioStream) { return; } @@ -607,7 +690,7 @@ Model({ isDeaf, }); } - if (type === 'video' && isSendingVideo === false) { + if (type === "video" && isSendingVideo === false) { /** * Since WebRTC "unified plan", the local track is tied to the * remote transceiver.sender and not the remote track. Therefore @@ -631,12 +714,15 @@ Model({ if (rtcSession.isCurrentUserInitiatorOfConnectionOffer) { return; } - if (rtcPeerConnection.peerConnection.iceConnectionState === 'connected') { + if (rtcPeerConnection.peerConnection.iceConnectionState === "connected") { return; } - this._addLogEntry(rtcSession.id, `calling back to recover ${rtcPeerConnection.peerConnection.iceConnectionState} connection, reason: ${reason}`); + this._addLogEntry( + rtcSession.id, + `calling back to recover ${rtcPeerConnection.peerConnection.iceConnectionState} connection, reason: ${reason}` + ); await this.notifyPeers([rtcSession.id], { - event: 'disconnect', + event: "disconnect", }); this._removePeer(rtcSession.id); this._callPeer(rtcSession); @@ -646,14 +732,19 @@ Model({ */ async _pingServer() { const channel = this.channel; - const { rtcSessions } = await this.messaging.rpc({ - route: '/mail/channel/ping', - params: { - 'channel_id': channel.id, - 'check_rtc_session_ids': channel.rtcSessions.map(rtcSession => rtcSession.id), - 'rtc_session_id': this.currentRtcSession.id, + const { rtcSessions } = await this.messaging.rpc( + { + route: "/mail/channel/ping", + params: { + channel_id: channel.id, + check_rtc_session_ids: channel.rtcSessions.map( + (rtcSession) => rtcSession.id + ), + rtc_session_id: this.currentRtcSession.id, + }, }, - }, { shadow: true }); + { shadow: true } + ); if (channel.exists()) { channel.updateRtcSessions(rtcSessions); } @@ -668,14 +759,14 @@ Model({ * @param {number} [param1.delay] in ms * @param {string} [param1.reason] */ - _recoverConnection(rtcSession, { delay = 0, reason = '' } = {}) { + _recoverConnection(rtcSession, { delay = 0, reason = "" } = {}) { if (rtcSession.connectionRecoveryTimeout) { return; } rtcSession.update({ connectionRecoveryTimeout: this.messaging.browser.setTimeout( this._onRecoverConnectionTimeout.bind(this, rtcSession, reason), - delay, + delay ), }); }, @@ -686,7 +777,9 @@ Model({ * @param {number} sessionId */ _removePeer(sessionId) { - const rtcSession = this.messaging.models['RtcSession'].findFromIdentifyingData({ id: sessionId }); + const rtcSession = this.messaging.models["RtcSession"].findFromIdentifyingData({ + id: sessionId, + }); if (rtcSession) { rtcSession.reset(); const rtcDataChannel = rtcSession.rtcDataChannel; @@ -699,11 +792,11 @@ Model({ this._removeRemoteTracks(rtcPeerConnection.peerConnection); rtcPeerConnection.delete(); } - this.messaging.models['RtcSession'].insert({ + this.messaging.models["RtcSession"].insert({ id: sessionId, isCurrentUserInitiatorOfConnectionOffer: clear(), }); - this._addLogEntry(sessionId, 'peer removed', { step: 'peer removed' }); + this._addLogEntry(sessionId, "peer removed", { step: "peer removed" }); }, /** * Terminates the Transceivers of the peer connection. @@ -739,14 +832,14 @@ Model({ return; } this.update({ isNotifyPeersRPCInProgress: true }); - await new Promise(resolve => setTimeout(resolve, this.peerNotificationWaitDelay)); + await new Promise((resolve) => setTimeout(resolve, this.peerNotificationWaitDelay)); const peerNotifications = this.peerNotificationsToSend; try { - await this.messaging.rpc({ - route: '/mail/rtc/session/notify_call_members', - params: { - 'peer_notifications': peerNotifications.map(peerNotification => - [ + await this.messaging.rpc( + { + route: "/mail/rtc/session/notify_call_members", + params: { + peer_notifications: peerNotifications.map((peerNotification) => [ peerNotification.senderId, peerNotification.targetTokens, JSON.stringify({ @@ -754,10 +847,11 @@ Model({ channelId: peerNotification.channelId, payload: peerNotification.payload, }), - ], - ), + ]), + }, }, - }, { shadow: true }); + { shadow: true } + ); if (!this.exists()) { return; } @@ -793,14 +887,14 @@ Model({ "protocol", "readyState", ]; - return JSON.stringify(Object.fromEntries(toLog.map(p => [p, dataChannel[p]]))); + return JSON.stringify(Object.fromEntries(toLog.map((p) => [p, dataChannel[p]]))); }, /** * @param {Boolean} isDeaf */ async _setDeafState(isDeaf) { this.currentRtcSession.updateAndBroadcast({ isDeaf }); - for (const session of this.messaging.models['RtcSession'].all()) { + for (const session of this.messaging.models["RtcSession"].all()) { if (!session.audioElement) { continue; } @@ -844,7 +938,7 @@ Model({ } await this._toggleLocalVideoTrack(trackOptions); for (const rtcSession of this.connectedRtcSessions) { - await rtcSession.updateRemoteTrack('video'); + await rtcSession.updateRemoteTrack("video"); } if (!this.currentRtcSession) { return; @@ -861,11 +955,11 @@ Model({ * @param {boolean} [param0.force] */ async _toggleLocalVideoTrack({ type, force }) { - if (type === 'user-video') { + if (type === "user-video") { const sendUserVideo = force !== undefined ? force : !this.sendUserVideo; await this._updateLocalVideoTrack(type, sendUserVideo); } - if (type === 'display') { + if (type === "display") { const sendDisplay = force !== undefined ? force : !this.sendDisplay; await this._updateLocalVideoTrack(type, sendDisplay); } @@ -888,19 +982,23 @@ Model({ if (!this.audioTrack) { return; } - this.audioTrack.enabled = !this.currentRtcSession.isMute && this.currentRtcSession.isTalking; - await this.notifyPeers(this.connectedRtcSessions.map(rtcSession => rtcSession.id), { - event: 'trackChange', - type: 'peerToPeer', - payload: { - type: 'audio', - state: { - isTalking: this.audioTrack.enabled, - isSelfMuted: this.currentRtcSession.isSelfMuted, - isDeaf: this.currentRtcSession.isDeaf, + this.audioTrack.enabled = + !this.currentRtcSession.isMute && this.currentRtcSession.isTalking; + await this.notifyPeers( + this.connectedRtcSessions.map((rtcSession) => rtcSession.id), + { + event: "trackChange", + type: "peerToPeer", + payload: { + type: "audio", + state: { + isTalking: this.audioTrack.enabled, + isSelfMuted: this.currentRtcSession.isSelfMuted, + isDeaf: this.currentRtcSession.isDeaf, + }, }, - }, - }); + } + ); }, /** * @private @@ -927,7 +1025,7 @@ Model({ srcStream: clear(), }); } - if (type === 'display') { + if (type === "display") { this.messaging.soundEffects.screenSharing.play(); } stopVideo(); @@ -935,15 +1033,19 @@ Model({ } let sourceWebMediaStream; try { - if (type === 'user-video') { + if (type === "user-video") { if (this.blurManager && this.blurManager.srcStream) { sourceWebMediaStream = this.blurManager.srcStream.webMediaStream; } else { - sourceWebMediaStream = await browser.navigator.mediaDevices.getUserMedia({ video: this.videoConfig }); + sourceWebMediaStream = await browser.navigator.mediaDevices.getUserMedia({ + video: this.videoConfig, + }); } } - if (type === 'display') { - sourceWebMediaStream = await browser.navigator.mediaDevices.getDisplayMedia({ video: this.videoConfig }); + if (type === "display") { + sourceWebMediaStream = await browser.navigator.mediaDevices.getDisplayMedia({ + video: this.videoConfig, + }); this.messaging.soundEffects.screenSharing.play(); } } catch { @@ -951,43 +1053,51 @@ Model({ message: sprintf( this.env._t(`"%s" requires "%s" access`), window.location.host, - type === 'user-video' ? 'camera' : 'display', + type === "user-video" ? "camera" : "display" ), - type: 'warning', + type: "warning", }); stopVideo(); return; } let videoStream = sourceWebMediaStream; - if (this.messaging.userSetting.useBlur && type === 'user-video') { + if (this.messaging.userSetting.useBlur && type === "user-video") { try { - this.update({ blurManager: { srcStream: { webMediaStream: sourceWebMediaStream, id: sourceWebMediaStream.id } }, }); + this.update({ + blurManager: { + srcStream: { + webMediaStream: sourceWebMediaStream, + id: sourceWebMediaStream.id, + }, + }, + }); const mediaStream = await this.blurManager.stream; videoStream = mediaStream.webMediaStream; } catch (_e) { this.messaging.notify({ - message: sprintf( - this.env._t('To %(name)s: %(message)s)'), { - name: _e.name, - message: _e.message, - }, - ), - type: 'warning', + message: sprintf(this.env._t("To %(name)s: %(message)s)"), { + name: _e.name, + message: _e.message, + }), + type: "warning", }); this.messaging.userSetting.update({ useBlur: false }); } } const videoTrack = videoStream ? videoStream.getVideoTracks()[0] : undefined; if (videoTrack) { - videoTrack.addEventListener('ended', async () => { + videoTrack.addEventListener("ended", async () => { await this._toggleVideoBroadcast({ force: false, type }); }); } this.update({ - sourceVideoStream: { webMediaStream: sourceWebMediaStream, id: sourceWebMediaStream.id }, + sourceVideoStream: { + webMediaStream: sourceWebMediaStream, + id: sourceWebMediaStream.id, + }, videoTrack, - sendUserVideo: type === 'user-video' && !!videoTrack, - sendDisplay: type === 'display' && !!videoTrack, + sendUserVideo: type === "user-video" && !!videoTrack, + sendDisplay: type === "display" && !!videoTrack, }); }, /** @@ -1012,7 +1122,10 @@ Model({ break; case "failed": case "disconnected": - await this._recoverConnection(rtcSession, { delay: this.recoveryDelay, reason: `connection ${state}` }); + await this._recoverConnection(rtcSession, { + delay: this.recoveryDelay, + reason: `connection ${state}`, + }); break; } }, @@ -1022,7 +1135,9 @@ Model({ * @param {RtcSession} rtcSession id of the rtcSession of the peer whose the connection changed */ async _onICEConnectionStateChange(connectionState, rtcSession) { - this._addLogEntry(rtcSession.id, `ICE connection state changed: ${connectionState}`, { state: connectionState }); + this._addLogEntry(rtcSession.id, `ICE connection state changed: ${connectionState}`, { + state: connectionState, + }); if (!rtcSession) { return; } @@ -1038,7 +1153,10 @@ Model({ break; case "failed": case "disconnected": - await this._recoverConnection(rtcSession, { delay: this.recoveryDelay, reason: `ice connection ${connectionState}` }); + await this._recoverConnection(rtcSession, { + delay: this.recoveryDelay, + reason: `ice connection ${connectionState}`, + }); break; } }, @@ -1050,7 +1168,10 @@ Model({ if (!this.channel) { return; } - if (!this.messaging.userSetting.usePushToTalk || !this.messaging.userSetting.isPushToTalkKey(ev)) { + if ( + !this.messaging.userSetting.usePushToTalk || + !this.messaging.userSetting.isPushToTalkKey(ev) + ) { return; } if (this.currentRtcSession.isMute) { @@ -1073,7 +1194,10 @@ Model({ if (!this.channel) { return; } - if (!this.messaging.userSetting.usePushToTalk || !this.messaging.userSetting.isPushToTalkKey(ev, { ignoreModifiers: true })) { + if ( + !this.messaging.userSetting.usePushToTalk || + !this.messaging.userSetting.isPushToTalkKey(ev, { ignoreModifiers: true }) + ) { return; } if (!this.currentRtcSession.isTalking) { @@ -1085,7 +1209,7 @@ Model({ this.update({ pushToTalkTimeout: this.messaging.browser.setTimeout( this._onPushToTalkTimeout, - this.messaging.userSetting.voiceActiveDuration, + this.messaging.userSetting.voiceActiveDuration ), }); }, @@ -1122,20 +1246,20 @@ Model({ * audio MediaStreamTrack of the current user */ audioTrack: attr(), - blurManager: one('BlurManager', { inverse: 'rtc' }), + blurManager: one("BlurManager", { inverse: "rtc" }), /** * The channel that is hosting the current RTC call. */ - channel: one('Thread', { inverse: 'rtc' }), + channel: one("Thread", { inverse: "rtc" }), /** * Contains the RTC Session that are connected. * Connected RTC Sessions have rtcPeerConnection set. */ - connectedRtcSessions: many('RtcSession', { inverse: 'rtcAsConnectedSession' }), + connectedRtcSessions: many("RtcSession", { inverse: "rtcAsConnectedSession" }), /** * String, peerToken of the current session used to identify them during the peer-to-peer transactions. */ - currentRtcSession: one('RtcSession', { inverse: 'rtcAsCurrentSession' }), + currentRtcSession: one("RtcSession", { inverse: "rtcAsCurrentSession" }), /** * callback to properly end the audio monitoring. * If set it indicates that we are currently monitoring the local @@ -1146,12 +1270,17 @@ Model({ * ICE servers used by RTCPeerConnection to retrieve the public IP address (STUN) * or to relay packets when necessary (TURN). */ - iceServers: attr({ default: [{ urls: ['stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302'] }] }), + iceServers: attr({ + default: [{ urls: ["stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"] }], + }), /** * list of connection states considered invalid, which means that * no action should be taken on such peerConnection. */ - invalidIceConnectionStates: attr({ default: new Set(['disconnected', 'failed', 'closed']), readonly: true }), + invalidIceConnectionStates: attr({ + default: new Set(["disconnected", "failed", "closed"]), + readonly: true, + }), isNotifyPeersRPCInProgress: attr({ default: false }), /** * Contains the logs of the current session by token. @@ -1162,8 +1291,12 @@ Model({ * List of transceivers in ordered for consistent insertion and retrieval order, relevant for * RTCPeerConnection.getTransceivers which returns transceivers in insertion order as per webRTC specifications. */ - orderedTransceiverNames: attr({ default: ['audio', 'video'], readonly: true, required: true }), - peerNotificationsToSend: many('RtcPeerNotification', { isCausal: true }), + orderedTransceiverNames: attr({ + default: ["audio", "video"], + readonly: true, + required: true, + }), + peerNotificationsToSend: many("RtcPeerNotification", { isCausal: true }), /** * Determines the delay to wait (in ms) before sending peer * notifications to the server. Sending many notifications at once @@ -1190,7 +1323,11 @@ Model({ /** * The protocols for each RTC ICE candidate types. */ - protocolsByCandidateTypes: attr({ default: { 'host': "HOST", 'srflx': "STUN", 'prflx': "STUN", 'relay': "TURN" }, readonly: true, required: true }), + protocolsByCandidateTypes: attr({ + default: { host: "HOST", srflx: "STUN", prflx: "STUN", relay: "TURN" }, + readonly: true, + required: true, + }), /** * timeoutId for the push to talk release delay. */ @@ -1203,7 +1340,7 @@ Model({ * How long to wait before recovering a connection that has failed in ms. */ recoveryDelay: attr({ default: 3000 }), - callSystrayMenu: one('CallSystrayMenu', { default: {}, inverse: 'rtc' }), + callSystrayMenu: one("CallSystrayMenu", { default: {}, inverse: "rtc" }), /** * True if we want to enable the video track of the current partner. */ @@ -1215,7 +1352,7 @@ Model({ /** * Ensures that we always have a single source stream and that replacing it will properly terminate its tracks. */ - sourceVideoStream: one('MediaStream', { isCausal: true }), + sourceVideoStream: one("MediaStream", { isCausal: true }), /** * MediaTrackConstraints for the user video track. * Some browsers do not support all constraints, for example firefox diff --git a/addons/mail/static/src/models/rtc_data_channel.js b/addons/mail/static/src/models/rtc_data_channel.js index 0877115c251..fb13e3fe42d 100644 --- a/addons/mail/static/src/models/rtc_data_channel.js +++ b/addons/mail/static/src/models/rtc_data_channel.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'RtcDataChannel', + name: "RtcDataChannel", lifecycleHooks: { _willDelete() { this.dataChannel.close(); @@ -11,6 +11,6 @@ Model({ }, fields: { dataChannel: attr({ required: true, readonly: true }), - rtcSession: one('RtcSession', { identifying: true, inverse: 'rtcDataChannel' }), + rtcSession: one("RtcSession", { identifying: true, inverse: "rtcDataChannel" }), }, }); diff --git a/addons/mail/static/src/models/rtc_peer_connection.js b/addons/mail/static/src/models/rtc_peer_connection.js index d4a0212d40b..46ea56d219c 100644 --- a/addons/mail/static/src/models/rtc_peer_connection.js +++ b/addons/mail/static/src/models/rtc_peer_connection.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; Model({ - name: 'RtcPeerConnection', + name: "RtcPeerConnection", lifecycleHooks: { _willDelete() { this.peerConnection.close(); - } + }, }, recordMethods: { /** @@ -22,15 +22,15 @@ Model({ * @private */ _onChangeAcceptsVideoStream() { - const transceiver = this.getTransceiver('video'); + const transceiver = this.getTransceiver("video"); if (!transceiver) { return; } const rtc = this.rtcSession.rtcAsConnectedSession; if (this.acceptsVideoStream) { - transceiver.direction = rtc.videoTrack ? 'sendrecv' : 'recvonly'; + transceiver.direction = rtc.videoTrack ? "sendrecv" : "recvonly"; } else { - transceiver.direction = rtc.videoTrack ? 'sendonly' : 'inactive'; + transceiver.direction = rtc.videoTrack ? "sendonly" : "inactive"; } }, }, @@ -43,7 +43,10 @@ Model({ * The download is allowed when there are views that display the video stream. */ compute() { - return Boolean(this.rtcSession.callParticipantCards && this.rtcSession.callParticipantCards.length > 0); + return Boolean( + this.rtcSession.callParticipantCards && + this.rtcSession.callParticipantCards.length > 0 + ); }, }), /** @@ -51,12 +54,12 @@ Model({ * If unset, this RTC Session is not considered as connected */ peerConnection: attr(), - rtcSession: one('RtcSession', { identifying: true, inverse: 'rtcPeerConnection' }), + rtcSession: one("RtcSession", { identifying: true, inverse: "rtcPeerConnection" }), }, onChanges: [ { - dependencies: ['acceptsVideoStream'], - methodName: '_onChangeAcceptsVideoStream', + dependencies: ["acceptsVideoStream"], + methodName: "_onChangeAcceptsVideoStream", }, ], }); diff --git a/addons/mail/static/src/models/rtc_peer_notification.js b/addons/mail/static/src/models/rtc_peer_notification.js index 25e8e454fee..ab6f808e6aa 100644 --- a/addons/mail/static/src/models/rtc_peer_notification.js +++ b/addons/mail/static/src/models/rtc_peer_notification.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, Model } from '@mail/model'; +import { attr, Model } from "@mail/model"; Model({ - name: 'RtcPeerNotification', + name: "RtcPeerNotification", fields: { channelId: attr({ readonly: true, required: true }), event: attr({ readonly: true, required: true }), diff --git a/addons/mail/static/src/models/rtc_session.js b/addons/mail/static/src/models/rtc_session.js index 6d1003cf790..7e2f704f7b2 100644 --- a/addons/mail/static/src/models/rtc_session.js +++ b/addons/mail/static/src/models/rtc_session.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'RtcSession', + name: "RtcSession", lifecycleHooks: { _willDelete() { this._removeAudio(); @@ -16,7 +16,7 @@ Model({ this.update({ broadcastTimer: clear() }); this.messaging.rpc( { - route: '/mail/rtc/session/update_and_broadcast', + route: "/mail/rtc/session/update_and_broadcast", params: { session_id: this.id, values: { @@ -27,7 +27,7 @@ Model({ }, }, }, - { shadow: true }, + { shadow: true } ); }, /** @@ -77,7 +77,8 @@ Model({ this.channelMember.persona.volumeSetting.update({ volume: this.volume }); } this.messaging.userSetting.saveVolumeSetting({ - partnerId: this.channelMember.persona.partner && this.channelMember.persona.partner.id, + partnerId: + this.channelMember.persona.partner && this.channelMember.persona.partner.id, guestId: this.channelMember.persona.guest && this.channelMember.persona.guest.id, volume: this.volume, }); @@ -122,12 +123,14 @@ Model({ } const stats = await this.rtcPeerConnection.peerConnection.getStats(); for (const { localCandidateId, remoteCandidateId, state, type } of stats.values()) { - if (type === 'candidate-pair' && state === 'succeeded' && localCandidateId) { + if (type === "candidate-pair" && state === "succeeded" && localCandidateId) { const localCandidate = stats.get(localCandidateId); const remoteCandidate = stats.get(remoteCandidateId); this.update({ localCandidateType: localCandidate ? localCandidate.candidateType : clear(), - remoteCandidateType: remoteCandidate ? remoteCandidate.candidateType : clear(), + remoteCandidateType: remoteCandidate + ? remoteCandidate.candidateType + : clear(), }); return; } @@ -146,14 +149,14 @@ Model({ const stream = new window.MediaStream(); stream.addTrack(track); - if (track.kind === 'audio') { + if (track.kind === "audio") { this._setAudio({ audioStream: stream, isSelfMuted: false, isTalking: false, }); } - if (track.kind === 'video') { + if (track.kind === "video") { this.update({ videoStream: stream }); } }, @@ -172,10 +175,13 @@ Model({ if (!this.rtcAsConnectedSession) { return; } - const track = trackKind === 'audio' ? this.rtcAsConnectedSession.audioTrack : this.rtcAsConnectedSession.videoTrack; - let transceiverDirection = track ? 'sendrecv' : 'recvonly'; - if (trackKind === 'video' && !this.rtcPeerConnection.acceptsVideoStream) { - transceiverDirection = track ? 'sendonly' : 'inactive'; + const track = + trackKind === "audio" + ? this.rtcAsConnectedSession.audioTrack + : this.rtcAsConnectedSession.videoTrack; + let transceiverDirection = track ? "sendrecv" : "recvonly"; + if (trackKind === "video" && !this.rtcPeerConnection.acceptsVideoStream) { + transceiverDirection = track ? "sendonly" : "inactive"; } let transceiver; if (initTransceiver) { @@ -198,12 +204,12 @@ Model({ } catch { // ignored, the transceiver is probably already removed } - if (trackKind === 'video') { + if (trackKind === "video") { this.rtcAsConnectedSession.notifyPeers([this.id], { - event: 'trackChange', - type: 'peerToPeer', + event: "trackChange", + type: "peerToPeer", payload: { - type: 'video', + type: "video", state: { isSendingVideo: false }, }, }); @@ -252,9 +258,15 @@ Model({ console.error(error); } audioElement.load(); - if (this.channelMember.persona.partner && this.channelMember.persona.partner.volumeSetting) { + if ( + this.channelMember.persona.partner && + this.channelMember.persona.partner.volumeSetting + ) { audioElement.volume = this.channelMember.persona.partner.volumeSetting.volume; - } else if (this.channelMember.persona.guest && this.channelMember.persona.guest.volumeSetting) { + } else if ( + this.channelMember.persona.guest && + this.channelMember.persona.guest.volumeSetting + ) { audioElement.volume = this.channelMember.persona.guest.volumeSetting.volume; } else { audioElement.volume = this.volume; @@ -276,7 +288,7 @@ Model({ } this.update({ isAudioInError: false }); } catch (error) { - if (typeof error === 'object' && error.name === 'NotAllowedError') { + if (typeof error === "object" && error.name === "NotAllowedError") { // Ignored as some browsers may reject play() calls that do not // originate from a user input. return; @@ -298,19 +310,19 @@ Model({ * MediaStream */ audioStream: attr(), - broadcastTimer: one('Timer', { inverse: 'rtcSessionOwnerAsBroadcast' }), + broadcastTimer: one("Timer", { inverse: "rtcSessionOwnerAsBroadcast" }), /** * The mail.channel of the session, rtc sessions are part and managed by * mail.channel */ - channel: one('Thread', { inverse: 'rtcSessions' }), - channelMember: one('ChannelMember', { inverse: 'rtcSession' }), + channel: one("Thread", { inverse: "rtcSessions" }), + channelMember: one("ChannelMember", { inverse: "rtcSession" }), connectionRecoveryTimeout: attr(), /** * State of the connection with this session, uses RTCPeerConnection.iceConnectionState * once a peerConnection has been initialized. */ - connectionState: attr({ default: 'Waiting for the peer to send a RTC offer' }), + connectionState: attr({ default: "Waiting for the peer to send a RTC offer" }), /** * Id of the record on the server. */ @@ -320,12 +332,12 @@ Model({ * this serves as an explicit inverse as it seems to confuse it with * other session-channel relations otherwise. */ - calledChannels: many('Thread', { inverse: 'rtcInvitingSession' }), + calledChannels: many("Thread", { inverse: "rtcInvitingSession" }), /** * The participant cards of this session, * this is used to know how many views are displaying this session. */ - callParticipantCards: many('CallParticipantCard', { inverse: 'rtcSession' }), + callParticipantCards: many("CallParticipantCard", { inverse: "rtcSession" }), /** * States whether there is currently an error with the audio element. */ @@ -354,7 +366,8 @@ Model({ /** * Determine whether current session is unable to speak. */ - isMute: attr({ default: false, + isMute: attr({ + default: false, compute() { return this.isSelfMuted || this.isDeaf; }, @@ -371,8 +384,12 @@ Model({ if (!this.messaging || !this.channelMember) { return; } - return (this.channelMember.persona.partner && this.messaging.currentPartner === this.channelMember.persona.partner) || - (this.channelMember.persona.guest && this.messaging.currentGuest === this.channelMember.persona.guest); + return ( + (this.channelMember.persona.partner && + this.messaging.currentPartner === this.channelMember.persona.partner) || + (this.channelMember.persona.guest && + this.messaging.currentGuest === this.channelMember.persona.guest) + ); }, }), /** @@ -408,8 +425,9 @@ Model({ * sessions from other channels with the same partner (sessions opened from different * tabs or devices). */ - rtcAsCurrentSession: one('Rtc', { inverse: 'currentRtcSession' }), - rtcAsConnectedSession: one('Rtc', { inverse: 'connectedRtcSessions', + rtcAsCurrentSession: one("Rtc", { inverse: "currentRtcSession" }), + rtcAsConnectedSession: one("Rtc", { + inverse: "connectedRtcSessions", compute() { if (!this.messaging || !this.messaging.rtc) { return clear(); @@ -420,11 +438,11 @@ Model({ return clear(); }, }), - rtcPeerConnection: one('RtcPeerConnection', { inverse: 'rtcSession' }), + rtcPeerConnection: one("RtcPeerConnection", { inverse: "rtcSession" }), /** * Contains the RTCDataChannel of the rtc session. */ - rtcDataChannel: one('RtcDataChannel', { inverse: 'rtcSession' }), + rtcDataChannel: one("RtcDataChannel", { inverse: "rtcSession" }), /** * MediaStream of the user's video. * @@ -435,7 +453,8 @@ Model({ /** * The volume of the audio played from this session. */ - volume: attr({ default: 0.5, + volume: attr({ + default: 0.5, compute() { if (this.localVolume !== undefined) { return this.localVolume; diff --git a/addons/mail/static/src/models/sound_effect.js b/addons/mail/static/src/models/sound_effect.js index a38e6f4eb7b..aea811eefaa 100644 --- a/addons/mail/static/src/models/sound_effect.js +++ b/addons/mail/static/src/models/sound_effect.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, Model } from '@mail/model'; +import { attr, Model } from "@mail/model"; Model({ - name: 'SoundEffect', + name: "SoundEffect", recordMethods: { /** * @param {Object} param0 @@ -15,7 +15,7 @@ Model({ if (this.messaging.isInQUnitTest) { return; } - if (typeof(Audio) === "undefined") { + if (typeof Audio === "undefined") { return; } if (!this.audio) { @@ -28,7 +28,7 @@ Model({ this.audio.currentTime = 0; this.audio.loop = loop; this.audio.volume = volume !== undefined ? volume : this.defaultVolume; - Promise.resolve(this.audio.play()).catch(()=>{}); + Promise.resolve(this.audio.play()).catch(() => {}); }, /** * Resets the audio to the start of the track and pauses it. diff --git a/addons/mail/static/src/models/sound_effects.js b/addons/mail/static/src/models/sound_effects.js index fde83b0cf4b..37bb77086a1 100644 --- a/addons/mail/static/src/models/sound_effects.js +++ b/addons/mail/static/src/models/sound_effects.js @@ -1,21 +1,57 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'SoundEffects', + name: "SoundEffects", fields: { - channelJoin: one('SoundEffect', { default: { defaultVolume: 0.3, path: '/mail/static/src/audio/channel_01_in' }, isCausal: true }), - channelLeave: one('SoundEffect', { default: { path: '/mail/static/src/audio/channel_04_out' }, isCausal: true }), - deafen: one('SoundEffect', { default: { defaultVolume: 0.15, path: '/mail/static/src/audio/deafen_new_01' }, isCausal: true }), - incomingCall: one('SoundEffect', { default: { defaultVolume: 0.15, path: '/mail/static/src/audio/call_02_in_' }, isCausal: true }), - memberLeave: one('SoundEffect', { default: { defaultVolume: 0.5, path: '/mail/static/src/audio/channel_01_out' }, isCausal: true }), - mute: one('SoundEffect', { default: { defaultVolume: 0.2, path: '/mail/static/src/audio/mute_1' }, isCausal: true }), - newMessage: one('SoundEffect', { default: { path: '/mail/static/src/audio/dm_02' }, isCausal: true }), - pushToTalkOn: one('SoundEffect', { default: { defaultVolume: 0.05, path: '/mail/static/src/audio/ptt_push_1' }, isCausal: true }), - pushToTalkOff: one('SoundEffect', { default: { defaultVolume: 0.05, path: '/mail/static/src/audio/ptt_release_1' }, isCausal: true }), - screenSharing: one('SoundEffect', { default: { defaultVolume: 0.5, path: '/mail/static/src/audio/share_02' }, isCausal: true }), - undeafen: one('SoundEffect', { default: { defaultVolume: 0.15, path: '/mail/static/src/audio/undeafen_new_01' }, isCausal: true }), - unmute: one('SoundEffect', { default: { defaultVolume: 0.2, path: '/mail/static/src/audio/unmute_1' }, isCausal: true }), + channelJoin: one("SoundEffect", { + default: { defaultVolume: 0.3, path: "/mail/static/src/audio/channel_01_in" }, + isCausal: true, + }), + channelLeave: one("SoundEffect", { + default: { path: "/mail/static/src/audio/channel_04_out" }, + isCausal: true, + }), + deafen: one("SoundEffect", { + default: { defaultVolume: 0.15, path: "/mail/static/src/audio/deafen_new_01" }, + isCausal: true, + }), + incomingCall: one("SoundEffect", { + default: { defaultVolume: 0.15, path: "/mail/static/src/audio/call_02_in_" }, + isCausal: true, + }), + memberLeave: one("SoundEffect", { + default: { defaultVolume: 0.5, path: "/mail/static/src/audio/channel_01_out" }, + isCausal: true, + }), + mute: one("SoundEffect", { + default: { defaultVolume: 0.2, path: "/mail/static/src/audio/mute_1" }, + isCausal: true, + }), + newMessage: one("SoundEffect", { + default: { path: "/mail/static/src/audio/dm_02" }, + isCausal: true, + }), + pushToTalkOn: one("SoundEffect", { + default: { defaultVolume: 0.05, path: "/mail/static/src/audio/ptt_push_1" }, + isCausal: true, + }), + pushToTalkOff: one("SoundEffect", { + default: { defaultVolume: 0.05, path: "/mail/static/src/audio/ptt_release_1" }, + isCausal: true, + }), + screenSharing: one("SoundEffect", { + default: { defaultVolume: 0.5, path: "/mail/static/src/audio/share_02" }, + isCausal: true, + }), + undeafen: one("SoundEffect", { + default: { defaultVolume: 0.15, path: "/mail/static/src/audio/undeafen_new_01" }, + isCausal: true, + }), + unmute: one("SoundEffect", { + default: { defaultVolume: 0.2, path: "/mail/static/src/audio/unmute_1" }, + isCausal: true, + }), }, }); diff --git a/addons/mail/static/src/models/suggested_recipient_info.js b/addons/mail/static/src/models/suggested_recipient_info.js index 71d8406acc5..81ca18af343 100644 --- a/addons/mail/static/src/models/suggested_recipient_info.js +++ b/addons/mail/static/src/models/suggested_recipient_info.js @@ -1,13 +1,15 @@ /** @odoo-module **/ -import { attr, many, one, Model } from '@mail/model'; +import { attr, many, one, Model } from "@mail/model"; -import { sprintf } from '@web/core/utils/strings'; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'SuggestedRecipientInfo', + name: "SuggestedRecipientInfo", fields: { - composerSuggestedRecipientViews: many('ComposerSuggestedRecipientView', { inverse: 'suggestedRecipientInfo' }), + composerSuggestedRecipientViews: many("ComposerSuggestedRecipientView", { + inverse: "suggestedRecipientInfo", + }), dialogText: attr({ compute() { return this.env._t("Please complete customer's information"); @@ -35,7 +37,8 @@ Model({ * Determines whether `this` will be added to recipients when posting a * new message on `this.thread`. */ - isSelected: attr({ default: true, + isSelected: attr({ + default: true, /** * Prevents selecting a recipient that does not have a partner. */ @@ -55,13 +58,13 @@ Model({ */ name: attr({ compute() { - return this.partner && this.partner.nameOrDisplayName || this.name; + return (this.partner && this.partner.nameOrDisplayName) || this.name; }, }), /** * Determines the optional `Partner` associated to `this`. */ - partner: one('Partner'), + partner: one("Partner"), /** * Determines why `this` is a suggestion for `this.thread`. It serves as * visual clue when displaying `this`. @@ -70,7 +73,7 @@ Model({ /** * Determines the `Thread` concerned by `this.` */ - thread: one('Thread', { inverse: 'suggestedRecipientInfoList', required: true }), + thread: one("Thread", { inverse: "suggestedRecipientInfoList", required: true }), titleText: attr({ compute() { return sprintf( diff --git a/addons/mail/static/src/models/thread.js b/addons/mail/static/src/models/thread.js index ebec9dac956..4d550b504a2 100644 --- a/addons/mail/static/src/models/thread.js +++ b/addons/mail/static/src/models/thread.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { attr, clear, insert, insertAndUnlink, link, many, one, Model, unlink } from '@mail/model'; -import { cleanSearchTerm } from '@mail/utils/utils'; -import * as mailUtils from '@mail/js/utils'; +import { attr, clear, insert, insertAndUnlink, link, many, one, Model, unlink } from "@mail/model"; +import { cleanSearchTerm } from "@mail/utils/utils"; +import * as mailUtils from "@mail/js/utils"; -import { sprintf } from '@web/core/utils/strings'; -import { url } from '@web/core/utils/urls'; +import { sprintf } from "@web/core/utils/strings"; +import { url } from "@web/core/utils/urls"; -import { str_to_datetime } from 'web.time'; +import { str_to_datetime } from "web.time"; const getSuggestedRecipientInfoNextTemporaryId = (function () { let tmpId = 0; @@ -18,7 +18,7 @@ const getSuggestedRecipientInfoNextTemporaryId = (function () { })(); Model({ - name: 'Thread', + name: "Thread", lifecycleHooks: { _willDelete() { if (this.isTemporary) { @@ -35,103 +35,111 @@ Model({ */ convertData(data) { const data2 = {}; - if ('authorizedGroupFullName' in data) { + if ("authorizedGroupFullName" in data) { data2.authorizedGroupFullName = data.authorizedGroupFullName; } - if ('channel' in data) { + if ("channel" in data) { data2.channel = data.channel; - data2.model = 'mail.channel'; + data2.model = "mail.channel"; } - if ('defaultDisplayMode' in data) { + if ("defaultDisplayMode" in data) { data2.defaultDisplayMode = data.defaultDisplayMode; } - if ('description' in data) { + if ("description" in data) { data2.description = data.description; } - if ('model' in data) { + if ("model" in data) { data2.model = data.model; } - if ('create_uid' in data) { + if ("create_uid" in data) { data2.creator = insert({ id: data.create_uid }); } - if ('group_based_subscription' in data) { + if ("group_based_subscription" in data) { data2.group_based_subscription = data.group_based_subscription; } - if ('id' in data) { + if ("id" in data) { data2.id = data.id; } - if ('invitedMembers' in data) { + if ("invitedMembers" in data) { data2.invitedMembers = data.invitedMembers; } - if ('is_minimized' in data && 'state' in data) { - data2.serverFoldState = data.is_minimized ? data.state : 'closed'; + if ("is_minimized" in data && "state" in data) { + data2.serverFoldState = data.is_minimized ? data.state : "closed"; } - if ('is_pinned' in data) { + if ("is_pinned" in data) { data2.isServerPinned = data.is_pinned; } - if ('last_interest_dt' in data && data.last_interest_dt) { + if ("last_interest_dt" in data && data.last_interest_dt) { data2.lastInterestDateTime = str_to_datetime(data.last_interest_dt); } - if ('last_message' in data && data.last_message) { - const messageData = this.messaging.models['Message'].convertData({ + if ("last_message" in data && data.last_message) { + const messageData = this.messaging.models["Message"].convertData({ id: data.last_message.id, model: data2.model, res_id: data2.id, }); data2.serverLastMessage = insert(messageData); } - if ('last_message_id' in data && data.last_message_id) { - const messageData = this.messaging.models['Message'].convertData({ + if ("last_message_id" in data && data.last_message_id) { + const messageData = this.messaging.models["Message"].convertData({ id: data.last_message_id, model: data2.model, res_id: data2.id, }); data2.serverLastMessage = insert(messageData); } - if ('message_needaction_counter' in data) { + if ("message_needaction_counter" in data) { data2.message_needaction_counter = data.message_needaction_counter; } - if ('name' in data) { + if ("name" in data) { data2.name = data.name; } - if ('seen_message_id' in data) { + if ("seen_message_id" in data) { data2.rawLastSeenByCurrentPartnerMessageId = data.seen_message_id; } - if ('uuid' in data) { + if ("uuid" in data) { data2.uuid = data.uuid; } // relations - if ('rtc_inviting_session' in data) { + if ("rtc_inviting_session" in data) { data2.rtcInvitingSession = insert(data.rtc_inviting_session); } - if ('rtcSessions' in data) { + if ("rtcSessions" in data) { data2.rtcSessions = data.rtcSessions; } - if ('seen_partners_info' in data) { + if ("seen_partners_info" in data) { if (!data.seen_partners_info) { data2.partnerSeenInfos = clear(); } else { data2.partnerSeenInfos = data.seen_partners_info.map( ({ fetched_message_id, partner_id, seen_message_id }) => { return { - lastFetchedMessage: fetched_message_id ? insert({ id: fetched_message_id }) : clear(), - lastSeenMessage: seen_message_id ? insert({ id: seen_message_id }) : clear(), + lastFetchedMessage: fetched_message_id + ? insert({ id: fetched_message_id }) + : clear(), + lastSeenMessage: seen_message_id + ? insert({ id: seen_message_id }) + : clear(), partner: { id: partner_id }, - }; - }); + }; + } + ); if (data.id || this.id) { - const messageIds = data.seen_partners_info.reduce((currentSet, { fetched_message_id, seen_message_id }) => { - if (fetched_message_id) { - currentSet.add(fetched_message_id); - } - if (seen_message_id) { - currentSet.add(seen_message_id); - } - return currentSet; - }, new Set()); + const messageIds = data.seen_partners_info.reduce( + (currentSet, { fetched_message_id, seen_message_id }) => { + if (fetched_message_id) { + currentSet.add(fetched_message_id); + } + if (seen_message_id) { + currentSet.add(seen_message_id); + } + return currentSet; + }, + new Set() + ); if (messageIds.size > 0) { - data2.messageSeenIndicators = [...messageIds].map(messageId => { + data2.messageSeenIndicators = [...messageIds].map((messageId) => { return { message: { id: messageId }, }; @@ -154,15 +162,15 @@ Model({ */ async createGroupChat({ default_display_mode, partners_to }) { const channelData = await this.messaging.rpc({ - model: 'mail.channel', - method: 'create_group', + model: "mail.channel", + method: "create_group", kwargs: { default_display_mode, partners_to, }, }); - return this.messaging.models['Thread'].insert( - this.messaging.models['Thread'].convertData(channelData) + return this.messaging.models["Thread"].insert( + this.messaging.models["Thread"].convertData(channelData) ); }, /** @@ -178,15 +186,17 @@ Model({ async fetchSuggestions(searchTerm, { thread } = {}) { const channelsData = await this.messaging.rpc( { - model: 'mail.channel', - method: 'get_mention_suggestions', + model: "mail.channel", + method: "get_mention_suggestions", kwargs: { search: searchTerm }, }, - { shadow: true }, + { shadow: true } + ); + this.messaging.models["Thread"].insert( + channelsData.map((channelData) => + this.messaging.models["Thread"].convertData(channelData) + ) ); - this.messaging.models['Thread'].insert(channelsData.map(channelData => - this.messaging.models['Thread'].convertData(channelData) - )); }, /** * Returns a sort function to determine the order of display of threads @@ -201,8 +211,14 @@ Model({ getSuggestionSortFunction(searchTerm, { thread } = {}) { const cleanedSearchTerm = cleanSearchTerm(searchTerm); return (a, b) => { - const isAPublicChannel = a.model === 'mail.channel' && a.channel.channel_type === 'channel' && (!a.authorizedGroupFullName); - const isBPublicChannel = b.model === 'mail.channel' && b.channel.channel_type === 'channel' && (!b.authorizedGroupFullName); + const isAPublicChannel = + a.model === "mail.channel" && + a.channel.channel_type === "channel" && + !a.authorizedGroupFullName; + const isBPublicChannel = + b.model === "mail.channel" && + b.channel.channel_type === "channel" && + !b.authorizedGroupFullName; if (isAPublicChannel && !isBPublicChannel) { return -1; } @@ -217,12 +233,18 @@ Model({ if (!isMemberOfA && isMemberOfB) { return 1; } - const cleanedAName = cleanSearchTerm(a.name || ''); - const cleanedBName = cleanSearchTerm(b.name || ''); - if (cleanedAName.startsWith(cleanedSearchTerm) && !cleanedBName.startsWith(cleanedSearchTerm)) { + const cleanedAName = cleanSearchTerm(a.name || ""); + const cleanedBName = cleanSearchTerm(b.name || ""); + if ( + cleanedAName.startsWith(cleanedSearchTerm) && + !cleanedBName.startsWith(cleanedSearchTerm) + ) { return -1; } - if (!cleanedAName.startsWith(cleanedSearchTerm) && cleanedBName.startsWith(cleanedSearchTerm)) { + if ( + !cleanedAName.startsWith(cleanedSearchTerm) && + cleanedBName.startsWith(cleanedSearchTerm) + ) { return 1; } if (cleanedAName < cleanedBName) { @@ -242,7 +264,7 @@ Model({ */ async loadPreviews(threads) { const channelIds = threads.reduce((list, thread) => { - if (thread.model === 'mail.channel') { + if (thread.model === "mail.channel") { return list.concat(thread.id); } return list; @@ -250,14 +272,21 @@ Model({ if (channelIds.length === 0) { return; } - const channelPreviews = await this.messaging.rpc({ - model: 'mail.channel', - method: 'channel_fetch_preview', - args: [channelIds], - }, { shadow: true }); - this.messaging.models['Message'].insert(channelPreviews.filter(p => p.last_message).map( - channelPreview => this.messaging.models['Message'].convertData(channelPreview.last_message) - )); + const channelPreviews = await this.messaging.rpc( + { + model: "mail.channel", + method: "channel_fetch_preview", + args: [channelIds], + }, + { shadow: true } + ); + this.messaging.models["Message"].insert( + channelPreviews + .filter((p) => p.last_message) + .map((channelPreview) => + this.messaging.models["Message"].convertData(channelPreview.last_message) + ) + ); }, /** * Performs the `channel_fold` RPC on `mail.channel`. @@ -266,14 +295,17 @@ Model({ * @param {string} state */ async performRpcChannelFold(channelId, state) { - return this.messaging.rpc({ - model: 'mail.channel', - method: 'channel_fold', - args: [[channelId]], - kwargs: { - state, - } - }, { shadow: true }); + return this.messaging.rpc( + { + model: "mail.channel", + method: "channel_fold", + args: [[channelId]], + kwargs: { + state, + }, + }, + { shadow: true } + ); }, /** * Performs the `channel_info` RPC on `mail.channel`. @@ -283,13 +315,18 @@ Model({ * @returns {Thread[]} */ async performRpcChannelInfo({ ids }) { - const channelInfos = await this.messaging.rpc({ - model: 'mail.channel', - method: 'channel_info', - args: [ids], - }, { shadow: true }); - const channels = this.messaging.models['Thread'].insert( - channelInfos.map(channelInfo => this.messaging.models['Thread'].convertData(channelInfo)) + const channelInfos = await this.messaging.rpc( + { + model: "mail.channel", + method: "channel_info", + args: [ids], + }, + { shadow: true } + ); + const channels = this.messaging.models["Thread"].insert( + channelInfos.map((channelInfo) => + this.messaging.models["Thread"].convertData(channelInfo) + ) ); return channels; }, @@ -301,13 +338,16 @@ Model({ * @param {integer[]} param0.lastMessageId */ async performRpcChannelSeen({ id, lastMessageId }) { - return this.messaging.rpc({ - route: `/mail/channel/set_last_seen_message`, - params: { - channel_id: id, - last_message_id: lastMessageId, + return this.messaging.rpc( + { + route: `/mail/channel/set_last_seen_message`, + params: { + channel_id: id, + last_message_id: lastMessageId, + }, }, - }, { shadow: true }); + { shadow: true } + ); }, /** * Performs the `channel_pin` RPC on `mail.channel`. @@ -317,14 +357,17 @@ Model({ * @param {boolean} [param0.pinned=false] */ async performRpcChannelPin({ channelId, pinned = false }) { - return this.messaging.rpc({ - model: 'mail.channel', - method: 'channel_pin', - args: [[channelId]], - kwargs: { - pinned, + return this.messaging.rpc( + { + model: "mail.channel", + method: "channel_pin", + args: [[channelId]], + kwargs: { + pinned, + }, }, - }, { shadow: true }); + { shadow: true } + ); }, /** * Performs the `channel_create` RPC on `mail.channel`. @@ -336,12 +379,12 @@ Model({ */ async performRpcCreateChannel({ name, group_id }) { const data = await this.messaging.rpc({ - model: 'mail.channel', - method: 'channel_create', + model: "mail.channel", + method: "channel_create", args: [name, group_id], }); - return this.messaging.models['Thread'].insert( - this.messaging.models['Thread'].convertData(data) + return this.messaging.models["Thread"].insert( + this.messaging.models["Thread"].convertData(data) ); }, /** @@ -353,10 +396,10 @@ Model({ */ async searchChannelsToOpen({ limit, searchTerm }) { const domain = [ - ['channel_type', '=', 'channel'], - ['name', 'ilike', searchTerm], + ["channel_type", "=", "channel"], + ["name", "ilike", searchTerm], ]; - const fields = ['channel_type', 'name']; + const fields = ["channel_type", "name"]; const channelsData = await this.messaging.rpc({ model: "mail.channel", method: "search_read", @@ -366,16 +409,18 @@ Model({ limit, }, }); - return this.insert(channelsData.map(channelData => - this.messaging.models['Thread'].convertData({ - channel: { - channel_type: channelData.channel_type, + return this.insert( + channelsData.map((channelData) => + this.messaging.models["Thread"].convertData({ + channel: { + channel_type: channelData.channel_type, + id: channelData.id, + }, id: channelData.id, - }, - id: channelData.id, - name: channelData.name, - }) - )); + name: channelData.name, + }) + ) + ); }, /** * Returns threads that match the given search term. More specially only @@ -391,7 +436,12 @@ Model({ */ searchSuggestions(searchTerm, { thread } = {}) { let threads; - if (thread && thread.model === 'mail.channel' && (thread.channel.channel_type !== 'channel' || (thread.channel.channel_type === 'channel' && thread.authorizedGroupFullName))) { + if ( + thread && + thread.model === "mail.channel" && + (thread.channel.channel_type !== "channel" || + (thread.channel.channel_type === "channel" && thread.authorizedGroupFullName)) + ) { // Only return the current channel when in the context of a // group restricted channel or group or chat. Indeed, the message with the mention // would appear in the target channel, so this prevents from @@ -399,16 +449,19 @@ Model({ // channel. threads = [thread]; } else { - threads = this.messaging.models['Thread'].all(); + threads = this.messaging.models["Thread"].all(); } const cleanedSearchTerm = cleanSearchTerm(searchTerm); - return [threads.filter(thread => - !thread.isTemporary && - thread.channel && - thread.channel.channel_type === 'channel' && - thread.displayName && - cleanSearchTerm(thread.displayName).includes(cleanedSearchTerm) - )]; + return [ + threads.filter( + (thread) => + !thread.isTemporary && + thread.channel && + thread.channel.channel_type === "channel" && + thread.displayName && + cleanSearchTerm(thread.displayName).includes(cleanedSearchTerm) + ), + ]; }, }, recordMethods: { @@ -421,8 +474,8 @@ Model({ async changeDescription(description) { this.update({ description }); return this.messaging.rpc({ - model: 'mail.channel', - method: 'channel_change_description', + model: "mail.channel", + method: "channel_change_description", args: [[this.id]], kwargs: { description }, }); @@ -451,12 +504,12 @@ Model({ } const requestSet = new Set(requestList); if (!this.hasActivities) { - requestSet.delete('activities'); + requestSet.delete("activities"); } - if (requestSet.has('attachments')) { + if (requestSet.has("attachments")) { this.update({ isLoadingAttachments: true }); } - if (requestSet.has('messages')) { + if (requestSet.has("messages")) { this.cache.loadNewMessages(); } const { @@ -467,22 +520,25 @@ Model({ mainAttachment, hasReadAccess, suggestedRecipients: suggestedRecipientsData, - } = await this.messaging.rpc({ - route: '/mail/thread/data', - params: { - request_list: [...requestSet], - thread_id: this.id, - thread_model: this.model, + } = await this.messaging.rpc( + { + route: "/mail/thread/data", + params: { + request_list: [...requestSet], + thread_id: this.id, + thread_model: this.model, + }, }, - }, { shadow: true }); + { shadow: true } + ); if (!this.exists()) { return; } const values = { hasWriteAccess, mainAttachment, hasReadAccess }; if (activitiesData) { Object.assign(values, { - activities: activitiesData.map(activityData => - this.messaging.models['Activity'].convertData(activityData) + activities: activitiesData.map((activityData) => + this.messaging.models["Activity"].convertData(activityData) ), }); } @@ -495,13 +551,13 @@ Model({ } if (followersData) { Object.assign(values, { - followers: followersData.map(followerData => - this.messaging.models['Follower'].convertData(followerData) + followers: followersData.map((followerData) => + this.messaging.models["Follower"].convertData(followerData) ), }); } if (suggestedRecipientsData) { - const recipientInfoList = suggestedRecipientsData.map(recipientInfoData => { + const recipientInfoList = suggestedRecipientsData.map((recipientInfoData) => { const [partner_id, emailInfo, lang, reason] = recipientInfoData; const [name, email] = emailInfo && mailUtils.parseEmail(emailInfo); return { @@ -518,7 +574,7 @@ Model({ }); } this.update(values); - this.messaging.messagingBus.trigger('o-thread-loaded-data', { thread: this }); + this.messaging.messagingBus.trigger("o-thread-loaded-data", { thread: this }); }, /** * Add current user to provided thread's followers. @@ -526,7 +582,7 @@ Model({ async follow() { await this.messaging.rpc({ model: this.model, - method: 'message_subscribe', + method: "message_subscribe", args: [[this.id]], kwargs: { partner_ids: [this.messaging.currentPartner.id], @@ -535,16 +591,19 @@ Model({ if (!this.exists()) { return; } - this.fetchData(['followers', 'suggestedRecipients']); + this.fetchData(["followers", "suggestedRecipients"]); }, /** * Performs the rpc to leave the rtc call of the channel. */ async performRpcLeaveCall() { - await this.messaging.rpc({ - route: '/mail/rtc/channel/leave_call', - params: { channel_id: this.id }, - }, { shadow: true }); + await this.messaging.rpc( + { + route: "/mail/rtc/channel/leave_call", + params: { channel_id: this.id }, + }, + { shadow: true } + ); }, /** * Leaves the current call if there is one, joins the call if the user was @@ -571,23 +630,26 @@ Model({ * @param {boolean} [param0.videoType] type of the video: 'user-video' or 'display' */ async _joinCall({ startWithVideo = false, videoType } = {}) { - if (this.model !== 'mail.channel') { + if (this.model !== "mail.channel") { return; } if (!this.messaging.device.hasRtcSupport) { this.messaging.notify({ message: this.env._t("Your browser does not support webRTC."), - type: 'warning', + type: "warning", }); return; } - const { rtcSessions, iceServers, sessionId, invitedMembers } = await this.messaging.rpc({ - route: '/mail/rtc/channel/join_call', - params: { - channel_id: this.id, - check_rtc_session_ids: this.rtcSessions.map(rtcSession => rtcSession.id), + const { rtcSessions, iceServers, sessionId, invitedMembers } = await this.messaging.rpc( + { + route: "/mail/rtc/channel/join_call", + params: { + channel_id: this.id, + check_rtc_session_ids: this.rtcSessions.map((rtcSession) => rtcSession.id), + }, }, - }, { shadow: true }); + { shadow: true } + ); if (!this.exists()) { return; } @@ -650,10 +712,10 @@ Model({ */ async join() { await this.messaging.rpc({ - model: 'mail.channel', - method: 'add_members', + model: "mail.channel", + method: "add_members", args: [[this.id]], - kwargs: { partner_ids: [this.messaging.currentPartner.id] } + kwargs: { partner_ids: [this.messaging.currentPartner.id] }, }); }, /** @@ -661,8 +723,8 @@ Model({ */ async leave() { await this.messaging.rpc({ - model: 'mail.channel', - method: 'action_unfollow', + model: "mail.channel", + method: "action_unfollow", args: [[this.id]], }); }, @@ -670,11 +732,14 @@ Model({ * Mark the specified conversation as fetched. */ async markAsFetched() { - await this.messaging.rpc({ - model: 'mail.channel', - method: 'channel_fetched', - args: [[this.id]], - }, { shadow: true }); + await this.messaging.rpc( + { + model: "mail.channel", + method: "channel_fetched", + args: [[this.id]], + }, + { shadow: true } + ); }, /** * Mark the specified conversation as read/seen. @@ -685,7 +750,7 @@ Model({ if (this.messaging.currentGuest) { return; } - if (this.model !== 'mail.channel') { + if (this.model !== "mail.channel") { return; } if (this.pendingSeenMessageId && message.id <= this.pendingSeenMessageId) { @@ -698,7 +763,7 @@ Model({ return; } this.update({ pendingSeenMessageId: message.id }); - return this.messaging.models['Thread'].performRpcChannelSeen({ + return this.messaging.models["Thread"].performRpcChannelSeen({ id: this.id, lastMessageId: message.id, }); @@ -707,7 +772,9 @@ Model({ * Marks as read all needaction messages with this thread as origin. */ async markNeedactionMessagesAsOriginThreadAsRead() { - await this.messaging.models['Message'].markAsRead(this.needactionMessagesAsOriginThread); + await this.messaging.models["Message"].markAsRead( + this.needactionMessagesAsOriginThread + ); }, /** * Notifies the server of new fold state. Useful for initial, @@ -716,11 +783,11 @@ Model({ * @param {string} state */ async notifyFoldStateToServer(state) { - if (this.model !== 'mail.channel') { + if (this.model !== "mail.channel") { // Server sync of fold state is only supported for channels. return; } - return this.messaging.models['Thread'].performRpcChannelFold(this.id, state); + return this.messaging.models["Thread"].performRpcChannelFold(this.id, state); }, /** * Notify server to leave the current channel. Useful for cross-tab @@ -729,11 +796,11 @@ Model({ * Only makes sense if isPendingPinned is set to the desired value. */ async notifyPinStateToServer() { - if (this.channel.channel_type === 'channel') { + if (this.channel.channel_type === "channel") { await this.leave(); return; } - await this.messaging.models['Thread'].performRpcChannelPin({ + await this.messaging.models["Thread"].performRpcChannelPin({ channelId: this.id, pinned: this.isPendingPinned, }); @@ -801,7 +868,9 @@ Model({ */ refreshCurrentPartnerIsTyping() { this.update({ - currentPartnerInactiveTypingTimer: { doReset: this.currentPartnerInactiveTypingTimer ? true : undefined }, + currentPartnerInactiveTypingTimer: { + doReset: this.currentPartnerInactiveTypingTimer ? true : undefined, + }, }); }, /** @@ -810,7 +879,7 @@ Model({ * @param {Member} member */ refreshOtherMemberTypingMember(member) { - this.messaging.models['OtherMemberLongTypingInThreadTimer'].insert({ + this.messaging.models["OtherMemberLongTypingInThreadTimer"].insert({ member, thread: this, timer: { @@ -832,7 +901,7 @@ Model({ // Manage typing member relation. const memberOfCurrentUser = this.channel.memberOfCurrentUser; const newOrderedTypingMembers = [ - ...this.orderedTypingMembers.filter(member => member !== memberOfCurrentUser), + ...this.orderedTypingMembers.filter((member) => member !== memberOfCurrentUser), memberOfCurrentUser, ]; this.update({ @@ -851,7 +920,7 @@ Model({ registerOtherMemberTypingMember(member) { this.update({ otherMembersLongTypingTimers: insert({ member }) }); const newOrderedTypingMembers = [ - ...this.orderedTypingMembers.filter(currentMember => currentMember !== member), + ...this.orderedTypingMembers.filter((currentMember) => currentMember !== member), member, ]; this.update({ @@ -868,8 +937,8 @@ Model({ async rename(name) { this.update({ name }); return this.messaging.rpc({ - model: 'mail.channel', - method: 'channel_rename', + model: "mail.channel", + method: "channel_rename", args: [[this.id]], kwargs: { name }, }); @@ -883,8 +952,8 @@ Model({ */ async setCustomName(newName) { return this.messaging.rpc({ - model: 'mail.channel', - method: 'channel_set_custom_name', + model: "mail.channel", + method: "channel_set_custom_name", args: [this.id], kwargs: { name: newName }, }); @@ -895,8 +964,8 @@ Model({ async setMainAttachment(attachment) { this.update({ mainAttachment: attachment }); await this.messaging.rpc({ - model: 'ir.attachment', - method: 'register_as_main_attachment', + model: "ir.attachment", + method: "register_as_main_attachment", args: [[this.mainAttachment.id]], }); }, @@ -935,7 +1004,9 @@ Model({ }); // Manage typing member relation. const memberOfCurrentUser = this.channel.memberOfCurrentUser; - const newOrderedTypingMembers = this.orderedTypingMembers.filter(member => member !== memberOfCurrentUser); + const newOrderedTypingMembers = this.orderedTypingMembers.filter( + (member) => member !== memberOfCurrentUser + ); this.update({ isCurrentPartnerTyping: false, orderedTypingMembers: newOrderedTypingMembers, @@ -955,7 +1026,9 @@ Model({ */ unregisterOtherMemberTypingMember(member) { this.update({ otherMembersLongTypingTimers: insertAndUnlink({ member }) }); - const newOrderedTypingMembers = this.orderedTypingMembers.filter(currentMember => currentMember !== member); + const newOrderedTypingMembers = this.orderedTypingMembers.filter( + (currentMember) => currentMember !== member + ); this.update({ orderedTypingMembers: newOrderedTypingMembers, typingMembers: unlink(member), @@ -977,14 +1050,17 @@ Model({ this.forceNotifyNextCurrentPartnerTypingStatus || this.isCurrentPartnerTyping !== this.currentPartnerLastNotifiedIsTyping ) { - if (this.model === 'mail.channel') { - await this.messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': this.id, - 'is_typing': this.isCurrentPartnerTyping, + if (this.model === "mail.channel") { + await this.messaging.rpc( + { + route: "/mail/channel/notify_typing", + params: { + channel_id: this.id, + is_typing: this.isCurrentPartnerTyping, + }, }, - }, { shadow: true }); + { shadow: true } + ); if (!this.exists()) { return; } @@ -1002,9 +1078,12 @@ Model({ * @private */ _onChangeLastSeenByCurrentPartnerMessageId() { - this.messaging.messagingBus.trigger('o-thread-last-seen-by-current-partner-message-id-changed', { - thread: this, - }); + this.messaging.messagingBus.trigger( + "o-thread-last-seen-by-current-partner-message-id-changed", + { + thread: this, + } + ); }, /** * Handles change of pinned state coming from the server. Useful to @@ -1032,13 +1111,13 @@ Model({ if (this.messaging.device.isSmall) { return; } - if (this.serverFoldState === 'closed') { + if (this.serverFoldState === "closed") { this.messaging.chatWindowManager.closeThread(this, { notifyServer: false, }); } else { this.messaging.chatWindowManager.openThread(this, { - isFolded: this.serverFoldState === 'folded', + isFolded: this.serverFoldState === "folded", notifyServer: false, }); } @@ -1067,60 +1146,69 @@ Model({ }, }, fields: { - accessRestrictedToGroupText: attr({ default: '', + accessRestrictedToGroupText: attr({ + default: "", compute() { if (!this.authorizedGroupFullName) { return clear(); } - return sprintf( - this.env._t('Access restricted to group "%(groupFullName)s"'), - { 'groupFullName': this.authorizedGroupFullName } - ); + return sprintf(this.env._t('Access restricted to group "%(groupFullName)s"'), { + groupFullName: this.authorizedGroupFullName, + }); }, }), /** * Determines the `mail.activity` that belong to `this`, assuming `this` * has activities (@see hasActivities). */ - activities: many('Activity', { inverse: 'thread' }), + activities: many("Activity", { inverse: "thread" }), activity_state: attr({ compute() { if (this.overdueActivities.length > 0) { - return 'overdue'; + return "overdue"; } if (this.todayActivities.length > 0) { - return 'today'; + return "today"; } if (this.futureActivities.length > 0) { - return 'planned'; + return "planned"; } return clear(); }, }), - allAttachments: many('Attachment', { inverse: 'allThreads', + allAttachments: many("Attachment", { + inverse: "allThreads", compute() { return this.originThreadAttachments; }, sort: [ - ['truthy-first', 'isUploading'], - ['greater-first', 'id'], + ["truthy-first", "isUploading"], + ["greater-first", "id"], ], }), areAttachmentsLoaded: attr({ default: false }), - attachmentsInWebClientView: many('Attachment', { inverse: 'threadsAsAttachmentsInWebClientView', readonly: true, - sort: [['greater-first', 'id']], + attachmentsInWebClientView: many("Attachment", { + inverse: "threadsAsAttachmentsInWebClientView", + readonly: true, + sort: [["greater-first", "id"]], }), authorizedGroupFullName: attr(), - cache: one('ThreadCache', { default: {}, inverse: 'thread', readonly: true, required: true }), - channel: one('Channel', { inverse: 'thread', isCausal: true, readonly: true }), + cache: one("ThreadCache", { + default: {}, + inverse: "thread", + readonly: true, + required: true, + }), + channel: one("Channel", { inverse: "thread", isCausal: true, readonly: true }), /** * States the chat window related to this thread (if any). */ - chatWindow: one('ChatWindow', { inverse: 'thread' }), + chatWindow: one("ChatWindow", { inverse: "thread" }), /** * Determines the composer state of this thread. */ - composer: one('Composer', { inverse: 'thread', + composer: one("Composer", { + inverse: "thread", compute() { if (this.mailbox) { return clear(); @@ -1128,7 +1216,7 @@ Model({ return {}; }, }), - creator: one('User'), + creator: one("User"), /** * Timer of current partner that was currently typing something, but * there is no change on the input for 5 seconds. This is used @@ -1136,7 +1224,9 @@ Model({ * partner has stopped typing something, due to making no changes * on the composer for some time. */ - currentPartnerInactiveTypingTimer: one('Timer', { inverse: 'threadAsCurrentPartnerInactiveTypingTimerOwner' }), + currentPartnerInactiveTypingTimer: one("Timer", { + inverse: "threadAsCurrentPartnerInactiveTypingTimerOwner", + }), /** * Last 'is_typing' status of current partner that has been notified * to other members. Useful to prevent spamming typing notifications @@ -1161,7 +1251,9 @@ Model({ * is still typing something, so that they should not assume he/she * has stopped typing something. */ - currentPartnerLongTypingTimer: one('Timer', { inverse: 'threadAsCurrentPartnerLongTypingTimerOwner' }), + currentPartnerLongTypingTimer: one("Timer", { + inverse: "threadAsCurrentPartnerLongTypingTimerOwner", + }), /** * Determines the default display mode of this channel. Should contain * either no value (to display the chat), or 'video_full_screen' to @@ -1185,21 +1277,21 @@ Model({ }), fetchMessagesParams: attr({ compute() { - if (this.model === 'mail.channel') { - return { 'channel_id': this.id }; + if (this.model === "mail.channel") { + return { channel_id: this.id }; } if (this.mailbox) { return {}; } return { - 'thread_id': this.id, - 'thread_model': this.model, + thread_id: this.id, + thread_model: this.model, }; }, }), fetchMessagesUrl: attr({ compute() { - if (this.model === 'mail.channel') { + if (this.model === "mail.channel") { return `/mail/channel/messages`; } if (this.mailbox) { @@ -1208,9 +1300,11 @@ Model({ return `/mail/thread/messages`; }, }), - followerOfCurrentPartner: one('Follower', { inverse: 'followedThreadAsFollowerOfCurrentPartner' }), - followersPartner: many('Partner', { related: 'followers.partner' }), - followers: many('Follower', { inverse: 'followedThread' }), + followerOfCurrentPartner: one("Follower", { + inverse: "followedThreadAsFollowerOfCurrentPartner", + }), + followersPartner: many("Partner", { related: "followers.partner" }), + followers: many("Follower", { inverse: "followedThread" }), /** * Determines whether the next request to notify current partner * typing status should always result to making RPC, regardless of @@ -1224,9 +1318,9 @@ Model({ * States the `Activity` that belongs to `this` and that are * planned in the future (due later than today). */ - futureActivities: many('Activity', { + futureActivities: many("Activity", { compute() { - return this.activities.filter(activity => activity.state === 'planned'); + return this.activities.filter((activity) => activity.state === "planned"); }, }), group_based_subscription: attr({ default: false }), @@ -1246,7 +1340,7 @@ Model({ if (!this.channel) { return clear(); } - return ['channel', 'chat', 'group'].includes(this.channel.channel_type); + return ["channel", "chat", "group"].includes(this.channel.channel_type); }, }), /** @@ -1255,7 +1349,7 @@ Model({ */ hasInviteFeature: attr({ compute() { - return this.model === 'mail.channel'; + return this.model === "mail.channel"; }, }), /** @@ -1266,7 +1360,7 @@ Model({ if (!this.channel) { return clear(); } - return ['channel', 'group'].includes(this.channel.channel_type); + return ["channel", "group"].includes(this.channel.channel_type); }, }), /** @@ -1278,12 +1372,13 @@ Model({ * Determine whether this thread has the seen indicators (V and VV) * enabled or not. */ - hasSeenIndicators: attr({ default: false, + hasSeenIndicators: attr({ + default: false, compute() { if (!this.channel) { return clear(); } - return ['chat', 'livechat'].includes(this.channel.channel_type); + return ["chat", "livechat"].includes(this.channel.channel_type); }, }), /** @@ -1297,7 +1392,11 @@ Model({ if (!this.channel) { return clear(); } - if (!this.uuid || !this.channel.channel_type || this.channel.channel_type === 'chat') { + if ( + !this.uuid || + !this.channel.channel_type || + this.channel.channel_type === "chat" + ) { return clear(); } return `${window.location.origin}/chat/${this.id}/${this.uuid}`; @@ -1306,7 +1405,7 @@ Model({ /** * List of members that have been invited to the RTC call of this channel. */ - invitedMembers: many('ChannelMember'), + invitedMembers: many("ChannelMember"), /** * Determines whether this description can be changed. * Only makes sense for channels. @@ -1316,7 +1415,7 @@ Model({ if (!this.channel) { return clear(); } - return ['channel', 'group'].includes(this.channel.channel_type); + return ["channel", "group"].includes(this.channel.channel_type); }, }), /** @@ -1328,7 +1427,7 @@ Model({ if (!this.channel) { return clear(); } - return ['chat', 'channel', 'group'].includes(this.channel.channel_type); + return ["chat", "channel", "group"].includes(this.channel.channel_type); }, }), /** @@ -1337,18 +1436,21 @@ Model({ * Useful to list chat channels, like in messaging menu with the filter * 'chat'. */ - isChatChannel: attr({ default: false, + isChatChannel: attr({ + default: false, compute() { if (!this.channel) { return clear(); } - return ['chat', 'group'].includes(this.channel.channel_type); + return ["chat", "group"].includes(this.channel.channel_type); }, }), - isCurrentPartnerFollowing: attr({ default: false, + isCurrentPartnerFollowing: attr({ + default: false, compute() { - return this.followers.some(follower => - follower.partner && follower.partner === this.messaging.currentPartner + return this.followers.some( + (follower) => + follower.partner && follower.partner === this.messaging.currentPartner ); }, }), @@ -1360,9 +1462,9 @@ Model({ compute() { return Boolean( this.messaging && - this.messaging.currentUser && - this.messaging.currentUser.isInternalUser && - this.isChannelDescriptionChangeable + this.messaging.currentUser && + this.messaging.currentUser.isInternalUser && + this.isChannelDescriptionChangeable ); }, }), @@ -1385,7 +1487,9 @@ Model({ */ isPinned: attr({ compute() { - return this.isPendingPinned !== undefined ? this.isPendingPinned : this.isServerPinned; + return this.isPendingPinned !== undefined + ? this.isPendingPinned + : this.isServerPinned; }, }), /** @@ -1405,29 +1509,28 @@ Model({ * pinning, and new message posted. It is contained as a Date object. */ lastInterestDateTime: attr(), - lastCurrentPartnerMessageSeenByEveryone: one('Message', { + lastCurrentPartnerMessageSeenByEveryone: one("Message", { compute() { - const otherPartnerSeenInfos = - this.partnerSeenInfos.filter(partnerSeenInfo => - partnerSeenInfo.partner !== this.messaging.currentPartner); + const otherPartnerSeenInfos = this.partnerSeenInfos.filter( + (partnerSeenInfo) => partnerSeenInfo.partner !== this.messaging.currentPartner + ); if (otherPartnerSeenInfos.length === 0) { return clear(); } - const otherPartnersLastSeenMessageIds = - otherPartnerSeenInfos.map(partnerSeenInfo => + const otherPartnersLastSeenMessageIds = otherPartnerSeenInfos.map( + (partnerSeenInfo) => partnerSeenInfo.lastSeenMessage ? partnerSeenInfo.lastSeenMessage.id : 0 - ); + ); if (otherPartnersLastSeenMessageIds.length === 0) { return clear(); } - const lastMessageSeenByAllId = Math.min( - ...otherPartnersLastSeenMessageIds - ); - const currentPartnerOrderedSeenMessages = - this.orderedNonTransientMessages.filter(message => + const lastMessageSeenByAllId = Math.min(...otherPartnersLastSeenMessageIds); + const currentPartnerOrderedSeenMessages = this.orderedNonTransientMessages.filter( + (message) => message.author === this.messaging.currentPartner && - message.id <= lastMessageSeenByAllId); + message.id <= lastMessageSeenByAllId + ); if ( !currentPartnerOrderedSeenMessages || @@ -1441,12 +1544,9 @@ Model({ /** * Last message of the thread, could be a transient one. */ - lastMessage: one('Message', { + lastMessage: one("Message", { compute() { - const { - length: l, - [l - 1]: lastMessage, - } = this.orderedMessages; + const { length: l, [l - 1]: lastMessage } = this.orderedMessages; if (lastMessage) { return lastMessage; } @@ -1456,10 +1556,10 @@ Model({ /** * States the last known needaction message having this thread as origin. */ - lastNeedactionMessageAsOriginThread: one('Message', { + lastNeedactionMessageAsOriginThread: one("Message", { compute() { const orderedNeedactionMessagesAsOriginThread = this.needactionMessagesAsOriginThread.sort( - (m1, m2) => m1.id < m2.id ? -1 : 1 + (m1, m2) => (m1.id < m2.id ? -1 : 1) ); const { length: l, @@ -1474,12 +1574,9 @@ Model({ /** * Last non-transient message. */ - lastNonTransientMessage: one('Message', { + lastNonTransientMessage: one("Message", { compute() { - const { - length: l, - [l - 1]: lastMessage, - } = this.orderedNonTransientMessages; + const { length: l, [l - 1]: lastMessage } = this.orderedNonTransientMessages; if (lastMessage) { return lastMessage; } @@ -1493,7 +1590,8 @@ Model({ * even if corresponding message is deleted. It is basically used to know which * messages are before or after it. */ - lastSeenByCurrentPartnerMessageId: attr({ default: 0, + lastSeenByCurrentPartnerMessageId: attr({ + default: 0, /** * Adjusts the last seen message received from the server to consider * the following messages also as read if they are either transient @@ -1515,8 +1613,12 @@ Model({ continue; } if ( - (message.author && this.messaging.currentPartner && message.author === this.messaging.currentPartner) || - (message.guestAuthor && this.messaging.currentGuest && message.guestAuthor === this.messaging.currentGuest) || + (message.author && + this.messaging.currentPartner && + message.author === this.messaging.currentPartner) || + (message.guestAuthor && + this.messaging.currentGuest && + message.guestAuthor === this.messaging.currentGuest) || message.isTransient ) { lastSeenByCurrentPartnerMessageId = message.id; @@ -1527,23 +1629,23 @@ Model({ return lastSeenByCurrentPartnerMessageId; }, }), - mailbox: one('Mailbox', { inverse: 'thread' }), - mainAttachment: one('Attachment'), + mailbox: one("Mailbox", { inverse: "thread" }), + mainAttachment: one("Attachment"), /** * Determines the last mentioned channels of the last composer related * to this thread. Useful to sync the composer when re-creating it. */ - mentionedChannelsBackup: many('Thread'), + mentionedChannelsBackup: many("Thread"), /** * Determines the last mentioned partners of the last composer related * to this thread. Useful to sync the composer when re-creating it. */ - mentionedPartnersBackup: many('Partner'), + mentionedPartnersBackup: many("Partner"), /** * Determines the message before which the "new message" separator must * be positioned, if any. */ - messageAfterNewMessageSeparator: one('Message', { + messageAfterNewMessageSeparator: one("Message", { compute() { if (!this.channel) { return clear(); @@ -1551,8 +1653,8 @@ Model({ if (this.channel.localMessageUnreadCounter === 0) { return clear(); } - const index = this.orderedMessages.findIndex(message => - message.id === this.lastSeenByCurrentPartnerMessageId + const index = this.orderedMessages.findIndex( + (message) => message.id === this.lastSeenByCurrentPartnerMessageId ); if (index === -1) { return clear(); @@ -1570,24 +1672,31 @@ Model({ * Note that this field is automatically computed by inverse * computed field. */ - messages: many('Message', { inverse: 'threads', readonly: true }), + messages: many("Message", { inverse: "threads", readonly: true }), /** * All messages that have been originally posted in this thread. */ - messagesAsOriginThread: many('Message', { inverse: 'originThread', isCausal: true }), + messagesAsOriginThread: many("Message", { inverse: "originThread", isCausal: true }), /** * Contains the message fetched/seen indicators for all messages of this thread. */ - messageSeenIndicators: many('MessageSeenIndicator', { inverse: 'thread' }), - messagingAsAllCurrentClientThreads: one('Messaging', { inverse: 'allCurrentClientThreads', + messageSeenIndicators: many("MessageSeenIndicator", { inverse: "thread" }), + messagingAsAllCurrentClientThreads: one("Messaging", { + inverse: "allCurrentClientThreads", compute() { - if (!this.messaging || !this.channel || !this.channel.memberOfCurrentUser || !this.isServerPinned) { + if ( + !this.messaging || + !this.channel || + !this.channel.memberOfCurrentUser || + !this.isServerPinned + ) { return clear(); } return this.messaging; }, }), - messagingAsRingingThread: one('Messaging', { inverse: 'ringingThreads', + messagingAsRingingThread: one("Messaging", { + inverse: "ringingThreads", compute() { if (this.rtcInvitingSession) { return this.messaging; @@ -1595,7 +1704,8 @@ Model({ return clear(); }, }), - messagingMenuAsPinnedAndUnreadChannel: one('MessagingMenu', { inverse: 'pinnedAndUnreadChannels', + messagingMenuAsPinnedAndUnreadChannel: one("MessagingMenu", { + inverse: "pinnedAndUnreadChannels", compute() { if (!this.messaging || !this.messaging.messagingMenu) { return clear(); @@ -1613,41 +1723,41 @@ Model({ /** * States all known needaction messages having this thread as origin. */ - needactionMessagesAsOriginThread: many('Message', { + needactionMessagesAsOriginThread: many("Message", { compute() { - return this.messagesAsOriginThread.filter(message => message.isNeedaction); + return this.messagesAsOriginThread.filter((message) => message.isNeedaction); }, }), /** * All messages ordered like they are displayed. */ - orderedMessages: many('Message', { + orderedMessages: many("Message", { compute() { - return this.messages.sort((m1, m2) => m1.id < m2.id ? -1 : 1); + return this.messages.sort((m1, m2) => (m1.id < m2.id ? -1 : 1)); }, }), /** * All messages ordered like they are displayed. This field does not * contain transient messages which are not "real" records. */ - orderedNonTransientMessages: many('Message', { + orderedNonTransientMessages: many("Message", { compute() { - return this.orderedMessages.filter(m => !m.isTransient); + return this.orderedMessages.filter((m) => !m.isTransient); }, }), /** * Ordered typing members on this thread, excluding the current partner. */ - orderedOtherTypingMembers: many('ChannelMember', { + orderedOtherTypingMembers: many("ChannelMember", { compute() { - return this.orderedTypingMembers.filter(member => !member.isMemberOfCurrentUser); + return this.orderedTypingMembers.filter((member) => !member.isMemberOfCurrentUser); }, }), /** * Ordered list of typing members. */ - orderedTypingMembers: many('ChannelMember'), - originThreadAttachments: many('Attachment', { inverse: 'originThread', isCausal: true }), + orderedTypingMembers: many("ChannelMember"), + originThreadAttachments: many("Attachment", { inverse: "originThread", isCausal: true }), /** * Registry of timers of partners currently typing in the thread, * excluding current partner. This is useful in order to @@ -1659,21 +1769,23 @@ Model({ * @see registerOtherMemberTypingMember * @see unregisterOtherMemberTypingMember */ - otherMembersLongTypingTimers: many('OtherMemberLongTypingInThreadTimer', { inverse: 'thread' }), + otherMembersLongTypingTimers: many("OtherMemberLongTypingInThreadTimer", { + inverse: "thread", + }), /** * States the `Activity` that belongs to `this` and that are * overdue (due earlier than today). */ - overdueActivities: many('Activity', { + overdueActivities: many("Activity", { compute() { - return this.activities.filter(activity => activity.state === 'overdue'); + return this.activities.filter((activity) => activity.state === "overdue"); }, }), /** * Contains the seen information for all members of the thread. * FIXME This field should be readonly once task-2336946 is done. */ - partnerSeenInfos: many('ThreadPartnerSeenInfo', { inverse: 'thread' }), + partnerSeenInfos: many("ThreadPartnerSeenInfo", { inverse: "thread" }), /** * Determine if there is a pending seen message change, which is a change * of seen message requested by the client but not yet confirmed by the @@ -1684,8 +1796,9 @@ Model({ /** * If set, the current thread is the thread that hosts the current RTC call. */ - rtc: one('Rtc', { inverse: 'channel' }), - callInviteRequestPopup: one('CallInviteRequestPopup', { inverse: 'thread', + rtc: one("Rtc", { inverse: "channel" }), + callInviteRequestPopup: one("CallInviteRequestPopup", { + inverse: "thread", compute() { if (this.rtcInvitingSession) { return {}; @@ -1697,8 +1810,8 @@ Model({ * The session that invited the current user, it is only set when the * invitation is still pending. */ - rtcInvitingSession: one('RtcSession', { inverse: 'calledChannels' }), - rtcSessions: many('RtcSession', { inverse: 'channel', isCausal: true }), + rtcInvitingSession: one("RtcSession", { inverse: "calledChannels" }), + rtcSessions: many("RtcSession", { inverse: "channel", isCausal: true }), /** * Determine the last fold state known by the server, which is the fold * state displayed after initialization or when the last pending @@ -1708,7 +1821,7 @@ Model({ * the code handling fold state change from the server should typically * update it. */ - serverFoldState: attr({ default: 'closed' }), + serverFoldState: attr({ default: "closed" }), /** * Last message considered by the server. * @@ -1716,12 +1829,20 @@ Model({ * * @see localMessageUnreadCounter */ - serverLastMessage: one('Message'), - suggestable: one('ComposerSuggestable', { default: {}, inverse: 'thread', readonly: true, required: true }), + serverLastMessage: one("Message"), + suggestable: one("ComposerSuggestable", { + default: {}, + inverse: "thread", + readonly: true, + required: true, + }), /** * Determines the `SuggestedRecipientInfo` concerning `this`. */ - suggestedRecipientInfoList: many('SuggestedRecipientInfo', { inverse: 'thread', isCausal: true }), + suggestedRecipientInfoList: many("SuggestedRecipientInfo", { + inverse: "thread", + isCausal: true, + }), /** * Determines the last content of the last composer related to this * thread. Useful to sync the composer when re-creating it. @@ -1742,7 +1863,7 @@ Model({ * to this thread. Useful to sync the composer when re-creating it. */ textInputSelectionDirectionBackup: attr({ default: "none" }), - threadViews: many('ThreadView', { inverse: 'thread' }), + threadViews: many("ThreadView", { inverse: "thread" }), /** * Clearable and cancellable throttled version of the * `_notifyCurrentPartnerTypingStatus` method. @@ -1753,7 +1874,8 @@ Model({ * * @see _notifyCurrentPartnerTypingStatus */ - throttleNotifyCurrentPartnerTypingStatus: one('Throttle', { inverse: 'threadAsThrottleNotifyCurrentPartnerTypingStatus', + throttleNotifyCurrentPartnerTypingStatus: one("Throttle", { + inverse: "threadAsThrottleNotifyCurrentPartnerTypingStatus", compute() { return { func: () => this._notifyCurrentPartnerTypingStatus(), @@ -1764,21 +1886,22 @@ Model({ * States the `Activity` that belongs to `this` and that are due * specifically today. */ - todayActivities: many('Activity', { + todayActivities: many("Activity", { compute() { - return this.activities.filter(activity => activity.state === 'today'); + return this.activities.filter((activity) => activity.state === "today"); }, }), - threadNeedactionPreviewViews: many('ThreadNeedactionPreviewView', { inverse: 'thread' }), + threadNeedactionPreviewViews: many("ThreadNeedactionPreviewView", { inverse: "thread" }), /** * Members that are currently typing something in the composer of this * thread, including current partner. */ - typingMembers: many('ChannelMember'), + typingMembers: many("ChannelMember"), /** * Text that represents the status on this thread about typing members. */ - typingStatusText: attr({ default: '', + typingStatusText: attr({ + default: "", compute() { if (this.orderedOtherTypingMembers.length === 0) { return clear(); @@ -1806,13 +1929,14 @@ Model({ /** * URL to access to the conversation. */ - url: attr({ default: '', + url: attr({ + default: "", /** * Compute an url string that can be used inside a href attribute */ compute() { - const baseHref = url('/web'); - if (this.model === 'mail.channel') { + const baseHref = url("/web"); + if (this.model === "mail.channel") { return `${baseHref}#action=mail.action_discuss&active_id=${this.model}_${this.id}`; } return `${baseHref}#model=${this.model}&id=${this.id}`; @@ -1822,24 +1946,25 @@ Model({ /** * The amount of videos broadcast in the current Rtc call */ - videoCount: attr({ default: 0, + videoCount: attr({ + default: 0, compute() { - return this.rtcSessions.filter(session => session.videoStream).length; + return this.rtcSessions.filter((session) => session.videoStream).length; }, }), }, onChanges: [ { - dependencies: ['lastSeenByCurrentPartnerMessageId'], - methodName: '_onChangeLastSeenByCurrentPartnerMessageId', + dependencies: ["lastSeenByCurrentPartnerMessageId"], + methodName: "_onChangeLastSeenByCurrentPartnerMessageId", }, { - dependencies: ['isServerPinned'], - methodName: '_onIsServerPinnedChanged', + dependencies: ["isServerPinned"], + methodName: "_onIsServerPinnedChanged", }, { - dependencies: ['serverFoldState'], - methodName: '_onServerFoldStateChanged', + dependencies: ["serverFoldState"], + methodName: "_onServerFoldStateChanged", }, ], }); diff --git a/addons/mail/static/src/models/thread_cache.js b/addons/mail/static/src/models/thread_cache.js index 66f4067ad03..37290cdd945 100644 --- a/addons/mail/static/src/models/thread_cache.js +++ b/addons/mail/static/src/models/thread_cache.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, link, many, one, Model } from '@mail/model'; +import { attr, clear, link, many, one, Model } from "@mail/model"; Model({ - name: 'ThreadCache', + name: "ThreadCache", recordMethods: { async loadMoreMessages() { if (this.isAllHistoryLoaded || this.isLoading) { @@ -14,12 +14,15 @@ Model({ return; } this.update({ isLoadingMore: true }); - const messageIds = this.fetchedMessages.map(message => message.id); + const messageIds = this.fetchedMessages.map((message) => message.id); const limit = 30; let fetchedMessages; let success; try { - fetchedMessages = await this._loadMessages({ limit, maxId: Math.min(...messageIds) }); + fetchedMessages = await this._loadMessages({ + limit, + maxId: Math.min(...messageIds), + }); success = true; } catch { success = false; @@ -32,7 +35,7 @@ Model({ this.update({ isAllHistoryLoaded: true }); } for (const threadView of this.threadViews) { - threadView.addComponentHint('more-messages-loaded', { fetchedMessages }); + threadView.addComponentHint("more-messages-loaded", { fetchedMessages }); } } this.update({ isLoadingMore: false }); @@ -48,13 +51,13 @@ Model({ this.update({ isCacheRefreshRequested: true }); return; } - const messageIds = this.fetchedMessages.map(message => message.id); + const messageIds = this.fetchedMessages.map((message) => message.id); const fetchedMessages = this._loadMessages({ minId: Math.max(...messageIds, 0) }); if (!fetchedMessages || fetchedMessages.length === 0) { return; } for (const threadView of this.threadViews) { - threadView.addComponentHint('new-messages-loaded', { fetchedMessages }); + threadView.addComponentHint("new-messages-loaded", { fetchedMessages }); } return fetchedMessages; }, @@ -114,12 +117,15 @@ Model({ this.update({ isLoading: true }); let messages; try { - messages = await this.messaging.models['Message'].performRpcMessageFetch(this.thread.fetchMessagesUrl, { - ...this.thread.fetchMessagesParams, - limit, - 'max_id': maxId, - 'min_id': minId, - }); + messages = await this.messaging.models["Message"].performRpcMessageFetch( + this.thread.fetchMessagesUrl, + { + ...this.thread.fetchMessagesParams, + limit, + max_id: maxId, + min_id: minId, + } + ); } catch (e) { if (this.exists()) { this.update({ @@ -141,7 +147,7 @@ Model({ if (!minId && messages.length < limit) { this.update({ isAllHistoryLoaded: true }); } - this.messaging.messagingBus.trigger('o-thread-cache-loaded-messages', { + this.messaging.messagingBus.trigger("o-thread-cache-loaded-messages", { fetchedMessages: messages, threadCache: this, }); @@ -171,9 +177,11 @@ Model({ return; } for (const threadView of this.threadViews) { - threadView.addComponentHint('messages-loaded', { fetchedMessages }); + threadView.addComponentHint("messages-loaded", { fetchedMessages }); } - this.messaging.messagingBus.trigger('o-thread-loaded-messages', { thread: this.thread }); + this.messaging.messagingBus.trigger("o-thread-loaded-messages", { + thread: this.thread, + }); }, }, fields: { @@ -193,12 +201,12 @@ Model({ * to manage "holes" in message list, while still allowing to display * new messages on main cache of thread in real-time. */ - fetchedMessages: many('Message', { + fetchedMessages: many("Message", { compute() { if (!this.thread) { return clear(); } - return this.rawFetchedMessages.filter(m => this.thread.messages.includes(m)); + return this.rawFetchedMessages.filter((m) => this.thread.messages.includes(m)); }, }), /** @@ -229,24 +237,18 @@ Model({ * cache (@see lastMessage field for that). @see fetchedMessages field * for a deeper explanation about "fetched" messages. */ - lastFetchedMessage: one('Message', { + lastFetchedMessage: one("Message", { compute() { - const { - length: l, - [l - 1]: lastFetchedMessage, - } = this.orderedFetchedMessages; + const { length: l, [l - 1]: lastFetchedMessage } = this.orderedFetchedMessages; if (!lastFetchedMessage) { return clear(); } return lastFetchedMessage; }, }), - lastMessage: one('Message', { + lastMessage: one("Message", { compute() { - const { - length: l, - [l - 1]: lastMessage, - } = this.orderedMessages; + const { length: l, [l - 1]: lastMessage } = this.orderedMessages; if (!lastMessage) { return clear(); } @@ -256,7 +258,7 @@ Model({ /** * List of messages linked to this cache. */ - messages: many('Message', { + messages: many("Message", { compute() { if (!this.thread) { return clear(); @@ -265,8 +267,8 @@ Model({ if (!this.lastFetchedMessage) { newerMessages = this.thread.messages; } else { - newerMessages = this.thread.messages.filter(message => - message.id > this.lastFetchedMessage.id + newerMessages = this.thread.messages.filter( + (message) => message.id > this.lastFetchedMessage.id ); } return [...this.fetchedMessages, ...this.temporaryMessages, ...newerMessages]; @@ -279,43 +281,50 @@ Model({ * cache (@see orderedMessages field for that). @see fetchedMessages * field for deeper explanation about "fetched" messages. */ - orderedFetchedMessages: many('Message', { + orderedFetchedMessages: many("Message", { compute() { - return this.fetchedMessages.sort((m1, m2) => m1.id < m2.id ? -1 : 1); + return this.fetchedMessages.sort((m1, m2) => (m1.id < m2.id ? -1 : 1)); }, }), /** * Ordered list of messages linked to this cache. */ - orderedMessages: many('Message', { + orderedMessages: many("Message", { compute() { - return this.messages.sort((m1, m2) => m1.id < m2.id ? -1 : 1); + return this.messages.sort((m1, m2) => (m1.id < m2.id ? -1 : 1)); }, }), /** * List of ordered non empty messages linked to this cache. */ - orderedNonEmptyMessages: many('Message', { + orderedNonEmptyMessages: many("Message", { compute() { - return this.orderedMessages.filter(message => !message.isEmpty); + return this.orderedMessages.filter((message) => !message.isEmpty); }, }), - rawFetchedMessages: many('Message'), - temporaryMessages: many('Message'), - thread: one('Thread', { identifying: true, inverse: 'cache' }), + rawFetchedMessages: many("Message"), + temporaryMessages: many("Message"), + thread: one("Thread", { identifying: true, inverse: "cache" }), /** * States the 'ThreadView' that are currently displaying `this`. */ - threadViews: many('ThreadView', { inverse: 'threadCache' }), + threadViews: many("ThreadView", { inverse: "threadCache" }), }, onChanges: [ { - dependencies: ['hasLoadingFailed', 'isCacheRefreshRequested', 'isLoaded', 'isLoading', 'thread.isTemporary', 'threadViews'], - methodName: '_onChangeForHasToLoadMessages', + dependencies: [ + "hasLoadingFailed", + "isCacheRefreshRequested", + "isLoaded", + "isLoading", + "thread.isTemporary", + "threadViews", + ], + methodName: "_onChangeForHasToLoadMessages", }, { - dependencies: ['hasToLoadMessages'], - methodName: '_onHasToLoadMessagesChanged', + dependencies: ["hasToLoadMessages"], + methodName: "_onHasToLoadMessagesChanged", }, ], }); diff --git a/addons/mail/static/src/models/thread_icon_view.js b/addons/mail/static/src/models/thread_icon_view.js index 58214f4470d..ac9eb7064d9 100644 --- a/addons/mail/static/src/models/thread_icon_view.js +++ b/addons/mail/static/src/models/thread_icon_view.js @@ -1,16 +1,26 @@ /** @odoo-module **/ -import { clear, one, Model } from '@mail/model'; +import { clear, one, Model } from "@mail/model"; Model({ - name: 'ThreadIconView', - template: 'mail.ThreadIconView', - identifyingMode: 'xor', + name: "ThreadIconView", + template: "mail.ThreadIconView", + identifyingMode: "xor", fields: { - chatWindowHeaderViewOwner: one('ChatWindowHeaderView', { identifying: true, inverse: 'threadIconView' }), - discussSidebarCategoryItemOwner: one('DiscussSidebarCategoryItem', { identifying: true, inverse: 'threadIconView' }), - discussSidebarMailboxViewOwner: one('DiscussSidebarMailboxView', { identifying: true, inverse: 'threadIconView' }), - thread: one('Thread', { required: true, + chatWindowHeaderViewOwner: one("ChatWindowHeaderView", { + identifying: true, + inverse: "threadIconView", + }), + discussSidebarCategoryItemOwner: one("DiscussSidebarCategoryItem", { + identifying: true, + inverse: "threadIconView", + }), + discussSidebarMailboxViewOwner: one("DiscussSidebarMailboxView", { + identifying: true, + inverse: "threadIconView", + }), + thread: one("Thread", { + required: true, compute() { if (this.chatWindowHeaderViewOwner) { return this.chatWindowHeaderViewOwner.chatWindowOwner.thread; @@ -26,11 +36,12 @@ Model({ } }, }), - threadTypingIconView: one('ThreadTypingIconView', { inverse: 'threadIconViewOwner', + threadTypingIconView: one("ThreadTypingIconView", { + inverse: "threadIconViewOwner", compute() { if ( this.thread.channel && - this.thread.channel.channel_type === 'chat' && + this.thread.channel.channel_type === "chat" && this.thread.channel.correspondent && this.thread.orderedOtherTypingMembers.length > 0 ) { @@ -39,6 +50,9 @@ Model({ return clear(); }, }), - threadViewTopbarOwner: one('ThreadViewTopbar', { identifying: true, inverse: 'threadIconView' }), + threadViewTopbarOwner: one("ThreadViewTopbar", { + identifying: true, + inverse: "threadIconView", + }), }, }); diff --git a/addons/mail/static/src/models/thread_needaction_preview_view.js b/addons/mail/static/src/models/thread_needaction_preview_view.js index fe3963be85a..b96d0a1e656 100644 --- a/addons/mail/static/src/models/thread_needaction_preview_view.js +++ b/addons/mail/static/src/models/thread_needaction_preview_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; -import { htmlToTextContentInline } from '@mail/js/utils'; +import { attr, clear, one, Model } from "@mail/model"; +import { htmlToTextContentInline } from "@mail/js/utils"; Model({ - name: 'ThreadNeedactionPreviewView', + name: "ThreadNeedactionPreviewView", recordMethods: { /** * @param {MouseEvent} ev @@ -28,27 +28,32 @@ Model({ * @param {MouseEvent} ev */ onClickMarkAsRead(ev) { - this.messaging.models['Message'].markAllAsRead([ - ['model', '=', this.thread.model], - ['res_id', '=', this.thread.id], + this.messaging.models["Message"].markAllAsRead([ + ["model", "=", this.thread.model], + ["res_id", "=", this.thread.id], ]); }, }, fields: { - inlineLastNeedactionMessageAsOriginThreadBody: attr({ default: "", + inlineLastNeedactionMessageAsOriginThreadBody: attr({ + default: "", compute() { if (!this.thread.lastNeedactionMessageAsOriginThread) { return clear(); } - return htmlToTextContentInline(this.thread.lastNeedactionMessageAsOriginThread.prettyBody); + return htmlToTextContentInline( + this.thread.lastNeedactionMessageAsOriginThread.prettyBody + ); }, }), isEmpty: attr({ compute() { - return !this.inlineLastNeedactionMessageAsOriginThreadBody && !this.lastTrackingValue; + return ( + !this.inlineLastNeedactionMessageAsOriginThreadBody && !this.lastTrackingValue + ); }, }), - lastTrackingValue: one('TrackingValue', { + lastTrackingValue: one("TrackingValue", { compute() { if (this.thread.lastMessage && this.thread.lastMessage.lastTrackingValue) { return this.thread.lastMessage.lastTrackingValue; @@ -61,7 +66,8 @@ Model({ * top-level click handler when clicking on this specific button. */ markAsReadRef: attr(), - messageAuthorPrefixView: one('MessageAuthorPrefixView', { inverse: 'threadNeedactionPreviewViewOwner', + messageAuthorPrefixView: one("MessageAuthorPrefixView", { + inverse: "threadNeedactionPreviewViewOwner", compute() { if ( this.thread.lastNeedactionMessageAsOriginThread && @@ -72,8 +78,12 @@ Model({ return clear(); }, }), - notificationListViewOwner: one('NotificationListView', { identifying: true, inverse: 'threadNeedactionPreviewViews' }), - personaImStatusIconView: one('PersonaImStatusIconView', { inverse: 'threadNeedactionPreviewViewOwner', + notificationListViewOwner: one("NotificationListView", { + identifying: true, + inverse: "threadNeedactionPreviewViews", + }), + personaImStatusIconView: one("PersonaImStatusIconView", { + inverse: "threadNeedactionPreviewViewOwner", compute() { if ( this.thread.channel && @@ -85,6 +95,6 @@ Model({ return clear(); }, }), - thread: one('Thread', { identifying: true, inverse: 'threadNeedactionPreviewViews' }), + thread: one("Thread", { identifying: true, inverse: "threadNeedactionPreviewViews" }), }, }); diff --git a/addons/mail/static/src/models/thread_partner_seen_info.js b/addons/mail/static/src/models/thread_partner_seen_info.js index 0262f9825ba..e67052c32cb 100644 --- a/addons/mail/static/src/models/thread_partner_seen_info.js +++ b/addons/mail/static/src/models/thread_partner_seen_info.js @@ -1,19 +1,19 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'ThreadPartnerSeenInfo', + name: "ThreadPartnerSeenInfo", fields: { - lastFetchedMessage: one('Message'), - lastSeenMessage: one('Message'), + lastFetchedMessage: one("Message"), + lastSeenMessage: one("Message"), /** * Partner that this seen info is related to. */ - partner: one('Partner', { identifying: true }), + partner: one("Partner", { identifying: true }), /** * Thread (channel) that this seen info is related to. */ - thread: one('Thread', { inverse: 'partnerSeenInfos', identifying: true }), + thread: one("Thread", { inverse: "partnerSeenInfos", identifying: true }), }, }); diff --git a/addons/mail/static/src/models/thread_textual_typing_status_view.js b/addons/mail/static/src/models/thread_textual_typing_status_view.js index 1b712e2ca3a..b6322127a8e 100644 --- a/addons/mail/static/src/models/thread_textual_typing_status_view.js +++ b/addons/mail/static/src/models/thread_textual_typing_status_view.js @@ -1,18 +1,20 @@ /** @odoo-module **/ -import { clear, one, Model } from '@mail/model'; +import { clear, one, Model } from "@mail/model"; Model({ - name: 'ThreadTextualTypingStatusView', - template: 'mail.ThreadTextualTypingStatusView', + name: "ThreadTextualTypingStatusView", + template: "mail.ThreadTextualTypingStatusView", fields: { - owner: one('ComposerView', { identifying: true, inverse: 'threadTextualTypingStatusView' }), - thread: one('Thread', { required: true, + owner: one("ComposerView", { identifying: true, inverse: "threadTextualTypingStatusView" }), + thread: one("Thread", { + required: true, compute() { return this.owner.composer.activeThread; }, }), - threadTypingIconView: one('ThreadTypingIconView', { inverse: 'threadTextualTypingStatusViewOwner', + threadTypingIconView: one("ThreadTypingIconView", { + inverse: "threadTextualTypingStatusViewOwner", compute() { if (this.thread.orderedOtherTypingMembers.length > 0) { return {}; diff --git a/addons/mail/static/src/models/thread_typing_icon_view.js b/addons/mail/static/src/models/thread_typing_icon_view.js index 7eb85505892..654c753a823 100644 --- a/addons/mail/static/src/models/thread_typing_icon_view.js +++ b/addons/mail/static/src/models/thread_typing_icon_view.js @@ -1,22 +1,29 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'ThreadTypingIconView', - template: 'mail.ThreadTypingIconView', - identifyingMode: 'xor', + name: "ThreadTypingIconView", + template: "mail.ThreadTypingIconView", + identifyingMode: "xor", fields: { - size: attr({ default: 'small', + size: attr({ + default: "small", compute() { if (this.threadTextualTypingStatusViewOwner) { - return 'medium'; + return "medium"; } return clear(); }, }), - threadIconViewOwner: one('ThreadIconView', { identifying: true, inverse: 'threadTypingIconView' }), - threadTextualTypingStatusViewOwner: one('ThreadTextualTypingStatusView', { identifying: true, inverse: 'threadTypingIconView' }), + threadIconViewOwner: one("ThreadIconView", { + identifying: true, + inverse: "threadTypingIconView", + }), + threadTextualTypingStatusViewOwner: one("ThreadTextualTypingStatusView", { + identifying: true, + inverse: "threadTypingIconView", + }), title: attr({ compute() { if (this.threadIconViewOwner) { diff --git a/addons/mail/static/src/models/thread_view.js b/addons/mail/static/src/models/thread_view.js index b5947849253..ccdcf11f399 100644 --- a/addons/mail/static/src/models/thread_view.js +++ b/addons/mail/static/src/models/thread_view.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, many, one, Model } from '@mail/model'; +import { attr, clear, many, one, Model } from "@mail/model"; Model({ - name: 'ThreadView', - template: 'mail.ThreadView', + name: "ThreadView", + template: "mail.ThreadView", lifecycleHooks: { _willDelete() { this.messaging.browser.clearTimeout(this.loaderTimeout); @@ -42,9 +42,9 @@ Model({ */ markComponentHintProcessed(hint) { this.update({ - componentHintList: this.componentHintList.filter(h => h !== hint), + componentHintList: this.componentHintList.filter((h) => h !== hint), }); - this.messaging.messagingBus.trigger('o-thread-view-hint-processed', { + this.messaging.messagingBus.trigger("o-thread-view-hint-processed", { hint, threadViewer: this.threadViewer, }); @@ -76,7 +76,11 @@ Model({ startEditingLastMessageFromCurrentUser() { const messageListViewItems = this.messageListView.messageListViewItems; messageListViewItems.reverse(); - const messageListViewItem = messageListViewItems.find(messageListViewItem => messageListViewItem.message.isCurrentUserOrGuestAuthor && messageListViewItem.message.canBeDeleted); + const messageListViewItem = messageListViewItems.find( + (messageListViewItem) => + messageListViewItem.message.isCurrentUserOrGuestAuthor && + messageListViewItem.message.canBeDeleted + ); if (messageListViewItem) { messageListViewItem.messageView.startEditing(); } @@ -117,7 +121,7 @@ Model({ _onThreadCacheChanged() { // clear obsolete hints this.update({ componentHintList: clear() }); - this.addComponentHint('change-of-thread-cache'); + this.addComponentHint("change-of-thread-cache"); if (this.threadCache) { this.threadCache.update({ isCacheRefreshRequested: true, @@ -132,16 +136,18 @@ Model({ if (this.threadCache && this.threadCache.isLoading) { if (!this.isLoading && !this.isPreparingLoading) { this.update({ isPreparingLoading: true }); - (new Promise(resolve => { - this.update({ loaderTimeout: this.messaging.browser.setTimeout(resolve, this.messaging.loadingBaseDelayDuration) }); - } - )).then(() => { + new Promise((resolve) => { + this.update({ + loaderTimeout: this.messaging.browser.setTimeout( + resolve, + this.messaging.loadingBaseDelayDuration + ), + }); + }).then(() => { if (!this.exists()) { return; } - const isLoading = this.threadCache - ? this.threadCache.isLoading - : false; + const isLoading = this.threadCache ? this.threadCache.isLoading : false; this.update({ isLoading, isPreparingLoading: false }); }); } @@ -168,27 +174,31 @@ Model({ if (!prevMessage.date && message.date) { return false; } - if (message.momentDate && prevMessage.momentDate && Math.abs(message.momentDate.diff(prevMessage.momentDate)) > 60000) { + if ( + message.momentDate && + prevMessage.momentDate && + Math.abs(message.momentDate.diff(prevMessage.momentDate)) > 60000 + ) { // more than 1 min. elasped return false; } if (prevMessage.dateDay !== message.dateDay) { return false; } - if (prevMessage.message_type !== 'comment' || message.message_type !== 'comment') { + if (prevMessage.message_type !== "comment" || message.message_type !== "comment") { return false; } - if (prevMessage.author !== message.author || prevMessage.guestAuthor !== message.guestAuthor) { + if ( + prevMessage.author !== message.author || + prevMessage.guestAuthor !== message.guestAuthor + ) { // from a different author return false; } if (prevMessage.originThread !== message.originThread) { return false; } - if ( - prevMessage.notifications.length > 0 || - message.notifications.length > 0 - ) { + if (prevMessage.notifications.length > 0 || message.notifications.length > 0) { // visual about notifications is restricted to non-squashed messages return false; } @@ -198,7 +208,7 @@ Model({ prevOriginThread && originThread && prevOriginThread.model === originThread.model && - originThread.model !== 'mail.channel' && + originThread.model !== "mail.channel" && prevOriginThread.id !== originThread.id ) { // messages linked to different document thread @@ -211,7 +221,8 @@ Model({ /** * Model for the component with the controls for RTC related settings. */ - callSettingsMenu: one('CallSettingsMenu', { inverse: 'threadViewOwner', + callSettingsMenu: one("CallSettingsMenu", { + inverse: "threadViewOwner", compute() { if (this.isCallSettingsMenuOpen) { return {}; @@ -219,15 +230,21 @@ Model({ return clear(); }, }), - channelMemberListView: one('ChannelMemberListView', { inverse: 'threadViewOwner', + channelMemberListView: one("ChannelMemberListView", { + inverse: "threadViewOwner", compute() { - if (this.thread && this.thread.hasMemberListFeature && this.hasMemberList && this.isMemberListOpened) { + if ( + this.thread && + this.thread.hasMemberListFeature && + this.hasMemberList && + this.isMemberListOpened + ) { return {}; } return clear(); }, }), - compact: attr({ related: 'threadViewer.compact' }), + compact: attr({ related: "threadViewer.compact" }), /** * List of component hints. Hints contain information that help * components make UI/UX decisions based on their UI state. @@ -247,7 +264,8 @@ Model({ * } */ componentHintList: attr({ default: [] }), - composerView: one('ComposerView', { inverse: 'threadView', + composerView: one("ComposerView", { + inverse: "threadView", compute() { if (!this.thread || this.thread.mailbox) { return clear(); @@ -261,12 +279,12 @@ Model({ /** * Determines which extra class this thread view component should have. */ - extraClass: attr({ related: 'threadViewer.extraClass' }), + extraClass: attr({ related: "threadViewer.extraClass" }), /** * Determines whether this thread viewer has a member list. * Only makes sense if thread.hasMemberListFeature is true. */ - hasMemberList: attr({ related: 'threadViewer.hasMemberList' }), + hasMemberList: attr({ related: "threadViewer.hasMemberList" }), /** * Determines whether this thread view should squash close messages. * See `_shouldMessageBeSquashed` for which conditions are considered @@ -274,15 +292,20 @@ Model({ */ hasSquashCloseMessages: attr({ compute() { - return Boolean(this.threadViewer && !this.threadViewer.chatter && this.thread && !this.thread.mailbox); + return Boolean( + this.threadViewer && + !this.threadViewer.chatter && + this.thread && + !this.thread.mailbox + ); }, }), /** * Determines whether this thread view has a top bar. */ - hasTopbar: attr({ related: 'threadViewer.hasTopbar' }), + hasTopbar: attr({ related: "threadViewer.hasTopbar" }), isCallSettingsMenuOpen: attr({ default: false }), - isComposerFocused: attr({ related: 'composerView.isFocused' }), + isComposerFocused: attr({ related: "composerView.isFocused" }), /** * States whether `this.threadCache` is currently loading messages. * @@ -315,7 +338,8 @@ Model({ * hint `message-received`. */ hasAutoScrollOnMessageReceived: attr({ default: true }), - hasComposerThreadName: attr({ default: false, + hasComposerThreadName: attr({ + default: false, compute() { if (this.threadViewer.discuss) { return this.threadViewer.discuss.activeThread === this.messaging.inbox.thread; @@ -328,7 +352,8 @@ Model({ * members typing on related thread. When this prop is not provided, * it defaults to composer component default value. */ - hasComposerThreadTyping: attr({ default: false, + hasComposerThreadTyping: attr({ + default: false, compute() { if (this.threadViewer.threadView_hasComposerThreadTyping !== undefined) { return this.threadViewer.threadView_hasComposerThreadTyping; @@ -339,13 +364,17 @@ Model({ /** * Last message in the context of the currently displayed thread cache. */ - lastMessage: one('Message', { related: 'thread.lastMessage' }), - lastMessageListViewItem: one('MessageListViewItem', { inverse: 'threadViewOwnerAsLastMessageListViewItem', + lastMessage: one("Message", { related: "thread.lastMessage" }), + lastMessageListViewItem: one("MessageListViewItem", { + inverse: "threadViewOwnerAsLastMessageListViewItem", compute() { if (!this.messageListView) { return clear(); } - const { length, [length - 1]: messageListViewItem } = this.messageListView.messageListViewItems; + const { + length, + [length - 1]: messageListViewItem, + } = this.messageListView.messageListViewItems; return messageListViewItem; }, }), @@ -353,32 +382,36 @@ Model({ * Most recent message in this ThreadView that has been shown to the * current partner in the currently displayed thread cache. */ - lastVisibleMessage: one('Message'), + lastVisibleMessage: one("Message"), loaderTimeout: attr(), - messageListView: one('MessageListView', { inverse: 'threadViewOwner', + messageListView: one("MessageListView", { + inverse: "threadViewOwner", compute() { - return ( - (this.thread && this.thread.isTemporary) || + return (this.thread && this.thread.isTemporary) || (this.threadCache && this.threadCache.isLoaded) - ) ? {} : clear(); + ? {} + : clear(); }, }), - messages: many('Message', { related: 'threadCache.messages' }), + messages: many("Message", { related: "threadCache.messages" }), /** * States the order mode of the messages on this thread view. * Either 'asc', or 'desc'. */ - order: attr({ related: 'threadViewer.order' }), + order: attr({ related: "threadViewer.order" }), /** * Determines the message that's currently being replied to. */ - replyingToMessageView: one('MessageView'), + replyingToMessageView: one("MessageView"), /** * Determines the call view of this thread. */ - callView: one('CallView', { inverse: 'threadView', + callView: one("CallView", { + inverse: "threadView", compute() { - return (this.thread && this.thread.model === 'mail.channel' && this.thread.rtcSessions.length > 0) + return this.thread && + this.thread.model === "mail.channel" && + this.thread.rtcSessions.length > 0 ? {} : clear(); }, @@ -386,17 +419,22 @@ Model({ /** * Determines the `Thread` currently displayed by `this`. */ - thread: one('Thread', { inverse: 'threadViews', related: 'threadViewer.thread' }), + thread: one("Thread", { inverse: "threadViews", related: "threadViewer.thread" }), /** * States the `ThreadCache` currently displayed by `this`. */ - threadCache: one('ThreadCache', { inverse: 'threadViews', related: 'threadViewer.threadCache' }), + threadCache: one("ThreadCache", { + inverse: "threadViews", + related: "threadViewer.threadCache", + }), threadCacheInitialScrollHeight: attr({ compute() { if (!this.threadCache) { return clear(); } - const threadCacheInitialScrollHeight = this.threadCacheInitialScrollHeights[this.threadCache.localId]; + const threadCacheInitialScrollHeight = this.threadCacheInitialScrollHeights[ + this.threadCache.localId + ]; if (threadCacheInitialScrollHeight !== undefined) { return threadCacheInitialScrollHeight; } @@ -408,7 +446,9 @@ Model({ if (!this.threadCache) { return clear(); } - const threadCacheInitialScrollPosition = this.threadCacheInitialScrollPositions[this.threadCache.localId]; + const threadCacheInitialScrollPosition = this.threadCacheInitialScrollPositions[ + this.threadCache.localId + ]; if (threadCacheInitialScrollPosition !== undefined) { return threadCacheInitialScrollPosition; } @@ -418,19 +458,26 @@ Model({ /** * List of saved initial scroll heights of thread caches. */ - threadCacheInitialScrollHeights: attr({ default: {}, related: 'threadViewer.threadCacheInitialScrollHeights' }), + threadCacheInitialScrollHeights: attr({ + default: {}, + related: "threadViewer.threadCacheInitialScrollHeights", + }), /** * List of saved initial scroll positions of thread caches. */ - threadCacheInitialScrollPositions: attr({ default: {}, related: 'threadViewer.threadCacheInitialScrollPositions' }), + threadCacheInitialScrollPositions: attr({ + default: {}, + related: "threadViewer.threadCacheInitialScrollPositions", + }), /** * Determines the `ThreadViewer` currently managing `this`. */ - threadViewer: one('ThreadViewer', { identifying: true, inverse: 'threadView' }), + threadViewer: one("ThreadViewer", { identifying: true, inverse: "threadView" }), /** * Determines the top bar of this thread view, if any. */ - topbar: one('ThreadViewTopbar', { inverse: 'threadView', + topbar: one("ThreadViewTopbar", { + inverse: "threadView", compute() { return this.hasTopbar ? {} : clear(); }, @@ -438,16 +485,22 @@ Model({ }, onChanges: [ { - dependencies: ['threadCache'], - methodName: '_onThreadCacheChanged', + dependencies: ["threadCache"], + methodName: "_onThreadCacheChanged", }, { - dependencies: ['threadCache.isLoading'], - methodName: '_onThreadCacheIsLoadingChanged', + dependencies: ["threadCache.isLoading"], + methodName: "_onThreadCacheIsLoadingChanged", }, { - dependencies: ['isComposerFocused', 'lastMessage', 'thread.lastNonTransientMessage', 'lastVisibleMessage', 'threadCache'], - methodName: '_computeThreadShouldBeSetAsSeen', + dependencies: [ + "isComposerFocused", + "lastMessage", + "thread.lastNonTransientMessage", + "lastVisibleMessage", + "threadCache", + ], + methodName: "_computeThreadShouldBeSetAsSeen", }, ], }); diff --git a/addons/mail/static/src/models/thread_view_topbar.js b/addons/mail/static/src/models/thread_view_topbar.js index 6bd1106c818..ab6c7e6b122 100644 --- a/addons/mail/static/src/models/thread_view_topbar.js +++ b/addons/mail/static/src/models/thread_view_topbar.js @@ -1,20 +1,20 @@ /** @odoo-module **/ -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, one, Model } from '@mail/model'; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'ThreadViewTopbar', - template: 'mail.ThreadViewTopbar', + name: "ThreadViewTopbar", + template: "mail.ThreadViewTopbar", componentSetup() { - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, lifecycleHooks: { _created() { - document.addEventListener('click', this._onClickCaptureGlobal, true); + document.addEventListener("click", this._onClickCaptureGlobal, true); }, _willDelete() { - document.removeEventListener('click', this._onClickCaptureGlobal, true); + document.removeEventListener("click", this._onClickCaptureGlobal, true); }, }, recordMethods: { @@ -40,9 +40,9 @@ Model({ * * @param {Event} ev */ - onClickHideMemberList(ev) { + onClickHideMemberList(ev) { this.threadView.update({ isMemberListOpened: false }); - this.threadView.addComponentHint('member-list-hidden'); + this.threadView.addComponentHint("member-list-hidden"); }, /** * Handles click on the "mark all as read" button of Inbox. @@ -50,7 +50,7 @@ Model({ * @param {MouseEvent} ev */ onClickInboxMarkAllAsRead(ev) { - this.messaging.models['Message'].markAllAsRead(); + this.messaging.models["Message"].markAllAsRead(); }, /** * Handles click on the "invite" button. @@ -110,9 +110,16 @@ Model({ const selection = window.getSelection(); this.update({ doFocusOnThreadNameInput: true, - doSetSelectionDirectionOnThreadNameInput: selection.anchorOffset < selection.focusOffset ? 'forward' : 'backward', - doSetSelectionEndOnThreadNameInput: Math.max(selection.focusOffset, selection.anchorOffset), - doSetSelectionStartOnThreadNameInput: Math.min(selection.focusOffset, selection.anchorOffset), + doSetSelectionDirectionOnThreadNameInput: + selection.anchorOffset < selection.focusOffset ? "forward" : "backward", + doSetSelectionEndOnThreadNameInput: Math.max( + selection.focusOffset, + selection.anchorOffset + ), + doSetSelectionStartOnThreadNameInput: Math.min( + selection.focusOffset, + selection.anchorOffset + ), isEditingThreadName: true, isMouseOverThreadName: false, pendingThreadName: this.thread.displayName, @@ -130,9 +137,16 @@ Model({ const selection = window.getSelection(); this.update({ doFocusOnThreadDescriptionInput: true, - doSetSelectionDirectionOnThreadDescriptionInput: selection.anchorOffset < selection.focusOffset ? 'forward' : 'backward', - doSetSelectionEndOnThreadDescriptionInput: Math.max(selection.focusOffset, selection.anchorOffset), - doSetSelectionStartOnThreadDescriptionInput: Math.min(selection.focusOffset, selection.anchorOffset), + doSetSelectionDirectionOnThreadDescriptionInput: + selection.anchorOffset < selection.focusOffset ? "forward" : "backward", + doSetSelectionEndOnThreadDescriptionInput: Math.max( + selection.focusOffset, + selection.anchorOffset + ), + doSetSelectionStartOnThreadDescriptionInput: Math.min( + selection.focusOffset, + selection.anchorOffset + ), isEditingThreadDescription: true, isMouseOverThreadDescription: false, pendingThreadDescription: this.thread.description || "", @@ -144,7 +158,7 @@ Model({ * @param {MouseEvent} ev */ onClickUnstarAll(ev) { - this.messaging.models['Message'].unstarAll(); + this.messaging.models["Message"].unstarAll(); }, /** * Handles click on the guest name. @@ -158,9 +172,16 @@ Model({ const selection = window.getSelection(); this.update({ doFocusOnGuestNameInput: true, - doSetSelectionDirectionOnGuestNameInput: selection.anchorOffset < selection.focusOffset ? 'forward' : 'backward', - doSetSelectionEndOnGuestNameInput: Math.max(selection.focusOffset, selection.anchorOffset), - doSetSelectionStartOnGuestNameInput: Math.min(selection.focusOffset, selection.anchorOffset), + doSetSelectionDirectionOnGuestNameInput: + selection.anchorOffset < selection.focusOffset ? "forward" : "backward", + doSetSelectionEndOnGuestNameInput: Math.max( + selection.focusOffset, + selection.anchorOffset + ), + doSetSelectionStartOnGuestNameInput: Math.min( + selection.focusOffset, + selection.anchorOffset + ), isEditingGuestName: true, isMouseOverUserName: false, pendingGuestName: this.messaging.currentGuest.name, @@ -260,12 +281,12 @@ Model({ */ onKeyDownGuestNameInput(ev) { switch (ev.key) { - case 'Enter': - if (this.pendingGuestName.trim() !== '') { + case "Enter": + if (this.pendingGuestName.trim() !== "") { this._applyGuestRename(); } break; - case 'Escape': + case "Escape": this._resetGuestNameInput(); break; } @@ -277,10 +298,10 @@ Model({ */ onKeyDownThreadNameInput(ev) { switch (ev.key) { - case 'Enter': + case "Enter": this._applyThreadRename(); break; - case 'Escape': + case "Escape": this._discardThreadRename(); break; } @@ -292,10 +313,10 @@ Model({ */ onKeyDownThreadDescriptionInput(ev) { switch (ev.key) { - case 'Enter': + case "Enter": this._applyThreadChangeDescription(); break; - case 'Escape': + case "Escape": this._discardThreadChangeDescription(); break; } @@ -382,7 +403,7 @@ Model({ */ _applyGuestRename() { if (this.hasGuestNameChanged) { - this.messaging.models['Guest'].performRpcGuestUpdateName({ + this.messaging.models["Guest"].performRpcGuestUpdateName({ id: this.messaging.currentGuest.id, name: this.pendingGuestName.trim(), }); @@ -398,13 +419,20 @@ Model({ isEditingThreadName: false, pendingThreadName: clear(), }); - if (this.thread.channel.channel_type === 'chat' && newName !== this.thread.channel.custom_channel_name) { + if ( + this.thread.channel.channel_type === "chat" && + newName !== this.thread.channel.custom_channel_name + ) { this.thread.setCustomName(newName); } - if (newName && this.thread.channel.channel_type === 'channel' && newName !== this.thread.name) { + if ( + newName && + this.thread.channel.channel_type === "channel" && + newName !== this.thread.name + ) { this.thread.rename(newName); } - if (this.thread.channel.channel_type === 'group' && newName !== this.thread.name) { + if (this.thread.channel.channel_type === "group" && newName !== this.thread.name) { this.thread.rename(newName); } }, @@ -444,17 +472,29 @@ Model({ * @param {MouseEvent} ev */ _onClickCaptureGlobal(ev) { - if (this.isEditingGuestName && this.guestNameInputRef.el && !this.guestNameInputRef.el.contains(ev.target)) { - if (this.pendingGuestName.trim() !== '') { + if ( + this.isEditingGuestName && + this.guestNameInputRef.el && + !this.guestNameInputRef.el.contains(ev.target) + ) { + if (this.pendingGuestName.trim() !== "") { this._applyGuestRename(); } else { this._resetGuestNameInput(); } } - if (this.isEditingThreadName && this.threadNameInputRef.el && !this.threadNameInputRef.el.contains(ev.target)) { + if ( + this.isEditingThreadName && + this.threadNameInputRef.el && + !this.threadNameInputRef.el.contains(ev.target) + ) { this._applyThreadRename(); } - if (this.isEditingThreadDescription && this.threadDescriptionInputRef.el && !this.threadDescriptionInputRef.el.contains(ev.target)) { + if ( + this.isEditingThreadDescription && + this.threadDescriptionInputRef.el && + !this.threadDescriptionInputRef.el.contains(ev.target) + ) { this._applyThreadChangeDescription(); } }, @@ -476,7 +516,7 @@ Model({ compute() { if (this.messaging.isCurrentUserGuest) { if (!this.thread) { - return ''; + return ""; } return `/mail/channel/${this.thread.id}/guest/${this.messaging.currentGuest.id}/avatar_128?unique=${this.messaging.currentGuest.name}`; } @@ -485,7 +525,7 @@ Model({ } return clear(); }, - default: '', + default: "", }), /** * Determines whether the guest name input needs to be focused. @@ -575,7 +615,7 @@ Model({ * States the OWL ref of the "guest name" input of this top bar. * Useful to focus it, or to know when a click is done outside of it. */ - guestNameInputRef: attr({ ref: 'guestNameInput' }), + guestNameInputRef: attr({ ref: "guestNameInput" }), /** * Determines whether the guest's name has been updated. * @@ -586,7 +626,7 @@ Model({ compute() { return Boolean( this.messaging.currentGuest && - this.pendingGuestName !== this.messaging.currentGuest.name + this.pendingGuestName !== this.messaging.currentGuest.name ); }, }), @@ -595,7 +635,10 @@ Model({ */ hasDescriptionArea: attr({ compute() { - return Boolean(this.thread && (this.thread.description || this.thread.isDescriptionEditableByCurrentUser)); + return Boolean( + this.thread && + (this.thread.description || this.thread.isDescriptionEditableByCurrentUser) + ); }, }), /** @@ -606,12 +649,12 @@ Model({ * States the OWL ref of the invite button. * Useful to provide anchor for the invite popover positioning. */ - inviteButtonRef: attr({ ref: 'inviteButton' }), + inviteButtonRef: attr({ ref: "inviteButton" }), /** * If set, this is the record of invite button popover that is currently * open in the topbar. */ - invitePopoverView: one('PopoverView', { inverse: 'threadViewTopbarOwnerAsInvite' }), + invitePopoverView: one("PopoverView", { inverse: "threadViewTopbarOwnerAsInvite" }), /** * States whether this thread description is highlighted. */ @@ -619,7 +662,7 @@ Model({ compute() { return Boolean( this.isMouseOverThreadDescription && - this.thread.isDescriptionEditableByCurrentUser + this.thread.isDescriptionEditableByCurrentUser ); }, }), @@ -670,18 +713,19 @@ Model({ /** * States the thread that is displayed by this top bar. */ - thread: one('Thread', { related: 'threadView.thread' }), + thread: one("Thread", { related: "threadView.thread" }), /** * States the OWL ref of the "thread name" input of this top bar. * Useful to focus it, or to know when a click is done outside of it. */ - threadNameInputRef: attr({ ref: 'threadNameInput' }), + threadNameInputRef: attr({ ref: "threadNameInput" }), /** * States the OWL ref of the thread description input of this top bar. * Useful to focus it, or to know when a click is done outside of it. */ - threadDescriptionInputRef: attr({ ref: 'threadDescriptionInput' }), - threadIconView: one('ThreadIconView', { inverse: 'threadViewTopbarOwner', + threadDescriptionInputRef: attr({ ref: "threadDescriptionInput" }), + threadIconView: one("ThreadIconView", { + inverse: "threadViewTopbarOwner", compute() { if (this.thread) { return {}; @@ -692,6 +736,6 @@ Model({ /** * States the thread view managing this top bar. */ - threadView: one('ThreadView', { identifying: true, inverse: 'topbar' }), + threadView: one("ThreadView", { identifying: true, inverse: "topbar" }), }, }); diff --git a/addons/mail/static/src/models/thread_viewer.js b/addons/mail/static/src/models/thread_viewer.js index a8fd92e9977..97390f2a552 100644 --- a/addons/mail/static/src/models/thread_viewer.js +++ b/addons/mail/static/src/models/thread_viewer.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; Model({ - name: 'ThreadViewer', - identifyingMode: 'xor', + name: "ThreadViewer", + identifyingMode: "xor", recordMethods: { /** * @param {integer} scrollHeight @@ -22,9 +22,13 @@ Model({ return; } this.update({ - threadCacheInitialScrollHeights: Object.assign({}, this.threadCacheInitialScrollHeights, { - [threadCache.localId]: scrollHeight, - }), + threadCacheInitialScrollHeights: Object.assign( + {}, + this.threadCacheInitialScrollHeights, + { + [threadCache.localId]: scrollHeight, + } + ), }); }, /** @@ -43,21 +47,25 @@ Model({ return; } this.update({ - threadCacheInitialScrollPositions: Object.assign({}, this.threadCacheInitialScrollPositions, { - [threadCache.localId]: scrollTop, - }), + threadCacheInitialScrollPositions: Object.assign( + {}, + this.threadCacheInitialScrollPositions, + { + [threadCache.localId]: scrollTop, + } + ), }); }, }, fields: { - chatter: one('Chatter', { identifying: true, inverse: 'threadViewer' }), - chatWindow: one('ChatWindow', { identifying: true, inverse: 'threadViewer' }), + chatter: one("Chatter", { identifying: true, inverse: "threadViewer" }), + chatWindow: one("ChatWindow", { identifying: true, inverse: "threadViewer" }), /** * true if the viewer is in a compact format, like in a chat window. */ compact: attr({ default: false }), - discuss: one('Discuss', { identifying: true, inverse: 'threadViewer' }), - discussPublicView: one('DiscussPublicView', { identifying: true, inverse: 'threadViewer' }), + discuss: one("Discuss", { identifying: true, inverse: "threadViewer" }), + discussPublicView: one("DiscussPublicView", { identifying: true, inverse: "threadViewer" }), /** * Determines which extra class this thread view component should have. */ @@ -80,15 +88,15 @@ Model({ * Determines the order mode of the messages on this thread viewer. * Either 'asc', or 'desc'. */ - order: attr({ default: 'asc' }), + order: attr({ default: "asc" }), /** * Determines the `Thread` that should be displayed by `this`. */ - thread: one('Thread'), + thread: one("Thread"), /** * States the `ThreadCache` that should be displayed by `this`. */ - threadCache: one('ThreadCache', { related: 'thread.cache' }), + threadCache: one("ThreadCache", { related: "thread.cache" }), /** * Determines the initial scroll height of thread caches, which is the * scroll height at the time the last scroll position was saved. @@ -107,7 +115,8 @@ Model({ /** * States the `ThreadView` currently displayed and managed by `this`. */ - threadView: one('ThreadView', { inverse: 'threadViewer', + threadView: one("ThreadView", { + inverse: "threadViewer", compute() { return this.hasThreadView ? {} : clear(); }, diff --git a/addons/mail/static/src/models/throttle.js b/addons/mail/static/src/models/throttle.js index db055bfb7c6..07b948d5f2b 100644 --- a/addons/mail/static/src/models/throttle.js +++ b/addons/mail/static/src/models/throttle.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { attr, clear, one, Model } from '@mail/model'; +import { attr, clear, one, Model } from "@mail/model"; /** * This model defines a "Throttle", which is an abstraction to throttle calls on a @@ -10,8 +10,8 @@ import { attr, clear, one, Model } from '@mail/model'; * are re-trigger a cooldown like a fresh throttle call. */ Model({ - name: 'Throttle', - identifyingMode: 'xor', + name: "Throttle", + identifyingMode: "xor", recordMethods: { /** * Clear any buffered function call and immediately terminates any cooling @@ -42,11 +42,12 @@ Model({ }, }, fields: { - cooldownTimer: one('Timer', { inverse: 'throttleOwner' }), + cooldownTimer: one("Timer", { inverse: "throttleOwner" }), /** * Duration, in milliseconds, of the cool down phase. */ - duration: attr({ required: true, + duration: attr({ + required: true, compute() { if (this.emojiGridViewAsOnScroll) { return 150; @@ -63,14 +64,26 @@ Model({ return clear(); }, }), - emojiGridViewAsOnScroll: one('EmojiGridView', { identifying: true, inverse: 'onScrollThrottle' }), + emojiGridViewAsOnScroll: one("EmojiGridView", { + identifying: true, + inverse: "onScrollThrottle", + }), /** * Inner function to be invoked and throttled. */ func: attr(), - messageListViewAsScroll: one('MessageListView', { identifying: true, inverse: 'scrollThrottle' }), - messagingAsUpdateImStatusRegister: one('Messaging', { identifying: true, inverse: 'updateImStatusRegisterThrottle' }), + messageListViewAsScroll: one("MessageListView", { + identifying: true, + inverse: "scrollThrottle", + }), + messagingAsUpdateImStatusRegister: one("Messaging", { + identifying: true, + inverse: "updateImStatusRegisterThrottle", + }), shouldInvoke: attr({ default: false }), - threadAsThrottleNotifyCurrentPartnerTypingStatus: one('Thread', { identifying: true, inverse: 'throttleNotifyCurrentPartnerTypingStatus' }), + threadAsThrottleNotifyCurrentPartnerTypingStatus: one("Thread", { + identifying: true, + inverse: "throttleNotifyCurrentPartnerTypingStatus", + }), }, }); diff --git a/addons/mail/static/src/models/timer.js b/addons/mail/static/src/models/timer.js index 20c23875a42..feb92f8c2d7 100644 --- a/addons/mail/static/src/models/timer.js +++ b/addons/mail/static/src/models/timer.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { one, Patch } from '@mail/model'; +import { one, Patch } from "@mail/model"; Patch({ - name: 'Timer', + name: "Timer", recordMethods: { /** * @override @@ -49,9 +49,18 @@ Patch({ }, }, fields: { - blurManagerOwnerAsFrameRequest: one('BlurManager', { identifying: true, inverse: 'frameRequestTimer' }), - callMainViewAsShowOverlay: one('CallMainView', { identifying: true, inverse: 'showOverlayTimer' }), - chatterOwnerAsAttachmentsLoader: one('Chatter', { identifying: true, inverse: 'attachmentsLoaderTimer' }), + blurManagerOwnerAsFrameRequest: one("BlurManager", { + identifying: true, + inverse: "frameRequestTimer", + }), + callMainViewAsShowOverlay: one("CallMainView", { + identifying: true, + inverse: "showOverlayTimer", + }), + chatterOwnerAsAttachmentsLoader: one("Chatter", { + identifying: true, + inverse: "attachmentsLoaderTimer", + }), duration: { compute() { if (this.blurManagerOwnerAsFrameRequest) { @@ -84,11 +93,27 @@ Patch({ return this._super(); }, }, - messageViewOwnerAsHighlight: one('MessageView', { identifying: true, inverse: 'highlightTimer' }), - otherMemberLongTypingInThreadTimerOwner: one('OtherMemberLongTypingInThreadTimer', { identifying: true, inverse: 'timer', isCausal: true }), - rtcSessionOwnerAsBroadcast: one('RtcSession', { identifying: true, inverse: 'broadcastTimer' }), - threadAsCurrentPartnerInactiveTypingTimerOwner: one('Thread', { identifying: true, inverse: 'currentPartnerInactiveTypingTimer' }), - threadAsCurrentPartnerLongTypingTimerOwner: one('Thread', { identifying: true, inverse: 'currentPartnerLongTypingTimer' }), - throttleOwner: one('Throttle', { identifying: true, inverse: 'cooldownTimer' }), + messageViewOwnerAsHighlight: one("MessageView", { + identifying: true, + inverse: "highlightTimer", + }), + otherMemberLongTypingInThreadTimerOwner: one("OtherMemberLongTypingInThreadTimer", { + identifying: true, + inverse: "timer", + isCausal: true, + }), + rtcSessionOwnerAsBroadcast: one("RtcSession", { + identifying: true, + inverse: "broadcastTimer", + }), + threadAsCurrentPartnerInactiveTypingTimerOwner: one("Thread", { + identifying: true, + inverse: "currentPartnerInactiveTypingTimer", + }), + threadAsCurrentPartnerLongTypingTimerOwner: one("Thread", { + identifying: true, + inverse: "currentPartnerLongTypingTimer", + }), + throttleOwner: one("Throttle", { identifying: true, inverse: "cooldownTimer" }), }, }); diff --git a/addons/mail/static/src/models/tracking_value.js b/addons/mail/static/src/models/tracking_value.js index d90869fad5e..d79620bac43 100644 --- a/addons/mail/static/src/models/tracking_value.js +++ b/addons/mail/static/src/models/tracking_value.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; -import { sprintf } from '@web/core/utils/strings'; +import { attr, one, Model } from "@mail/model"; +import { sprintf } from "@web/core/utils/strings"; Model({ - name: 'TrackingValue', + name: "TrackingValue", fields: { /** * States the original field of changed tracking value, such as "Status", "Date". @@ -19,8 +19,8 @@ Model({ }, }), id: attr({ identifying: true }), - messageOwner: one('Message', { inverse: 'trackingValues', readonly: true, required: true }), - newValue: one('TrackingValueItem', { inverse: 'trackingValueAsNewValue' }), - oldValue: one('TrackingValueItem', { inverse: 'trackingValueAsOldValue' }), + messageOwner: one("Message", { inverse: "trackingValues", readonly: true, required: true }), + newValue: one("TrackingValueItem", { inverse: "trackingValueAsNewValue" }), + oldValue: one("TrackingValueItem", { inverse: "trackingValueAsOldValue" }), }, }); diff --git a/addons/mail/static/src/models/tracking_value_item.js b/addons/mail/static/src/models/tracking_value_item.js index da564dc0cfb..3472a869968 100644 --- a/addons/mail/static/src/models/tracking_value_item.js +++ b/addons/mail/static/src/models/tracking_value_item.js @@ -1,17 +1,17 @@ /** @odoo-module **/ -import { attr, one, Model } from '@mail/model'; +import { attr, one, Model } from "@mail/model"; -import { format } from 'web.field_utils'; -import { session } from '@web/session'; -import { registry } from '@web/core/registry'; -import { deserializeDateTime } from '@web/core/l10n/dates'; +import { format } from "web.field_utils"; +import { session } from "@web/session"; +import { registry } from "@web/core/registry"; +import { deserializeDateTime } from "@web/core/l10n/dates"; const formatters = registry.category("formatters"); Model({ - name: 'TrackingValueItem', - identifyingMode: 'xor', + name: "TrackingValueItem", + identifyingMode: "xor", fields: { /** * Used when the currency changes as the tracking value. This only makes sense for field of type monetary. @@ -31,7 +31,7 @@ Model({ * tracking in Python. Also see `create_tracking_values` in Python. */ switch (this.fieldType) { - case 'boolean': + case "boolean": return this.value ? this.env._t("Yes") : this.env._t("No"); /** * many2one formatter exists but is expecting id/name_get or data @@ -40,33 +40,33 @@ Model({ * Selection formatter exists but requires knowing all * possibilities and they are not given in this context. */ - case 'char': - case 'many2one': - case 'selection': + case "char": + case "many2one": + case "selection": return format.char(this.value); - case 'date': + case "date": if (this.value) { return format.date(moment.utc(this.value)); } return format.date(this.value); - case 'datetime': { + case "datetime": { const value = this.value ? deserializeDateTime(this.value) : this.value; return formatters.get("datetime")(value); } - case 'float': + case "float": return format.float(this.value); - case 'integer': + case "integer": return format.integer(this.value); - case 'text': + case "text": return format.text(this.value); - case 'monetary': + case "monetary": return format.monetary(this.value, undefined, { currency: this.currencyId ? session.currencies[this.currencyId] : undefined, forceString: true, }); - default : + default: return this.value; } }, @@ -76,8 +76,8 @@ Model({ return this.formattedValue || this.env._t("None"); }, }), - trackingValueAsNewValue: one('TrackingValue', { identifying: true, inverse: 'newValue' }), - trackingValueAsOldValue: one('TrackingValue', { identifying: true, inverse: 'oldValue' }), + trackingValueAsNewValue: one("TrackingValue", { identifying: true, inverse: "newValue" }), + trackingValueAsOldValue: one("TrackingValue", { identifying: true, inverse: "oldValue" }), /** * The original value of the tracking value item. */ diff --git a/addons/mail/static/src/models/use_drag_visible_drop_zone.js b/addons/mail/static/src/models/use_drag_visible_drop_zone.js index d2fda640f8e..37a49b25cb5 100644 --- a/addons/mail/static/src/models/use_drag_visible_drop_zone.js +++ b/addons/mail/static/src/models/use_drag_visible_drop_zone.js @@ -1,28 +1,28 @@ /** @odoo-module **/ -import { attr, decrement, increment, one, Model } from '@mail/model'; +import { attr, decrement, increment, one, Model } from "@mail/model"; Model({ - name: 'UseDragVisibleDropZone', - identifyingMode: 'xor', + name: "UseDragVisibleDropZone", + identifyingMode: "xor", lifecycleHooks: { _created() { - document.addEventListener('dragenter', this._onDragenterListener, true); - document.addEventListener('dragleave', this._onDragleaveListener, true); - document.addEventListener('drop', this._onDropListener); + document.addEventListener("dragenter", this._onDragenterListener, true); + document.addEventListener("dragleave", this._onDragleaveListener, true); + document.addEventListener("drop", this._onDropListener); // 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()); + window.addEventListener("dragover", (ev) => ev.preventDefault()); + window.addEventListener("drop", (ev) => ev.preventDefault()); }, _willDelete() { - document.removeEventListener('dragenter', this._onDragenterListener, true); - document.removeEventListener('dragleave', this._onDragleaveListener, true); - document.removeEventListener('drop', this._onDropListener); + document.removeEventListener("dragenter", this._onDragenterListener, true); + document.removeEventListener("dragleave", this._onDragleaveListener, true); + document.removeEventListener("drop", this._onDropListener); - window.removeEventListener('dragover', ev => ev.preventDefault()); - window.removeEventListener('drop', ev => ev.preventDefault()); + window.removeEventListener("dragover", (ev) => ev.preventDefault()); + window.removeEventListener("drop", (ev) => ev.preventDefault()); }, }, recordMethods: { @@ -38,7 +38,7 @@ Model({ if ( this.dragCount === 0 && ev.dataTransfer && - ev.dataTransfer.types.includes('Files') + ev.dataTransfer.types.includes("Files") ) { this.update({ isVisible: true }); } @@ -66,8 +66,11 @@ Model({ }, }, fields: { - chatterOwner: one('Chatter', { identifying: true, inverse: 'useDragVisibleDropZone' }), - composerViewOwner: one('ComposerView', { identifying: true, inverse: 'useDragVisibleDropZone' }), + chatterOwner: one("Chatter", { identifying: true, inverse: "useDragVisibleDropZone" }), + composerViewOwner: one("ComposerView", { + identifying: true, + inverse: "useDragVisibleDropZone", + }), /** * 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. diff --git a/addons/mail/static/src/models/user.js b/addons/mail/static/src/models/user.js index 8414efbde07..aebeaa1ade0 100644 --- a/addons/mail/static/src/models/user.js +++ b/addons/mail/static/src/models/user.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { attr, clear, insert, many, one, Model } from '@mail/model'; +import { attr, clear, insert, many, one, Model } from "@mail/model"; Model({ - name: 'User', + name: "User", modelMethods: { /** * @param {Object} data @@ -11,14 +11,14 @@ Model({ */ convertData(data) { const data2 = {}; - if ('id' in data) { + if ("id" in data) { data2.id = data.id; } - if ('partner_id' in data) { + if ("partner_id" in data) { if (!data.partner_id) { data2.partner = clear(); } else { - const partnerNameGet = data['partner_id']; + const partnerNameGet = data["partner_id"]; const partnerData = { display_name: partnerNameGet[1], id: partnerNameGet[0], @@ -37,17 +37,20 @@ Model({ * @param {integer[]} param0.ids */ async performRpcRead({ context, fields, ids }) { - const usersData = await this.messaging.rpc({ - model: 'res.users', - method: 'read', - args: [ids, fields], - kwargs: { - context, + const usersData = await this.messaging.rpc( + { + model: "res.users", + method: "read", + args: [ids, fields], + kwargs: { + context, + }, }, - }, { shadow: true }); - return this.messaging.models['User'].insert(usersData.map(userData => - this.messaging.models['User'].convertData(userData) - )); + { shadow: true } + ); + return this.messaging.models["User"].insert( + usersData.map((userData) => this.messaging.models["User"].convertData(userData)) + ); }, }, recordMethods: { @@ -55,9 +58,9 @@ Model({ * Fetches the partner of this user. */ async fetchPartner() { - return this.messaging.models['User'].performRpcRead({ + return this.messaging.models["User"].performRpcRead({ ids: [this.id], - fields: ['partner_id'], + fields: ["partner_id"], context: { active_test: false }, }); }, @@ -82,7 +85,7 @@ Model({ // another tab or by another user. this.messaging.notify({ message: this.env._t("You can only chat with existing users."), - type: 'warning', + type: "warning", }); return; } @@ -91,7 +94,7 @@ Model({ if (!chat || !chat.thread.isPinned) { // if chat is not pinned then it has to be pinned client-side // and server-side, which is a side effect of following rpc - chat = await this.messaging.models['Channel'].performRpcCreateChat({ + chat = await this.messaging.models["Channel"].performRpcCreateChat({ partnerIds: [this.partner.id], }); if (!this.exists()) { @@ -100,8 +103,10 @@ Model({ } if (!chat) { this.messaging.notify({ - message: this.env._t("An unexpected error occurred during the creation of the chat."), - type: 'warning', + message: this.env._t( + "An unexpected error occurred during the creation of the chat." + ), + type: "warning", }); return; } @@ -145,7 +150,7 @@ Model({ // another tab or by another user. this.messaging.notify({ message: this.env._t("You can only open the profile of existing users."), - type: 'warning', + type: "warning", }); return; } @@ -153,7 +158,7 @@ Model({ }, }, fields: { - activitiesAsAssignee: many('Activity', { inverse: 'assignee' }), + activitiesAsAssignee: many("Activity", { inverse: "assignee" }), id: attr({ identifying: true }), /** * Determines whether this user is an internal user. An internal user is @@ -162,7 +167,8 @@ Model({ */ isInternalUser: attr(), display_name: attr(), - displayName: attr({ default: "", + displayName: attr({ + default: "", compute() { if (this.display_name) { return this.display_name; @@ -173,13 +179,13 @@ Model({ return clear(); }, }), - model: attr({ default: 'res.user' }), + model: attr({ default: "res.user" }), nameOrDisplayName: attr({ compute() { - return this.partner && this.partner.nameOrDisplayName || this.display_name; + return (this.partner && this.partner.nameOrDisplayName) || this.display_name; }, }), - partner: one('Partner', { inverse: 'user' }), - res_users_settings_id: one('res.users.settings', { inverse: 'user_id' }), + partner: one("Partner", { inverse: "user" }), + res_users_settings_id: one("res.users.settings", { inverse: "user_id" }), }, }); diff --git a/addons/mail/static/src/models/user_notification_manager.js b/addons/mail/static/src/models/user_notification_manager.js index 4f523bde97a..135e3ea1e5f 100644 --- a/addons/mail/static/src/models/user_notification_manager.js +++ b/addons/mail/static/src/models/user_notification_manager.js @@ -1,11 +1,11 @@ /** @odoo-module **/ -import { attr, clear, Model } from '@mail/model'; +import { attr, clear, Model } from "@mail/model"; -import { url } from '@web/core/utils/urls'; +import { url } from "@web/core/utils/urls"; Model({ - name: 'UserNotificationManager', + name: "UserNotificationManager", recordMethods: { /** * Send a notification, preferably a native one. If native @@ -25,7 +25,7 @@ Model({ this._sendOdooNotification(message, { title, type }); return; } - if (!this.messaging.env.services['multi_tab'].isOnMainTab()) { + if (!this.messaging.env.services["multi_tab"].isOnMainTab()) { return; } try { @@ -34,7 +34,7 @@ Model({ // Notification without Serviceworker in Chrome Android doesn't works anymore // So we fallback to the notification service in this case // https://bugs.chromium.org/p/chromium/issues/detail?id=481856 - if (error.message.includes('ServiceWorkerRegistration')) { + if (error.message.includes("ServiceWorkerRegistration")) { this._sendOdooNotification(message, { title, type }); } else { throw error; @@ -69,7 +69,7 @@ Model({ icon: this.icon, } ); - notification.addEventListener('click', this._onClickNotification); + notification.addEventListener("click", this._onClickNotification); }, /** * Send a notification through the notification service. @@ -78,8 +78,8 @@ Model({ * @param {Object} options */ async _sendOdooNotification(message, options) { - this.messaging.env.services['notification'].add(message, options); - if (this.canPlayAudio && this.messaging.env.services['multi_tab'].isOnMainTab()) { + this.messaging.env.services["notification"].add(message, options); + if (this.canPlayAudio && this.messaging.env.services["multi_tab"].isOnMainTab()) { try { await this.audio.play(); } catch { @@ -101,12 +101,12 @@ Model({ } const audioElement = new Audio(); audioElement.src = audioElement.canPlayType("audio/ogg; codecs=vorbis") - ? url('/mail/static/src/audio/ting.ogg') - : url('/mail/static/src/audio/ting.mp3'); + ? url("/mail/static/src/audio/ting.ogg") + : url("/mail/static/src/audio/ting.mp3"); return audioElement; }, }), - canPlayAudio: attr({ default: typeof(Audio) !== 'undefined' }), + canPlayAudio: attr({ default: typeof Audio !== "undefined" }), canSendNativeNotification: attr({ /** * Determines whether or not sending native notification is @@ -115,13 +115,13 @@ Model({ compute() { return Boolean( this.messaging.browser.Notification && - this.messaging.browser.Notification.permission === 'granted' + this.messaging.browser.Notification.permission === "granted" ); }, }), /** * Icon to be displayed by the notification. */ - icon: attr({ default: '/mail/static/src/img/odoobot_transparent.png' }), + icon: attr({ default: "/mail/static/src/img/odoobot_transparent.png" }), }, }); diff --git a/addons/mail/static/src/models/user_setting.js b/addons/mail/static/src/models/user_setting.js index 3c60d5d2770..00cde9c6e57 100644 --- a/addons/mail/static/src/models/user_setting.js +++ b/addons/mail/static/src/models/user_setting.js @@ -2,7 +2,7 @@ import { browser } from "@web/core/browser/browser"; -import { attr, clear, Model } from '@mail/model'; +import { attr, clear, Model } from "@mail/model"; /** * Models various user settings. It is used as a complement to @@ -11,14 +11,14 @@ import { attr, clear, Model } from '@mail/model'; * own settings. */ Model({ - name: 'UserSetting', + name: "UserSetting", lifecycleHooks: { _created() { this._loadLocalSettings(); - browser.addEventListener('storage', this._onStorage); + browser.addEventListener("storage", this._onStorage); }, _willDelete() { - browser.removeEventListener('storage', this._onStorage); + browser.removeEventListener("storage", this._onStorage); for (const timeout of Object.values(this.volumeSettingsTimeouts)) { this.messaging.browser.clearTimeout(timeout); } @@ -63,7 +63,7 @@ Model({ if (!this.pushToTalkKey) { return; } - const [shiftKey, ctrlKey, altKey, key] = this.pushToTalkKey.split('.'); + const [shiftKey, ctrlKey, altKey, key] = this.pushToTalkKey.split("."); return { shiftKey: !!shiftKey, ctrlKey: !!ctrlKey, @@ -73,8 +73,8 @@ Model({ }, pushToTalkKeyToString() { const { shiftKey, ctrlKey, altKey, key } = this.pushToTalkKeyFormat(); - const f = (k, name) => k ? name : ''; - return `${f(ctrlKey, 'Ctrl + ')}${f(altKey, 'Alt + ')}${f(shiftKey, 'Shift + ')}${key}`; + const f = (k, name) => (k ? name : ""); + return `${f(ctrlKey, "Ctrl + ")}${f(altKey, "Alt + ")}${f(shiftKey, "Shift + ")}${key}`; }, /** * @param {String} audioInputDeviceId @@ -83,7 +83,10 @@ Model({ this.update({ audioInputDeviceId, }); - browser.localStorage.setItem('mail_user_setting_audio_input_device_id', audioInputDeviceId); + browser.localStorage.setItem( + "mail_user_setting_audio_input_device_id", + audioInputDeviceId + ); await this.messaging.rtc.updateLocalAudioTrack(true); }, /** @@ -99,7 +102,9 @@ Model({ * @param {event} ev */ async setPushToTalkKey(ev) { - const pushToTalkKey = `${ev.shiftKey || ''}.${ev.ctrlKey || ev.metaKey || ''}.${ev.altKey || ''}.${ev.key}`; + const pushToTalkKey = `${ev.shiftKey || ""}.${ev.ctrlKey || ev.metaKey || ""}.${ + ev.altKey || "" + }.${ev.key}`; this.update({ localPushToTalkKey: pushToTalkKey }); if (this.messaging.currentUser) { this._saveSettings(); @@ -120,7 +125,7 @@ Model({ ...this.volumeSettingsTimeouts, [partnerId]: this.messaging.browser.setTimeout( this._onSaveVolumeSettingTimeout.bind(this, { guestId, partnerId, volume }), - 5000, + 5000 ), }, }); @@ -130,7 +135,10 @@ Model({ */ async setThresholdValue(voiceActivationThreshold) { this.update({ voiceActivationThreshold }); - browser.localStorage.setItem('mail_user_setting_voice_threshold', voiceActivationThreshold.toString()); + browser.localStorage.setItem( + "mail_user_setting_voice_threshold", + voiceActivationThreshold.toString() + ); await this.messaging.rtc.updateVoiceActivation(); }, async togglePushToTalk() { @@ -157,7 +165,9 @@ Model({ "mail_user_setting_audio_input_device_id" ); this.update({ - voiceActivationThreshold: voiceActivationThresholdString ? parseFloat(voiceActivationThresholdString) : undefined, + voiceActivationThreshold: voiceActivationThresholdString + ? parseFloat(voiceActivationThresholdString) + : undefined, audioInputDeviceId: audioInputDeviceId || undefined, }); }, @@ -172,7 +182,7 @@ Model({ * @param {Event} ev */ async _onStorage(ev) { - if (ev.key === 'mail_user_setting_voice_threshold') { + if (ev.key === "mail_user_setting_voice_threshold") { this.update({ voiceActivationThreshold: ev.newValue }); await this.messaging.rtc.updateVoiceActivation(); } @@ -187,15 +197,18 @@ Model({ this.update({ globalSettingsTimeout: clear() }); await this.messaging.rpc( { - model: 'res.users.settings', - method: 'set_res_users_settings', - args: [[this.messaging.currentUser.res_users_settings_id.id], { - push_to_talk_key: this.pushToTalkKey, - use_push_to_talk: this.usePushToTalk, - voice_active_duration: this.voiceActiveDuration, - }], + model: "res.users.settings", + method: "set_res_users_settings", + args: [ + [this.messaging.currentUser.res_users_settings_id.id], + { + push_to_talk_key: this.pushToTalkKey, + use_push_to_talk: this.usePushToTalk, + voice_active_duration: this.voiceActiveDuration, + }, + ], }, - { shadow: true }, + { shadow: true } ); }, /** @@ -213,8 +226,8 @@ Model({ this.update({ volumeSettingsTimeouts: newVolumeSettingsTimeouts }); await this.messaging.rpc( { - model: 'res.users.settings', - method: 'set_volume_setting', + model: "res.users.settings", + method: "set_volume_setting", args: [ [this.messaging.currentUser.res_users_settings_id.id], partnerId, @@ -224,7 +237,7 @@ Model({ guest_id: guestId, }, }, - { shadow: true }, + { shadow: true } ); }, /** @@ -235,7 +248,7 @@ Model({ this.update({ globalSettingsTimeout: this.messaging.browser.setTimeout( this._onSaveGlobalSettingsTimeout, - 2000, + 2000 ), }); }, @@ -244,7 +257,7 @@ Model({ /** * DeviceId of the audio input selected by the user */ - audioInputDeviceId: attr({ default: '' }), + audioInputDeviceId: attr({ default: "" }), backgroundBlurAmount: attr({ default: 10 }), edgeBlurAmount: attr({ default: 10 }), globalSettingsTimeout: attr(), @@ -258,7 +271,8 @@ Model({ /** * String that encodes the push-to-talk key with its modifiers. */ - pushToTalkKey: attr({ default: '', + pushToTalkKey: attr({ + default: "", compute() { if (this.localPushToTalkKey !== undefined) { return this.localPushToTalkKey; @@ -276,7 +290,8 @@ Model({ /** * If true, push-to-talk will be used over voice activation. */ - usePushToTalk: attr({ default: false, + usePushToTalk: attr({ + default: false, compute() { if (this.localUsePushToTalk !== undefined) { return this.localUsePushToTalk; @@ -298,7 +313,8 @@ Model({ * Duration in milliseconds the voice remains active after releasing the * push-to-talk key. */ - voiceActiveDuration: attr({ default: 0, + voiceActiveDuration: attr({ + default: 0, compute() { if (this.localVoiceActiveDuration !== undefined) { return this.localVoiceActiveDuration; @@ -316,8 +332,8 @@ Model({ }, onChanges: [ { - dependencies: ['useBlur'], - methodName: '_onChangeUseBlur', + dependencies: ["useBlur"], + methodName: "_onChangeUseBlur", }, ], }); diff --git a/addons/mail/static/src/models/web_client_view_attachment_view.js b/addons/mail/static/src/models/web_client_view_attachment_view.js index 2c844867652..9560e09fc54 100644 --- a/addons/mail/static/src/models/web_client_view_attachment_view.js +++ b/addons/mail/static/src/models/web_client_view_attachment_view.js @@ -1,15 +1,15 @@ /** @odoo-module **/ -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, one, Model } from '@mail/model'; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, one, Model } from "@mail/model"; -import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs'; +import { hidePDFJSButtons } from "@web/legacy/js/libs/pdfjs"; Model({ - name: 'WebClientViewAttachmentView', - template: 'mail.WebClientViewAttachmentView', + name: "WebClientViewAttachmentView", + template: "mail.WebClientViewAttachmentView", componentSetup() { - useUpdateToModel({ methodName: 'onComponentUpdate' }); + useUpdateToModel({ methodName: "onComponentUpdate" }); }, recordMethods: { /** @@ -17,16 +17,24 @@ Model({ */ onClickNext(ev) { ev.preventDefault(); - const index = this.thread.attachmentsInWebClientView.findIndex(attachment => attachment === this.thread.mainAttachment); - this.setMainAttachmentFromIndex(index === this.thread.attachmentsInWebClientView.length - 1 ? 0 : index + 1); + const index = this.thread.attachmentsInWebClientView.findIndex( + (attachment) => attachment === this.thread.mainAttachment + ); + this.setMainAttachmentFromIndex( + index === this.thread.attachmentsInWebClientView.length - 1 ? 0 : index + 1 + ); }, /** * @param {MouseEvent} ev */ onClickPrevious(ev) { ev.preventDefault(); - const index = this.thread.attachmentsInWebClientView.findIndex(attachment => attachment === this.thread.mainAttachment); - this.setMainAttachmentFromIndex(index === 0 ? this.thread.attachmentsInWebClientView.length - 1 : index - 1); + const index = this.thread.attachmentsInWebClientView.findIndex( + (attachment) => attachment === this.thread.mainAttachment + ); + this.setMainAttachmentFromIndex( + index === 0 ? this.thread.attachmentsInWebClientView.length - 1 : index - 1 + ); }, onComponentUpdate() { if (this.iframeViewerPdfRef.el) { @@ -52,13 +60,17 @@ Model({ }, fields: { id: attr({ identifying: true }), - iframeViewerPdfRef: attr({ ref: 'iframeViewerPdf' }), - thread: one('Thread', { required: true }), + iframeViewerPdfRef: attr({ ref: "iframeViewerPdf" }), + thread: one("Thread", { required: true }), }, onChanges: [ { - dependencies: ['thread.areAttachmentsLoaded', 'thread.attachmentsInWebClientView', 'thread.isLoadingAttachments'], - methodName: '_onChangeThreadAttachmentsInWebClientView', + dependencies: [ + "thread.areAttachmentsLoaded", + "thread.attachmentsInWebClientView", + "thread.isLoadingAttachments", + ], + methodName: "_onChangeThreadAttachmentsInWebClientView", }, ], }); diff --git a/addons/mail/static/src/models/welcome_view.js b/addons/mail/static/src/models/welcome_view.js index 68c4a5de429..f9735217413 100644 --- a/addons/mail/static/src/models/welcome_view.js +++ b/addons/mail/static/src/models/welcome_view.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model'; -import { attr, clear, one, Model } from '@mail/model'; +import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model"; +import { attr, clear, one, Model } from "@mail/model"; const getNextGuestNameInputId = (function () { let id = 0; @@ -9,10 +9,10 @@ const getNextGuestNameInputId = (function () { })(); Model({ - name: 'WelcomeView', - template: 'mail.WelcomeView', + name: "WelcomeView", + template: "mail.WelcomeView", componentSetup() { - useUpdateToModel({ methodName: 'onComponentUpdate', modelName: 'WelcomeView' }); + useUpdateToModel({ methodName: "onComponentUpdate", modelName: "WelcomeView" }); }, recordMethods: { /** @@ -21,7 +21,7 @@ Model({ */ async joinChannel() { if (this.hasGuestNameChanged) { - await this.messaging.models['Guest'].performRpcGuestUpdateName({ + await this.messaging.models["Guest"].performRpcGuestUpdateName({ id: this.messaging.currentGuest.id, name: this.pendingGuestName.trim(), }); @@ -53,7 +53,7 @@ Model({ * @param {KeyboardEvent} ev */ onKeydownGuestNameInput(ev) { - if (ev.key === 'Enter') { + if (ev.key === "Enter") { this.joinChannel(); } }, @@ -63,7 +63,7 @@ Model({ */ async performRpcAddGuestAsMember() { await this.messaging.rpc({ - route: '/mail/channel/add_guest_as_member', + route: "/mail/channel/add_guest_as_member", params: { channel_id: this.channel.id, channel_uuid: this.channel.uuid, @@ -81,7 +81,7 @@ Model({ this.update({ isDoFocusGuestNameInput: false }); this.guestNameInputRef.el.focus(); // place cursor at end of text - const { length } = (this.pendingGuestName || ''); + const { length } = this.pendingGuestName || ""; this.guestNameInputRef.el.setSelectionRange(length, length); } }, @@ -100,16 +100,16 @@ Model({ * States the channel to redirect to once the user clicks on the * 'joinButton'. */ - channel: one('Thread', { readonly: true, required: true }), + channel: one("Thread", { readonly: true, required: true }), /** * States discuss public view on which this welcome view is displayed. */ - discussPublicView: one('DiscussPublicView', { identifying: true, inverse: 'welcomeView' }), + discussPublicView: one("DiscussPublicView", { identifying: true, inverse: "welcomeView" }), /** * States the OWL ref the to input element containing the * 'pendingGuestName'. */ - guestNameInputRef: attr({ ref: 'guestNameInput' }), + guestNameInputRef: attr({ ref: "guestNameInput" }), /** * States the value to use for `id`, `for`, and `name` attributes of * the guest name input and its label. @@ -129,7 +129,9 @@ Model({ */ hasGuestNameChanged: attr({ compute() { - return Boolean(this.messaging.currentGuest && this.originalGuestName !== this.pendingGuestName); + return Boolean( + this.messaging.currentGuest && this.originalGuestName !== this.pendingGuestName + ); }, }), /** @@ -145,15 +147,16 @@ Model({ */ isJoinButtonDisabled: attr({ compute() { - return Boolean(this.messaging.currentGuest && this.pendingGuestName.trim() === ''); + return Boolean(this.messaging.currentGuest && this.pendingGuestName.trim() === ""); }, }), /** * States the media preview embedded in this welcome view. */ - callDemoView: one('CallDemoView', { inverse: 'welcomeView', + callDemoView: one("CallDemoView", { + inverse: "welcomeView", compute() { - return (this.channel && this.channel.defaultDisplayMode === 'video_full_screen') + return this.channel && this.channel.defaultDisplayMode === "video_full_screen" ? {} : clear(); }, @@ -170,6 +173,6 @@ Model({ * Will be used to update the current guest's name when joining the * channel by clicking on the 'joinButton'. */ - pendingGuestName: attr({ default: '' }), + pendingGuestName: attr({ default: "" }), }, }); diff --git a/addons/mail/static/src/public/discuss_public_boot.js b/addons/mail/static/src/public/discuss_public_boot.js index 94b1763dbc5..c3866a11514 100644 --- a/addons/mail/static/src/public/discuss_public_boot.js +++ b/addons/mail/static/src/public/discuss_public_boot.js @@ -1,16 +1,16 @@ /** @odoo-module **/ -import { data } from 'mail.discuss_public_channel_template'; -import { messagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env'; +import { data } from "mail.discuss_public_channel_template"; +import { messagingToLegacyEnv } from "@mail/utils/make_messaging_to_legacy_env"; -import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container'; -import { DiscussPublicViewContainer } from '@mail/components/discuss_public_view_container/discuss_public_view_container'; -import { PopoverManagerContainer } from '@mail/components/popover_manager_container/popover_manager_container'; -import { messagingService } from '@mail/services/messaging_service'; +import { DialogManagerContainer } from "@mail/components/dialog_manager_container/dialog_manager_container"; +import { DiscussPublicViewContainer } from "@mail/components/discuss_public_view_container/discuss_public_view_container"; +import { PopoverManagerContainer } from "@mail/components/popover_manager_container/popover_manager_container"; +import { messagingService } from "@mail/services/messaging_service"; -import { MainComponentsContainer } from '@web/core/main_components_container'; -import { registry } from '@web/core/registry'; -import { makeEnv, startServices } from '@web/env'; +import { MainComponentsContainer } from "@web/core/main_components_container"; +import { registry } from "@web/core/registry"; +import { makeEnv, startServices } from "@web/env"; import { makeLegacyCrashManagerService, makeLegacyDialogMappingService, @@ -18,15 +18,15 @@ import { makeLegacyRpcService, makeLegacySessionService, mapLegacyEnvToWowlEnv, -} from '@web/legacy/utils'; -import { session } from '@web/session'; -import { templates } from '@web/core/assets'; +} from "@web/legacy/utils"; +import { session } from "@web/session"; +import { templates } from "@web/core/assets"; -import * as AbstractService from 'web.AbstractService'; -import * as legacyEnv from 'web.env'; -import * as legacySession from 'web.session'; +import * as AbstractService from "web.AbstractService"; +import * as legacyEnv from "web.env"; +import * as legacySession from "web.session"; -import { Component, mount, whenReady } from '@odoo/owl'; +import { Component, mount, whenReady } from "@odoo/owl"; Component.env = legacyEnv; @@ -36,27 +36,32 @@ Component.env = legacyEnv; const messagingValuesService = { start() { return {}; - } + }, }; AbstractService.prototype.deployServices(Component.env); - const serviceRegistry = registry.category('services'); - serviceRegistry.add('legacy_rpc', makeLegacyRpcService(Component.env)); - serviceRegistry.add('legacy_session', makeLegacySessionService(Component.env, legacySession)); - serviceRegistry.add('legacy_notification', makeLegacyNotificationService(Component.env)); - serviceRegistry.add('legacy_crash_manager', makeLegacyCrashManagerService(Component.env)); - serviceRegistry.add('legacy_dialog_mapping', makeLegacyDialogMappingService(Component.env)); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_rpc", makeLegacyRpcService(Component.env)); + serviceRegistry.add("legacy_session", makeLegacySessionService(Component.env, legacySession)); + serviceRegistry.add("legacy_notification", makeLegacyNotificationService(Component.env)); + serviceRegistry.add("legacy_crash_manager", makeLegacyCrashManagerService(Component.env)); + serviceRegistry.add("legacy_dialog_mapping", makeLegacyDialogMappingService(Component.env)); - serviceRegistry.add('messaging', messagingService); - serviceRegistry.add('messagingValues', messagingValuesService); + serviceRegistry.add("messaging", messagingService); + serviceRegistry.add("messagingValues", messagingValuesService); - registry.category('wowlToLegacyServiceMappers').add('make_messaging_to_legacy_env', messagingToLegacyEnv); + registry + .category("wowlToLegacyServiceMappers") + .add("make_messaging_to_legacy_env", messagingToLegacyEnv); - const mainComponentsRegistry = registry.category('main_components'); - mainComponentsRegistry.add('DiscussPublicViewContainer', { Component: DiscussPublicViewContainer, props: { data } }); + const mainComponentsRegistry = registry.category("main_components"); + mainComponentsRegistry.add("DiscussPublicViewContainer", { + Component: DiscussPublicViewContainer, + props: { data }, + }); // needed by the attachment viewer - mainComponentsRegistry.add('DialogManagerContainer', { Component: DialogManagerContainer }); - mainComponentsRegistry.add('PopoverManagerContainer', { Component: PopoverManagerContainer }); + mainComponentsRegistry.add("DialogManagerContainer", { Component: DialogManagerContainer }); + mainComponentsRegistry.add("PopoverManagerContainer", { Component: PopoverManagerContainer }); await legacySession.is_bound; Object.assign(odoo, { @@ -64,7 +69,7 @@ Component.env = legacyEnv; db: session.db, server_version: session.server_version, server_version_info: session.server_version_info, - isEnterprise: session.server_version_info.slice(-1)[0] === 'e', + isEnterprise: session.server_version_info.slice(-1)[0] === "e", }, isReady: false, }); diff --git a/addons/mail/static/src/services/messaging_service.js b/addons/mail/static/src/services/messaging_service.js index 115b2c0904a..e46a2b3fa40 100644 --- a/addons/mail/static/src/services/messaging_service.js +++ b/addons/mail/static/src/services/messaging_service.js @@ -1,19 +1,19 @@ /** @odoo-module **/ -import { ModelManager } from '@mail/model/model_manager'; +import { ModelManager } from "@mail/model/model_manager"; export const messagingService = { dependencies: [ - 'effect', - 'bus_service', - 'localization', - 'messagingValues', - 'orm', - 'presence', - 'router', - 'rpc', - 'ui', - 'user', + "effect", + "bus_service", + "localization", + "messagingValues", + "orm", + "presence", + "router", + "rpc", + "ui", + "user", ], start(env, { messagingValues }) { diff --git a/addons/mail/static/src/services/systray_service.js b/addons/mail/static/src/services/systray_service.js index 6d840866f91..25305386ee1 100644 --- a/addons/mail/static/src/services/systray_service.js +++ b/addons/mail/static/src/services/systray_service.js @@ -1,18 +1,30 @@ /** @odoo-module **/ -import { ActivityMenuContainer } from '@mail/components/activity_menu_container/activity_menu_container'; -import { MessagingMenuContainer } from '@mail/components/messaging_menu_container/messaging_menu_container'; -import { CallSystrayMenuContainer } from '@mail/components/call_systray_menu_container/call_systray_menu_container'; +import { ActivityMenuContainer } from "@mail/components/activity_menu_container/activity_menu_container"; +import { MessagingMenuContainer } from "@mail/components/messaging_menu_container/messaging_menu_container"; +import { CallSystrayMenuContainer } from "@mail/components/call_systray_menu_container/call_systray_menu_container"; -import { registry } from '@web/core/registry'; +import { registry } from "@web/core/registry"; -const systrayRegistry = registry.category('systray'); +const systrayRegistry = registry.category("systray"); export const systrayService = { - dependencies: ['messaging'], + dependencies: ["messaging"], start() { - systrayRegistry.add('mail.ActivityMenu', { Component: ActivityMenuContainer }, { sequence: 20 }); - systrayRegistry.add('mail.MessagingMenuContainer', { Component: MessagingMenuContainer }, { sequence: 25 }); - systrayRegistry.add('mail.CallSystrayMenuContainer', { Component: CallSystrayMenuContainer }, { sequence: 100 }); + systrayRegistry.add( + "mail.ActivityMenu", + { Component: ActivityMenuContainer }, + { sequence: 20 } + ); + systrayRegistry.add( + "mail.MessagingMenuContainer", + { Component: MessagingMenuContainer }, + { sequence: 25 } + ); + systrayRegistry.add( + "mail.CallSystrayMenuContainer", + { Component: CallSystrayMenuContainer }, + { sequence: 100 } + ); }, }; diff --git a/addons/mail/static/src/utils/make_messaging_to_legacy_env.js b/addons/mail/static/src/utils/make_messaging_to_legacy_env.js index a9a32563dd3..66b3de310e9 100644 --- a/addons/mail/static/src/utils/make_messaging_to_legacy_env.js +++ b/addons/mail/static/src/utils/make_messaging_to_legacy_env.js @@ -1,9 +1,8 @@ - /** @odoo-module **/ export function makeMessagingToLegacyEnv(legacyEnv) { return { - dependencies: ['messaging'], + dependencies: ["messaging"], start(_, { messaging }) { legacyEnv.services.messaging = messaging; }, diff --git a/addons/mail/static/src/utils/media_monitoring.js b/addons/mail/static/src/utils/media_monitoring.js index 922ceaa2328..2fb5bb01216 100644 --- a/addons/mail/static/src/utils/media_monitoring.js +++ b/addons/mail/static/src/utils/media_monitoring.js @@ -29,7 +29,7 @@ export async function monitorAudio(track, processorOptions) { const stream = new window.MediaStream([monitoredTrack]); const AudioContext = window.AudioContext || window.webkitAudioContext; if (!AudioContext) { - throw 'missing audio context'; + throw "missing audio context"; } const audioContext = new AudioContext(); const source = audioContext.createMediaStreamSource(stream); @@ -46,7 +46,7 @@ export async function monitorAudio(track, processorOptions) { processor.disconnect(); source.disconnect(); monitoredTrack.stop(); - } + }; } //------------------------------------------------------------------------------ @@ -60,7 +60,17 @@ export async function monitorAudio(track, processorOptions) { * @returns {Object} returnValue * @returns {function} returnValue.disconnect disconnect callback */ -function _loadScriptProcessor(source, audioContext, { frequencyRange = HUMAN_VOICE_FREQUENCY_RANGE, minimumActiveCycles = 10, onThreshold, onTic, volumeThreshold = 0.3 } = {}) { +function _loadScriptProcessor( + source, + audioContext, + { + frequencyRange = HUMAN_VOICE_FREQUENCY_RANGE, + minimumActiveCycles = 10, + onThreshold, + onTic, + volumeThreshold = 0.3, + } = {} +) { // audio setup const bitSize = 1024; const analyser = audioContext.createAnalyser(); @@ -72,7 +82,7 @@ function _loadScriptProcessor(source, audioContext, { frequencyRange = HUMAN_VOI // timing variables const processInterval = 50; // how many ms between each computation - const intervalInFrames = processInterval / 1000 * analyser.context.sampleRate; + const intervalInFrames = (processInterval / 1000) * analyser.context.sampleRate; let nextUpdateFrame = processInterval; // process variables @@ -89,7 +99,11 @@ function _loadScriptProcessor(source, audioContext, { frequencyRange = HUMAN_VOI nextUpdateFrame += intervalInFrames; // computes volume and threshold - const normalizedVolume = getFrequencyAverage(analyser, frequencyRange[0], frequencyRange[1]); + const normalizedVolume = getFrequencyAverage( + analyser, + frequencyRange[0], + frequencyRange[1] + ); if (normalizedVolume >= volumeThreshold) { activityBuffer = minimumActiveCycles; } else if (normalizedVolume < volumeThreshold && activityBuffer > 0) { @@ -119,17 +133,27 @@ function _loadScriptProcessor(source, audioContext, { frequencyRange = HUMAN_VOI * @returns {Object} returnValue * @returns {function} returnValue.disconnect disconnect callback */ -async function _loadAudioWorkletProcessor(source, audioContext, { frequencyRange = HUMAN_VOICE_FREQUENCY_RANGE, minimumActiveCycles = 10, onThreshold, onTic, volumeThreshold = 0.3 } = {}) { +async function _loadAudioWorkletProcessor( + source, + audioContext, + { + frequencyRange = HUMAN_VOICE_FREQUENCY_RANGE, + minimumActiveCycles = 10, + onThreshold, + onTic, + volumeThreshold = 0.3, + } = {} +) { await audioContext.resume(); // Safari does not support Worklet.addModule - await audioContext.audioWorklet.addModule('/mail/rtc/audio_worklet_processor'); - const thresholdProcessor = new window.AudioWorkletNode(audioContext, 'audio-processor', { + await audioContext.audioWorklet.addModule("/mail/rtc/audio_worklet_processor"); + const thresholdProcessor = new window.AudioWorkletNode(audioContext, "audio-processor", { processorOptions: { minimumActiveCycles, volumeThreshold, frequencyRange, postAllTics: !!onTic, - } + }, }); source.connect(thresholdProcessor).connect(audioContext.destination); thresholdProcessor.port.onmessage = (event) => { @@ -174,6 +198,6 @@ function getFrequencyAverage(analyser, lowerFrequency, higherFrequency) { * @returns {number} the index of the targetFrequency within binCount */ function _getFrequencyIndex(targetFrequency, sampleRate, binCount) { - const index = Math.round(targetFrequency / (sampleRate / 2) * binCount); + const index = Math.round((targetFrequency / (sampleRate / 2)) * binCount); return Math.min(Math.max(0, index), binCount); } diff --git a/addons/mail/static/src/utils/messaging_component.js b/addons/mail/static/src/utils/messaging_component.js index 563311d2d23..e1a6e0705cb 100644 --- a/addons/mail/static/src/utils/messaging_component.js +++ b/addons/mail/static/src/utils/messaging_component.js @@ -1,8 +1,8 @@ /** @odoo-module */ -import { useModels } from '@mail/component_hooks/use_models'; +import { useModels } from "@mail/component_hooks/use_models"; -import { useRef } from '@odoo/owl'; +import { useRef } from "@odoo/owl"; export const componentRegistry = {}; @@ -24,36 +24,38 @@ export function registerMessagingComponent(ComponentClass) { const decoratedName = `@messaging ${name}`; // Defining the class in an object and immediately taking it out so that it // has "decoratedName" as its class name in stack traces and stuff. - const MessagingClass = { [decoratedName]: class extends ComponentClass { - setup() { - this.root = useRef('root'); - useModels(); - super.setup(); - } - get className() { - let res = ''; - if (this.props.className) { - res += this.props.className; + const MessagingClass = { + [decoratedName]: class extends ComponentClass { + setup() { + this.root = useRef("root"); + useModels(); + super.setup(); } - if (this.props.classNameObj) { - for (const [key, val] of Object.entries(this.props.classNameObj)) { - if (val) { - res += ' ' + key; + get className() { + let res = ""; + if (this.props.className) { + res += this.props.className; + } + if (this.props.classNameObj) { + for (const [key, val] of Object.entries(this.props.classNameObj)) { + if (val) { + res += " " + key; + } } } + return res; } - return res; - } - get messaging() { - return this.env.services.messaging.modelManager.messaging; - } - /** - * @returns {string} - */ - toString() { - return `component(${decoratedName}, props: ${Object.entries(this.props || {})})`; - } - } }[decoratedName]; + get messaging() { + return this.env.services.messaging.modelManager.messaging; + } + /** + * @returns {string} + */ + toString() { + return `component(${decoratedName}, props: ${Object.entries(this.props || {})})`; + } + }, + }[decoratedName]; // Create an object whose prototype is the component registry with the values of the original // Component.components. This means that trying to get a value from this object will first look // into the original Component's components, and fall back on the registry if not found. diff --git a/addons/mail/static/src/utils/utils.js b/addons/mail/static/src/utils/utils.js index ab88c7c3dd5..e28abfff368 100644 --- a/addons/mail/static/src/utils/utils.js +++ b/addons/mail/static/src/utils/utils.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import { delay } from 'web.concurrency'; -import { unaccent } from 'web.utils'; +import { delay } from "web.concurrency"; +import { unaccent } from "web.utils"; //------------------------------------------------------------------------------ // Public @@ -62,9 +62,4 @@ async function nextTick() { // Export //------------------------------------------------------------------------------ -export { - cleanSearchTerm, - isEventHandled, - markEventHandled, - nextTick, -}; +export { cleanSearchTerm, isEventHandled, markEventHandled, nextTick }; diff --git a/addons/mail/static/src/views/fields/assign_user_command_hook.js b/addons/mail/static/src/views/fields/assign_user_command_hook.js index 97eedc79e39..39440f3e485 100644 --- a/addons/mail/static/src/views/fields/assign_user_command_hook.js +++ b/addons/mail/static/src/views/fields/assign_user_command_hook.js @@ -4,7 +4,7 @@ import { useCommand } from "@web/core/commands/command_hook"; import { useService } from "@web/core/utils/hooks"; import { Domain } from "@web/core/domain"; -import { useComponent, useEnv } from '@odoo/owl'; +import { useComponent, useEnv } from "@odoo/owl"; /** * Use this hook to add "Assign to.." and "Assign/Unassign me" to the command palette. diff --git a/addons/mail/static/src/views/fields/emojis_char_field/emojis_char_field.js b/addons/mail/static/src/views/fields/emojis_char_field/emojis_char_field.js index 69787ba6bcc..a8d18312180 100644 --- a/addons/mail/static/src/views/fields/emojis_char_field/emojis_char_field.js +++ b/addons/mail/static/src/views/fields/emojis_char_field/emojis_char_field.js @@ -2,12 +2,12 @@ import { CharField } from "@web/views/fields/char/char_field"; import { patch } from "@web/core/utils/patch"; -import MailEmojisMixin from '@mail/js/emojis_mixin'; -import { EmojisDropdown } from '@mail/js/emojis_dropdown'; -import { EmojisFieldCommon } from '@mail/views/fields/emojis_field_common'; +import MailEmojisMixin from "@mail/js/emojis_mixin"; +import { EmojisDropdown } from "@mail/js/emojis_dropdown"; +import { EmojisFieldCommon } from "@mail/views/fields/emojis_field_common"; import { registry } from "@web/core/registry"; -import { useRef } from '@odoo/owl'; +import { useRef } from "@odoo/owl"; /** * Extension of the FieldChar that will add emojis support @@ -15,14 +15,14 @@ import { useRef } from '@odoo/owl'; export class EmojisCharField extends CharField { setup() { super.setup(); - this.targetEditElement = useRef('input'); + this.targetEditElement = useRef("input"); this._setupOverride(); } -}; +} -patch(EmojisCharField.prototype, 'emojis_char_field_mail_mixin', MailEmojisMixin); -patch(EmojisCharField.prototype, 'emojis_char_field_field_mixin', EmojisFieldCommon); -EmojisCharField.template = 'mail.EmojisCharField'; +patch(EmojisCharField.prototype, "emojis_char_field_mail_mixin", MailEmojisMixin); +patch(EmojisCharField.prototype, "emojis_char_field_field_mixin", EmojisFieldCommon); +EmojisCharField.template = "mail.EmojisCharField"; EmojisCharField.components = { ...CharField.components, EmojisDropdown }; -EmojisCharField.additionalClasses = [...(CharField.additionalClasses || []), 'o_field_text']; +EmojisCharField.additionalClasses = [...(CharField.additionalClasses || []), "o_field_text"]; registry.category("fields").add("char_emojis", EmojisCharField); diff --git a/addons/mail/static/src/views/fields/emojis_field_common.js b/addons/mail/static/src/views/fields/emojis_field_common.js index e8efe52df9b..9f0a6ea8bb6 100644 --- a/addons/mail/static/src/views/fields/emojis_field_common.js +++ b/addons/mail/static/src/views/fields/emojis_field_common.js @@ -1,20 +1,22 @@ /** @odoo-module **/ -import MailEmojisMixin from '@mail/js/emojis_mixin'; +import MailEmojisMixin from "@mail/js/emojis_mixin"; const _onEmojiClickMixin = MailEmojisMixin.onEmojiClick; -import { useRef, onMounted } from '@odoo/owl'; +import { useRef, onMounted } from "@odoo/owl"; /* * Common code for EmojisTextField and EmojisCharField */ export const EmojisFieldCommon = { _setupOverride() { - this.targetReadonlyElement = useRef('targetReadonlyElement'); - this.emojisDropdown = useRef('emojisDropdown'); + this.targetReadonlyElement = useRef("targetReadonlyElement"); + this.emojisDropdown = useRef("emojisDropdown"); if (this.props.readonly) { onMounted(() => { - this.targetReadonlyElement.el.innerHTML = this._formatText(this.targetReadonlyElement.el.textContent); + this.targetReadonlyElement.el.innerHTML = this._formatText( + this.targetReadonlyElement.el.textContent + ); }); } this.onEmojiClick = this._onEmojiClick.bind(this); diff --git a/addons/mail/static/src/views/fields/emojis_text_field/emojis_text_field.js b/addons/mail/static/src/views/fields/emojis_text_field/emojis_text_field.js index 4d1de9e9847..924fcc0cd0d 100644 --- a/addons/mail/static/src/views/fields/emojis_text_field/emojis_text_field.js +++ b/addons/mail/static/src/views/fields/emojis_text_field/emojis_text_field.js @@ -2,9 +2,9 @@ import { TextField } from "@web/views/fields/text/text_field"; import { patch } from "@web/core/utils/patch"; -import MailEmojisMixin from '@mail/js/emojis_mixin'; -import { EmojisDropdown } from '@mail/js/emojis_dropdown'; -import { EmojisFieldCommon } from '@mail/views/fields/emojis_field_common'; +import MailEmojisMixin from "@mail/js/emojis_mixin"; +import { EmojisDropdown } from "@mail/js/emojis_dropdown"; +import { EmojisFieldCommon } from "@mail/views/fields/emojis_field_common"; import { registry } from "@web/core/registry"; /** @@ -16,11 +16,11 @@ export class EmojisTextField extends TextField { this.targetEditElement = this.textareaRef; this._setupOverride(); } -}; +} -patch(EmojisTextField.prototype, 'emojis_text_field_mail_mixin', MailEmojisMixin); -patch(EmojisTextField.prototype, 'emojis_text_field_field_mixin', EmojisFieldCommon); -EmojisTextField.template = 'mail.EmojisTextField'; +patch(EmojisTextField.prototype, "emojis_text_field_mail_mixin", MailEmojisMixin); +patch(EmojisTextField.prototype, "emojis_text_field_field_mixin", EmojisFieldCommon); +EmojisTextField.template = "mail.EmojisTextField"; EmojisTextField.components = { ...TextField.components, EmojisDropdown }; -EmojisTextField.additionalClasses = [...(TextField.additionalClasses || []), 'o_field_text']; +EmojisTextField.additionalClasses = [...(TextField.additionalClasses || []), "o_field_text"]; registry.category("fields").add("text_emojis", EmojisTextField); diff --git a/addons/mail/static/src/views/fields/many2many_tags_email/many2many_tags_email.js b/addons/mail/static/src/views/fields/many2many_tags_email/many2many_tags_email.js index 6078c714342..f2b8fd05734 100644 --- a/addons/mail/static/src/views/fields/many2many_tags_email/many2many_tags_email.js +++ b/addons/mail/static/src/views/fields/many2many_tags_email/many2many_tags_email.js @@ -7,7 +7,7 @@ import { sprintf } from "@web/core/utils/strings"; import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field"; import { TagsList } from "@web/views/fields/many2many_tags/tags_list"; -import { onMounted, onWillUpdateProps } from '@odoo/owl'; +import { onMounted, onWillUpdateProps } from "@odoo/owl"; export class FieldMany2ManyTagsEmailTagsList extends TagsList {} FieldMany2ManyTagsEmailTagsList.template = "FieldMany2ManyTagsEmailTagsList"; @@ -45,16 +45,20 @@ export class FieldMany2ManyTagsEmail extends Many2ManyTagsField { // Remove records with invalid data, open form view to edit those and readd them if they are updated correctly. const dialogDefs = []; for (const record of invalidRecords) { - dialogDefs.push(this.openMany2xRecord({ - resId: record.resId, - context: props.record.getFieldContext(this.props.name), - title: sprintf(this.env._t("Edit: %s"), record.data.display_name), - })); + dialogDefs.push( + this.openMany2xRecord({ + resId: record.resId, + context: props.record.getFieldContext(this.props.name), + title: sprintf(this.env._t("Edit: %s"), record.data.display_name), + }) + ); } this.openedDialogs += invalidRecords.length; - const invalidRecordIds = invalidRecords.map(rec => rec.resId); + const invalidRecordIds = invalidRecords.map((rec) => rec.resId); if (invalidRecordIds.length) { - this.props.value.replaceWith(props.value.currentIds.filter(id => !invalidRecordIds.includes(id))); + this.props.value.replaceWith( + props.value.currentIds.filter((id) => !invalidRecordIds.includes(id)) + ); } return Promise.all(dialogDefs).then(() => { this.openedDialogs -= invalidRecords.length; @@ -73,22 +77,20 @@ export class FieldMany2ManyTagsEmail extends Many2ManyTagsField { acc[record.resId] = record.data.email; return acc; }, {}); - tags.forEach(tag => tag.email = emailByResId[tag.resId]); + tags.forEach((tag) => (tag.email = emailByResId[tag.resId])); return tags; } -}; +} FieldMany2ManyTagsEmail.components = { ...FieldMany2ManyTagsEmail.components, TagsList: FieldMany2ManyTagsEmailTagsList, }; -FieldMany2ManyTagsEmail.fieldsToFetch = Object.assign({}, - Many2ManyTagsField.fieldsToFetch, - {email: {name: 'email', type: 'char'}} -); +FieldMany2ManyTagsEmail.fieldsToFetch = Object.assign({}, Many2ManyTagsField.fieldsToFetch, { + email: { name: "email", type: "char" }, +}); FieldMany2ManyTagsEmail.additionalClasses = ["o_field_many2many_tags"]; registry.category("fields").add("many2many_tags_email", FieldMany2ManyTagsEmail); - diff --git a/addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.js b/addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.js index ee9adce96a4..b7abd36d2e0 100644 --- a/addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.js +++ b/addons/mail/static/src/views/fields/many2one_avatar_user_field/many2one_avatar_user_field.js @@ -27,7 +27,9 @@ export class KanbanMany2OneAvatarUserField extends Many2OneAvatarUserField { * we add a new one, we need to filter it out. */ get m2oFieldProps() { - return Object.fromEntries(Object.entries(this.props).filter(([key, _val]) => key in Many2OneAvatarField.props)); + return Object.fromEntries( + Object.entries(this.props).filter(([key, _val]) => key in Many2OneAvatarField.props) + ); } } KanbanMany2OneAvatarUserField.template = "mail.KanbanMany2OneAvatarUserField"; diff --git a/addons/mail/static/src/views/form/form_compiler.js b/addons/mail/static/src/views/form/form_compiler.js index aa5bbcfe179..f16ff7e19c3 100644 --- a/addons/mail/static/src/views/form/form_compiler.js +++ b/addons/mail/static/src/views/form/form_compiler.js @@ -18,36 +18,38 @@ function compileChatter(node, params) { let isAttachmentBoxVisibleInitially = false; for (const childNode of node.children) { const options = evaluateExpr(childNode.getAttribute("options") || "{}"); - switch (childNode.getAttribute('name')) { - case 'activity_ids': + switch (childNode.getAttribute("name")) { + case "activity_ids": hasActivities = true; break; - case 'message_follower_ids': + case "message_follower_ids": hasFollowers = true; - hasParentReloadOnFollowersUpdate = Boolean(options['post_refresh']); - isAttachmentBoxVisibleInitially = isAttachmentBoxVisibleInitially || Boolean(options['open_attachments']); + hasParentReloadOnFollowersUpdate = Boolean(options["post_refresh"]); + isAttachmentBoxVisibleInitially = + isAttachmentBoxVisibleInitially || Boolean(options["open_attachments"]); break; - case 'message_ids': + case "message_ids": hasMessageList = true; - hasParentReloadOnAttachmentsChanged = options['post_refresh'] === 'always'; - hasParentReloadOnMessagePosted = Boolean(options['post_refresh']); - isAttachmentBoxVisibleInitially = isAttachmentBoxVisibleInitially || Boolean(options['open_attachments']); + hasParentReloadOnAttachmentsChanged = options["post_refresh"] === "always"; + hasParentReloadOnMessagePosted = Boolean(options["post_refresh"]); + isAttachmentBoxVisibleInitially = + isAttachmentBoxVisibleInitially || Boolean(options["open_attachments"]); break; } } const chatterContainerXml = createElement("ChatterContainer"); setAttributes(chatterContainerXml, { - "chatter": params.chatter, - "hasActivities": hasActivities, - "hasFollowers": hasFollowers, - "hasMessageList": hasMessageList, - "hasParentReloadOnAttachmentsChanged": hasParentReloadOnAttachmentsChanged, - "hasParentReloadOnFollowersUpdate": hasParentReloadOnFollowersUpdate, - "hasParentReloadOnMessagePosted": hasParentReloadOnMessagePosted, - "isAttachmentBoxVisibleInitially": isAttachmentBoxVisibleInitially, - "threadId": params.threadId, - "threadModel": params.threadModel, - "webRecord": params.webRecord, + chatter: params.chatter, + hasActivities: hasActivities, + hasFollowers: hasFollowers, + hasMessageList: hasMessageList, + hasParentReloadOnAttachmentsChanged: hasParentReloadOnAttachmentsChanged, + hasParentReloadOnFollowersUpdate: hasParentReloadOnFollowersUpdate, + hasParentReloadOnMessagePosted: hasParentReloadOnMessagePosted, + isAttachmentBoxVisibleInitially: isAttachmentBoxVisibleInitially, + threadId: params.threadId, + threadModel: params.threadModel, + webRecord: params.webRecord, }); const chatterContainerHookXml = createElement("div"); chatterContainerHookXml.classList.add("o_FormRenderer_chatterContainer"); @@ -57,11 +59,13 @@ function compileChatter(node, params) { function compileAttachmentPreview(node, params) { const webClientViewAttachmentViewContainerHookXml = createElement("div"); - webClientViewAttachmentViewContainerHookXml.classList.add('o_attachment_preview'); - const webClientViewAttachmentViewContainerXml = createElement("WebClientViewAttachmentViewContainer"); + webClientViewAttachmentViewContainerHookXml.classList.add("o_attachment_preview"); + const webClientViewAttachmentViewContainerXml = createElement( + "WebClientViewAttachmentViewContainer" + ); setAttributes(webClientViewAttachmentViewContainerXml, { - "threadId": params.threadId, - "threadModel": params.threadModel, + threadId: params.threadId, + threadModel: params.threadModel, }); append(webClientViewAttachmentViewContainerHookXml, webClientViewAttachmentViewContainerXml); return webClientViewAttachmentViewContainerHookXml; @@ -85,11 +89,11 @@ export class MailFormCompiler extends ViewCompiler { "t-if": `!hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}`, "t-attf-class": "o-aside", }); - const chatterContainerXml = chatterContainerHookXml.querySelector('ChatterContainer'); + const chatterContainerXml = chatterContainerHookXml.querySelector("ChatterContainer"); setAttributes(chatterContainerXml, { - "hasExternalBorder": "false", - "hasMessageListScrollAdjust": "true", - "isInFormSheetBg": "false", + hasExternalBorder: "false", + hasMessageListScrollAdjust: "true", + isInFormSheetBg: "false", }); } const attachmentViewHookXml = res.querySelector(".o_attachment_preview"); @@ -152,24 +156,24 @@ registry.category("form_compilers").add("attachment_preview_compiler", { }), }); -patch(FormCompiler.prototype, 'mail', { +patch(FormCompiler.prototype, "mail", { compile(node, params) { // TODO no chatter if in dialog? const res = this._super(node, params); - const chatterContainerHookXml = res.querySelector('.o_FormRenderer_chatterContainer'); + const chatterContainerHookXml = res.querySelector(".o_FormRenderer_chatterContainer"); if (!chatterContainerHookXml) { return res; // no chatter, keep the result as it is } - const chatterContainerXml = chatterContainerHookXml.querySelector('ChatterContainer'); + const chatterContainerXml = chatterContainerHookXml.querySelector("ChatterContainer"); setAttributes(chatterContainerXml, { - "hasExternalBorder": "true", - "hasMessageListScrollAdjust": "false", - "isInFormSheetBg": "false", + hasExternalBorder: "true", + hasMessageListScrollAdjust: "false", + isInFormSheetBg: "false", }); - if (chatterContainerHookXml.parentNode.classList.contains('o_form_sheet')) { + if (chatterContainerHookXml.parentNode.classList.contains("o_form_sheet")) { return res; // if chatter is inside sheet, keep it there } - const formSheetBgXml = res.querySelector('.o_form_sheet_bg'); + const formSheetBgXml = res.querySelector(".o_form_sheet_bg"); const parentXml = formSheetBgXml && formSheetBgXml.parentNode; if (!parentXml) { return res; // miss-config: a sheet-bg is required for the rest @@ -177,19 +181,21 @@ patch(FormCompiler.prototype, 'mail', { if (params.hasAttachmentViewerInArch) { // in sheet bg (attachment viewer present) const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true); - sheetBgChatterContainerHookXml.classList.add('o-isInFormSheetBg'); + sheetBgChatterContainerHookXml.classList.add("o-isInFormSheetBg"); setAttributes(sheetBgChatterContainerHookXml, { - 't-if': `this.props.hasAttachmentViewer`, + "t-if": `this.props.hasAttachmentViewer`, }); append(formSheetBgXml, sheetBgChatterContainerHookXml); - const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector('ChatterContainer'); + const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector( + "ChatterContainer" + ); setAttributes(sheetBgChatterContainerXml, { - "isInFormSheetBg": "true", + isInFormSheetBg: "true", }); } // after sheet bg (standard position, below form) setAttributes(chatterContainerHookXml, { - 't-if': `!this.props.hasAttachmentViewer and uiService.size < ${SIZES.XXL}`, + "t-if": `!this.props.hasAttachmentViewer and uiService.size < ${SIZES.XXL}`, }); append(parentXml, chatterContainerHookXml); return res; diff --git a/addons/mail/static/src/views/form/form_controller.js b/addons/mail/static/src/views/form/form_controller.js index e63adaae95f..f9f0448e098 100644 --- a/addons/mail/static/src/views/form/form_controller.js +++ b/addons/mail/static/src/views/form/form_controller.js @@ -1,7 +1,10 @@ /** @odoo-module */ import { useModels } from "@mail/component_hooks/use_models"; -import { ChatterContainer, getChatterNextTemporaryId } from "@mail/components/chatter_container/chatter_container"; +import { + ChatterContainer, + getChatterNextTemporaryId, +} from "@mail/components/chatter_container/chatter_container"; import { WebClientViewAttachmentViewContainer } from "@mail/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container"; import { browser } from "@web/core/browser/browser"; @@ -15,7 +18,7 @@ import { evalDomain } from "@web/views/utils"; import { MailFormCompiler } from "./form_compiler"; -import { onMounted, onWillDestroy, onWillUnmount } from '@odoo/owl'; +import { onMounted, onWillDestroy, onWillUnmount } from "@odoo/owl"; patch(FormController.prototype, "mail", { setup() { @@ -31,7 +34,9 @@ patch(FormController.prototype, "mail", { return; } const messaging = this.env.services.messaging.modelManager.messaging; - this.chatter = messaging.models['Chatter'].insert({ id: getChatterNextTemporaryId() }); + this.chatter = messaging.models["Chatter"].insert({ + id: getChatterNextTemporaryId(), + }); if (owl.status(this) === "destroyed") { this.chatter.delete(); } @@ -84,7 +89,7 @@ patch(FormController.prototype, "mail", { ) { return false; } - const thread = this.getMessaging().models['Thread'].insert({ + const thread = this.getMessaging().models["Thread"].insert({ id: this.model.root.resId, model: this.model.root.resModel, }); diff --git a/addons/mail/static/src/views/form/form_renderer.js b/addons/mail/static/src/views/form/form_renderer.js index e59b82c60b0..606c9d99ce4 100644 --- a/addons/mail/static/src/views/form/form_renderer.js +++ b/addons/mail/static/src/views/form/form_renderer.js @@ -6,7 +6,7 @@ import { WebClientViewAttachmentViewContainer } from "@mail/components/web_clien import { patch } from "@web/core/utils/patch"; import { FormRenderer } from "@web/views/form/form_renderer"; -patch(FormRenderer.prototype, 'mail', { +patch(FormRenderer.prototype, "mail", { get compileParams() { return { ...this._super(), diff --git a/addons/mail/static/src/views/open_chat_hook.js b/addons/mail/static/src/views/open_chat_hook.js index cdba5a16075..28de2fadc1d 100644 --- a/addons/mail/static/src/views/open_chat_hook.js +++ b/addons/mail/static/src/views/open_chat_hook.js @@ -9,7 +9,7 @@ export const helpers = { return { userId: id }; } }, -} +}; export function useOpenChat(resModel) { const messagingService = useService("messaging"); diff --git a/addons/mail/static/src/webclient/commands/mail_providers.js b/addons/mail/static/src/webclient/commands/mail_providers.js index 32a4ab1facb..700251272ee 100644 --- a/addons/mail/static/src/webclient/commands/mail_providers.js +++ b/addons/mail/static/src/webclient/commands/mail_providers.js @@ -3,7 +3,7 @@ import { _lt } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; -import { Component, xml } from '@odoo/owl'; +import { Component, xml } from "@odoo/owl"; class DialogCommand extends Component {} DialogCommand.template = xml` @@ -33,7 +33,7 @@ commandProviderRegistry.add("partner", { async provide(newEnv, options) { const messaging = await newEnv.services.messaging.get(); const suggestions = []; - await messaging.models['Partner'].imSearch({ + await messaging.models["Partner"].imSearch({ callback(partners) { partners.forEach((partner) => { suggestions.push({ @@ -59,7 +59,7 @@ commandProviderRegistry.add("channel", { namespace: "#", async provide(newEnv, options) { const messaging = await newEnv.services.messaging.get(); - const channels = await messaging.models['Thread'].searchChannelsToOpen({ + const channels = await messaging.models["Thread"].searchChannelsToOpen({ limit: 10, searchTerm: options.searchValue, }); diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer.js b/addons/mail/static/src/widgets/form_renderer/form_renderer.js index 9bbf6ac9057..3be606686df 100644 --- a/addons/mail/static/src/widgets/form_renderer/form_renderer.js +++ b/addons/mail/static/src/widgets/form_renderer/form_renderer.js @@ -1,12 +1,12 @@ /** @odoo-module **/ -import { ChatterContainer } from '@mail/components/chatter_container/chatter_container'; -import { WebClientViewAttachmentViewContainer } from '@mail/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container'; -import { Listener } from '@mail/model/model_listener'; +import { ChatterContainer } from "@mail/components/chatter_container/chatter_container"; +import { WebClientViewAttachmentViewContainer } from "@mail/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container"; +import { Listener } from "@mail/model/model_listener"; -import dom from 'web.dom'; -import FormRenderer from 'web.FormRenderer'; -import { ComponentWrapper } from 'web.OwlCompatibility'; +import dom from "web.dom"; +import FormRenderer from "web.FormRenderer"; +import { ComponentWrapper } from "web.OwlCompatibility"; class ChatterContainerWrapperComponent extends ComponentWrapper {} @@ -25,7 +25,7 @@ FormRenderer.include({ init(parent, state, params) { this._super(...arguments); this.modelsListener = new Listener({ - name: 'interchangeChatter', + name: "interchangeChatter", onChange: () => this._interchangeChatter(), }); this.hasChatter = params.hasChatter && !params.isFromFormViewDialog; @@ -35,7 +35,9 @@ FormRenderer.include({ this.mailFields = params.mailFields; this.messaging = undefined; if (owl.Component.env.services.messaging) { - owl.Component.env.services.messaging.get().then(messaging => this.messaging = messaging); + owl.Component.env.services.messaging + .get() + .then((messaging) => (this.messaging = messaging)); } this._chatterContainerComponent = undefined; /** @@ -59,7 +61,11 @@ FormRenderer.include({ if (this.hasAttachmentViewerFeature) { this.attachmentViewerTarget = document.createElement("div"); this.attachmentViewerTarget.classList.add("o_attachment_preview"); - this.webClientViewAttachmentViewContainer = new ComponentWrapper(this, WebClientViewAttachmentViewContainer, this._makeWebClientViewAttachmentViewContainerProps()); + this.webClientViewAttachmentViewContainer = new ComponentWrapper( + this, + WebClientViewAttachmentViewContainer, + this._makeWebClientViewAttachmentViewContainerProps() + ); this.webClientViewAttachmentViewContainer.mount(this.attachmentViewerTarget); } this.attachmentViewerTargetPlaceholder = undefined; @@ -68,7 +74,7 @@ FormRenderer.include({ this._chatterContainerComponent = new ChatterContainerWrapperComponent( this, ChatterContainer, - this._makeChatterContainerProps(), + this._makeChatterContainerProps() ); await this._chatterContainerComponent.mount(this._chatterContainerTarget); }, @@ -76,14 +82,14 @@ FormRenderer.include({ * @override */ _renderNode(node) { - if (node.tag === 'div' && node.attrs.class === 'oe_chatter') { + if (node.tag === "div" && node.attrs.class === "oe_chatter") { if (!this.hasChatter) { return document.createElement("div"); } this.chatterContainerTargetPlaceholder = this._chatterContainerTarget.cloneNode(false); return this.chatterContainerTargetPlaceholder; } - if (node.tag === 'div' && node.attrs.class === 'o_attachment_preview') { + if (node.tag === "div" && node.attrs.class === "o_attachment_preview") { if (!this.hasAttachmentViewerFeature) { return document.createElement("div"); } @@ -135,7 +141,9 @@ FormRenderer.include({ // isChatterInSheet can only be written from this specific life-cycle method because the // parentNode is not accessible before the target node is actually in DOM. Ideally this // should be determined statically in `_processNode` but the parent is not provided. - this.isChatterInSheet = this._chatterContainerTarget.parentNode.classList.contains('o_form_sheet'); + this.isChatterInSheet = this._chatterContainerTarget.parentNode.classList.contains( + "o_form_sheet" + ); this._interchangeChatter(); } }, @@ -166,24 +174,27 @@ FormRenderer.include({ * @private */ _interchangeChatter() { - const $sheetBg = this.$('.o_form_sheet_bg'); - this._chatterContainerTarget.classList.remove('o-aside'); - this._chatterContainerTarget.classList.remove('o-isInFormSheetBg'); - if (this.isChatterInSheet) { // in sheet - const $sheet = this.$('.o_form_sheet'); + const $sheetBg = this.$(".o_form_sheet_bg"); + this._chatterContainerTarget.classList.remove("o-aside"); + this._chatterContainerTarget.classList.remove("o-isInFormSheetBg"); + if (this.isChatterInSheet) { + // in sheet + const $sheet = this.$(".o_form_sheet"); dom.append($sheet, $(this._chatterContainerTarget), { callbacks: [], in_DOM: this._isInDom, }); - } else if (this.hasAttachmentViewer()) { // in sheet-bg - this._chatterContainerTarget.classList.add('o-isInFormSheetBg'); + } else if (this.hasAttachmentViewer()) { + // in sheet-bg + this._chatterContainerTarget.classList.add("o-isInFormSheetBg"); dom.append($sheetBg, $(this._chatterContainerTarget), { callbacks: [], in_DOM: this._isInDom, }); - } else { // after sheet-bg + } else { + // after sheet-bg if (this._isChatterAside()) { - this._chatterContainerTarget.classList.add('o-aside'); + this._chatterContainerTarget.classList.add("o-aside"); } $(this._chatterContainerTarget).insertAfter($sheetBg); } @@ -216,7 +227,8 @@ FormRenderer.include({ hasFollowers: this.chatterFields.hasMessageFollowerIds, hasMessageList: this.chatterFields.hasMessageIds, hasMessageListScrollAdjust: isChatterAside, - hasParentReloadOnAttachmentsChanged: this.chatterFields.hasRecordReloadOnAttachmentsChanged, + hasParentReloadOnAttachmentsChanged: this.chatterFields + .hasRecordReloadOnAttachmentsChanged, hasParentReloadOnFollowersUpdate: this.chatterFields.hasRecordReloadOnFollowersUpdate, hasParentReloadOnMessagePosted: this.chatterFields.hasRecordReloadOnMessagePosted, isAttachmentBoxVisibleInitially: this.chatterFields.isAttachmentBoxVisibleInitially, diff --git a/addons/mail/static/src/worklets/audio_processor.js b/addons/mail/static/src/worklets/audio_processor.js index 4d14d4e2d7d..060b6cfba55 100644 --- a/addons/mail/static/src/worklets/audio_processor.js +++ b/addons/mail/static/src/worklets/audio_processor.js @@ -14,13 +14,20 @@ class ThresholdProcessor extends globalThis.AudioWorkletProcessor { sending events to the main thread on all tics when not necessary. * @param {number} [param0.processorOptions.volumeThreshold] the minimum value for audio detection */ - constructor({ processorOptions: { frequencyRange, minimumActiveCycles = 10, postAllTics, volumeThreshold = 0.3 } }) { + constructor({ + processorOptions: { + frequencyRange, + minimumActiveCycles = 10, + postAllTics, + volumeThreshold = 0.3, + }, + }) { super(); // timing variables this.processInterval = 50; // how many ms between each computation this.minimumActiveCycles = minimumActiveCycles; - this.intervalInFrames = this.processInterval / 1000 * globalThis.sampleRate; + this.intervalInFrames = (this.processInterval / 1000) * globalThis.sampleRate; this.nextUpdateFrame = this.processInterval; // process variables @@ -36,7 +43,7 @@ class ThresholdProcessor extends globalThis.AudioWorkletProcessor { process(inputs, outputs, parameters) { const input = inputs[0]; if (input.length < 1) { - return; + return; } const samples = input[0]; @@ -48,15 +55,23 @@ class ThresholdProcessor extends globalThis.AudioWorkletProcessor { this.nextUpdateFrame += this.intervalInFrames; // computes volume and threshold - const startIndex = _getFrequencyIndex(this.frequencyRange[0], globalThis.sampleRate, samples.length); - const endIndex = _getFrequencyIndex(this.frequencyRange[1], globalThis.sampleRate, samples.length); + const startIndex = _getFrequencyIndex( + this.frequencyRange[0], + globalThis.sampleRate, + samples.length + ); + const endIndex = _getFrequencyIndex( + this.frequencyRange[1], + globalThis.sampleRate, + samples.length + ); let sum = 0; for (let i = startIndex; i < endIndex; ++i) { sum += samples[i]; } // Normalizing the volume so that volume mostly fits in the [0,1] range. const preNormalizationVolume = sum / (endIndex - startIndex); - const preLogarithmVolume = (preNormalizationVolume * 50) + 1; + const preLogarithmVolume = preNormalizationVolume * 50 + 1; if (preLogarithmVolume <= 0) { this.volume = 0; } else { @@ -75,10 +90,10 @@ class ThresholdProcessor extends globalThis.AudioWorkletProcessor { this.port.postMessage({ volume: this.volume, isAboveThreshold: this.isAboveThreshold }); return true; } - this.postAllTics && this.port.postMessage({ volume: this.volume, isAboveThreshold: this.isAboveThreshold }); + this.postAllTics && + this.port.postMessage({ volume: this.volume, isAboveThreshold: this.isAboveThreshold }); return true; } - } /** @@ -88,7 +103,7 @@ class ThresholdProcessor extends globalThis.AudioWorkletProcessor { * @returns {number} the index of the targetFrequency within samplesSize */ function _getFrequencyIndex(targetFrequency, sampleRate, samplesSize) { - const index = Math.round(targetFrequency / (sampleRate / 2) * samplesSize); + const index = Math.round((targetFrequency / (sampleRate / 2)) * samplesSize); return Math.min(Math.max(0, index), samplesSize); } diff --git a/addons/mail/static/tests/helpers/mock_server/controllers/bus.js b/addons/mail/static/tests/helpers/mock_server/controllers/bus.js index 35048d973ee..eb1d5c144b7 100644 --- a/addons/mail/static/tests/helpers/mock_server/controllers/bus.js +++ b/addons/mail/static/tests/helpers/mock_server/controllers/bus.js @@ -3,12 +3,12 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/controllers/bus', { +patch(MockServer.prototype, "mail/controllers/bus", { /** * @override */ async _performRPC(route, args) { - if (route === '/mail/chat_post') { + if (route === "/mail/chat_post") { const uuid = args.uuid; const message_content = args.message_content; const context = args.context; @@ -26,14 +26,14 @@ patch(MockServer.prototype, 'mail/controllers/bus', { * @returns {Object} one key for list of followers and one for subtypes */ async _mockRouteMailChatPost(uuid, message_content, context = {}) { - const mailChannel = this.getRecords('mail.channel', [['uuid', '=', uuid]])[0]; + const mailChannel = this.getRecords("mail.channel", [["uuid", "=", uuid]])[0]; if (!mailChannel) { return false; } let user_id; // find the author from the user session - if ('mockedUserId' in context) { + if ("mockedUserId" in context) { // can be falsy to simulate not being logged in user_id = context.mockedUserId; } else { @@ -42,7 +42,7 @@ patch(MockServer.prototype, 'mail/controllers/bus', { let author_id; let email_from; if (user_id) { - const author = this.getRecords('res.users', [['id', '=', user_id]])[0]; + const author = this.getRecords("res.users", [["id", "=", user_id]])[0]; author_id = author.partner_id; email_from = `${author.display_name} <${author.email}>`; } else { @@ -59,8 +59,8 @@ patch(MockServer.prototype, 'mail/controllers/bus', { author_id, email_from, body, - message_type: 'comment', - subtype_xmlid: 'mail.mt_comment', + message_type: "comment", + subtype_xmlid: "mail.mt_comment", }, context ); diff --git a/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js b/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js index 897738a306f..364d296da3c 100644 --- a/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js +++ b/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js @@ -3,29 +3,29 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/controllers/discuss', { +patch(MockServer.prototype, "mail/controllers/discuss", { /** * @override */ async performRPC(route, args) { - if (route === '/mail/attachment/upload') { - const ufile = args.body.get('ufile'); - const is_pending = args.body.get('is_pending') === 'true'; - const model = is_pending ? 'mail.compose.message' : args.body.get('thread_model'); - const id = is_pending ? 0 : parseInt(args.body.get('thread_id')); - const attachmentId = this.mockCreate('ir.attachment', { + if (route === "/mail/attachment/upload") { + const ufile = args.body.get("ufile"); + const is_pending = args.body.get("is_pending") === "true"; + const model = is_pending ? "mail.compose.message" : args.body.get("thread_model"); + const id = is_pending ? 0 : parseInt(args.body.get("thread_id")); + const attachmentId = this.mockCreate("ir.attachment", { // datas, mimetype: ufile.type, name: ufile.name, res_id: id, res_model: model, }); - const attachment = this.getRecords('ir.attachment', [['id', '=', attachmentId]])[0]; + const attachment = this.getRecords("ir.attachment", [["id", "=", attachmentId]])[0]; return { - 'filename': attachment.name, - 'id': attachment.id, - 'mimetype': attachment.mimetype, - 'size': attachment.file_size + filename: attachment.name, + id: attachment.id, + mimetype: attachment.mimetype, + size: attachment.file_size, }; } return this._super(...arguments); @@ -34,72 +34,94 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @override */ async _performRPC(route, args) { - if (route === '/mail/attachment/delete') { + if (route === "/mail/attachment/delete") { const { attachment_id } = args; return this._mockRouteMailAttachmentRemove(attachment_id); } - if (route === '/mail/channel/messages') { + if (route === "/mail/channel/messages") { const { channel_id, min_id, max_id, limit } = args; return this._mockRouteMailChannelMessages(channel_id, max_id, min_id, limit); } - if (route === '/mail/channel/notify_typing') { + if (route === "/mail/channel/notify_typing") { const id = args.channel_id; const is_typing = args.is_typing; const context = args.context; return this._mockRouteMailChannelNotifyTyping(id, is_typing, context); } - if (new RegExp('/mail/channel/\\d+/partner/\\d+/avatar_128').test(route)) { + if (new RegExp("/mail/channel/\\d+/partner/\\d+/avatar_128").test(route)) { return; } - if (route === '/mail/channel/ping') { + if (route === "/mail/channel/ping") { return; } - if (route === '/mail/history/messages') { + if (route === "/mail/history/messages") { const { min_id, max_id, limit } = args; return this._mockRouteMailMessageHistory(min_id, max_id, limit); } - if (route === '/mail/init_messaging') { + if (route === "/mail/init_messaging") { return this._mockRouteMailInitMessaging(); } - if (route === '/mail/inbox/messages') { + if (route === "/mail/inbox/messages") { const { min_id, max_id, limit } = args; return this._mockRouteMailMessageInbox(min_id, max_id, limit); } - if (route === '/mail/link_preview') { + if (route === "/mail/link_preview") { return this._mockRouteMailLinkPreview(args.message_id); } - if (route === '/mail/load_message_failures') { + if (route === "/mail/load_message_failures") { return this._mockRouteMailLoadMessageFailures(); } - if (route === '/mail/message/post') { - if (args.thread_model === 'mail.channel') { - return this._mockMailChannelMessagePost(args.thread_id, args.post_data, args.context); + if (route === "/mail/message/post") { + if (args.thread_model === "mail.channel") { + return this._mockMailChannelMessagePost( + args.thread_id, + args.post_data, + args.context + ); } - return this._mockMailThreadMessagePost(args.thread_model, [args.thread_id], args.post_data, args.context); + return this._mockMailThreadMessagePost( + args.thread_model, + [args.thread_id], + args.post_data, + args.context + ); } - if (route === '/mail/read_subscription_data') { + if (route === "/mail/read_subscription_data") { const follower_id = args.follower_id; return this._mockRouteMailReadSubscriptionData(follower_id); } - if (route === '/mail/rtc/channel/join_call') { - return this._mockRouteMailRtcChannelJoinCall(args.channel_id, args.check_rtc_session_ids); + if (route === "/mail/rtc/channel/join_call") { + return this._mockRouteMailRtcChannelJoinCall( + args.channel_id, + args.check_rtc_session_ids + ); } - if (route === '/mail/rtc/channel/leave_call') { + if (route === "/mail/rtc/channel/leave_call") { return this._mockRouteMailRtcChannelLeaveCall(args.channel_id); } - if (route === '/mail/rtc/session/update_and_broadcast') { + if (route === "/mail/rtc/session/update_and_broadcast") { return; } - if (route === '/mail/starred/messages') { + if (route === "/mail/starred/messages") { const { min_id, max_id, limit } = args; return this._mockRouteMailMessageStarredMessages(min_id, max_id, limit); } - if (route === '/mail/thread/data') { - return this._mockRouteMailThreadData(args.thread_model, args.thread_id, args.request_list); + if (route === "/mail/thread/data") { + return this._mockRouteMailThreadData( + args.thread_model, + args.thread_id, + args.request_list + ); } - if (route === '/mail/thread/messages') { + if (route === "/mail/thread/messages") { const { min_id, max_id, limit, thread_model, thread_id } = args; - return this._mockRouteMailThreadFetchMessages(thread_model, thread_id, max_id, min_id, limit); + return this._mockRouteMailThreadFetchMessages( + thread_model, + thread_id, + max_id, + min_id, + limit + ); } return this._super(route, args); }, @@ -119,8 +141,10 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @param {integer} attachment_id */ async _mockRouteMailAttachmentRemove(attachment_id) { - this.pyEnv['bus.bus']._sendone(this.currentPartnerId, 'ir.attachment/delete', { id: attachment_id }); - return this.pyEnv['ir.attachment'].unlink([attachment_id]); + this.pyEnv["bus.bus"]._sendone(this.currentPartnerId, "ir.attachment/delete", { + id: attachment_id, + }); + return this.pyEnv["ir.attachment"].unlink([attachment_id]); }, /** * Simulates the `/mail/channel/messages` route. @@ -132,15 +156,15 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @param {integer} min_id * @returns {Object} list of messages */ - async _mockRouteMailChannelMessages(channel_id, max_id = false, min_id = false, limit = 30) { + async _mockRouteMailChannelMessages(channel_id, max_id = false, min_id = false, limit = 30) { const domain = [ - ['res_id', '=', channel_id], - ['model', '=', 'mail.channel'], - ['message_type', '!=', 'user_notification'], + ["res_id", "=", channel_id], + ["model", "=", "mail.channel"], + ["message_type", "!=", "user_notification"], ]; const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit); - this._mockMailMessageSetMessageDone(messages.map(message => message.id)); - return this._mockMailMessageMessageFormat(messages.map(message => message.id)); + this._mockMailMessageSetMessageDone(messages.map((message) => message.id)); + return this._mockMailMessageMessageFormat(messages.map((message) => message.id)); }, /** * Simulates the `/mail/channel/notify_typing` route. @@ -152,7 +176,10 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { */ async _mockRouteMailChannelNotifyTyping(channel_id, is_typing, context = {}) { const partnerId = context.mockedPartnerId || this.currentPartnerId; - const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partnerId]]); + const [memberOfCurrentUser] = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel_id], + ["partner_id", "=", partnerId], + ]); this._mockMailChannelMember_NotifyTyping([memberOfCurrentUser.id], is_typing); }, /** @@ -171,23 +198,25 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @param {integer} message_id * @returns {Object} */ - _mockRouteMailLinkPreview(message_id) { + _mockRouteMailLinkPreview(message_id) { const linkPreviews = []; - const [message] = this.pyEnv['mail.message'].searchRead([['id', '=', message_id]]); - if (message.body === 'https://make-link-preview.com') { - const linkPreviewId = this.pyEnv['mail.link.preview'].create({ + const [message] = this.pyEnv["mail.message"].searchRead([["id", "=", message_id]]); + if (message.body === "https://make-link-preview.com") { + const linkPreviewId = this.pyEnv["mail.link.preview"].create({ og_description: "test description", - og_title: 'Article title', - og_type: 'article', - source_url: 'https://make-link-preview.com', + og_title: "Article title", + og_type: "article", + source_url: "https://make-link-preview.com", }); - const [linkPreview] = this.pyEnv['mail.link.preview'].searchRead([['id', '=', linkPreviewId]]); + const [linkPreview] = this.pyEnv["mail.link.preview"].searchRead([ + ["id", "=", linkPreviewId], + ]); linkPreviews.push(this._mockMailLinkPreviewFormat(linkPreview)); let target = this.pyEnv.currentPartner; - if (message.model === 'mail.channel') { - target = this.pyEnv['mail.channel'].search([['id', '=', message.res_id]]); + if (message.model === "mail.channel") { + target = this.pyEnv["mail.channel"].search([["id", "=", message.res_id]]); } - this.pyEnv['bus.bus']._sendmany([[target, 'mail.link.preview/insert', linkPreviews]]); + this.pyEnv["bus.bus"]._sendmany([[target, "mail.link.preview/insert", linkPreviews]]); } }, /** @@ -197,9 +226,9 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @returns {Object} */ _mockRouteMailMessageHistory(min_id = false, max_id = false, limit = 30) { - const domain = [['needaction', '=', false]]; + const domain = [["needaction", "=", false]]; const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit); - return this._mockMailMessageMessageFormat(messages.map(message => message.id)); + return this._mockMailMessageMessageFormat(messages.map((message) => message.id)); }, /** * Simulates the `/mail/inbox/messages` route. @@ -208,9 +237,9 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @returns {Object} */ _mockRouteMailMessageInbox(min_id = false, max_id = false, limit = 30) { - const domain = [['needaction', '=', true]]; + const domain = [["needaction", "=", true]]; const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit); - return this._mockMailMessageMessageFormat(messages.map(message => message.id)); + return this._mockMailMessageMessageFormat(messages.map((message) => message.id)); }, /** * Simulates the `/mail/starred/messages` route. @@ -219,9 +248,9 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @returns {Object} */ _mockRouteMailMessageStarredMessages(min_id = false, max_id = false, limit = 30) { - const domain = [['starred_partner_ids', 'in', [this.currentPartnerId]]]; + const domain = [["starred_partner_ids", "in", [this.currentPartnerId]]]; const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit); - return this._mockMailMessageMessageFormat(messages.map(message => message.id)); + return this._mockMailMessageMessageFormat(messages.map((message) => message.id)); }, /** * Simulates the `/mail/read_subscription_data` route. @@ -231,27 +260,27 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @returns {Object[]} list of followed subtypes */ async _mockRouteMailReadSubscriptionData(follower_id) { - const follower = this.getRecords('mail.followers', [['id', '=', follower_id]])[0]; - const subtypes = this.getRecords('mail.message.subtype', [ - '&', - ['hidden', '=', false], - '|', - ['res_model', '=', follower.res_model], - ['res_model', '=', false], + const follower = this.getRecords("mail.followers", [["id", "=", follower_id]])[0]; + const subtypes = this.getRecords("mail.message.subtype", [ + "&", + ["hidden", "=", false], + "|", + ["res_model", "=", follower.res_model], + ["res_model", "=", false], ]); - const subtypes_list = subtypes.map(subtype => { - const parent = this.getRecords('mail.message.subtype', [ - ['id', '=', subtype.parent_id], + const subtypes_list = subtypes.map((subtype) => { + const parent = this.getRecords("mail.message.subtype", [ + ["id", "=", subtype.parent_id], ])[0]; return { - 'default': subtype.default, - 'followed': follower.subtype_ids.includes(subtype.id), - 'id': subtype.id, - 'internal': subtype.internal, - 'name': subtype.name, - 'parent_model': parent ? parent.res_model : false, - 'res_model': subtype.res_model, - 'sequence': subtype.sequence, + default: subtype.default, + followed: follower.subtype_ids.includes(subtype.id), + id: subtype.id, + internal: subtype.internal, + name: subtype.name, + parent_model: parent ? parent.res_model : false, + res_model: subtype.res_model, + sequence: subtype.sequence, }; }); // NOTE: server is also doing a sort here, not reproduced for simplicity @@ -265,24 +294,31 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @returns {integer[]} [check_rtc_session_ids] */ async _mockRouteMailRtcChannelJoinCall(channel_id, check_rtc_session_ids = []) { - const [currentChannelMember] = this.getRecords('mail.channel.member', [ - ['channel_id', '=', channel_id], - ['partner_id', '=', this.currentPartnerId], + const [currentChannelMember] = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel_id], + ["partner_id", "=", this.currentPartnerId], ]); - const sessionId = this.pyEnv['mail.channel.rtc.session'].create({ + const sessionId = this.pyEnv["mail.channel.rtc.session"].create({ channel_member_id: currentChannelMember.id, channel_id, // on the server, this is a related field from channel_member_id and not explicitly set }); - const channelMembers = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id]]); - const rtcSessions = this.getRecords('mail.channel.rtc.session', [ - ['channel_member_id', 'in', channelMembers.map(channelMember => channelMember.id)], + const channelMembers = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel_id], + ]); + const rtcSessions = this.getRecords("mail.channel.rtc.session", [ + ["channel_member_id", "in", channelMembers.map((channelMember) => channelMember.id)], ]); return { - 'iceServers': false, - 'rtcSessions': [ - ['insert', rtcSessions.map(rtcSession => this._mockMailChannelRtcSession_MailChannelRtcSessionFormat(rtcSession.id))], + iceServers: false, + rtcSessions: [ + [ + "insert", + rtcSessions.map((rtcSession) => + this._mockMailChannelRtcSession_MailChannelRtcSessionFormat(rtcSession.id) + ), + ], ], - 'sessionId': sessionId, + sessionId: sessionId, }; }, /** @@ -292,34 +328,38 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @param {integer} channelId */ async _mockRouteMailRtcChannelLeaveCall(channel_id) { - const channelMembers = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id]]); - const rtcSessions = this.getRecords('mail.channel.rtc.session', [ - ['channel_member_id', 'in', channelMembers.map(channelMember => channelMember.id)], + const channelMembers = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel_id], + ]); + const rtcSessions = this.getRecords("mail.channel.rtc.session", [ + ["channel_member_id", "in", channelMembers.map((channelMember) => channelMember.id)], ]); const notifications = []; - const channelInfo = this._mockMailChannelRtcSession_MailChannelRtcSessionFormatByChannel(rtcSessions.map(rtcSession => rtcSession.id)); + const channelInfo = this._mockMailChannelRtcSession_MailChannelRtcSessionFormatByChannel( + rtcSessions.map((rtcSession) => rtcSession.id) + ); for (const [channelId, sessionsData] of Object.entries(channelInfo)) { const notificationRtcSessions = sessionsData.map((sessionsDataPoint) => { - return { 'id': sessionsDataPoint.id }; + return { id: sessionsDataPoint.id }; }); notifications.push([ channelId, - 'mail.channel/rtc_sessions_update', + "mail.channel/rtc_sessions_update", { - 'id': Number(channelId), // JS object keys are strings, but the type from the server is number - 'rtcSessions': [['insert-and-unlink', notificationRtcSessions]], - } + id: Number(channelId), // JS object keys are strings, but the type from the server is number + rtcSessions: [["insert-and-unlink", notificationRtcSessions]], + }, ]); } for (const rtcSession of rtcSessions) { const target = rtcSession.guest_id || rtcSession.partner_id; notifications.push([ target, - 'mail.channel.rtc.session/ended', - { 'sessionId': rtcSession.id }, + "mail.channel.rtc.session/ended", + { sessionId: rtcSession.id }, ]); } - this.pyEnv['bus.bus']._sendmany(notifications); + this.pyEnv["bus.bus"]._sendmany(notifications); }, /** * Simulates the `/mail/thread/data` route. @@ -331,34 +371,49 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { */ async _mockRouteMailThreadData(thread_model, thread_id, request_list) { const res = { - 'hasWriteAccess': true, // mimic user with write access by default - 'hasReadAccess': true, + hasWriteAccess: true, // mimic user with write access by default + hasReadAccess: true, }; - const thread = this.pyEnv[thread_model].searchRead([['id', '=', thread_id]])[0]; + const thread = this.pyEnv[thread_model].searchRead([["id", "=", thread_id]])[0]; if (!thread) { - res['hasReadAccess'] = false; + res["hasReadAccess"] = false; return res; } - if (request_list.includes('activities')) { - const activities = this.pyEnv['mail.activity'].searchRead([['id', 'in', thread.activity_ids || []]]); - res['activities'] = this._mockMailActivityActivityFormat(activities.map(activity => activity.id)); + if (request_list.includes("activities")) { + const activities = this.pyEnv["mail.activity"].searchRead([ + ["id", "in", thread.activity_ids || []], + ]); + res["activities"] = this._mockMailActivityActivityFormat( + activities.map((activity) => activity.id) + ); } - if (request_list.includes('attachments')) { - const attachments = this.pyEnv['ir.attachment'].searchRead( - [['res_id', '=', thread.id], ['res_model', '=', thread_model]], - ); // order not done for simplicity - res['attachments'] = this._mockIrAttachment_attachmentFormat(attachments.map(attachment => attachment.id)); - res['mainAttachment'] = thread.message_main_attachment_id ? { 'id': thread.message_main_attachment_id[0] } : [['clear']]; + if (request_list.includes("attachments")) { + const attachments = this.pyEnv["ir.attachment"].searchRead([ + ["res_id", "=", thread.id], + ["res_model", "=", thread_model], + ]); // order not done for simplicity + res["attachments"] = this._mockIrAttachment_attachmentFormat( + attachments.map((attachment) => attachment.id) + ); + res["mainAttachment"] = thread.message_main_attachment_id + ? { id: thread.message_main_attachment_id[0] } + : [["clear"]]; } - if (request_list.includes('followers')) { - const followers = this.pyEnv['mail.followers'].searchRead([['id', 'in', thread.message_follower_ids || []]]); + if (request_list.includes("followers")) { + const followers = this.pyEnv["mail.followers"].searchRead([ + ["id", "in", thread.message_follower_ids || []], + ]); // search read returns many2one relations as an array [id, display_name]. // But the original route does not. Thus, we need to change it now. - followers.forEach(follower => follower.partner_id = follower.partner_id[0]); - res['followers'] = followers; + followers.forEach((follower) => (follower.partner_id = follower.partner_id[0])); + res["followers"] = followers; } - if (request_list.includes('suggestedRecipients')) { - res['suggestedRecipients'] = this._mockMailThread_MessageGetSuggestedRecipients(thread_model, [thread.id])[thread_id]; + if (request_list.includes("suggestedRecipients")) { + res[ + "suggestedRecipients" + ] = this._mockMailThread_MessageGetSuggestedRecipients(thread_model, [thread.id])[ + thread_id + ]; } return res; }, @@ -373,14 +428,20 @@ patch(MockServer.prototype, 'mail/controllers/discuss', { * @param {integer} limit * @returns {Object[]} list of messages */ - async _mockRouteMailThreadFetchMessages(res_model, res_id, max_id = false, min_id = false, limit = 30) { + async _mockRouteMailThreadFetchMessages( + res_model, + res_id, + max_id = false, + min_id = false, + limit = 30 + ) { const domain = [ - ['res_id', '=', res_id], - ['model', '=', res_model], - ['message_type', '!=', 'user_notification'], + ["res_id", "=", res_id], + ["model", "=", res_model], + ["message_type", "!=", "user_notification"], ]; const messages = this._mockMailMessage_MessageFetch(domain, max_id, min_id, limit); - this._mockMailMessageSetMessageDone(messages.map(message => message.id)); - return this._mockMailMessageMessageFormat(messages.map(message => message.id)); + this._mockMailMessageSetMessageDone(messages.map((message) => message.id)); + return this._mockMailMessageMessageFormat(messages.map((message) => message.id)); }, }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/ir_attachment.js b/addons/mail/static/tests/helpers/mock_server/models/ir_attachment.js index 55939e85d0f..ba4fc298b5a 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/ir_attachment.js +++ b/addons/mail/static/tests/helpers/mock_server/models/ir_attachment.js @@ -3,9 +3,9 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/ir_attachment', { +patch(MockServer.prototype, "mail/models/ir_attachment", { async _performRPC(route, args) { - if (args.model === 'ir.attachment' && args.method === 'register_as_main_attachment') { + if (args.model === "ir.attachment" && args.method === "register_as_main_attachment") { const ids = args.args[0]; return this._mockIrAttachmentRegisterAsMainAttachment(ids); } @@ -19,19 +19,24 @@ patch(MockServer.prototype, 'mail/models/ir_attachment', { * @returns {Object} */ _mockIrAttachment_attachmentFormat(ids) { - const attachments = this.mockRead('ir.attachment', [ids]); - return attachments.map(attachment => { + const attachments = this.mockRead("ir.attachment", [ids]); + return attachments.map((attachment) => { const res = { - 'checksum': attachment.checksum, - 'filename': attachment.name, - 'id': attachment.id, - 'mimetype': attachment.mimetype, - 'name': attachment.name, + checksum: attachment.checksum, + filename: attachment.name, + id: attachment.id, + mimetype: attachment.mimetype, + name: attachment.name, }; - res['originThread'] = [['insert', { - 'id': attachment.res_id, - 'model': attachment.res_model, - }]]; + res["originThread"] = [ + [ + "insert", + { + id: attachment.res_id, + model: attachment.res_model, + }, + ], + ]; return res; }); }, @@ -44,19 +49,20 @@ patch(MockServer.prototype, 'mail/models/ir_attachment', { * @returns {boolean} dummy value for mock server */ _mockIrAttachmentRegisterAsMainAttachment(ids, force = true) { - const [attachment] = this.getRecords('ir.attachment', [['id', 'in', ids]]); + const [attachment] = this.getRecords("ir.attachment", [["id", "in", ids]]); if (!attachment.res_model) { return true; // dummy value for mock server } - if (!this.models[attachment.res_model].fields['message_main_attachment_id']) { + if (!this.models[attachment.res_model].fields["message_main_attachment_id"]) { return true; // dummy value for mock server } - const [record] = this.pyEnv[attachment.res_model].searchRead([['id', '=', attachment.res_id]]); + const [record] = this.pyEnv[attachment.res_model].searchRead([ + ["id", "=", attachment.res_id], + ]); if (force || !record.message_main_attachment_id) { - this.pyEnv[attachment.res_model].write( - [record.id], - { message_main_attachment_id: attachment.id }, - ); + this.pyEnv[attachment.res_model].write([record.id], { + message_main_attachment_id: attachment.id, + }); } return true; // dummy value for mock server }, diff --git a/addons/mail/static/tests/helpers/mock_server/models/ir_websocket.js b/addons/mail/static/tests/helpers/mock_server/models/ir_websocket.js index a1de48b9b3e..578a089debc 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/ir_websocket.js +++ b/addons/mail/static/tests/helpers/mock_server/models/ir_websocket.js @@ -6,7 +6,7 @@ import { MockServer } from "@web/../tests/helpers/mock_server"; // ensure bus override is applied first. import "@bus/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/ir_websocket', { +patch(MockServer.prototype, "mail/models/ir_websocket", { /** * Simulates `_get_im_status` on `ir.websocket`. * @@ -16,9 +16,12 @@ patch(MockServer.prototype, 'mail/models/ir_websocket', { */ _mockIrWebsocket__getImStatus(imStatusIdsByModel) { const imStatus = this._super(imStatusIdsByModel); - const { 'mail.guest': guestIds } = imStatusIdsByModel; + const { "mail.guest": guestIds } = imStatusIdsByModel; if (guestIds) { - imStatus['Guest'] = this.pyEnv['mail.guest'].searchRead([['id', 'in', guestIds]], { context: { 'active_test': false }, fields: ['im_status'] }); + imStatus["Guest"] = this.pyEnv["mail.guest"].searchRead([["id", "in", guestIds]], { + context: { active_test: false }, + fields: ["im_status"], + }); } return imStatus; }, diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js b/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js index 5df1f9f92ce..e47812f80b7 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_activity.js @@ -3,21 +3,21 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/mail_activity', { +patch(MockServer.prototype, "mail/models/mail_activity", { async _performRPC(route, args) { - if (args.model === 'mail.activity' && args.method === 'action_feedback') { + if (args.model === "mail.activity" && args.method === "action_feedback") { const ids = args.args[0]; return this._mockMailActivityActionFeedback(ids); } - if (args.model === 'mail.activity' && args.method === 'action_feedback_schedule_next') { + if (args.model === "mail.activity" && args.method === "action_feedback_schedule_next") { const ids = args.args[0]; return this._mockMailActivityActionFeedbackScheduleNext(ids); } - if (args.model === 'mail.activity' && args.method === 'activity_format') { + if (args.model === "mail.activity" && args.method === "activity_format") { const ids = args.args[0]; return this._mockMailActivityActivityFormat(ids); } - if (args.model === 'mail.activity' && args.method === 'get_activity_data') { + if (args.model === "mail.activity" && args.method === "get_activity_data") { const res_model = args.args[0] || args.kwargs.res_model; const domain = args.args[1] || args.kwargs.domain; return this._mockMailActivityGetActivityData(res_model, domain); @@ -31,12 +31,14 @@ patch(MockServer.prototype, 'mail/models/mail_activity', { * @param {number[]} ids * @returns {Object[]} */ - _mockMailActivityActivityFormat(ids) { - let res = this.mockRead('mail.activity', [ids]); - res = res.map(record => { + _mockMailActivityActivityFormat(ids) { + let res = this.mockRead("mail.activity", [ids]); + res = res.map((record) => { if (record.mail_template_ids) { - record.mail_template_ids = record.mail_template_ids.map(template_id => { - const template = this.getRecords('mail.template', [['id', '=', template_id]])[0]; + record.mail_template_ids = record.mail_template_ids.map((template_id) => { + const template = this.getRecords("mail.template", [ + ["id", "=", template_id], + ])[0]; return { id: template.id, name: template.name, @@ -56,8 +58,8 @@ patch(MockServer.prototype, 'mail/models/mail_activity', { * @returns {Object} */ _mockMailActivityActionDone(ids) { - const activities = this.getRecords('mail.activity', [['id', 'in', ids]]); - this.mockUnlink('mail.activity', [activities.map(activity => activity.id)]); + const activities = this.getRecords("mail.activity", [["id", "in", ids]]); + this.mockUnlink("mail.activity", [activities.map((activity) => activity.id)]); }, /** * Simulates `action_feedback` on `mail.activity`. @@ -81,11 +83,11 @@ patch(MockServer.prototype, 'mail/models/mail_activity', { _mockMailActivityActionFeedbackScheduleNext(ids) { this._mockMailActivityActionDone(ids); return { - name: 'Schedule an Activity', - view_mode: 'form', - res_model: 'mail.activity', - views: [[false, 'form']], - type: 'ir.actions.act_window', + name: "Schedule an Activity", + view_mode: "form", + res_model: "mail.activity", + views: [[false, "form"]], + type: "ir.actions.act_window", }; }, /** @@ -96,24 +98,29 @@ patch(MockServer.prototype, 'mail/models/mail_activity', { * @param {string} domain * @returns {Object} */ - _mockMailActivityGetActivityData(res_model, domain) { + _mockMailActivityGetActivityData(res_model, domain) { const self = this; const records = this.getRecords(res_model, domain); - const activityTypes = this.getRecords('mail.activity.type', []); - const activityIds = _.pluck(records, 'activity_ids').flat(); + const activityTypes = this.getRecords("mail.activity.type", []); + const activityIds = _.pluck(records, "activity_ids").flat(); const groupedActivities = {}; const resIdToDeadline = {}; - const groups = self.mockReadGroup('mail.activity', { - domain: [['id', 'in', activityIds]], - fields: ['res_id', 'activity_type_id', 'ids:array_agg(id)', 'date_deadline:min(date_deadline)'], - groupby: ['res_id', 'activity_type_id'], + const groups = self.mockReadGroup("mail.activity", { + domain: [["id", "in", activityIds]], + fields: [ + "res_id", + "activity_type_id", + "ids:array_agg(id)", + "date_deadline:min(date_deadline)", + ], + groupby: ["res_id", "activity_type_id"], lazy: false, }); groups.forEach(function (group) { // mockReadGroup doesn't correctly return all asked fields - const activites = self.getRecords('mail.activity', group.__domain); + const activites = self.getRecords("mail.activity", group.__domain); group.activity_type_id = group.activity_type_id[0]; let minDate; activites.forEach(function (activity) { @@ -125,11 +132,11 @@ patch(MockServer.prototype, 'mail/models/mail_activity', { resIdToDeadline[group.res_id] = minDate; let state; if (group.date_deadline === moment().format("YYYY-MM-DD")) { - state = 'today'; + state = "today"; } else if (moment(group.date_deadline) > moment()) { - state = 'planned'; + state = "planned"; } else { - state = 'overdue'; + state = "overdue"; } if (!groupedActivities[group.res_id]) { groupedActivities[group.res_id] = {}; @@ -138,7 +145,7 @@ patch(MockServer.prototype, 'mail/models/mail_activity', { count: group.__count, state: state, o_closest_deadline: group.date_deadline, - ids: _.pluck(activites, 'id'), + ids: _.pluck(activites, "id"), }; }); @@ -147,7 +154,9 @@ patch(MockServer.prototype, 'mail/models/mail_activity', { let mailTemplates = []; if (type.mail_template_ids) { mailTemplates = type.mail_template_ids.map((template_id) => { - const template = this.getRecords('mail.template', [['id', '=', template_id]])[0]; + const template = this.getRecords("mail.template", [ + ["id", "=", template_id], + ])[0]; return { id: template.id, name: template.name, @@ -156,7 +165,7 @@ patch(MockServer.prototype, 'mail/models/mail_activity', { } return [type.id, type.display_name, mailTemplates]; }), - activity_res_ids: _.sortBy(_.pluck(records, 'id'), function (id) { + activity_res_ids: _.sortBy(_.pluck(records, "id"), function (id) { return moment(resIdToDeadline[id]); }), grouped_activities: groupedActivities, diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js b/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js index e2cc7b808c0..0aaf17c1215 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_channel.js @@ -3,85 +3,90 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -import { datetime_to_str } from 'web.time'; +import { datetime_to_str } from "web.time"; -patch(MockServer.prototype, 'mail/models/mail_channel', { +patch(MockServer.prototype, "mail/models/mail_channel", { async _performRPC(route, args) { - if (args.model === 'mail.channel' && args.method === 'action_unfollow') { + if (args.model === "mail.channel" && args.method === "action_unfollow") { const ids = args.args[0]; return this._mockMailChannelActionUnfollow(ids); } - if (args.model === 'mail.channel' && args.method === 'channel_fetched') { + if (args.model === "mail.channel" && args.method === "channel_fetched") { const ids = args.args[0]; return this._mockMailChannelChannelFetched(ids); } - if (args.model === 'mail.channel' && args.method === 'channel_fetch_preview') { + if (args.model === "mail.channel" && args.method === "channel_fetch_preview") { const ids = args.args[0]; return this._mockMailChannelChannelFetchPreview(ids); } - if (args.model === 'mail.channel' && args.method === 'channel_fold') { + if (args.model === "mail.channel" && args.method === "channel_fold") { const ids = args.args[0]; const state = args.args[1] || args.kwargs.state; return this._mockMailChannelChannelFold(ids, state); } - if (args.model === 'mail.channel' && args.method === 'channel_get') { + if (args.model === "mail.channel" && args.method === "channel_get") { const partners_to = args.args[0] || args.kwargs.partners_to; - const pin = args.args[1] !== undefined - ? args.args[1] - : args.kwargs.pin !== undefined + const pin = + args.args[1] !== undefined + ? args.args[1] + : args.kwargs.pin !== undefined ? args.kwargs.pin : undefined; return this._mockMailChannelChannelGet(partners_to, pin); } - if (args.model === 'mail.channel' && args.method === 'channel_info') { + if (args.model === "mail.channel" && args.method === "channel_info") { const ids = args.args[0]; return this._mockMailChannelChannelInfo(ids); } - if (args.model === 'mail.channel' && args.method === 'add_members') { + if (args.model === "mail.channel" && args.method === "add_members") { const ids = args.args[0]; const partner_ids = args.args[1] || args.kwargs.partner_ids; return this._mockMailChannelAddMembers(ids, partner_ids); } - if (args.model === 'mail.channel' && args.method === 'channel_pin') { + if (args.model === "mail.channel" && args.method === "channel_pin") { const ids = args.args[0]; const pinned = args.args[1] || args.kwargs.pinned; return this._mockMailChannelChannelPin(ids, pinned); } - if (args.model === 'mail.channel' && args.method === 'channel_rename') { + if (args.model === "mail.channel" && args.method === "channel_rename") { const ids = args.args[0]; const name = args.args[1] || args.kwargs.name; return this._mockMailChannelChannelRename(ids, name); } - if (route === '/mail/channel/set_last_seen_message') { + if (route === "/mail/channel/set_last_seen_message") { const id = args.channel_id; const last_message_id = args.last_message_id; return this._mockMailChannel_ChannelSeen([id], last_message_id); } - if (args.model === 'mail.channel' && args.method === 'channel_set_custom_name') { + if (args.model === "mail.channel" && args.method === "channel_set_custom_name") { const ids = args.args[0]; const name = args.args[1] || args.kwargs.name; return this._mockMailChannelChannelSetCustomName(ids, name); } - if (args.model === 'mail.channel' && args.method === 'create_group') { + if (args.model === "mail.channel" && args.method === "create_group") { const partners_to = args.args[0] || args.kwargs.partners_to; return this._mockMailChannelCreateGroup(partners_to); } - if (args.model === 'mail.channel' && args.method === 'execute_command_leave') { + if (args.model === "mail.channel" && args.method === "execute_command_leave") { return this._mockMailChannelExecuteCommandLeave(args); } - if (args.model === 'mail.channel' && args.method === 'execute_command_who') { + if (args.model === "mail.channel" && args.method === "execute_command_who") { return this._mockMailChannelExecuteCommandWho(args); } - if (args.model === 'mail.channel' && args.method === 'write' && 'image_128' in args.args[1]) { + if ( + args.model === "mail.channel" && + args.method === "write" && + "image_128" in args.args[1] + ) { const ids = args.args[0]; return this._mockMailChannelWriteImage128(ids[0]); } - if (args.model === 'mail.channel' && args.method === 'load_more_members') { + if (args.model === "mail.channel" && args.method === "load_more_members") { const [channel_ids] = args.args; const { known_member_ids } = args.kwargs; return this._mockMailChannelLoadMoreMembers(channel_ids, known_member_ids); } - if (args.model === 'mail.channel' && args.method === 'get_mention_suggestions') { + if (args.model === "mail.channel" && args.method === "get_mention_suggestions") { return this._mockMailChannelGetMentionSuggestions(args); } return this._super(route, args); @@ -96,36 +101,38 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @returns {integer|false} */ _mockMailChannelMessagePost(id, kwargs, context) { - const message_type = kwargs.message_type || 'notification'; - const channel = this.getRecords('mail.channel', [['id', '=', id]])[0]; - if (channel.channel_type !== 'channel') { - const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]); - this.pyEnv['mail.channel.member'].write( - [memberOfCurrentUser.id], - { - last_interest_dt: datetime_to_str(new Date()), - is_pinned: true, - }, - ); + const message_type = kwargs.message_type || "notification"; + const channel = this.getRecords("mail.channel", [["id", "=", id]])[0]; + if (channel.channel_type !== "channel") { + const [memberOfCurrentUser] = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel.id], + ["partner_id", "=", this.currentPartnerId], + ]); + this.pyEnv["mail.channel.member"].write([memberOfCurrentUser.id], { + last_interest_dt: datetime_to_str(new Date()), + is_pinned: true, + }); } const messageData = this._mockMailThreadMessagePost( - 'mail.channel', + "mail.channel", [id], Object.assign(kwargs, { message_type, }), - context, + context ); if (kwargs.author_id === this.currentPartnerId) { this._mockMailChannel_SetLastSeenMessage([channel.id], messageData.id); } // simulate compute of message_unread_counter - const otherMembers = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '!=', this.currentPartnerId]]); + const otherMembers = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel.id], + ["partner_id", "!=", this.currentPartnerId], + ]); for (const member of otherMembers) { - this.pyEnv['mail.channel.member'].write( - [member.id], - { message_unread_counter: member.message_unread_counter + 1 }, - ); + this.pyEnv["mail.channel.member"].write([member.id], { + message_unread_counter: member.message_unread_counter + 1, + }); } return messageData; }, @@ -136,19 +143,19 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @param {integer[]} ids */ _mockMailChannelActionUnfollow(ids) { - const channel = this.getRecords('mail.channel', [['id', 'in', ids]])[0]; - const [channelMember] = this.getRecords('mail.channel.member', [['channel_id', 'in', ids], ['partner_id', '=', this.currentPartnerId]]); + const channel = this.getRecords("mail.channel", [["id", "in", ids]])[0]; + const [channelMember] = this.getRecords("mail.channel.member", [ + ["channel_id", "in", ids], + ["partner_id", "=", this.currentPartnerId], + ]); if (!channelMember) { return true; } - this.pyEnv['mail.channel'].write( - [channel.id], - { - channel_member_ids: [[2, channelMember.id]], - }, - ); - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/leave', { - 'id': channel.id, + this.pyEnv["mail.channel"].write([channel.id], { + channel_member_ids: [[2, channelMember.id]], + }); + this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.channel/leave", { + id: channel.id, }); /** * Leave message not posted here because it would send the new message @@ -172,24 +179,21 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @param {integer[]} partner_ids */ _mockMailChannelAddMembers(ids, partner_ids) { - const [channel] = this.getRecords('mail.channel', [['id', 'in', ids]]); - const partners = this.getRecords('res.partner', [['id', 'in', partner_ids]]); + const [channel] = this.getRecords("mail.channel", [["id", "in", ids]]); + const partners = this.getRecords("res.partner", [["id", "in", partner_ids]]); for (const partner of partners) { - this.pyEnv['mail.channel.member'].create({ + this.pyEnv["mail.channel.member"].create({ channel_id: channel.id, partner_id: partner.id, }); const body = `
invited ${partner.name} to the channel
`; const message_type = "notification"; const subtype_xmlid = "mail.mt_comment"; - this._mockMailChannelMessagePost( - channel.id, - { body, message_type, subtype_xmlid }, - ); + this._mockMailChannelMessagePost(channel.id, { body, message_type, subtype_xmlid }); } - this.pyEnv['bus.bus']._sendone(channel, 'mail.channel/joined', { - 'channel': this._mockMailChannelChannelInfo([channel.id])[0], - 'invited_by_user_id': this.currentUserId, + this.pyEnv["bus.bus"]._sendone(channel, "mail.channel/joined", { + channel: this._mockMailChannelChannelInfo([channel.id])[0], + invited_by_user_id: this.currentUserId, }); }, /** @@ -202,7 +206,7 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { */ _mockMailChannel_broadcast(ids, partner_ids) { const notifications = this._mockMailChannel_channelChannelNotifications(ids, partner_ids); - this.pyEnv['bus.bus']._sendmany(notifications); + this.pyEnv["bus.bus"]._sendmany(notifications); }, /** * Simulates `_channel_channel_notifications` on `mail.channel`. @@ -215,16 +219,18 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { _mockMailChannel_channelChannelNotifications(ids, partner_ids) { const notifications = []; for (const partner_id of partner_ids) { - const user = this.getRecords('res.users', [['partner_id', 'in', partner_id]])[0]; + const user = this.getRecords("res.users", [["partner_id", "in", partner_id]])[0]; if (!user) { continue; } // Note: `channel_info` on the server is supposed to be called with // the proper user context but this is not done here for simplicity. const channelInfos = this._mockMailChannelChannelInfo(ids); - const [relatedPartner] = this.pyEnv['res.partner'].searchRead([['id', '=', partner_id]]); + const [relatedPartner] = this.pyEnv["res.partner"].searchRead([ + ["id", "=", partner_id], + ]); for (const channelInfo of channelInfos) { - notifications.push([relatedPartner, 'mail.channel/legacy_insert', channelInfo]); + notifications.push([relatedPartner, "mail.channel/legacy_insert", channelInfo]); } } return notifications; @@ -237,11 +243,11 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @param {string} extra_info */ _mockMailChannelChannelFetched(ids) { - const channels = this.getRecords('mail.channel', [['id', 'in', ids]]); + const channels = this.getRecords("mail.channel", [["id", "in", ids]]); for (const channel of channels) { - const channelMessages = this.getRecords('mail.message', [ - ['model', '=', 'mail.channel'], - ['res_id', '=', channel.id], + const channelMessages = this.getRecords("mail.message", [ + ["model", "=", "mail.channel"], + ["res_id", "=", channel.id], ]); const lastMessage = channelMessages.reduce((lastMessage, message) => { if (message.id > lastMessage.id) { @@ -252,16 +258,18 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { if (!lastMessage) { continue; } - const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]); - this.pyEnv['mail.channel.member'].write( - [memberOfCurrentUser.id], - { fetched_message_id: lastMessage.id }, - ); - this.pyEnv['bus.bus']._sendone(channel, 'mail.channel.member/fetched', { - 'channel_id': channel.id, - 'id': memberOfCurrentUser.id, - 'last_message_id': lastMessage.id, - 'partner_id': this.currentPartnerId, + const [memberOfCurrentUser] = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel.id], + ["partner_id", "=", this.currentPartnerId], + ]); + this.pyEnv["mail.channel.member"].write([memberOfCurrentUser.id], { + fetched_message_id: lastMessage.id, + }); + this.pyEnv["bus.bus"]._sendone(channel, "mail.channel.member/fetched", { + channel_id: channel.id, + id: memberOfCurrentUser.id, + last_message_id: lastMessage.id, + partner_id: this.currentPartnerId, }); } }, @@ -273,11 +281,11 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @returns {Object[]} list of channels previews */ _mockMailChannelChannelFetchPreview(ids) { - const channels = this.getRecords('mail.channel', [['id', 'in', ids]]); - return channels.map(channel => { - const channelMessages = this.getRecords('mail.message', [ - ['model', '=', 'mail.channel'], - ['res_id', '=', channel.id], + const channels = this.getRecords("mail.channel", [["id", "in", ids]]); + return channels.map((channel) => { + const channelMessages = this.getRecords("mail.message", [ + ["model", "=", "mail.channel"], + ["res_id", "=", channel.id], ]); const lastMessage = channelMessages.reduce((lastMessage, message) => { if (message.id > lastMessage.id) { @@ -287,7 +295,9 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { }, channelMessages[0]); return { id: channel.id, - last_message: lastMessage ? this._mockMailMessageMessageFormat([lastMessage.id])[0] : false, + last_message: lastMessage + ? this._mockMailMessageMessageFormat([lastMessage.id])[0] + : false, }; }); }, @@ -300,21 +310,28 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @param {state} [state] */ _mockMailChannelChannelFold(ids, state) { - const channels = this.getRecords('mail.channel', [['id', 'in', ids]]); + const channels = this.getRecords("mail.channel", [["id", "in", ids]]); for (const channel of channels) { - const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]); - const foldState = state ? state : memberOfCurrentUser.fold_state === 'open' ? 'folded' : 'open'; + const [memberOfCurrentUser] = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel.id], + ["partner_id", "=", this.currentPartnerId], + ]); + const foldState = state + ? state + : memberOfCurrentUser.fold_state === "open" + ? "folded" + : "open"; const vals = { fold_state: foldState, - is_minimized: foldState !== 'closed', + is_minimized: foldState !== "closed", }; - this.pyEnv['mail.channel.member'].write([memberOfCurrentUser.id], vals); - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.record/insert', { - 'Thread': { - 'id': channel.id, - 'model': 'mail.channel', - 'serverFoldState': memberOfCurrentUser.fold_state, - } + this.pyEnv["mail.channel.member"].write([memberOfCurrentUser.id], vals); + this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.record/insert", { + Thread: { + id: channel.id, + model: "mail.channel", + serverFoldState: memberOfCurrentUser.fold_state, + }, }); } }, @@ -333,18 +350,22 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { if (!partners_to.includes(this.currentPartnerId)) { partners_to.push(this.currentPartnerId); } - const partners = this.getRecords('res.partner', [['id', 'in', partners_to]]); + const partners = this.getRecords("res.partner", [["id", "in", partners_to]]); // NOTE: this mock is not complete, which is done for simplicity. // Indeed if a chat already exists for the given partners, the server // is supposed to return this existing chat. But the mock is currently // always creating a new chat, because no test is relying on receiving // an existing chat. - const id = this.pyEnv['mail.channel'].create({ - channel_member_ids: partners.map(partner => [0, 0, { - partner_id: partner.id, - }]), - channel_type: 'chat', - name: partners.map(partner => partner.name).join(", "), + const id = this.pyEnv["mail.channel"].create({ + channel_member_ids: partners.map((partner) => [ + 0, + 0, + { + partner_id: partner.id, + }, + ]), + channel_type: "chat", + name: partners.map((partner) => partner.name).join(", "), }); return this._mockMailChannelChannelInfo([id])[0]; }, @@ -356,15 +377,17 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @returns {Object[]} */ _mockMailChannelChannelInfo(ids) { - const channels = this.getRecords('mail.channel', [['id', 'in', ids]]); - return channels.map(channel => { - const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]); - const messages = this.getRecords('mail.message', [ - ['model', '=', 'mail.channel'], - ['res_id', '=', channel.id], + const channels = this.getRecords("mail.channel", [["id", "in", ids]]); + return channels.map((channel) => { + const members = this.getRecords("mail.channel.member", [ + ["id", "in", channel.channel_member_ids], ]); - const [group_public_id] = this.getRecords('res.groups', [ - ['id', '=', channel.group_public_id], + const messages = this.getRecords("mail.message", [ + ["model", "=", "mail.channel"], + ["res_id", "=", channel.id], + ]); + const [group_public_id] = this.getRecords("res.groups", [ + ["id", "=", channel.group_public_id], ]); const lastMessageId = messages.reduce((lastMessageId, message) => { if (!lastMessageId || message.id > lastMessageId) { @@ -372,10 +395,10 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { } return lastMessageId; }, undefined); - const messageNeedactionCounter = this.getRecords('mail.notification', [ - ['res_partner_id', '=', this.currentPartnerId], - ['is_read', '=', false], - ['mail_message_id', 'in', messages.map(message => message.id)], + const messageNeedactionCounter = this.getRecords("mail.notification", [ + ["res_partner_id", "=", this.currentPartnerId], + ["is_read", "=", false], + ["mail_message_id", "in", messages.map((message) => message.id)], ]).length; const channelData = { avatarCacheKey: channel.avatarCacheKey, @@ -388,33 +411,54 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { message_needaction_counter: messageNeedactionCounter, authorizedGroupFullName: group_public_id ? group_public_id.name : false, }); - const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId]]); + const [memberOfCurrentUser] = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel.id], + ["partner_id", "=", this.currentPartnerId], + ]); if (memberOfCurrentUser) { Object.assign(res, { is_minimized: memberOfCurrentUser.is_minimized, is_pinned: memberOfCurrentUser.is_pinned, last_interest_dt: memberOfCurrentUser.last_interest_dt, message_unread_counter: memberOfCurrentUser.message_unread_counter, - state: memberOfCurrentUser.fold_state || 'open', + state: memberOfCurrentUser.fold_state || "open", }); Object.assign(channelData, { custom_channel_name: memberOfCurrentUser.custom_channel_name, serverMessageUnreadCounter: memberOfCurrentUser.message_unread_counter, }); if (memberOfCurrentUser.rtc_inviting_session_id) { - res['rtc_inviting_session'] = { 'id': memberOfCurrentUser.rtc_inviting_session_id }; - } - channelData['channelMembers'] = [['insert', this._mockMailChannelMember_MailChannelMemberFormat([memberOfCurrentUser.id])]]; - } - if (channel.channel_type !== 'channel') { - res['seen_partners_info'] = members.filter(member => member.partner_id).map(member => { - return { - partner_id: member.partner_id, - seen_message_id: member.seen_message_id, - fetched_message_id: member.fetched_message_id, + res["rtc_inviting_session"] = { + id: memberOfCurrentUser.rtc_inviting_session_id, }; - }); - channelData['channelMembers'] = [['insert', this._mockMailChannelMember_MailChannelMemberFormat(members.map(member => member.id))]]; + } + channelData["channelMembers"] = [ + [ + "insert", + this._mockMailChannelMember_MailChannelMemberFormat([ + memberOfCurrentUser.id, + ]), + ], + ]; + } + if (channel.channel_type !== "channel") { + res["seen_partners_info"] = members + .filter((member) => member.partner_id) + .map((member) => { + return { + partner_id: member.partner_id, + seen_message_id: member.seen_message_id, + fetched_message_id: member.fetched_message_id, + }; + }); + channelData["channelMembers"] = [ + [ + "insert", + this._mockMailChannelMember_MailChannelMemberFormat( + members.map((member) => member.id) + ), + ], + ]; } res.channel = channelData; return res; @@ -428,20 +472,27 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @param {boolean} [pinned=false] */ async _mockMailChannelChannelPin(ids, pinned = false) { - const [channel] = this.getRecords('mail.channel', [['id', 'in', ids]]); - const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel.id], ['partner_id', '=', this.currentPartnerId], ['is_pinned', '!=', pinned]]); + const [channel] = this.getRecords("mail.channel", [["id", "in", ids]]); + const [memberOfCurrentUser] = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel.id], + ["partner_id", "=", this.currentPartnerId], + ["is_pinned", "!=", pinned], + ]); if (memberOfCurrentUser) { - this.pyEnv['mail.channel.member'].write( - [memberOfCurrentUser.id], - { is_pinned: pinned }, - ); + this.pyEnv["mail.channel.member"].write([memberOfCurrentUser.id], { + is_pinned: pinned, + }); } if (!pinned) { - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/unpin', { - 'id': channel.id, + this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.channel/unpin", { + id: channel.id, }); } else { - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/legacy_insert', this._mockMailChannelChannelInfo([channel.id])[0]); + this.pyEnv["bus.bus"]._sendone( + this.pyEnv.currentPartner, + "mail.channel/legacy_insert", + this._mockMailChannelChannelInfo([channel.id])[0] + ); } }, /** @@ -455,13 +506,13 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { // Update record const channel_id = ids[0]; if (!channel_id) { - throw new Error('Should only be one channel in channel_seen mock params'); + throw new Error("Should only be one channel in channel_seen mock params"); } - const channel = this.getRecords('mail.channel', [['id', '=', channel_id]])[0]; - const messagesBeforeGivenLastMessage = this.getRecords('mail.message', [ - ['id', '<=', last_message_id], - ['model', '=', 'mail.channel'], - ['res_id', '=', channel.id], + const channel = this.getRecords("mail.channel", [["id", "=", channel_id]])[0]; + const messagesBeforeGivenLastMessage = this.getRecords("mail.message", [ + ["id", "<=", last_message_id], + ["model", "=", "mail.channel"], + ["res_id", "=", channel.id], ]); if (!messagesBeforeGivenLastMessage || messagesBeforeGivenLastMessage.length === 0) { return; @@ -469,16 +520,26 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { if (!channel) { return; } - const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', this.currentPartnerId]]); - if (memberOfCurrentUser.seen_message_id && memberOfCurrentUser.seen_message_id >= last_message_id) { + const [memberOfCurrentUser] = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel_id], + ["partner_id", "=", this.currentPartnerId], + ]); + if ( + memberOfCurrentUser.seen_message_id && + memberOfCurrentUser.seen_message_id >= last_message_id + ) { return; } this._mockMailChannel_SetLastSeenMessage([channel.id], last_message_id); - this.pyEnv['bus.bus']._sendone(channel.channel_type === 'chat' ? channel : this.pyEnv.currentPartner, 'mail.channel.member/seen', { - 'channel_id': channel.id, - 'last_message_id': last_message_id, - 'partner_id': this.currentPartnerId, - }); + this.pyEnv["bus.bus"]._sendone( + channel.channel_type === "chat" ? channel : this.pyEnv.currentPartner, + "mail.channel.member/seen", + { + channel_id: channel.id, + last_message_id: last_message_id, + partner_id: this.currentPartnerId, + } + ); }, /** * Simulates `channel_rename` on `mail.channel`. @@ -487,17 +548,14 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @param {integer[]} ids */ _mockMailChannelChannelRename(ids, name) { - const channel = this.getRecords('mail.channel', [['id', 'in', ids]])[0]; - this.pyEnv['mail.channel'].write( - [channel.id], - { name }, - ); - this.pyEnv['bus.bus']._sendone(channel, 'mail.record/insert', { - 'Thread': { - 'id': channel.id, - 'model': 'mail.channel', - 'name': name, - } + const channel = this.getRecords("mail.channel", [["id", "in", ids]])[0]; + this.pyEnv["mail.channel"].write([channel.id], { name }); + this.pyEnv["bus.bus"]._sendone(channel, "mail.record/insert", { + Thread: { + id: channel.id, + model: "mail.channel", + name: name, + }, }); }, /** @@ -508,16 +566,18 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { */ _mockMailChannelChannelSetCustomName(ids, name) { const channelId = ids[0]; // simulate ensure_one. - const [memberIdOfCurrentUser] = this.pyEnv['mail.channel.member'].search([['partner_id', '=', this.currentPartnerId], ['channel_id', '=', channelId]]); - this.pyEnv['mail.channel.member'].write( - [memberIdOfCurrentUser], - { custom_channel_name: name }, - ); - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.record/insert', { - 'Channel': { - 'custom_channel_name': name, - 'id': channelId, - } + const [memberIdOfCurrentUser] = this.pyEnv["mail.channel.member"].search([ + ["partner_id", "=", this.currentPartnerId], + ["channel_id", "=", channelId], + ]); + this.pyEnv["mail.channel.member"].write([memberIdOfCurrentUser], { + custom_channel_name: name, + }); + this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.record/insert", { + Channel: { + custom_channel_name: name, + id: channelId, + }, }); }, /** @@ -528,13 +588,16 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @returns {Object} */ async _mockMailChannelCreateGroup(partners_to) { - const partners = this.getRecords('res.partner', [['id', 'in', partners_to]]); - const id = this.pyEnv['mail.channel'].create({ - channel_type: 'group', - channel_member_ids: partners.map(partner => [0, 0, { partner_id: partner.id }]), - name: '', + const partners = this.getRecords("res.partner", [["id", "in", partners_to]]); + const id = this.pyEnv["mail.channel"].create({ + channel_type: "group", + channel_member_ids: partners.map((partner) => [0, 0, { partner_id: partner.id }]), + name: "", }); - this._mockMailChannel_broadcast(id, partners.map(partner => partner.id)); + this._mockMailChannel_broadcast( + id, + partners.map((partner) => partner.id) + ); return this._mockMailChannelChannelInfo([id])[0]; }, /** @@ -543,8 +606,8 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @private */ _mockMailChannelExecuteCommandLeave(args) { - const channel = this.getRecords('mail.channel', [['id', 'in', args.args[0]]])[0]; - if (channel.channel_type === 'channel') { + const channel = this.getRecords("mail.channel", [["id", "in", args.args[0]]])[0]; + if (channel.channel_type === "channel") { this._mockMailChannelActionUnfollow([channel.id]); } else { this._mockMailChannelChannelPin(channel.uuid, false); @@ -557,20 +620,32 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { */ _mockMailChannelExecuteCommandWho(args) { const ids = args.args[0]; - const channels = this.getRecords('mail.channel', [['id', 'in', ids]]); + const channels = this.getRecords("mail.channel", [["id", "in", ids]]); for (const channel of channels) { - const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]); - const otherPartnerIds = members.filter(member => member.partner_id && member.partner_id !== this.currentPartnerId).map(member => member.partner_id); - const otherPartners = this.getRecords('res.partner', [['id', 'in', otherPartnerIds]]); + const members = this.getRecords("mail.channel.member", [ + ["id", "in", channel.channel_member_ids], + ]); + const otherPartnerIds = members + .filter( + (member) => member.partner_id && member.partner_id !== this.currentPartnerId + ) + .map((member) => member.partner_id); + const otherPartners = this.getRecords("res.partner", [["id", "in", otherPartnerIds]]); let message = "You are alone in this channel."; if (otherPartners.length > 0) { - message = `Users in this channel: ${otherPartners.map(partner => partner.name).join(', ')} and you`; + message = `Users in this channel: ${otherPartners + .map((partner) => partner.name) + .join(", ")} and you`; } - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.channel/transient_message', { - 'body': `${message}`, - 'model': 'mail.channel', - 'res_id': channel.id, - }); + this.pyEnv["bus.bus"]._sendone( + this.pyEnv.currentPartner, + "mail.channel/transient_message", + { + body: `${message}`, + model: "mail.channel", + res_id: channel.id, + } + ); } }, /** @@ -580,7 +655,7 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @returns {Array[]} */ _mockMailChannelGetMentionSuggestions(args) { - const search = args.kwargs.search || ''; + const search = args.kwargs.search || ""; const limit = args.kwargs.limit || 8; /** @@ -596,7 +671,7 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { */ const mentionSuggestionsFilter = function (channels, search, limit) { const matchingChannels = channels - .filter(channel => { + .filter((channel) => { // no search term is considered as return all if (!search) { return true; @@ -606,10 +681,13 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { return true; } return false; - }).map(channel => { + }) + .map((channel) => { // expected format return { - authorizedGroupFullName: channel.group_public_id ? channel.group_public_id.name : false, + authorizedGroupFullName: channel.group_public_id + ? channel.group_public_id.name + : false, channel: { channel_type: channel.channel_type, id: channel.id, @@ -623,7 +701,11 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { return matchingChannels; }; - const mentionSuggestions = mentionSuggestionsFilter(this.models['mail.channel'].records, search, limit); + const mentionSuggestions = mentionSuggestionsFilter( + this.models["mail.channel"].records, + search, + limit + ); return mentionSuggestions; }, @@ -633,18 +715,15 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @param {integer} id */ _mockMailChannelWriteImage128(id) { - this.pyEnv['mail.channel'].write( - [id], - { - avatarCacheKey: moment.utc().format("YYYYMMDDHHmmss"), + this.pyEnv["mail.channel"].write([id], { + avatarCacheKey: moment.utc().format("YYYYMMDDHHmmss"), + }); + const channel = this.pyEnv["mail.channel"].searchRead([["id", "=", id]])[0]; + this.pyEnv["bus.bus"]._sendone(channel, "mail.record/insert", { + Channel: { + avatarCacheKey: channel.avatarCacheKey, + id: id, }, - ); - const channel = this.pyEnv['mail.channel'].searchRead([['id', '=', id]])[0]; - this.pyEnv['bus.bus']._sendone(channel, 'mail.record/insert', { - 'Channel': { - 'avatarCacheKey': channel.avatarCacheKey, - 'id': id, - } }); }, /** @@ -655,48 +734,51 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @param {integer[]} known_member_ids */ _mockMailChannelLoadMoreMembers(channel_ids, known_member_ids) { - const members = this.pyEnv['mail.channel.member'].searchRead([ - ['id', 'not in', known_member_ids], - ['channel_id', 'in', channel_ids], - ], { limit: 100 }); - const memberCount = this.pyEnv['mail.channel.member'].searchCount([ - ['channel_id', 'in', channel_ids], + const members = this.pyEnv["mail.channel.member"].searchRead( + [ + ["id", "not in", known_member_ids], + ["channel_id", "in", channel_ids], + ], + { limit: 100 } + ); + const memberCount = this.pyEnv["mail.channel.member"].searchCount([ + ["channel_id", "in", channel_ids], ]); const membersData = []; for (const member of members) { let persona; if (member.partner_id) { - const [partner] = this.pyEnv['res.partner'].searchRead( - [['id', '=', member.partner_id[0]]], - { fields: ['id', 'name', 'im_status'] } + const [partner] = this.pyEnv["res.partner"].searchRead( + [["id", "=", member.partner_id[0]]], + { fields: ["id", "name", "im_status"] } ); persona = { - 'partner': { - 'id': partner.id, - 'name': partner.name, - 'im_status': partner.im_status, + partner: { + id: partner.id, + name: partner.name, + im_status: partner.im_status, }, }; } if (member.guest_id) { - const [guest] = this.pyEnv['mail.guest'].searchRead( - [['id', '=', member.guest_id[0]]], - { fields: ['id', 'name'] } + const [guest] = this.pyEnv["mail.guest"].searchRead( + [["id", "=", member.guest_id[0]]], + { fields: ["id", "name"] } ); persona = { - 'guest': { - 'id': guest.id, - 'name': guest.name, + guest: { + id: guest.id, + name: guest.name, }, }; } membersData.push({ - 'id': member.id, - 'persona': persona, + id: member.id, + persona: persona, }); } return { - channelMembers: [['insert', membersData]], + channelMembers: [["insert", membersData]], memberCount, }; }, @@ -708,8 +790,11 @@ patch(MockServer.prototype, 'mail/models/mail_channel', { * @param {integer} message_id */ _mockMailChannel_SetLastSeenMessage(ids, message_id) { - const [memberOfCurrentUser] = this.getRecords('mail.channel.member', [['channel_id', 'in', ids], ['partner_id', '=', this.currentPartnerId]]); - this.pyEnv['mail.channel.member'].write([memberOfCurrentUser.id], { + const [memberOfCurrentUser] = this.getRecords("mail.channel.member", [ + ["channel_id", "in", ids], + ["partner_id", "=", this.currentPartnerId], + ]); + this.pyEnv["mail.channel.member"].write([memberOfCurrentUser.id], { fetched_message_id: message_id, seen_message_id: message_id, }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_channel_member.js b/addons/mail/static/tests/helpers/mock_server/models/mail_channel_member.js index e54411740a6..8776ec0fcd7 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_channel_member.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_channel_member.js @@ -3,7 +3,7 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/mail_channel_member', { +patch(MockServer.prototype, "mail/models/mail_channel_member", { /** * Simulates `notify_typing` on `mail.channel.member`. * @@ -12,18 +12,18 @@ patch(MockServer.prototype, 'mail/models/mail_channel_member', { * @param {boolean} is_typing */ _mockMailChannelMember_NotifyTyping(ids, is_typing) { - const members = this.getRecords('mail.channel.member', [['id', 'in', ids]]); + const members = this.getRecords("mail.channel.member", [["id", "in", ids]]); const notifications = []; for (const member of members) { - const [channel] = this.getRecords('mail.channel', [['id', '=', member.channel_id]]); + const [channel] = this.getRecords("mail.channel", [["id", "=", member.channel_id]]); const [data] = this._mockMailChannelMember_MailChannelMemberFormat([member.id]); Object.assign(data, { - 'isTyping': is_typing, + isTyping: is_typing, }); - notifications.push([channel, 'mail.channel.member/typing_status', data]); - notifications.push([channel.uuid, 'mail.channel.member/typing_status', data]); + notifications.push([channel, "mail.channel.member/typing_status", data]); + notifications.push([channel.uuid, "mail.channel.member/typing_status", data]); } - this.pyEnv['bus.bus']._sendmany(notifications); + this.pyEnv["bus.bus"]._sendmany(notifications); }, /** * Simulates `_mail_channel_member_format` on `mail.channel.member`. @@ -33,27 +33,27 @@ patch(MockServer.prototype, 'mail/models/mail_channel_member', { * @returns {Object[]} */ _mockMailChannelMember_MailChannelMemberFormat(ids) { - const members = this.getRecords('mail.channel.member', [['id', 'in', ids]]); + const members = this.getRecords("mail.channel.member", [["id", "in", ids]]); const dataList = []; for (const member of members) { let persona; if (member.partner_id) { - persona = { 'partner': this._mockMailChannelMember_GetPartnerData([member.id]) }; + persona = { partner: this._mockMailChannelMember_GetPartnerData([member.id]) }; } if (member.guest_id) { - const [guest] = this.getRecords('mail.guest', [['id', '=', member.guest_id]]); + const [guest] = this.getRecords("mail.guest", [["id", "=", member.guest_id]]); persona = { - 'guest': { - 'id': guest.id, - 'im_status': guest.im_status, - 'name': guest.name, + guest: { + id: guest.id, + im_status: guest.im_status, + name: guest.name, }, }; } const data = { - 'channel': { 'id': member.channel_id }, - 'id': member.id, - 'persona': persona, + channel: { id: member.channel_id }, + id: member.id, + persona: persona, }; dataList.push(data); } @@ -67,7 +67,7 @@ patch(MockServer.prototype, 'mail/models/mail_channel_member', { * @returns {Object} */ _mockMailChannelMember_GetPartnerData(ids) { - const [member] = this.getRecords('mail.channel.member', [['id', 'in', ids]]); + const [member] = this.getRecords("mail.channel.member", [["id", "in", ids]]); return this._mockResPartnerMailPartnerFormat([member.partner_id]).get(member.partner_id); }, }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_channel_rtc_session.js b/addons/mail/static/tests/helpers/mock_server/models/mail_channel_rtc_session.js index 7dec2295179..94c61056551 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_channel_rtc_session.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_channel_rtc_session.js @@ -3,7 +3,7 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/mail_channel_rtc_session', { +patch(MockServer.prototype, "mail/models/mail_channel_rtc_session", { /** * Simulates `_mail_rtc_session_format` on `mail.channel.rtc.session`. * @@ -12,14 +12,16 @@ patch(MockServer.prototype, 'mail/models/mail_channel_rtc_session', { * @returns {Object} */ _mockMailChannelRtcSession_MailChannelRtcSessionFormat(id) { - const [rtcSession] = this.getRecords('mail.channel.rtc.session', [['id', '=', id]]); + const [rtcSession] = this.getRecords("mail.channel.rtc.session", [["id", "=", id]]); return { - 'id': rtcSession.id, - 'channelMember': this._mockMailChannelMember_MailChannelMemberFormat([rtcSession.channel_member_id])[0], - 'isCameraOn': rtcSession.is_camera_on, - 'isDeaf': rtcSession.is_deaf, - 'isSelfMuted': rtcSession.is_self_muted, - 'isScreenSharingOn': rtcSession.is_screen_sharing_on, + id: rtcSession.id, + channelMember: this._mockMailChannelMember_MailChannelMemberFormat([ + rtcSession.channel_member_id, + ])[0], + isCameraOn: rtcSession.is_camera_on, + isDeaf: rtcSession.is_deaf, + isSelfMuted: rtcSession.is_self_muted, + isScreenSharingOn: rtcSession.is_screen_sharing_on, }; }, /** @@ -30,14 +32,16 @@ patch(MockServer.prototype, 'mail/models/mail_channel_rtc_session', { * @returns {Object} */ _mockMailChannelRtcSession_MailChannelRtcSessionFormatByChannel(ids) { - const rtcSessions = this.getRecords('mail.channel.rtc.session', [['id', 'in', ids]]); + const rtcSessions = this.getRecords("mail.channel.rtc.session", [["id", "in", ids]]); const data = {}; for (const rtcSession of rtcSessions) { if (!data[rtcSession.channel_id]) { data[rtcSession.channel_id] = []; } - data[rtcSession.channel_id].push(this._mockMailChannelRtcSession_MailChannelRtcSessionFormat(rtcSession.id)); + data[rtcSession.channel_id].push( + this._mockMailChannelRtcSession_MailChannelRtcSessionFormat(rtcSession.id) + ); } return data; - } + }, }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_link_preview.js b/addons/mail/static/tests/helpers/mock_server/models/mail_link_preview.js index 30b20a18b8d..2d58fd597c0 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_link_preview.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_link_preview.js @@ -3,7 +3,7 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/mail_link_preview', { +patch(MockServer.prototype, "mail/models/mail_link_preview", { /** * Simulates the `_link_preview_format` method of `mail.link.preview`. * @@ -19,7 +19,7 @@ patch(MockServer.prototype, 'mail/models/mail_link_preview', { og_mimetype: linkPreview.og_mimetype, og_title: linkPreview.og_title, og_type: linkPreview.og_type, - source_url: linkPreview.source_url + source_url: linkPreview.source_url, }; }, }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_message.js b/addons/mail/static/tests/helpers/mock_server/models/mail_message.js index 3e0e9102c01..9c207a8af48 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_message.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_message.js @@ -3,25 +3,25 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/mail_message', { +patch(MockServer.prototype, "mail/models/mail_message", { async _performRPC(route, args) { - if (args.model === 'mail.message' && args.method === 'mark_all_as_read') { + if (args.model === "mail.message" && args.method === "mark_all_as_read") { const domain = args.args[0] || args.kwargs.domain; return this._mockMailMessageMarkAllAsRead(domain); } - if (args.model === 'mail.message' && args.method === 'message_format') { + if (args.model === "mail.message" && args.method === "message_format") { const ids = args.args[0]; return this._mockMailMessageMessageFormat(ids); } - if (args.model === 'mail.message' && args.method === 'set_message_done') { + if (args.model === "mail.message" && args.method === "set_message_done") { const ids = args.args[0]; return this._mockMailMessageSetMessageDone(ids); } - if (args.model === 'mail.message' && args.method === 'toggle_message_starred') { + if (args.model === "mail.message" && args.method === "toggle_message_starred") { const ids = args.args[0]; return this._mockMailMessageToggleMessageStarred(ids); } - if (args.model === 'mail.message' && args.method === 'unstar_all') { + if (args.model === "mail.message" && args.method === "unstar_all") { return this._mockMailMessageUnstarAll(); } return this._super(route, args); @@ -35,19 +35,19 @@ patch(MockServer.prototype, 'mail/models/mail_message', { */ _mockMailMessageMarkAllAsRead(domain) { const notifDomain = [ - ['res_partner_id', '=', this.currentPartnerId], - ['is_read', '=', false], + ["res_partner_id", "=", this.currentPartnerId], + ["is_read", "=", false], ]; if (domain) { - const messages = this.getRecords('mail.message', domain); - const ids = messages.map(messages => messages.id); + const messages = this.getRecords("mail.message", domain); + const ids = messages.map((messages) => messages.id); this._mockMailMessageSetMessageDone(ids); return ids; } - const notifications = this.getRecords('mail.notification', notifDomain); - this.pyEnv['mail.notification'].write( - notifications.map(notification => notification.id), - { is_read: true }, + const notifications = this.getRecords("mail.notification", notifDomain); + this.pyEnv["mail.notification"].write( + notifications.map((notification) => notification.id), + { is_read: true } ); const messageIds = []; for (const notification of notifications) { @@ -55,22 +55,21 @@ patch(MockServer.prototype, 'mail/models/mail_message', { messageIds.push(notification.mail_message_id); } } - const messages = this.getRecords('mail.message', [['id', 'in', messageIds]]); + const messages = this.getRecords("mail.message", [["id", "in", messageIds]]); // simulate compute that should be done based on notifications for (const message of messages) { - this.pyEnv['mail.message'].write( - [message.id], - { - needaction: false, - needaction_partner_ids: message.needaction_partner_ids.filter( - partnerId => partnerId !== this.currentPartnerId - ), - }, - ); + this.pyEnv["mail.message"].write([message.id], { + needaction: false, + needaction_partner_ids: message.needaction_partner_ids.filter( + (partnerId) => partnerId !== this.currentPartnerId + ), + }); } - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/mark_as_read', { - 'message_ids': messageIds, - 'needaction_inbox_counter': this._mockResPartner_GetNeedactionCount(this.currentPartnerId), + this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.message/mark_as_read", { + message_ids: messageIds, + needaction_inbox_counter: this._mockResPartner_GetNeedactionCount( + this.currentPartnerId + ), }); return messageIds; }, @@ -86,12 +85,12 @@ patch(MockServer.prototype, 'mail/models/mail_message', { */ _mockMailMessage_MessageFetch(domain, max_id, min_id, limit = 30) { if (max_id) { - domain.push(['id', '<', max_id]); + domain.push(["id", "<", max_id]); } if (min_id) { - domain.push(['id', '>', min_id]); + domain.push(["id", ">", min_id]); } - let messages = this.getRecords('mail.message', domain); + const messages = this.getRecords("mail.message", domain); // sorted from highest ID to lowest ID (i.e. from youngest to oldest) messages.sort(function (m1, m2) { return m1.id < m2.id ? 1 : -1; @@ -108,56 +107,60 @@ patch(MockServer.prototype, 'mail/models/mail_message', { * @returns {Object[]} */ _mockMailMessageMessageFormat(ids) { - const messages = this.getRecords('mail.message', [['id', 'in', ids]]); + const messages = this.getRecords("mail.message", [["id", "in", ids]]); // sorted from highest ID to lowest ID (i.e. from most to least recent) messages.sort(function (m1, m2) { return m1.id < m2.id ? 1 : -1; }); - return messages.map(message => { - const thread = message.model && this.getRecords(message.model, [ - ['id', '=', message.res_id], - ])[0]; + return messages.map((message) => { + const thread = + message.model && this.getRecords(message.model, [["id", "=", message.res_id]])[0]; let formattedAuthor; if (message.author_id) { - const [author] = this.getRecords('res.partner', [['id', '=', message.author_id]], { active_test: false }); + const [author] = this.getRecords("res.partner", [["id", "=", message.author_id]], { + active_test: false, + }); formattedAuthor = { - 'id': author.id, - 'name': author.name, + id: author.id, + name: author.name, }; } else { - formattedAuthor = [['clear']]; + formattedAuthor = [["clear"]]; } - const attachments = this.getRecords('ir.attachment', [ - ['id', 'in', message.attachment_ids], + const attachments = this.getRecords("ir.attachment", [ + ["id", "in", message.attachment_ids], ]); - const formattedAttachments = this._mockIrAttachment_attachmentFormat(attachments.map(attachment => attachment.id)); - const allNotifications = this.getRecords('mail.notification', [ - ['mail_message_id', '=', message.id], + const formattedAttachments = this._mockIrAttachment_attachmentFormat( + attachments.map((attachment) => attachment.id) + ); + const allNotifications = this.getRecords("mail.notification", [ + ["mail_message_id", "=", message.id], ]); const historyPartnerIds = allNotifications - .filter(notification => notification.is_read) - .map(notification => notification.res_partner_id); + .filter((notification) => notification.is_read) + .map((notification) => notification.res_partner_id); const needactionPartnerIds = allNotifications - .filter(notification => !notification.is_read) - .map(notification => notification.res_partner_id); + .filter((notification) => !notification.is_read) + .map((notification) => notification.res_partner_id); let notifications = this._mockMailNotification_FilteredForWebClient( - allNotifications.map(notification => notification.id) + allNotifications.map((notification) => notification.id) ); notifications = this._mockMailNotification_NotificationFormat( - notifications.map(notification => notification.id) + notifications.map((notification) => notification.id) ); - const trackingValueIds = this.getRecords('mail.tracking.value', [ - ['id', 'in', message.tracking_value_ids], + const trackingValueIds = this.getRecords("mail.tracking.value", [ + ["id", "in", message.tracking_value_ids], ]); - const formattedTrackingValues = this._mockMailTrackingValue_TrackingValueFormat(trackingValueIds); - const partners = this.getRecords( - 'res.partner', - [['id', 'in', message.partner_ids]], + const formattedTrackingValues = this._mockMailTrackingValue_TrackingValueFormat( + trackingValueIds ); - const linkPreviews = this.getRecords('mail.link.preview', [ - ['id', 'in', message.link_preview_ids], + const partners = this.getRecords("res.partner", [["id", "in", message.partner_ids]]); + const linkPreviews = this.getRecords("mail.link.preview", [ + ["id", "in", message.link_preview_ids], ]); - const linkPreviewsFormatted = linkPreviews.map(linkPreview => this._mockMailLinkPreviewFormat(linkPreview)); + const linkPreviewsFormatted = linkPreviews.map((linkPreview) => + this._mockMailLinkPreviewFormat(linkPreview) + ); const response = Object.assign({}, message, { attachment_ids: formattedAttachments, @@ -166,21 +169,26 @@ patch(MockServer.prototype, 'mail/models/mail_message', { linkPreviews: linkPreviewsFormatted, needaction_partner_ids: needactionPartnerIds, notifications, - parentMessage: message.parent_id ? this._mockMailMessageMessageFormat([message.parent_id])[0] : false, - recipients: partners.map(p => ({ id: p.id, name: p.name })), - record_name: thread && (thread.name !== undefined ? thread.name : thread.display_name), + parentMessage: message.parent_id + ? this._mockMailMessageMessageFormat([message.parent_id])[0] + : false, + recipients: partners.map((p) => ({ id: p.id, name: p.name })), + record_name: + thread && (thread.name !== undefined ? thread.name : thread.display_name), trackingValues: formattedTrackingValues, }); - delete response['author_id']; + delete response["author_id"]; if (message.subtype_id) { - const subtype = this.getRecords('mail.message.subtype', [ - ['id', '=', message.subtype_id], + const subtype = this.getRecords("mail.message.subtype", [ + ["id", "=", message.subtype_id], ])[0]; response.subtype_description = subtype.description; } if (message.author_guest_id) { - const [guest] = this.pyEnv['mail.guest'].searchRead([['id', '=', message.author_guest_id]]); - response['guestAuthor'] = { id: guest.id, name: guest.name }; + const [guest] = this.pyEnv["mail.guest"].searchRead([ + ["id", "=", message.author_guest_id], + ]); + response["guestAuthor"] = { id: guest.id, name: guest.name }; } return response; }); @@ -193,25 +201,25 @@ patch(MockServer.prototype, 'mail/models/mail_message', { * @returns {Object[]} */ _mockMailMessage_MessageNotificationFormat(ids) { - const messages = this.getRecords('mail.message', [['id', 'in', ids]]); - return messages.map(message => { - let notifications = this.getRecords('mail.notification', [ - ['mail_message_id', '=', message.id], + const messages = this.getRecords("mail.message", [["id", "in", ids]]); + return messages.map((message) => { + let notifications = this.getRecords("mail.notification", [ + ["mail_message_id", "=", message.id], ]); notifications = this._mockMailNotification_FilteredForWebClient( - notifications.map(notification => notification.id) + notifications.map((notification) => notification.id) ); notifications = this._mockMailNotification_NotificationFormat( - notifications.map(notification => notification.id) + notifications.map((notification) => notification.id) ); return { - 'date': message.date, - 'id': message.id, - 'message_type': message.message_type, - 'model': message.model, - 'notifications': notifications, - 'res_id': message.res_id, - 'res_model_name': message.res_model_name, + date: message.date, + id: message.id, + message_type: message.message_type, + model: message.model, + notifications: notifications, + res_id: message.res_id, + res_model_name: message.res_model_name, }; }); }, @@ -225,31 +233,30 @@ patch(MockServer.prototype, 'mail/models/mail_message', { * @param {integer[]} ids */ _mockMailMessageSetMessageDone(ids) { - const messages = this.getRecords('mail.message', [['id', 'in', ids]]); + const messages = this.getRecords("mail.message", [["id", "in", ids]]); - const notifications = this.getRecords('mail.notification', [ - ['res_partner_id', '=', this.currentPartnerId], - ['is_read', '=', false], - ['mail_message_id', 'in', messages.map(messages => messages.id)] + const notifications = this.getRecords("mail.notification", [ + ["res_partner_id", "=", this.currentPartnerId], + ["is_read", "=", false], + ["mail_message_id", "in", messages.map((messages) => messages.id)], ]); - this.pyEnv['mail.notification'].write( - notifications.map(notification => notification.id), - { is_read: true }, + this.pyEnv["mail.notification"].write( + notifications.map((notification) => notification.id), + { is_read: true } ); // simulate compute that should be done based on notifications for (const message of messages) { - this.pyEnv['mail.message'].write( - [message.id], - { - needaction: false, - needaction_partner_ids: message.needaction_partner_ids.filter( - partnerId => partnerId !== this.currentPartnerId - ), - }, - ); - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/mark_as_read', { - 'message_ids': [message.id], - 'needaction_inbox_counter': this._mockResPartner_GetNeedactionCount(this.currentPartnerId), + this.pyEnv["mail.message"].write([message.id], { + needaction: false, + needaction_partner_ids: message.needaction_partner_ids.filter( + (partnerId) => partnerId !== this.currentPartnerId + ), + }); + this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.message/mark_as_read", { + message_ids: [message.id], + needaction_inbox_counter: this._mockResPartner_GetNeedactionCount( + this.currentPartnerId + ), }); } }, @@ -260,16 +267,15 @@ patch(MockServer.prototype, 'mail/models/mail_message', { * @returns {integer[]} ids */ _mockMailMessageToggleMessageStarred(ids) { - const messages = this.getRecords('mail.message', [['id', 'in', ids]]); + const messages = this.getRecords("mail.message", [["id", "in", ids]]); for (const message of messages) { const wasStared = message.starred_partner_ids.includes(this.currentPartnerId); - this.pyEnv['mail.message'].write( - [message.id], - { starred_partner_ids: [[wasStared ? 3 : 4, this.currentPartnerId]] } - ); - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/toggle_star', { - 'message_ids': [message.id], - 'starred': !wasStared, + this.pyEnv["mail.message"].write([message.id], { + starred_partner_ids: [[wasStared ? 3 : 4, this.currentPartnerId]], + }); + this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.message/toggle_star", { + message_ids: [message.id], + starred: !wasStared, }); } }, @@ -279,16 +285,16 @@ patch(MockServer.prototype, 'mail/models/mail_message', { * @private */ _mockMailMessageUnstarAll() { - const messages = this.getRecords('mail.message', [ - ['starred_partner_ids', 'in', this.currentPartnerId], + const messages = this.getRecords("mail.message", [ + ["starred_partner_ids", "in", this.currentPartnerId], ]); - this.pyEnv['mail.message'].write( - messages.map(message => message.id), + this.pyEnv["mail.message"].write( + messages.map((message) => message.id), { starred_partner_ids: [[3, this.currentPartnerId]] } ); - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/toggle_star', { - 'message_ids': messages.map(message => message.id), - 'starred': false, + this.pyEnv["bus.bus"]._sendone(this.pyEnv.currentPartner, "mail.message/toggle_star", { + message_ids: messages.map((message) => message.id), + starred: false, }); }, }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_notification.js b/addons/mail/static/tests/helpers/mock_server/models/mail_notification.js index 0d56a95cbd2..6f2548730ae 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_notification.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_notification.js @@ -3,7 +3,7 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/mail_notification', { +patch(MockServer.prototype, "mail/models/mail_notification", { /** * Simulates `_filtered_for_web_client` on `mail.notification`. * @@ -12,19 +12,24 @@ patch(MockServer.prototype, 'mail/models/mail_notification', { * @returns {Object[]} */ _mockMailNotification_FilteredForWebClient(ids) { - const notifications = this.getRecords('mail.notification', [ - ['id', 'in', ids], - ]); - return notifications.filter(notification => { - const partner = this.getRecords('res.partner', [['id', '=', notification.res_partner_id]])[0]; - if (['bounce', 'exception', 'canceled'].includes(notification.notification_status) || - (partner && partner.partner_share)) { + const notifications = this.getRecords("mail.notification", [["id", "in", ids]]); + return notifications.filter((notification) => { + const partner = this.getRecords("res.partner", [ + ["id", "=", notification.res_partner_id], + ])[0]; + if ( + ["bounce", "exception", "canceled"].includes(notification.notification_status) || + (partner && partner.partner_share) + ) { return true; } - const message = this.getRecords('mail.message', [['id', '=', notification.mail_message_id]])[0]; - const subtypes = (message.subtype_id) ? - this.getRecords('mail.message.subtype', [['id', '=', message.subtype_id]]) : []; - return (subtypes.length == 0) || subtypes[0].track_recipients; + const message = this.getRecords("mail.message", [ + ["id", "=", notification.mail_message_id], + ])[0]; + const subtypes = message.subtype_id + ? this.getRecords("mail.message.subtype", [["id", "=", message.subtype_id]]) + : []; + return subtypes.length == 0 || subtypes[0].track_recipients; }); }, /** @@ -35,17 +40,20 @@ patch(MockServer.prototype, 'mail/models/mail_notification', { * @returns {Object[]} */ _mockMailNotification_NotificationFormat(ids) { - const notifications = this.getRecords('mail.notification', [['id', 'in', ids]]); - return notifications.map(notification => { - const partner = this.getRecords('res.partner', [['id', '=', notification.res_partner_id]])[0]; + const notifications = this.getRecords("mail.notification", [["id", "in", ids]]); + return notifications.map((notification) => { + const partner = this.getRecords("res.partner", [ + ["id", "=", notification.res_partner_id], + ])[0]; return { - 'id': notification.id, - 'notification_type': notification.notification_type, - 'notification_status': notification.notification_status, - 'failure_type': notification.failure_type, - 'res_partner_id': partner ? [partner && partner.id, partner && partner.display_name] : undefined, + id: notification.id, + notification_type: notification.notification_type, + notification_status: notification.notification_status, + failure_type: notification.failure_type, + res_partner_id: partner + ? [partner && partner.id, partner && partner.display_name] + : undefined, }; }); }, - }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_thread.js b/addons/mail/static/tests/helpers/mock_server/models/mail_thread.js index 28b2ad03b11..ad396cd8a00 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_thread.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_thread.js @@ -3,30 +3,33 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -import { datetime_to_str } from 'web.time'; +import { datetime_to_str } from "web.time"; -patch(MockServer.prototype, 'mail/models/mail_thread', { +patch(MockServer.prototype, "mail/models/mail_thread", { async _performRPC(route, args) { - if (args.method === 'message_subscribe') { + if (args.method === "message_subscribe") { const ids = args.args[0]; const partner_ids = args.args[1] || args.kwargs.partner_ids; const subtype_ids = args.args[2] || args.kwargs.subtype_ids; return this._mockMailThreadMessageSubscribe(args.model, ids, partner_ids, subtype_ids); } - if (args.method === 'message_unsubscribe') { + if (args.method === "message_unsubscribe") { const ids = args.args[0]; const partner_ids = args.args[1] || args.kwargs.partner_ids; return this._mockMailThreadMessageUnsubscribe(args.model, ids, partner_ids); } - if (args.method === 'message_post') { + if (args.method === "message_post") { const id = args.args[0]; const kwargs = args.kwargs; const context = kwargs.context; delete kwargs.context; return this._mockMailThreadMessagePost(args.model, [id], kwargs, context); } - if (args.method === 'notify_cancel_by_type') { - return this._mockMailThreadNotifyCancelByType(args.model, args.kwargs.notification_type); + if (args.method === "notify_cancel_by_type") { + return this._mockMailThreadNotifyCancelByType( + args.model, + args.kwargs.notification_type + ); } return this._super(route, args); }, @@ -44,35 +47,27 @@ patch(MockServer.prototype, 'mail/models/mail_thread', { // For simplicity partner is not guessed from email_from here, but // that would be the first step on the server. let user_id; - if ('mockedUserId' in context) { + if ("mockedUserId" in context) { // can be falsy to simulate not being logged in - user_id = context.mockedUserId - ? context.mockedUserId - : this.publicUserId; + user_id = context.mockedUserId ? context.mockedUserId : this.publicUserId; } else { user_id = this.currentUserId; } - const user = this.getRecords( - 'res.users', - [['id', '=', user_id]], - { active_test: false }, - )[0]; - const author = this.getRecords( - 'res.partner', - [['id', '=', user.partner_id]], - { active_test: false }, - )[0]; + const user = this.getRecords("res.users", [["id", "=", user_id]], { + active_test: false, + })[0]; + const author = this.getRecords("res.partner", [["id", "=", user.partner_id]], { + active_test: false, + })[0]; author_id = author.id; email_from = `${author.display_name} <${author.email}>`; } if (email_from === undefined) { if (author_id) { - const author = this.getRecords( - 'res.partner', - [['id', '=', author_id]], - { active_test: false }, - )[0]; + const author = this.getRecords("res.partner", [["id", "=", author_id]], { + active_test: false, + })[0]; email_from = `${author.display_name} <${author.email}>`; } } @@ -96,8 +91,13 @@ patch(MockServer.prototype, 'mail/models/mail_thread', { * @param {string} [param3.reason] * @returns {Object} */ - _mockMailThread_MessageAddSuggestedRecipient(model, ids, result, { email, partner, reason = '' } = {}) { - const record = this.getRecords(model, [['id', 'in', 'ids']])[0]; + _mockMailThread_MessageAddSuggestedRecipient( + model, + ids, + result, + { email, partner, reason = "" } = {} + ) { + const record = this.getRecords(model, [["id", "in", "ids"]])[0]; // for simplicity result[record.id].push([partner, email, reason]); return result; @@ -111,17 +111,21 @@ patch(MockServer.prototype, 'mail/models/mail_thread', { * @returns {Object} */ _mockMailThread_MessageGetSuggestedRecipients(model, ids) { - if (model === 'res.fake') { + if (model === "res.fake") { return this._mockResFake_MessageGetSuggestedRecipients(model, ids); } - const result = ids.reduce((result, id) => result[id] = [], {}); - const records = this.getRecords(model, [['id', 'in', ids]]); + const result = ids.reduce((result, id) => (result[id] = []), {}); + const records = this.getRecords(model, [["id", "in", ids]]); for (const record in records) { if (record.user_id) { - const user = this.getRecords('res.users', [['id', '=', record.user_id]]); + const user = this.getRecords("res.users", [["id", "=", record.user_id]]); if (user.partner_id) { - const reason = this.models[model].fields['user_id'].string; - this._mockMailThread_MessageAddSuggestedRecipient(result, user.partner_id, reason); + const reason = this.models[model].fields["user_id"].string; + this._mockMailThread_MessageAddSuggestedRecipient( + result, + user.partner_id, + reason + ); } } } @@ -140,38 +144,36 @@ patch(MockServer.prototype, 'mail/models/mail_thread', { _mockMailThreadMessagePost(model, ids, kwargs, context) { const id = ids[0]; // ensure_one if (kwargs.attachment_ids) { - const attachments = this.getRecords('ir.attachment', [ - ['id', 'in', kwargs.attachment_ids], - ['res_model', '=', 'mail.compose.message'], - ['res_id', '=', 0], + const attachments = this.getRecords("ir.attachment", [ + ["id", "in", kwargs.attachment_ids], + ["res_model", "=", "mail.compose.message"], + ["res_id", "=", 0], ]); - const attachmentIds = attachments.map(attachment => attachment.id); - this.pyEnv['ir.attachment'].write( - attachmentIds, - { - res_id: id, - res_model: model, - }, - ); - kwargs.attachment_ids = attachmentIds.map(attachmentId => [4, attachmentId]); + const attachmentIds = attachments.map((attachment) => attachment.id); + this.pyEnv["ir.attachment"].write(attachmentIds, { + res_id: id, + res_model: model, + }); + kwargs.attachment_ids = attachmentIds.map((attachmentId) => [4, attachmentId]); } - const subtype_xmlid = kwargs.subtype_xmlid || 'mail.mt_note'; + const subtype_xmlid = kwargs.subtype_xmlid || "mail.mt_note"; const [author_id, email_from] = this._MockMailThread_MessageComputeAuthor( model, ids, kwargs.author_id, - kwargs.email_from, context, + kwargs.email_from, + context ); const values = Object.assign({}, kwargs, { author_id, email_from, - is_discussion: subtype_xmlid === 'mail.mt_comment', - is_note: subtype_xmlid === 'mail.mt_note', + is_discussion: subtype_xmlid === "mail.mt_comment", + is_note: subtype_xmlid === "mail.mt_note", model, res_id: id, }); delete values.subtype_xmlid; - const messageId = this.pyEnv['mail.message'].create(values); + const messageId = this.pyEnv["mail.message"].create(values); this._mockMailThread_NotifyThread(model, ids, messageId); return this._mockMailMessageMessageFormat([messageId])[0]; }, @@ -188,14 +190,14 @@ patch(MockServer.prototype, 'mail/models/mail_thread', { _mockMailThreadMessageSubscribe(model, ids, partner_ids, subtype_ids) { for (const id of ids) { for (const partner_id of partner_ids) { - const followerId = this.pyEnv['mail.followers'].create({ + const followerId = this.pyEnv["mail.followers"].create({ is_active: true, partner_id, res_id: id, res_model: model, subtype_ids: subtype_ids, }); - this.pyEnv['res.partner'].write([partner_id], { + this.pyEnv["res.partner"].write([partner_id], { message_follower_ids: [followerId], }); } @@ -212,34 +214,44 @@ patch(MockServer.prototype, 'mail/models/mail_thread', { * @returns {boolean} */ _mockMailThread_NotifyThread(model, ids, messageId) { - const message = this.getRecords('mail.message', [['id', '=', messageId]])[0]; + const message = this.getRecords("mail.message", [["id", "=", messageId]])[0]; const messageFormat = this._mockMailMessageMessageFormat([messageId])[0]; const notifications = []; - if (model === 'mail.channel') { + if (model === "mail.channel") { // members - const channels = this.getRecords('mail.channel', [['id', '=', message.res_id]]); + const channels = this.getRecords("mail.channel", [["id", "=", message.res_id]]); for (const channel of channels) { - notifications.push([channel, 'mail.channel/new_message', { - 'id': channel.id, - 'message': messageFormat, - }]); + notifications.push([ + channel, + "mail.channel/new_message", + { + id: channel.id, + message: messageFormat, + }, + ]); // notify update of last_interest_dt const now = datetime_to_str(new Date()); - const members = this.getRecords('mail.channel.member', [['id', 'in', channel.channel_member_ids]]); - this.pyEnv['mail.channel.member'].write( - members.map(member => member.id), - { last_interest_dt: now }, + const members = this.getRecords("mail.channel.member", [ + ["id", "in", channel.channel_member_ids], + ]); + this.pyEnv["mail.channel.member"].write( + members.map((member) => member.id), + { last_interest_dt: now } ); for (const member of members) { // simplification, send everything on the current user "test" bus, but it should send to each member instead - notifications.push([member, 'mail.channel/last_interest_dt_changed', { - 'id': channel.id, - 'last_interest_dt': member.last_interest_dt, - }]); + notifications.push([ + member, + "mail.channel/last_interest_dt_changed", + { + id: channel.id, + last_interest_dt: member.last_interest_dt, + }, + ]); } } } - this.pyEnv['bus.bus']._sendmany(notifications); + this.pyEnv["bus.bus"]._sendmany(notifications); }, /** * Simulates `message_unsubscribe` on `mail.thread`. @@ -254,22 +266,31 @@ patch(MockServer.prototype, 'mail/models/mail_thread', { if (!partner_ids) { return true; } - const followers = this.getRecords('mail.followers', [ - ['res_model', '=', model], - ['res_id', 'in', ids], - ['partner_id', 'in', partner_ids || []], + const followers = this.getRecords("mail.followers", [ + ["res_model", "=", model], + ["res_id", "in", ids], + ["partner_id", "in", partner_ids || []], ]); - this.pyEnv['mail.followers'].unlink(followers.map(follower => follower.id)); + this.pyEnv["mail.followers"].unlink(followers.map((follower) => follower.id)); }, /** * Simulates `_message_track` on `mail.thread` */ - _mockMailThread_MessageTrack(modelName, trackedFieldNames, initialTrackedFieldValuesByRecordId) { + _mockMailThread_MessageTrack( + modelName, + trackedFieldNames, + initialTrackedFieldValuesByRecordId + ) { const trackFieldNamesToField = this.mockFieldsGet(modelName, trackedFieldNames); const tracking = {}; const records = this.models[modelName].records; for (const record of records) { - tracking[record.id] = this._mockMailBaseModel__MailTrack(modelName, trackFieldNamesToField, initialTrackedFieldValuesByRecordId[record.id], record); + tracking[record.id] = this._mockMailBaseModel__MailTrack( + modelName, + trackFieldNamesToField, + initialTrackedFieldValuesByRecordId[record.id], + record + ); } for (const record of records) { const { trackingValueIds, changedFieldNames } = tracking[record.id] || {}; @@ -296,7 +317,7 @@ patch(MockServer.prototype, 'mail/models/mail_thread', { this._mockMailThread_MessageTrack( model, this._mockMailThread_TrackGetFields(model), - initialTrackedFieldValuesByRecordId, + initialTrackedFieldValuesByRecordId ); }, /** @@ -340,23 +361,29 @@ patch(MockServer.prototype, 'mail/models/mail_thread', { */ _mockMailThreadNotifyCancelByType(model, notificationType) { // Query matching notifications - const notifications = this.getRecords('mail.notification', [ - ['notification_type', '=', notificationType], - ['notification_status', 'in', ['bounce', 'exception']], - ]).filter(notification => { - const message = this.getRecords('mail.message', [['id', '=', notification.mail_message_id]])[0]; + const notifications = this.getRecords("mail.notification", [ + ["notification_type", "=", notificationType], + ["notification_status", "in", ["bounce", "exception"]], + ]).filter((notification) => { + const message = this.getRecords("mail.message", [ + ["id", "=", notification.mail_message_id], + ])[0]; return message.model === model && message.author_id === this.currentPartnerId; }); // Update notification status - this.pyEnv['mail.notification'].write( - notifications.map(notification => notification.id), - { notification_status: 'canceled' }, + this.pyEnv["mail.notification"].write( + notifications.map((notification) => notification.id), + { notification_status: "canceled" } ); // Send bus notifications to update status of notifications in the web client - this.pyEnv['bus.bus']._sendone(this.pyEnv.currentPartner, 'mail.message/notification_update', { - 'elements': this._mockMailMessage_MessageNotificationFormat( - notifications.map(notification => notification.mail_message_id) - ), - }); + this.pyEnv["bus.bus"]._sendone( + this.pyEnv.currentPartner, + "mail.message/notification_update", + { + elements: this._mockMailMessage_MessageNotificationFormat( + notifications.map((notification) => notification.mail_message_id) + ), + } + ); }, }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/mail_tracking_value.js b/addons/mail/static/tests/helpers/mock_server/models/mail_tracking_value.js index 0ed8cd75b8a..5a0d2749ef4 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/mail_tracking_value.js +++ b/addons/mail/static/tests/helpers/mock_server/models/mail_tracking_value.js @@ -3,21 +3,23 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/mail_tracking_value', { +patch(MockServer.prototype, "mail/models/mail_tracking_value", { /** * @override */ init(data, options) { this._super(data, options); - if (this.currentPartnerId && this.models && 'res.partner' in this.models) { - this.currentPartner = this.getRecords('res.partner', [['id', '=', this.currentPartnerId]])[0]; + if (this.currentPartnerId && this.models && "res.partner" in this.models) { + this.currentPartner = this.getRecords("res.partner", [ + ["id", "=", this.currentPartnerId], + ])[0]; } // creation of the ir.model.fields records, required for tracked fields for (const modelName in this.models) { - const fieldNamesToFields = this.models[modelName].fields; + const fieldNamesToFields = this.models[modelName].fields; for (const fname in fieldNamesToFields) { - if (fieldNamesToFields[fname].tracking) { - this.mockCreate('ir.model.fields', { model: modelName, name: fname }); + if (fieldNamesToFields[fname].tracking) { + this.mockCreate("ir.model.fields", { model: modelName, name: fname }); } } } @@ -36,50 +38,66 @@ patch(MockServer.prototype, 'mail/models/mail_tracking_value', { /** * Simulates `create_tracking_values` on `mail.tracking.value` */ - _mockMailTrackingValue_CreateTrackingValues(initialValue, newValue, fieldName, field, modelName) { + _mockMailTrackingValue_CreateTrackingValues( + initialValue, + newValue, + fieldName, + field, + modelName + ) { let isTracked = true; - const irField = this.models['ir.model.fields'].records.find(field => field.model === modelName && field.name === fieldName); + const irField = this.models["ir.model.fields"].records.find( + (field) => field.model === modelName && field.name === fieldName + ); if (!irField) { return; } - const values = { field: irField['id'], field_desc: field['string'], field_type: field['type'] }; + const values = { + field: irField["id"], + field_desc: field["string"], + field_type: field["type"], + }; switch (values.field_type) { - case 'char': - case 'datetime': - case 'float': - case 'integer': - case 'monetary': - case 'text': + case "char": + case "datetime": + case "float": + case "integer": + case "monetary": + case "text": values[`old_value_${values.field_type}`] = initialValue; values[`new_value_${values.field_type}`] = newValue; break; - case 'date': - values['old_value_datetime'] = initialValue; - values['new_value_datetime'] = newValue; + case "date": + values["old_value_datetime"] = initialValue; + values["new_value_datetime"] = newValue; break; - case 'boolean': - values['old_value_integer'] = initialValue ? 1 : 0; - values['new_value_integer'] = newValue ? 1 : 0; + case "boolean": + values["old_value_integer"] = initialValue ? 1 : 0; + values["new_value_integer"] = newValue ? 1 : 0; break; - case 'selection': - values['old_value_char'] = initialValue; - values['new_value_char'] = newValue; + case "selection": + values["old_value_char"] = initialValue; + values["new_value_char"] = newValue; break; - case 'many2one': - initialValue = initialValue ? this.pyEnv[field.relation].searchRead([['id', '=', initialValue]])[0] : initialValue; - newValue = newValue ? this.pyEnv[field.relation].searchRead([['id', '=', newValue]])[0] : newValue; - values['old_value_integer'] = initialValue ? initialValue.id : 0; - values['new_value_integer'] = newValue ? newValue.id : 0; - values['old_value_char'] = initialValue ? initialValue.display_name : ''; - values['new_value_char'] = newValue ? newValue.display_name : ''; + case "many2one": + initialValue = initialValue + ? this.pyEnv[field.relation].searchRead([["id", "=", initialValue]])[0] + : initialValue; + newValue = newValue + ? this.pyEnv[field.relation].searchRead([["id", "=", newValue]])[0] + : newValue; + values["old_value_integer"] = initialValue ? initialValue.id : 0; + values["new_value_integer"] = newValue ? newValue.id : 0; + values["old_value_char"] = initialValue ? initialValue.display_name : ""; + values["new_value_char"] = newValue ? newValue.display_name : ""; break; default: isTracked = false; } if (isTracked) { - return this.pyEnv['mail.tracking.value'].create(values); + return this.pyEnv["mail.tracking.value"].create(values); } return false; }, @@ -87,16 +105,16 @@ patch(MockServer.prototype, 'mail/models/mail_tracking_value', { * Simulates `_tracking_value_format` on `mail.tracking.value` */ _mockMailTrackingValue_TrackingValueFormat(tracking_value_ids) { - const trackingValues = tracking_value_ids.map(tracking => ({ + const trackingValues = tracking_value_ids.map((tracking) => ({ changedField: tracking.field_desc, id: tracking.id, newValue: { fieldType: tracking.field_type, - value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'new') + value: this._mockMailTrackingValue_GetDisplayValue(tracking, "new"), }, oldValue: { fieldType: tracking.field_type, - value: this._mockMailTrackingValue_GetDisplayValue(tracking, 'old') + value: this._mockMailTrackingValue_GetDisplayValue(tracking, "old"), }, })); return trackingValues; @@ -106,25 +124,25 @@ patch(MockServer.prototype, 'mail/models/mail_tracking_value', { */ _mockMailTrackingValue_GetDisplayValue(record, type) { switch (record.field_type) { - case 'float': - case 'integer': - case 'monetary': - case 'text': + case "float": + case "integer": + case "monetary": + case "text": return record[`${type}_value_${record.field_type}`]; - case 'datetime': + case "datetime": if (record[`${type}_value_datetime`]) { const datetime = record[`${type}_value_datetime`]; return `${datetime}Z`; } else { return record[`${type}_value_datetime`]; } - case 'date': + case "date": if (record[`${type}_value_datetime`]) { return record[`${type}_value_datetime`]; } else { return record[`${type}_value_datetime`]; } - case 'boolean': + case "boolean": return !!record[`${type}_value_integer`]; default: return record[`${type}_value_char`]; diff --git a/addons/mail/static/tests/helpers/mock_server/models/models.js b/addons/mail/static/tests/helpers/mock_server/models/models.js index 7127f2c45f7..734472a12ae 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/models.js +++ b/addons/mail/static/tests/helpers/mock_server/models/models.js @@ -3,7 +3,7 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/models', { +patch(MockServer.prototype, "mail/models/models", { /** * Simulates `_mail_track` on `base` * @@ -13,14 +13,25 @@ patch(MockServer.prototype, 'mail/models/models', { * @param {Object} record * @returns {Object} */ - _mockMailBaseModel__MailTrack(model, trackedFieldNamesToField, initialTrackedFieldValues, record) { + _mockMailBaseModel__MailTrack( + model, + trackedFieldNamesToField, + initialTrackedFieldValues, + record + ) { const trackingValueIds = []; const changedFieldNames = []; for (const fname in trackedFieldNamesToField) { const initialValue = initialTrackedFieldValues[fname]; const newValue = record[fname]; if (initialValue !== newValue) { - const tracking = this._mockMailTrackingValue_CreateTrackingValues(initialValue, newValue, fname, trackedFieldNamesToField[fname], model); + const tracking = this._mockMailTrackingValue_CreateTrackingValues( + initialValue, + newValue, + fname, + trackedFieldNamesToField[fname], + model + ); if (tracking) { trackingValueIds.push(tracking); } diff --git a/addons/mail/static/tests/helpers/mock_server/models/res_fake.js b/addons/mail/static/tests/helpers/mock_server/models/res_fake.js index 0bf48bf1924..a4791d8f7a7 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/res_fake.js +++ b/addons/mail/static/tests/helpers/mock_server/models/res_fake.js @@ -3,7 +3,7 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/res_fake', { +patch(MockServer.prototype, "mail/models/res_fake", { /** * Simulates `_message_get_suggested_recipients` on `res.fake`. * @@ -14,29 +14,21 @@ patch(MockServer.prototype, 'mail/models/res_fake', { */ _mockResFake_MessageGetSuggestedRecipients(model, ids) { const result = {}; - const records = this.getRecords(model, [['id', 'in', ids]]); + const records = this.getRecords(model, [["id", "in", ids]]); for (const record of records) { result[record.id] = []; if (record.email_cc) { - result[record.id].push([ - false, - record.email_cc, - undefined, - 'CC email', - ]); + result[record.id].push([false, record.email_cc, undefined, "CC email"]); } - const partners = this.getRecords( - 'res.partner', - [['id', 'in', record.partner_ids]], - ); + const partners = this.getRecords("res.partner", [["id", "in", record.partner_ids]]); if (partners.length) { for (const partner of partners) { result[record.id].push([ partner.id, partner.display_name, undefined, - 'Email partner', + "Email partner", ]); } } diff --git a/addons/mail/static/tests/helpers/mock_server/models/res_partner.js b/addons/mail/static/tests/helpers/mock_server/models/res_partner.js index b2f8b435ef6..dffdfb298c7 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/res_partner.js +++ b/addons/mail/static/tests/helpers/mock_server/models/res_partner.js @@ -3,20 +3,20 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/res_partner', { +patch(MockServer.prototype, "mail/models/res_partner", { async _performRPC(route, args) { - if (args.model === 'res.partner' && args.method === 'im_search') { + if (args.model === "res.partner" && args.method === "im_search") { const name = args.args[0] || args.kwargs.search; const limit = args.args[1] || args.kwargs.limit; return this._mockResPartnerImSearch(name, limit); } - if (args.model === 'res.partner' && args.method === 'search_for_channel_invite') { + if (args.model === "res.partner" && args.method === "search_for_channel_invite") { const search_term = args.args[0] || args.kwargs.search_term; const channel_id = args.args[1] || args.kwargs.channel_id; const limit = args.args[2] || args.kwargs.limit; return this._mockResPartnerSearchForChannelInvite(search_term, channel_id, limit); } - if (args.model === 'res.partner' && args.method === 'get_mention_suggestions') { + if (args.model === "res.partner" && args.method === "get_mention_suggestions") { return this._mockResPartnerGetMentionSuggestions(args); } return this._super(route, args); @@ -29,7 +29,7 @@ patch(MockServer.prototype, 'mail/models/res_partner', { * @returns {Array[]} */ _mockResPartnerGetMentionSuggestions(args) { - const search = (args.args[0] || args.kwargs.search || '').toLowerCase(); + const search = (args.args[0] || args.kwargs.search || "").toLowerCase(); const limit = args.args[1] || args.kwargs.limit || 8; const channel_id = args.args[2] || args.kwargs.channel_id; @@ -45,34 +45,47 @@ patch(MockServer.prototype, 'mail/models/res_partner', { * @returns {Object[]} */ const mentionSuggestionsFilter = (partners, search, limit) => { - const matchingPartners = [...this._mockResPartnerMailPartnerFormat( - partners - .filter(partner => { - if (channel_id) { - const [member] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partner.id]]); - if (!member) { - return false; + const matchingPartners = [ + ...this._mockResPartnerMailPartnerFormat( + partners + .filter((partner) => { + if (channel_id) { + const [member] = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel_id], + ["partner_id", "=", partner.id], + ]); + if (!member) { + return false; + } } - } - // no search term is considered as return all - if (!search) { - return true; - } - // otherwise name or email must match search term - if (partner.name && partner.name.toLowerCase().includes(search)) { - return true; - } - if (partner.email && partner.email.toLowerCase().includes(search)) { - return true; - } - return false; - }) - .map(partner => partner.id) - ).values()].map(partnerFormat => { + // no search term is considered as return all + if (!search) { + return true; + } + // otherwise name or email must match search term + if (partner.name && partner.name.toLowerCase().includes(search)) { + return true; + } + if (partner.email && partner.email.toLowerCase().includes(search)) { + return true; + } + return false; + }) + .map((partner) => partner.id) + ).values(), + ].map((partnerFormat) => { if (channel_id) { - const [member] = this.getRecords('mail.channel.member', [['channel_id', '=', channel_id], ['partner_id', '=', partnerFormat.id]]); - partnerFormat['persona'] = { - 'channelMembers': [['insert', this._mockMailChannelMember_MailChannelMemberFormat([member.id])]], + const [member] = this.getRecords("mail.channel.member", [ + ["channel_id", "=", channel_id], + ["partner_id", "=", partnerFormat.id], + ]); + partnerFormat["persona"] = { + channelMembers: [ + [ + "insert", + this._mockMailChannelMember_MailChannelMemberFormat([member.id]), + ], + ], }; } return partnerFormat; @@ -83,16 +96,18 @@ patch(MockServer.prototype, 'mail/models/res_partner', { }; // add main suggestions based on users - const partnersFromUsers = this.getRecords('res.users', []) - .map(user => this.getRecords('res.partner', [['id', '=', user.partner_id]])[0]) - .filter(partner => partner); + const partnersFromUsers = this.getRecords("res.users", []) + .map((user) => this.getRecords("res.partner", [["id", "=", user.partner_id]])[0]) + .filter((partner) => partner); const mainMatchingPartners = mentionSuggestionsFilter(partnersFromUsers, search, limit); let extraMatchingPartners = []; // if not enough results add extra suggestions based on partners const remainingLimit = limit - mainMatchingPartners.length; if (mainMatchingPartners.length < limit) { - const partners = this.getRecords('res.partner', [['id', 'not in', mainMatchingPartners.map(partner => partner.id)]]); + const partners = this.getRecords("res.partner", [ + ["id", "not in", mainMatchingPartners.map((partner) => partner.id)], + ]); extraMatchingPartners = mentionSuggestionsFilter(partners, search, remainingLimit); } return mainMatchingPartners.concat(extraMatchingPartners); @@ -105,10 +120,10 @@ patch(MockServer.prototype, 'mail/models/res_partner', { * @returns {integer} */ _mockResPartner_GetNeedactionCount(id) { - const partner = this.getRecords('res.partner', [['id', '=', id]])[0]; - return this.getRecords('mail.notification', [ - ['res_partner_id', '=', partner.id], - ['is_read', '=', false], + const partner = this.getRecords("res.partner", [["id", "=", id]])[0]; + return this.getRecords("mail.notification", [ + ["res_partner_id", "=", partner.id], + ["is_read", "=", false], ]).length; }, /** @@ -119,12 +134,12 @@ patch(MockServer.prototype, 'mail/models/res_partner', { * @param {integer} [limit=20] * @returns {Object[]} */ - _mockResPartnerImSearch(name = '', limit = 20) { + _mockResPartnerImSearch(name = "", limit = 20) { name = name.toLowerCase(); // simulates ILIKE // simulates domain with relational parts (not supported by mock server) - const matchingPartners = this.getRecords('res.users', []) - .filter(user => { - const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0]; + const matchingPartners = this.getRecords("res.users", []) + .filter((user) => { + const partner = this.getRecords("res.partner", [["id", "=", user.partner_id]])[0]; // user must have a partner if (!partner) { return false; @@ -141,15 +156,21 @@ patch(MockServer.prototype, 'mail/models/res_partner', { return true; } return false; - }).map(user => { - const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0]; + }) + .map((user) => { + const partner = this.getRecords("res.partner", [["id", "=", user.partner_id]])[0]; return { id: partner.id, name: partner.name, }; - }).sort((a, b) => (a.name === b.name) ? (a.id - b.id) : (a.name > b.name) ? 1 : -1); + }) + .sort((a, b) => (a.name === b.name ? a.id - b.id : a.name > b.name ? 1 : -1)); matchingPartners.length = Math.min(matchingPartners.length, limit); - return [...this._mockResPartnerMailPartnerFormat(matchingPartners.map(partner => partner.id)).values()]; + return [ + ...this._mockResPartnerMailPartnerFormat( + matchingPartners.map((partner) => partner.id) + ).values(), + ]; }, /** * Simulates `mail_partner_format` on `res.partner`. @@ -159,33 +180,38 @@ patch(MockServer.prototype, 'mail/models/res_partner', { * @returns {Map} */ _mockResPartnerMailPartnerFormat(ids) { - const partners = this.getRecords( - 'res.partner', - [['id', 'in', ids]], - { active_test: false } - ); + const partners = this.getRecords("res.partner", [["id", "in", ids]], { + active_test: false, + }); // Servers is also returning `is_internal_user` but not // done here for simplification. - return new Map(partners.map(partner => { - const users = this.getRecords('res.users', [['id', 'in', partner.user_ids]]); - const internalUsers = users.filter(user => !user.share); - let mainUser; - if (internalUsers.length > 0) { - mainUser = internalUsers[0]; - } else if (users.length > 0) { - mainUser = users[0]; - } - return [partner.id, { - "active": partner.active, - "email": partner.email, - "id": partner.id, - "im_status": partner.im_status, - "name": partner.name, - "user": mainUser ? { - 'id': mainUser.id, - } : [['clear']], - }]; - })); + return new Map( + partners.map((partner) => { + const users = this.getRecords("res.users", [["id", "in", partner.user_ids]]); + const internalUsers = users.filter((user) => !user.share); + let mainUser; + if (internalUsers.length > 0) { + mainUser = internalUsers[0]; + } else if (users.length > 0) { + mainUser = users[0]; + } + return [ + partner.id, + { + active: partner.active, + email: partner.email, + id: partner.id, + im_status: partner.im_status, + name: partner.name, + user: mainUser + ? { + id: mainUser.id, + } + : [["clear"]], + }, + ]; + }) + ); }, /** * Simulates `search_for_channel_invite` on `res.partner`. @@ -199,34 +225,38 @@ patch(MockServer.prototype, 'mail/models/res_partner', { _mockResPartnerSearchForChannelInvite(search_term, channel_id, limit = 30) { search_term = search_term.toLowerCase(); // simulates ILIKE // simulates domain with relational parts (not supported by mock server) - const matchingPartners = [...this._mockResPartnerMailPartnerFormat( - this.getRecords('res.users', []) - .filter(user => { - const partner = this.getRecords('res.partner', [['id', '=', user.partner_id]])[0]; - // user must have a partner - if (!partner) { - return false; - } - // not current partner - if (partner.id === this.currentPartnerId) { - return false; - } - // no name is considered as return all - if (!search_term) { - return true; - } - if (partner.name && partner.name.toLowerCase().includes(search_term)) { - return true; - } - return false; - }) - .map(user => user.partner_id) - ).values()]; + const matchingPartners = [ + ...this._mockResPartnerMailPartnerFormat( + this.getRecords("res.users", []) + .filter((user) => { + const partner = this.getRecords("res.partner", [ + ["id", "=", user.partner_id], + ])[0]; + // user must have a partner + if (!partner) { + return false; + } + // not current partner + if (partner.id === this.currentPartnerId) { + return false; + } + // no name is considered as return all + if (!search_term) { + return true; + } + if (partner.name && partner.name.toLowerCase().includes(search_term)) { + return true; + } + return false; + }) + .map((user) => user.partner_id) + ).values(), + ]; const count = matchingPartners.length; matchingPartners.length = Math.min(count, limit); return { count, - partners: matchingPartners + partners: matchingPartners, }; }, /** @@ -237,23 +267,25 @@ patch(MockServer.prototype, 'mail/models/res_partner', { * @returns {Object[]} */ _mockResPartner_MessageFetchFailed(id) { - const partner = this.getRecords('res.partner', [['id', '=', id]])[0]; - const messages = this.getRecords('mail.message', [ - ['author_id', '=', partner.id], - ['res_id', '!=', 0], - ['model', '!=', false], - ['message_type', '!=', 'user_notification'], - ]).filter(message => { + const partner = this.getRecords("res.partner", [["id", "=", id]])[0]; + const messages = this.getRecords("mail.message", [ + ["author_id", "=", partner.id], + ["res_id", "!=", 0], + ["model", "!=", false], + ["message_type", "!=", "user_notification"], + ]).filter((message) => { // Purpose is to simulate the following domain on mail.message: // ['notification_ids.notification_status', 'in', ['bounce', 'exception']], // But it's not supported by getRecords domain to follow a relation. - const notifications = this.getRecords('mail.notification', [ - ['mail_message_id', '=', message.id], - ['notification_status', 'in', ['bounce', 'exception']], + const notifications = this.getRecords("mail.notification", [ + ["mail_message_id", "=", message.id], + ["notification_status", "in", ["bounce", "exception"]], ]); return notifications.length > 0; }); - return this._mockMailMessage_MessageNotificationFormat(messages.map(message => message.id)); + return this._mockMailMessage_MessageNotificationFormat( + messages.map((message) => message.id) + ); }, /** * Simulates `_get_channels_as_member` on `res.partner`. @@ -262,21 +294,23 @@ patch(MockServer.prototype, 'mail/models/res_partner', { * @param {integer[]} ids * @returns {Object} */ - _mockResPartner_GetChannelsAsMember(ids) { - const partner = this.getRecords('res.partner', [['id', 'in', ids]])[0]; - const channelMembers = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id]]); - const channels = this.getRecords('mail.channel', [ - ['channel_type', 'in', ['channel', 'group']], - ['channel_member_ids', 'in', channelMembers.map(member => member.id)], + _mockResPartner_GetChannelsAsMember(ids) { + const partner = this.getRecords("res.partner", [["id", "in", ids]])[0]; + const channelMembers = this.getRecords("mail.channel.member", [ + ["partner_id", "=", partner.id], ]); - const directMessagesMembers = this.getRecords('mail.channel.member', [['partner_id', '=', partner.id], ['is_pinned', '=', true]]); - const directMessages = this.getRecords('mail.channel', [ - ['channel_type', '=', 'chat'], - ['channel_member_ids', 'in', directMessagesMembers.map(member => member.id)], + const channels = this.getRecords("mail.channel", [ + ["channel_type", "in", ["channel", "group"]], + ["channel_member_ids", "in", channelMembers.map((member) => member.id)], ]); - return [ - ...channels, - ...directMessages, - ]; + const directMessagesMembers = this.getRecords("mail.channel.member", [ + ["partner_id", "=", partner.id], + ["is_pinned", "=", true], + ]); + const directMessages = this.getRecords("mail.channel", [ + ["channel_type", "=", "chat"], + ["channel_member_ids", "in", directMessagesMembers.map((member) => member.id)], + ]); + return [...channels, ...directMessages]; }, }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/res_users.js b/addons/mail/static/tests/helpers/mock_server/models/res_users.js index b9983ca6f46..67bf4fd90c8 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/res_users.js +++ b/addons/mail/static/tests/helpers/mock_server/models/res_users.js @@ -3,11 +3,11 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -import { date_to_str } from 'web.time'; +import { date_to_str } from "web.time"; -patch(MockServer.prototype, 'mail/models/res_users', { +patch(MockServer.prototype, "mail/models/res_users", { async _performRPC(route, args) { - if (args.model === 'res.users' && args.method === 'systray_get_activities') { + if (args.model === "res.users" && args.method === "systray_get_activities") { return this._mockResUsersSystrayGetActivities(); } return this._super(route, args); @@ -20,18 +20,32 @@ patch(MockServer.prototype, 'mail/models/res_users', { * @returns {Object} */ _mockResUsers_InitMessaging(ids) { - const user = this.getRecords('res.users', [['id', 'in', ids]])[0]; + const user = this.getRecords("res.users", [["id", "in", ids]])[0]; const userSettings = this._mockResUsersSettings_FindOrCreateForUser(user.id); return { - channels: this._mockMailChannelChannelInfo(this._mockResPartner_GetChannelsAsMember(user.partner_id).map(channel => channel.id)), - current_partner: this._mockResPartnerMailPartnerFormat(user.partner_id).get(user.partner_id), + channels: this._mockMailChannelChannelInfo( + this._mockResPartner_GetChannelsAsMember(user.partner_id).map( + (channel) => channel.id + ) + ), + current_partner: this._mockResPartnerMailPartnerFormat(user.partner_id).get( + user.partner_id + ), current_user_id: this.currentUserId, - current_user_settings: this._mockResUsersSettings_ResUsersSettingsFormat(userSettings.id), + current_user_settings: this._mockResUsersSettings_ResUsersSettingsFormat( + userSettings.id + ), menu_id: false, // not useful in QUnit tests needaction_inbox_counter: this._mockResPartner_GetNeedactionCount(user.partner_id), - partner_root: this._mockResPartnerMailPartnerFormat(this.partnerRootId).get(this.partnerRootId), - shortcodes: this.pyEnv['mail.shortcode'].searchRead([], { fields: ['source', 'substitution'] }), - starred_counter: this.getRecords('mail.message', [['starred_partner_ids', 'in', user.partner_id]]).length, + partner_root: this._mockResPartnerMailPartnerFormat(this.partnerRootId).get( + this.partnerRootId + ), + shortcodes: this.pyEnv["mail.shortcode"].searchRead([], { + fields: ["source", "substitution"], + }), + starred_counter: this.getRecords("mail.message", [ + ["starred_partner_ids", "in", user.partner_id], + ]).length, }; }, /** @@ -40,20 +54,20 @@ patch(MockServer.prototype, 'mail/models/res_users', { * @private */ _mockResUsersSystrayGetActivities() { - const activities = this.pyEnv['mail.activity'].searchRead([]); + const activities = this.pyEnv["mail.activity"].searchRead([]); const userActivitiesByModelName = {}; for (const activity of activities) { const today = date_to_str(new Date()); - if (today === activity['date_deadline']) { - activity['states'] = 'today'; - } else if (today > activity['date_deadline']) { - activity['states'] = 'overdue'; + if (today === activity["date_deadline"]) { + activity["states"] = "today"; + } else if (today > activity["date_deadline"]) { + activity["states"] = "overdue"; } else { - activity['states'] = 'planned'; + activity["states"] = "planned"; } } for (const activity of activities) { - const modelName = activity['res_model']; + const modelName = activity["res_model"]; if (!userActivitiesByModelName[modelName]) { userActivitiesByModelName[modelName] = { id: modelName, // for simplicity @@ -63,15 +77,17 @@ patch(MockServer.prototype, 'mail/models/res_users', { planned_count: 0, today_count: 0, total_count: 0, - type: 'activity', + type: "activity", }; } - userActivitiesByModelName[modelName][`${activity['states']}_count`] += 1; - userActivitiesByModelName[modelName]['total_count'] += 1; - userActivitiesByModelName[modelName].actions = [{ - icon: 'fa-clock-o', - name: 'Summary', - }]; + userActivitiesByModelName[modelName][`${activity["states"]}_count`] += 1; + userActivitiesByModelName[modelName]["total_count"] += 1; + userActivitiesByModelName[modelName].actions = [ + { + icon: "fa-clock-o", + name: "Summary", + }, + ]; } return Object.values(userActivitiesByModelName); }, diff --git a/addons/mail/static/tests/helpers/mock_server/models/res_users_settings.js b/addons/mail/static/tests/helpers/mock_server/models/res_users_settings.js index 5efe5145641..233af661680 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/res_users_settings.js +++ b/addons/mail/static/tests/helpers/mock_server/models/res_users_settings.js @@ -3,13 +3,13 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/res_users_settings', { +patch(MockServer.prototype, "mail/models/res_users_settings", { async _performRPC(route, args) { - if (args.model === 'res.users.settings' && args.method === '_find_or_create_for_user') { + if (args.model === "res.users.settings" && args.method === "_find_or_create_for_user") { const user_id = args.args[0][0]; return this._mockResUsersSettings_FindOrCreateForUser(user_id); } - if (args.model === 'res.users.settings' && args.method === 'set_res_users_settings') { + if (args.model === "res.users.settings" && args.method === "set_res_users_settings") { const id = args.args[0][0]; const newSettings = args.kwargs.new_settings; return this._mockResUsersSettingsSetResUsersSettings(id, newSettings); @@ -23,10 +23,10 @@ patch(MockServer.prototype, 'mail/models/res_users_settings', { * @returns {Object} */ _mockResUsersSettings_FindOrCreateForUser(user_id) { - let settings = this.getRecords('res.users.settings', [['user_id', '=', user_id]])[0]; + let settings = this.getRecords("res.users.settings", [["user_id", "=", user_id]])[0]; if (!settings) { - const settingsId = this.pyEnv['res.users.settings'].create({ user_id: user_id }); - settings = this.getRecords('res.users.settings', [['id', '=', settingsId]])[0]; + const settingsId = this.pyEnv["res.users.settings"].create({ user_id: user_id }); + settings = this.getRecords("res.users.settings", [["id", "=", settingsId]])[0]; } return settings; }, @@ -39,16 +39,28 @@ patch(MockServer.prototype, 'mail/models/res_users_settings', { * @returns {Object} */ _mockResUsersSettings_ResUsersSettingsFormat(id, fieldsToFormat) { - const [settings] = this.getRecords('res.users.settings', [['id', '=', id]]); - const ormAutomaticFields = new Set(['create_date', 'create_uid', 'display_name', 'name', 'write_date', 'write_uid', '__last_update']); - const filterPredicate = fieldsToFormat ? ([fieldName]) => fieldsToFormat.includes(fieldName) : ([fieldName]) => !ormAutomaticFields.has(fieldName); + const [settings] = this.getRecords("res.users.settings", [["id", "=", id]]); + const ormAutomaticFields = new Set([ + "create_date", + "create_uid", + "display_name", + "name", + "write_date", + "write_uid", + "__last_update", + ]); + const filterPredicate = fieldsToFormat + ? ([fieldName]) => fieldsToFormat.includes(fieldName) + : ([fieldName]) => !ormAutomaticFields.has(fieldName); const res = Object.fromEntries(Object.entries(settings).filter(filterPredicate)); - if (Object.prototype.hasOwnProperty.call(res, 'user_id')) { + if (Object.prototype.hasOwnProperty.call(res, "user_id")) { res.user_id = { id: settings.user_id }; } - if (Object.prototype.hasOwnProperty.call(res, 'volume_settings_ids')) { - const volumeSettings = this._mockResUsersSettingsVolumes_DiscussUsersSettingsVolumeFormat(settings.volume_settings_ids); - res.volume_settings_ids = [['insert', volumeSettings]]; + if (Object.prototype.hasOwnProperty.call(res, "volume_settings_ids")) { + const volumeSettings = this._mockResUsersSettingsVolumes_DiscussUsersSettingsVolumeFormat( + settings.volume_settings_ids + ); + res.volume_settings_ids = [["insert", volumeSettings]]; } return res; }, @@ -60,19 +72,22 @@ patch(MockServer.prototype, 'mail/models/res_users_settings', { * @param {Object} newSettings */ _mockResUsersSettingsSetResUsersSettings(id, newSettings) { - const oldSettings = this.getRecords('res.users.settings', [['id', '=', id]])[0]; + const oldSettings = this.getRecords("res.users.settings", [["id", "=", id]])[0]; const changedSettings = {}; for (const setting in newSettings) { if (setting in oldSettings && newSettings[setting] !== oldSettings[setting]) { changedSettings[setting] = newSettings[setting]; } } - this.pyEnv['res.users.settings'].write( - [id], - changedSettings, - ); - const [relatedUser] = this.pyEnv['res.users'].searchRead([['id', '=', oldSettings.user_id]]); - const [relatedPartner] = this.pyEnv['res.partner'].searchRead([['id', '=', relatedUser.partner_id]]); - this.pyEnv['bus.bus']._sendone(relatedPartner, 'mail.record/insert', { 'res.users.settings': { ...changedSettings, id } }); + this.pyEnv["res.users.settings"].write([id], changedSettings); + const [relatedUser] = this.pyEnv["res.users"].searchRead([ + ["id", "=", oldSettings.user_id], + ]); + const [relatedPartner] = this.pyEnv["res.partner"].searchRead([ + ["id", "=", relatedUser.partner_id], + ]); + this.pyEnv["bus.bus"]._sendone(relatedPartner, "mail.record/insert", { + "res.users.settings": { ...changedSettings, id }, + }); }, }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/res_users_settings_volumes.js b/addons/mail/static/tests/helpers/mock_server/models/res_users_settings_volumes.js index 025e9891aee..14ea9cc0b0e 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/res_users_settings_volumes.js +++ b/addons/mail/static/tests/helpers/mock_server/models/res_users_settings_volumes.js @@ -1,12 +1,12 @@ /** @odoo-module **/ // ensure bus mock server is loaded first. -import '@bus/../tests/helpers/mock_server'; +import "@bus/../tests/helpers/mock_server"; import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; -patch(MockServer.prototype, 'mail/models/res_users_settings_volumes', { +patch(MockServer.prototype, "mail/models/res_users_settings_volumes", { /** * Simulates `discuss_users_settings_volume_format` on `res.users.settings.volumes`. * @@ -14,14 +14,24 @@ patch(MockServer.prototype, 'mail/models/res_users_settings_volumes', { * @returns {Object} */ _mockResUsersSettingsVolumes_DiscussUsersSettingsVolumeFormat(ids) { - const volumeSettingsRecords = this.getRecords('res.users.settings.volumes', [['id', 'in', ids]]); - return volumeSettingsRecords.map(volumeSettingsRecord => { - const [relatedGuest] = this.getRecords('mail.guest', [['id', '=', volumeSettingsRecord.guest_id]]); - const [relatedPartner] = this.getRecords('res.partner', [['id', '=', volumeSettingsRecord.partner_id]]); + const volumeSettingsRecords = this.getRecords("res.users.settings.volumes", [ + ["id", "in", ids], + ]); + return volumeSettingsRecords.map((volumeSettingsRecord) => { + const [relatedGuest] = this.getRecords("mail.guest", [ + ["id", "=", volumeSettingsRecord.guest_id], + ]); + const [relatedPartner] = this.getRecords("res.partner", [ + ["id", "=", volumeSettingsRecord.partner_id], + ]); return { - guest_id: relatedGuest ? { id: relatedGuest.id, name: relatedGuest.name } : [['clear']], + guest_id: relatedGuest + ? { id: relatedGuest.id, name: relatedGuest.name } + : [["clear"]], id: volumeSettingsRecord.id, - partner_id: relatedPartner ? { id: relatedPartner.id, name: relatedPartner.name } : [['clear']], + partner_id: relatedPartner + ? { id: relatedPartner.id, name: relatedPartner.name } + : [["clear"]], volume: volumeSettingsRecord.volume, }; }); diff --git a/addons/mail/static/tests/helpers/model_definitions_setup.js b/addons/mail/static/tests/helpers/model_definitions_setup.js index ef8ce699495..8043f78cec6 100644 --- a/addons/mail/static/tests/helpers/model_definitions_setup.js +++ b/addons/mail/static/tests/helpers/model_definitions_setup.js @@ -1,44 +1,56 @@ /** @odoo-module **/ -import { TEST_GROUP_IDS, TEST_USER_IDS } from '@bus/../tests/helpers/test_constants'; +import { TEST_GROUP_IDS, TEST_USER_IDS } from "@bus/../tests/helpers/test_constants"; import { addFakeModel, addModelNamesToFetch, insertModelFields, - insertRecords -} from '@bus/../tests/helpers/model_definitions_helpers'; + insertRecords, +} from "@bus/../tests/helpers/model_definitions_helpers"; //-------------------------------------------------------------------------- // Models //-------------------------------------------------------------------------- addModelNamesToFetch([ - 'mail.activity', 'mail.activity.type', 'mail.channel', 'mail.channel.member', - 'mail.channel.rtc.session', 'mail.followers', 'mail.guest', 'mail.link.preview', 'mail.message', - 'mail.message.subtype', 'mail.notification', 'mail.shortcode', 'mail.template', - 'mail.tracking.value', 'res.users.settings', 'res.users.settings.volumes' + "mail.activity", + "mail.activity.type", + "mail.channel", + "mail.channel.member", + "mail.channel.rtc.session", + "mail.followers", + "mail.guest", + "mail.link.preview", + "mail.message", + "mail.message.subtype", + "mail.notification", + "mail.shortcode", + "mail.template", + "mail.tracking.value", + "res.users.settings", + "res.users.settings.volumes", ]); -addFakeModel('res.fake', { - message_ids: { string: 'Messages', type: 'one2many', relation: 'mail.message' }, - activity_ids: { string: "Activities", type: 'one2many', relation: 'mail.activity' }, - email_cc: { type: 'char' }, - partner_ids: { relation: 'res.partner', string: "Related partners", type: 'one2many' }, +addFakeModel("res.fake", { + message_ids: { string: "Messages", type: "one2many", relation: "mail.message" }, + activity_ids: { string: "Activities", type: "one2many", relation: "mail.activity" }, + email_cc: { type: "char" }, + partner_ids: { relation: "res.partner", string: "Related partners", type: "one2many" }, }); -addFakeModel('m2x.avatar.user', { - user_id: { type: 'many2one', relation: 'res.users' }, - user_ids: { type: 'many2many', relation: 'res.users', string: 'Users'}, +addFakeModel("m2x.avatar.user", { + user_id: { type: "many2one", relation: "res.users" }, + user_ids: { type: "many2many", relation: "res.users", string: "Users" }, }); //-------------------------------------------------------------------------- // Insertion of fields //-------------------------------------------------------------------------- -insertModelFields('mail.activity', { - chaining_type: { default: 'suggest' }, +insertModelFields("mail.activity", { + chaining_type: { default: "suggest" }, }); -insertModelFields('mail.channel', { +insertModelFields("mail.channel", { author_id: { default() { return this.currentPartnerId; @@ -50,38 +62,46 @@ insertModelFields('mail.channel', { return [[0, 0, { partner_id: this.currentPartnerId }]]; }, }, - channel_type: { default: 'channel' }, + channel_type: { default: "channel" }, group_based_subscription: { string: "Group based subscription", type: "boolean" }, group_public_id: { default() { return TEST_GROUP_IDS.groupUserId; }, }, - uuid: { default: () => _.uniqueId('mail.channel_uuid-') }, + uuid: { default: () => _.uniqueId("mail.channel_uuid-") }, }); -insertModelFields('mail.channel.member', { - fold_state: { default: 'open' }, +insertModelFields("mail.channel.member", { + fold_state: { default: "open" }, is_pinned: { default: true }, message_unread_counter: { default: 0 }, }); -insertModelFields('mail.message', { +insertModelFields("mail.message", { author_id: { default: TEST_USER_IDS.currentPartnerId }, - history_partner_ids: { relation: 'res.partner', string: "Partners with History", type: 'many2many' }, - is_discussion: { string: 'Discussion', type: 'boolean' }, - is_note: { string: "Discussion", type: 'boolean' }, - is_notification: { string: "Note", type: 'boolean' }, - needaction_partner_ids: { relation: 'res.partner', string: "Partners with Need Action", type: 'many2many' }, - res_model_name: { string: "Res Model Name", type: 'char' }, + history_partner_ids: { + relation: "res.partner", + string: "Partners with History", + type: "many2many", + }, + is_discussion: { string: "Discussion", type: "boolean" }, + is_note: { string: "Discussion", type: "boolean" }, + is_notification: { string: "Note", type: "boolean" }, + needaction_partner_ids: { + relation: "res.partner", + string: "Partners with Need Action", + type: "many2many", + }, + res_model_name: { string: "Res Model Name", type: "char" }, }); -insertModelFields('mail.message.subtype', { - subtype_xmlid: { type: 'char' }, +insertModelFields("mail.message.subtype", { + subtype_xmlid: { type: "char" }, }); -insertModelFields('mail.tracking.value', { - changed_field: { string: 'Changed field', type: 'char' }, - new_value: { string: 'New value', type: 'char' }, - old_value: { string: 'Old value', type: 'char' }, +insertModelFields("mail.tracking.value", { + changed_field: { string: "Changed field", type: "char" }, + new_value: { string: "New value", type: "char" }, + old_value: { string: "Old value", type: "char" }, }); -insertModelFields('res.users.settings', { +insertModelFields("res.users.settings", { is_discuss_sidebar_category_channel_open: { default: true }, is_discuss_sidebar_category_chat_open: { default: true }, }); @@ -90,15 +110,25 @@ insertModelFields('res.users.settings', { // Insertion of records //-------------------------------------------------------------------------- -insertRecords('mail.activity.type', [ - { icon: 'fa-envelope', id: 1, name: "Email" }, - { icon: 'fa-upload', id: 28, name: "Upload Document" }, +insertRecords("mail.activity.type", [ + { icon: "fa-envelope", id: 1, name: "Email" }, + { icon: "fa-upload", id: 28, name: "Upload Document" }, ]); -insertRecords('mail.message.subtype', [ - { default: false, internal: true, name: "Activities", sequence: 90, subtype_xmlid: 'mail.mt_activities' }, +insertRecords("mail.message.subtype", [ { - default: false, internal: true, name: "Note", sequence: 100, subtype_xmlid: 'mail.mt_note', - track_recipients: true + default: false, + internal: true, + name: "Activities", + sequence: 90, + subtype_xmlid: "mail.mt_activities", }, - { name: "Discussions", sequence: 0, subtype_xmlid: 'mail.mt_comment', track_recipients: true }, + { + default: false, + internal: true, + name: "Note", + sequence: 100, + subtype_xmlid: "mail.mt_note", + track_recipients: true, + }, + { name: "Discussions", sequence: 0, subtype_xmlid: "mail.mt_comment", track_recipients: true }, ]); diff --git a/addons/mail/static/tests/helpers/patch_ui_size.js b/addons/mail/static/tests/helpers/patch_ui_size.js index 745f4da3c77..74322f66198 100644 --- a/addons/mail/static/tests/helpers/patch_ui_size.js +++ b/addons/mail/static/tests/helpers/patch_ui_size.js @@ -1,10 +1,10 @@ /** @odoo-module **/ -import { browser } from '@web/core/browser/browser'; -import { MEDIAS_BREAKPOINTS, SIZES, uiService } from '@web/core/ui/ui_service'; +import { browser } from "@web/core/browser/browser"; +import { MEDIAS_BREAKPOINTS, SIZES, uiService } from "@web/core/ui/ui_service"; import { patchWithCleanup } from "@web/../tests/helpers/utils"; -import config from 'web.config'; +import config from "web.config"; /** * Return the width corresponding to the given size. If an upper and lower bound @@ -58,7 +58,7 @@ function legacyPatchUiSize(height, size, width) { device: { ...legacyEnv.device, isMobile: size <= SIZES.SM, - } + }, }); patchWithCleanup(config, { device: { @@ -82,8 +82,8 @@ function legacyPatchUiSize(height, size, width) { * @param {number|undefined} [params.height] */ function patchUiSize({ height, size, width }) { - if (!size && !width || size && width) { - throw new Error('Either size or width must be given to the patchUiSize function'); + if ((!size && !width) || (size && width)) { + throw new Error("Either size or width must be given to the patchUiSize function"); } size = size === undefined ? getSizeFromWidth(width) : size; width = width || getWidthFromSize(size); @@ -100,7 +100,4 @@ function patchUiSize({ height, size, width }) { legacyPatchUiSize(height, size, width); } -export { - patchUiSize, - SIZES -}; +export { patchUiSize, SIZES }; diff --git a/addons/mail/static/tests/helpers/test_utils.js b/addons/mail/static/tests/helpers/test_utils.js index f0ee2891d6b..11d8b959487 100644 --- a/addons/mail/static/tests/helpers/test_utils.js +++ b/addons/mail/static/tests/helpers/test_utils.js @@ -1,17 +1,17 @@ /** @odoo-module **/ -import { getPyEnv, startServer } from '@bus/../tests/helpers/mock_python_environment'; +import { getPyEnv, startServer } from "@bus/../tests/helpers/mock_python_environment"; -import { nextTick } from '@mail/utils/utils'; -import { getAdvanceTime } from '@mail/../tests/helpers/time_control'; -import { getWebClientReady } from '@mail/../tests/helpers/webclient_setup'; +import { nextTick } from "@mail/utils/utils"; +import { getAdvanceTime } from "@mail/../tests/helpers/time_control"; +import { getWebClientReady } from "@mail/../tests/helpers/webclient_setup"; import { wowlServicesSymbol } from "@web/legacy/utils"; import { registerCleanup } from "@web/../tests/helpers/cleanup"; import { getFixture, makeDeferred, patchWithCleanup } from "@web/../tests/helpers/utils"; import { doAction, getActionManagerServerData } from "@web/../tests/webclient/helpers"; -import { App, EventBus } from '@odoo/owl'; +import { App, EventBus } from "@odoo/owl"; const { afterNextRender } = App; //------------------------------------------------------------------------------ @@ -27,11 +27,11 @@ const { afterNextRender } = App; */ function _createFakeDataTransfer(files) { return { - dropEffect: 'all', - effectAllowed: 'all', + dropEffect: "all", + effectAllowed: "all", files, items: [], - types: ['Files'], + types: ["Files"], }; } @@ -80,7 +80,7 @@ function getAfterEvent({ messagingBus }) { }); // Set up the promise to resolve if the event is triggered. const eventProm = makeDeferred(); - const eventHandler = ev => { + const eventHandler = (ev) => { if (!predicate || predicate(ev.detail)) { eventProm.resolve(); } @@ -111,7 +111,7 @@ function getClick({ afterNextRender }) { function getMouseenter({ afterNextRender }) { return async function mouseenter(selector) { await afterNextRender(() => - document.querySelector(selector).dispatchEvent(new window.MouseEvent('mouseenter')) + document.querySelector(selector).dispatchEvent(new window.MouseEvent("mouseenter")) ); }; } @@ -123,56 +123,61 @@ function getOpenDiscuss(afterEvent, webClient, { context = {}, params, ...props id: 104, context, params, - tag: 'mail.action_discuss', - type: 'ir.actions.client', + tag: "mail.action_discuss", + type: "ir.actions.client", }; if (waitUntilMessagesLoaded) { let threadId = context.active_id; - if (typeof threadId === 'string') { - threadId = parseInt(threadId.split('_')[1]); + if (typeof threadId === "string") { + threadId = parseInt(threadId.split("_")[1]); } - return afterNextRender(() => afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => doAction(webClient, actionOpenDiscuss, { props }), - message: "should wait until discuss loaded its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - (!threadId || threadViewer.thread.id === threadId) - ); - }, - })); + return afterNextRender(() => + afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => doAction(webClient, actionOpenDiscuss, { props }), + message: "should wait until discuss loaded its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + (!threadId || threadViewer.thread.id === threadId) + ); + }, + }) + ); } return afterNextRender(() => doAction(webClient, actionOpenDiscuss, { props })); }; } function getOpenFormView(afterEvent, openView) { - return async function openFormView(action, { props, waitUntilDataLoaded = true, waitUntilMessagesLoaded = true } = {}) { - action['views'] = [[false, 'form']]; + return async function openFormView( + action, + { props, waitUntilDataLoaded = true, waitUntilMessagesLoaded = true } = {} + ) { + action["views"] = [[false, "form"]]; const func = () => openView(action, props); - const waitData = func => afterNextRender(() => afterEvent({ - eventName: 'o-thread-loaded-data', - func, - message: "should wait until chatter loaded its data", - predicate: ({ thread }) => { - return ( - thread.model === action.res_model && - thread.id === action.res_id - ); - }, - })); - const waitMessages = func => afterNextRender(() => afterEvent({ - eventName: 'o-thread-loaded-messages', - func, - message: "should wait until chatter loaded its messages", - predicate: ({ thread }) => { - return ( - thread.model === action.res_model && - thread.id === action.res_id - ); - }, - })); + const waitData = (func) => + afterNextRender(() => + afterEvent({ + eventName: "o-thread-loaded-data", + func, + message: "should wait until chatter loaded its data", + predicate: ({ thread }) => { + return thread.model === action.res_model && thread.id === action.res_id; + }, + }) + ); + const waitMessages = (func) => + afterNextRender(() => + afterEvent({ + eventName: "o-thread-loaded-messages", + func, + message: "should wait until chatter loaded its messages", + predicate: ({ thread }) => { + return thread.model === action.res_model && thread.id === action.res_id; + }, + }) + ); if (waitUntilDataLoaded && waitUntilMessagesLoaded) { return waitData(() => waitMessages(func)); } @@ -224,19 +229,17 @@ function getOpenFormView(afterEvent, openView) { async function start(param0 = {}) { // patch _.debounce and _.throttle to be fast and synchronous. patchWithCleanup(_, { - debounce: func => func, - throttle: func => func, + debounce: (func) => func, + throttle: (func) => func, }); - const { - discuss = {}, - hasTimeControl, - waitUntilMessagingCondition = 'initialized', - } = param0; + const { discuss = {}, hasTimeControl, waitUntilMessagingCondition = "initialized" } = param0; const advanceTime = hasTimeControl ? getAdvanceTime() : undefined; - const target = param0['target'] || getFixture(); - param0['target'] = target; - if (!['none', 'created', 'initialized'].includes(waitUntilMessagingCondition)) { - throw Error(`Unknown parameter value ${waitUntilMessagingCondition} for 'waitUntilMessaging'.`); + const target = param0["target"] || getFixture(); + param0["target"] = target; + if (!["none", "created", "initialized"].includes(waitUntilMessagingCondition)) { + throw Error( + `Unknown parameter value ${waitUntilMessagingCondition} for 'waitUntilMessaging'.` + ); } const messagingBus = new EventBus(); const afterEvent = getAfterEvent({ messagingBus }); @@ -248,10 +251,10 @@ async function start(param0 = {}) { let webClient; await afterNextRender(async () => { webClient = await getWebClientReady({ ...param0, messagingBus }); - if (waitUntilMessagingCondition === 'created') { + if (waitUntilMessagingCondition === "created") { await webClient.env.services.messaging.modelManager.messagingCreatedPromise; } - if (waitUntilMessagingCondition === 'initialized') { + if (waitUntilMessagingCondition === "initialized") { await webClient.env.services.messaging.modelManager.messagingCreatedPromise; await webClient.env.services.messaging.modelManager.messagingInitializedPromise; } @@ -266,7 +269,7 @@ async function start(param0 = {}) { delete owl.Component.env; }); const openView = async (action, options) => { - action['type'] = action['type'] || 'ir.actions.act_window'; + action["type"] = action["type"] || "ir.actions.act_window"; await afterNextRender(() => doAction(webClient, action, { props: options })); }; return { @@ -298,8 +301,8 @@ async function start(param0 = {}) { * @see testUtils.file.createFile */ function dragenterFiles(el, files) { - const ev = new Event('dragenter', { bubbles: true }); - Object.defineProperty(ev, 'dataTransfer', { + const ev = new Event("dragenter", { bubbles: true }); + Object.defineProperty(ev, "dataTransfer", { value: _createFakeDataTransfer(files), }); el.dispatchEvent(ev); @@ -313,8 +316,8 @@ function dragenterFiles(el, files) { * @see testUtils.file.createFile */ function dropFiles(el, files) { - const ev = new Event('drop', { bubbles: true }); - Object.defineProperty(ev, 'dataTransfer', { + const ev = new Event("drop", { bubbles: true }); + Object.defineProperty(ev, "dataTransfer", { value: _createFakeDataTransfer(files), }); el.dispatchEvent(ev); @@ -328,8 +331,8 @@ function dropFiles(el, files) { * @see testUtils.file.createFile */ function pasteFiles(el, files) { - const ev = new Event('paste', { bubbles: true }); - Object.defineProperty(ev, 'clipboardData', { + const ev = new Event("paste", { bubbles: true }); + Object.defineProperty(ev, "clipboardData", { value: _createFakeDataTransfer(files), }); el.dispatchEvent(ev); @@ -343,13 +346,17 @@ function pasteFiles(el, files) { * @param {string} selector * @param {string} content */ - async function insertText(selector, content) { +async function insertText(selector, content) { await afterNextRender(() => { document.querySelector(selector).focus(); for (const char of content) { - document.execCommand('insertText', false, char); - document.querySelector(selector).dispatchEvent(new window.KeyboardEvent('keydown', { key: char })); - document.querySelector(selector).dispatchEvent(new window.KeyboardEvent('keyup', { key: char })); + document.execCommand("insertText", false, char); + document + .querySelector(selector) + .dispatchEvent(new window.KeyboardEvent("keydown", { key: char })); + document + .querySelector(selector) + .dispatchEvent(new window.KeyboardEvent("keyup", { key: char })); } }); } diff --git a/addons/mail/static/tests/helpers/time_control.js b/addons/mail/static/tests/helpers/time_control.js index 600735b5aa8..682c5e7fbc7 100644 --- a/addons/mail/static/tests/helpers/time_control.js +++ b/addons/mail/static/tests/helpers/time_control.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { nextTick } from '@mail/utils/utils'; +import { nextTick } from "@mail/utils/utils"; -import { browser } from '@web/core/browser/browser'; +import { browser } from "@web/core/browser/browser"; import { patchWithCleanup } from "@web/../tests/helpers/utils"; export function getAdvanceTime() { @@ -11,12 +11,12 @@ export function getAdvanceTime() { // key: timeoutId, value: func + remaining duration const timeouts = new Map(); patchWithCleanup(browser, { - clearTimeout: id => { + clearTimeout: (id) => { timeouts.delete(id); - timedOutIds = timedOutIds.filter(i => i !== id); + timedOutIds = timedOutIds.filter((i) => i !== id); }, setTimeout: (func, duration) => { - const timeoutId = _.uniqueId('timeout_'); + const timeoutId = _.uniqueId("timeout_"); const timeout = { id: timeoutId, isTimedOut: false, diff --git a/addons/mail/static/tests/helpers/webclient_setup.js b/addons/mail/static/tests/helpers/webclient_setup.js index 722ad413d9f..b88d36b9ba6 100644 --- a/addons/mail/static/tests/helpers/webclient_setup.js +++ b/addons/mail/static/tests/helpers/webclient_setup.js @@ -1,44 +1,55 @@ /** @odoo-module **/ -import { busService } from '@bus/services/bus_service'; -import { imStatusService } from '@bus/im_status_service'; -import { multiTabService } from '@bus/multi_tab_service'; -import { makeMultiTabToLegacyEnv } from '@bus/services/legacy/make_multi_tab_to_legacy_env'; -import { makeBusServiceToLegacyEnv } from '@bus/services/legacy/make_bus_service_to_legacy_env'; -import { makeFakePresenceService } from '@bus/../tests/helpers/mock_services'; +import { busService } from "@bus/services/bus_service"; +import { imStatusService } from "@bus/im_status_service"; +import { multiTabService } from "@bus/multi_tab_service"; +import { makeMultiTabToLegacyEnv } from "@bus/services/legacy/make_multi_tab_to_legacy_env"; +import { makeBusServiceToLegacyEnv } from "@bus/services/legacy/make_bus_service_to_legacy_env"; +import { makeFakePresenceService } from "@bus/../tests/helpers/mock_services"; -import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container'; -import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container'; -import { DiscussContainer } from '@mail/components/discuss_container/discuss_container'; -import { PopoverManagerContainer } from '@mail/components/popover_manager_container/popover_manager_container'; -import { messagingService } from '@mail/services/messaging_service'; -import { systrayService } from '@mail/services/systray_service'; -import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env'; +import { ChatWindowManagerContainer } from "@mail/components/chat_window_manager_container/chat_window_manager_container"; +import { DialogManagerContainer } from "@mail/components/dialog_manager_container/dialog_manager_container"; +import { DiscussContainer } from "@mail/components/discuss_container/discuss_container"; +import { PopoverManagerContainer } from "@mail/components/popover_manager_container/popover_manager_container"; +import { messagingService } from "@mail/services/messaging_service"; +import { systrayService } from "@mail/services/systray_service"; +import { makeMessagingToLegacyEnv } from "@mail/utils/make_messaging_to_legacy_env"; -import { registry } from '@web/core/registry'; +import { registry } from "@web/core/registry"; import { patchWithCleanup } from "@web/../tests/helpers/utils"; import { createWebClient } from "@web/../tests/webclient/helpers"; const ROUTES_TO_IGNORE = [ - '/web/webclient/load_menus', - '/web/dataset/call_kw/res.users/load_views', - '/web/dataset/call_kw/res.users/systray_get_activities' + "/web/webclient/load_menus", + "/web/dataset/call_kw/res.users/load_views", + "/web/dataset/call_kw/res.users/systray_get_activities", ]; -const WEBCLIENT_PARAMETER_NAMES = new Set(['legacyParams', 'mockRPC', 'serverData', 'target', 'webClientClass']); +const WEBCLIENT_PARAMETER_NAMES = new Set([ + "legacyParams", + "mockRPC", + "serverData", + "target", + "webClientClass", +]); const SERVICES_PARAMETER_NAMES = new Set([ - 'legacyServices', 'loadingBaseDelayDuration', 'messagingBeforeCreationDeferred', - 'messagingBus', 'services', + "legacyServices", + "loadingBaseDelayDuration", + "messagingBeforeCreationDeferred", + "messagingBus", + "services", ]); /** * Add required components to the main component registry. */ - function setupMainComponentRegistry() { - const mainComponentRegistry = registry.category('main_components'); - mainComponentRegistry.add('ChatWindowManagerContainer', { Component: ChatWindowManagerContainer }); - mainComponentRegistry.add('DialogManagerContainer', { Component: DialogManagerContainer }); - registry.category('actions').add('mail.action_discuss', DiscussContainer); - mainComponentRegistry.add('PopoverManagerContainer', { Component: PopoverManagerContainer }); +function setupMainComponentRegistry() { + const mainComponentRegistry = registry.category("main_components"); + mainComponentRegistry.add("ChatWindowManagerContainer", { + Component: ChatWindowManagerContainer, + }); + mainComponentRegistry.add("DialogManagerContainer", { Component: DialogManagerContainer }); + registry.category("actions").add("mail.action_discuss", DiscussContainer); + mainComponentRegistry.add("PopoverManagerContainer", { Component: PopoverManagerContainer }); } /** @@ -59,8 +70,8 @@ function setupMessagingServiceRegistries({ messagingBeforeCreationDeferred = Promise.resolve(), messagingBus, services, - }) { - const serviceRegistry = registry.category('services'); +}) { + const serviceRegistry = registry.category("services"); patchWithCleanup(messagingService, { async _startModelManager(modelManager, messagingValues) { @@ -80,7 +91,7 @@ function setupMessagingServiceRegistries({ messagingBus, userNotificationManager: { canPlayAudio: false }, }; - } + }, }; services = { @@ -100,9 +111,15 @@ function setupMessagingServiceRegistries({ serviceRegistry.add(serviceName, service); }); - registry.category('wowlToLegacyServiceMappers').add('bus_service_to_legacy_env', makeBusServiceToLegacyEnv); - registry.category('wowlToLegacyServiceMappers').add('multi_tab_to_legacy_env', makeMultiTabToLegacyEnv); - registry.category('wowlToLegacyServiceMappers').add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv); + registry + .category("wowlToLegacyServiceMappers") + .add("bus_service_to_legacy_env", makeBusServiceToLegacyEnv); + registry + .category("wowlToLegacyServiceMappers") + .add("multi_tab_to_legacy_env", makeMultiTabToLegacyEnv); + registry + .category("wowlToLegacyServiceMappers") + .add("messaging_service_to_legacy_env", makeMessagingToLegacyEnv); } /** @@ -117,7 +134,7 @@ function setupMessagingServiceRegistries({ * @param {EventBus} [param0.messagingBus] The event bus to be used by messaging. * @returns {WebClient} */ - async function getWebClientReady(param0) { +async function getWebClientReady(param0) { setupMainComponentRegistry(); const servicesParameters = {}; @@ -138,7 +155,4 @@ function setupMessagingServiceRegistries({ return createWebClient(webClientParameters); } -export { - getWebClientReady, - ROUTES_TO_IGNORE, -}; +export { getWebClientReady, ROUTES_TO_IGNORE }; diff --git a/addons/mail/static/tests/models/autocomplete_input_view.js b/addons/mail/static/tests/models/autocomplete_input_view.js index fe38faf2af0..675cdb371dd 100644 --- a/addons/mail/static/tests/models/autocomplete_input_view.js +++ b/addons/mail/static/tests/models/autocomplete_input_view.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { Patch } from '@mail/model'; +import { Patch } from "@mail/model"; Patch({ - name: 'AutocompleteInputView', + name: "AutocompleteInputView", recordMethods: { onSource(req, res) { this._super(req, res); - this.messaging.messagingBus.trigger('o-AutocompleteInput-source'); + this.messaging.messagingBus.trigger("o-AutocompleteInput-source"); }, }, }); diff --git a/addons/mail/static/tests/models/clock_watcher_qunit_tests.js b/addons/mail/static/tests/models/clock_watcher_qunit_tests.js index 5ddd70c94d5..4a990588cfc 100644 --- a/addons/mail/static/tests/models/clock_watcher_qunit_tests.js +++ b/addons/mail/static/tests/models/clock_watcher_qunit_tests.js @@ -1,13 +1,13 @@ /** @odoo-module **/ -import { one, Patch } from '@mail/model'; +import { one, Patch } from "@mail/model"; Patch({ - name: 'ClockWatcher', + name: "ClockWatcher", fields: { - qunitTestOwner: one('QUnitTest', { + qunitTestOwner: one("QUnitTest", { identifying: true, - inverse: 'clockWatcher', + inverse: "clockWatcher", }), }, }); diff --git a/addons/mail/static/tests/models/emoji_registry_qunit_tests.js b/addons/mail/static/tests/models/emoji_registry_qunit_tests.js index cc2830601b8..9cb5d74bc56 100644 --- a/addons/mail/static/tests/models/emoji_registry_qunit_tests.js +++ b/addons/mail/static/tests/models/emoji_registry_qunit_tests.js @@ -1,98 +1,71 @@ /** @odoo-module **/ -import { Patch } from '@mail/model'; +import { Patch } from "@mail/model"; Patch({ - name: 'EmojiRegistry', + name: "EmojiRegistry", recordMethods: { async loadEmojiData() { const dataEmojiCategories = [ - { - "name": "Smileys & Emotion", - "title": "🤠", - "sortId": 1 - }, - { - "name": "People & Body", - "title": "🤟", - "sortId": 2 - }]; - const dataEmojis = [ { - "codepoints": "😀", - "name": "grinning face", - "shortcodes": [ - ":grinning:" - ], - "emoticons": [], - "category": "Smileys & Emotion", - "keywords": [ - "face", - "grin", - "grinning face" - ] + name: "Smileys & Emotion", + title: "🤠", + sortId: 1, }, { - "codepoints": "🤣", - "name": "rolling on the floor laughing", - "shortcodes": [ - ":rofl:" - ], - "emoticons": [], - "category": "Smileys & Emotion", - "keywords": [ + name: "People & Body", + title: "🤟", + sortId: 2, + }, + ]; + const dataEmojis = [ + { + codepoints: "😀", + name: "grinning face", + shortcodes: [":grinning:"], + emoticons: [], + category: "Smileys & Emotion", + keywords: ["face", "grin", "grinning face"], + }, + { + codepoints: "🤣", + name: "rolling on the floor laughing", + shortcodes: [":rofl:"], + emoticons: [], + category: "Smileys & Emotion", + keywords: [ "face", "floor", "laugh", "rofl", "rolling", "rolling on the floor laughing", - "rotfl" - ] + "rotfl", + ], }, { - "codepoints": "😊", - "name": "smiling face with smiling eyes", - "shortcodes": [ - ":smiling_face_with_smiling_eyes:" - ], - "emoticons": [], - "category": "Smileys & Emotion", - "keywords": [ - "blush", - "eye", - "face", - "smile", - "smiling face with smiling eyes" - ] + codepoints: "😊", + name: "smiling face with smiling eyes", + shortcodes: [":smiling_face_with_smiling_eyes:"], + emoticons: [], + category: "Smileys & Emotion", + keywords: ["blush", "eye", "face", "smile", "smiling face with smiling eyes"], }, { - "codepoints": "👋", - "name": "waving hand", - "shortcodes": [ - ":waving_hand:" - ], - "emoticons": [], - "category": "People & Body", - "keywords": [ - "hand", - "wave", - "waving" - ] + codepoints: "👋", + name: "waving hand", + shortcodes: [":waving_hand:"], + emoticons: [], + category: "People & Body", + keywords: ["hand", "wave", "waving"], }, { - "codepoints": "🤚", - "name": "raised back of hand", - "shortcodes": [ - ":raised_back_of_hand:" - ], - "emoticons": [], - "category": "People & Body", - "keywords": [ - "backhand", - "raised", - "raised back of hand" - ] + codepoints: "🤚", + name: "raised back of hand", + shortcodes: [":raised_back_of_hand:"], + emoticons: [], + category: "People & Body", + keywords: ["backhand", "raised", "raised back of hand"], }, ]; this._populateFromEmojiData(dataEmojiCategories, dataEmojis); diff --git a/addons/mail/static/tests/models/qunit_test.js b/addons/mail/static/tests/models/qunit_test.js index db6120e1bf8..da352906be6 100644 --- a/addons/mail/static/tests/models/qunit_test.js +++ b/addons/mail/static/tests/models/qunit_test.js @@ -1,24 +1,24 @@ /** @odoo-module **/ -import { one, Model } from '@mail/model'; +import { one, Model } from "@mail/model"; Model({ - name: 'QUnitTest', + name: "QUnitTest", fields: { - clockWatcher: one('ClockWatcher', { - inverse: 'qunitTestOwner', + clockWatcher: one("ClockWatcher", { + inverse: "qunitTestOwner", }), - throttle1: one('Throttle', { - inverse: 'qunitTestOwner1', + throttle1: one("Throttle", { + inverse: "qunitTestOwner1", }), - throttle2: one('Throttle', { - inverse: 'qunitTestOwner2', + throttle2: one("Throttle", { + inverse: "qunitTestOwner2", }), - timer1: one('Timer', { - inverse: 'qunitTestOwner1', + timer1: one("Timer", { + inverse: "qunitTestOwner1", }), - timer2: one('Timer', { - inverse: 'qunitTestOwner2', + timer2: one("Timer", { + inverse: "qunitTestOwner2", }), }, }); diff --git a/addons/mail/static/tests/models/test_models.js b/addons/mail/static/tests/models/test_models.js index 30c1f15bf82..1efe5104e8b 100644 --- a/addons/mail/static/tests/models/test_models.js +++ b/addons/mail/static/tests/models/test_models.js @@ -1,46 +1,43 @@ /** @odoo-module **/ -import { attr, many, one, Model } from '@mail/model'; +import { attr, many, one, Model } from "@mail/model"; Model({ - name: 'TestAddress', + name: "TestAddress", fields: { id: attr({ identifying: true, }), addressInfo: attr(), - contact: one('TestContact', { - inverse: 'address', + contact: one("TestContact", { + inverse: "address", }), }, }); Model({ - name: 'TestContact', + name: "TestContact", fields: { id: attr({ identifying: true, }), - address: one('TestAddress', { - inverse: 'contact', + address: one("TestAddress", { + inverse: "contact", }), - favorite: one('TestHobby', { - default: { description: 'football' }, + favorite: one("TestHobby", { + default: { description: "football" }, }), - hobbies: many('TestHobby', { - default: [ - { description: 'hiking' }, - { description: 'fishing' }, - ], + hobbies: many("TestHobby", { + default: [{ description: "hiking" }, { description: "fishing" }], }), - tasks: many('TestTask', { - inverse: 'responsible' + tasks: many("TestTask", { + inverse: "responsible", }), }, }); Model({ - name: 'TestHobby', + name: "TestHobby", fields: { description: attr({ identifying: true, @@ -49,7 +46,7 @@ Model({ }); Model({ - name: 'TestTask', + name: "TestTask", fields: { id: attr({ identifying: true, @@ -58,8 +55,8 @@ Model({ difficulty: attr({ default: 1, }), - responsible: one('TestContact', { - inverse: 'tasks' + responsible: one("TestContact", { + inverse: "tasks", }), }, }); diff --git a/addons/mail/static/tests/models/throttle_qunit_tests.js b/addons/mail/static/tests/models/throttle_qunit_tests.js index 9e6f64908e8..1e37257e1cd 100644 --- a/addons/mail/static/tests/models/throttle_qunit_tests.js +++ b/addons/mail/static/tests/models/throttle_qunit_tests.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { one, Patch } from '@mail/model'; +import { one, Patch } from "@mail/model"; Patch({ - name: 'Throttle', + name: "Throttle", fields: { duration: { compute() { @@ -16,13 +16,13 @@ Patch({ return this._super(); }, }, - qunitTestOwner1: one('QUnitTest', { + qunitTestOwner1: one("QUnitTest", { identifying: true, - inverse: 'throttle1', + inverse: "throttle1", }), - qunitTestOwner2: one('QUnitTest', { + qunitTestOwner2: one("QUnitTest", { identifying: true, - inverse: 'throttle2', + inverse: "throttle2", }), }, }); diff --git a/addons/mail/static/tests/models/timer_qunit_tests.js b/addons/mail/static/tests/models/timer_qunit_tests.js index bd1519cc174..bc74b23664c 100644 --- a/addons/mail/static/tests/models/timer_qunit_tests.js +++ b/addons/mail/static/tests/models/timer_qunit_tests.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { one, Patch } from '@mail/model'; +import { one, Patch } from "@mail/model"; Patch({ - name: 'Timer', + name: "Timer", fields: { duration: { compute() { @@ -16,13 +16,13 @@ Patch({ return this._super(); }, }, - qunitTestOwner1: one('QUnitTest', { + qunitTestOwner1: one("QUnitTest", { identifying: true, - inverse: 'timer1', + inverse: "timer1", }), - qunitTestOwner2: one('QUnitTest', { + qunitTestOwner2: one("QUnitTest", { identifying: true, - inverse: 'timer2', + inverse: "timer2", }), }, }); diff --git a/addons/mail/static/tests/qunit_mobile_suite_tests/components/discuss_mobile_mailbox_selection_tests.js b/addons/mail/static/tests/qunit_mobile_suite_tests/components/discuss_mobile_mailbox_selection_tests.js index 52087c9f166..cbca2afd468 100644 --- a/addons/mail/static/tests/qunit_mobile_suite_tests/components/discuss_mobile_mailbox_selection_tests.js +++ b/addons/mail/static/tests/qunit_mobile_suite_tests/components/discuss_mobile_mailbox_selection_tests.js @@ -1,101 +1,104 @@ /** @odoo-module **/ -import { patchUiSize } from '@mail/../tests/helpers/patch_ui_size'; -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { patchUiSize } from "@mail/../tests/helpers/patch_ui_size"; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_mobile_mailbox_selection', {}, function () { -QUnit.module('discuss_mobile_mailbox_selection_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_mobile_mailbox_selection", {}, function () { + QUnit.module("discuss_mobile_mailbox_selection_tests.js"); -QUnit.test('select another mailbox', async function (assert) { - assert.expect(7); + QUnit.test("select another mailbox", async function (assert) { + assert.expect(7); - patchUiSize({ height: 360, width: 640 }); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_DiscussView', - "should display discuss initially" - ); - assert.hasClass( - document.querySelector('.o_DiscussView'), - 'o-isDeviceSmall', - "discuss should be opened in mobile mode" - ); - assert.containsOnce( - document.body, - '.o_DiscussView_thread', - "discuss should display a thread initially" - ); - assert.strictEqual( - document.querySelector('.o_DiscussView_thread').dataset.threadId, - messaging.inbox.thread.id, - "inbox mailbox should be opened initially" - ); - assert.containsOnce( - document.body, - `.o_DiscussMobileMailboxSelectionItemView[ + patchUiSize({ height: 360, width: 640 }); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_DiscussView", + "should display discuss initially" + ); + assert.hasClass( + document.querySelector(".o_DiscussView"), + "o-isDeviceSmall", + "discuss should be opened in mobile mode" + ); + assert.containsOnce( + document.body, + ".o_DiscussView_thread", + "discuss should display a thread initially" + ); + assert.strictEqual( + document.querySelector(".o_DiscussView_thread").dataset.threadId, + messaging.inbox.thread.id, + "inbox mailbox should be opened initially" + ); + assert.containsOnce( + document.body, + `.o_DiscussMobileMailboxSelectionItemView[ data-mailbox-local-id="${messaging.starred.localId}" ]`, - "should have a button to open starred mailbox" - ); + "should have a button to open starred mailbox" + ); - await click(`.o_DiscussMobileMailboxSelectionItemView[ + await click(`.o_DiscussMobileMailboxSelectionItemView[ data-mailbox-local-id="${messaging.starred.localId}"] `); - assert.containsOnce( - document.body, - '.o_DiscussView_thread', - "discuss should still have a thread after clicking on starred mailbox" - ); - assert.strictEqual( - document.querySelector('.o_DiscussView_thread').dataset.threadId, - messaging.starred.thread.id, - "starred mailbox should be opened after clicking on it" - ); -}); + assert.containsOnce( + document.body, + ".o_DiscussView_thread", + "discuss should still have a thread after clicking on starred mailbox" + ); + assert.strictEqual( + document.querySelector(".o_DiscussView_thread").dataset.threadId, + messaging.starred.thread.id, + "starred mailbox should be opened after clicking on it" + ); + }); -QUnit.test('auto-select "Inbox" when discuss had channel as active thread', async function (assert) { - assert.expect(3); + QUnit.test( + 'auto-select "Inbox" when discuss had channel as active thread', + async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); - patchUiSize({ height: 360, width: 640 }); - const { click, messaging, openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, + patchUiSize({ height: 360, width: 640 }); + const { click, messaging, openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.hasClass( + document.querySelector( + '.o_MobileMessagingNavbarView_tab[data-tab-id="channel"]' + ), + "o-active", + "'channel' tab should be active initially when loading discuss with channel id as active_id" + ); + + await click('.o_MobileMessagingNavbarView_tab[data-tab-id="mailbox"]'); + assert.hasClass( + document.querySelector( + '.o_MobileMessagingNavbarView_tab[data-tab-id="mailbox"]' + ), + "o-active", + "'mailbox' tab should be selected after click on mailbox tab" + ); + assert.hasClass( + document.querySelector( + `.o_DiscussMobileMailboxSelectionItemView[data-mailbox-local-id="${messaging.inbox.localId}"]` + ), + "o-active", + "'Inbox' mailbox should be auto-selected after click on mailbox tab" + ); + } + ); + }); }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.hasClass( - document.querySelector('.o_MobileMessagingNavbarView_tab[data-tab-id="channel"]'), - 'o-active', - "'channel' tab should be active initially when loading discuss with channel id as active_id" - ); - - await click('.o_MobileMessagingNavbarView_tab[data-tab-id="mailbox"]'); - assert.hasClass( - document.querySelector('.o_MobileMessagingNavbarView_tab[data-tab-id="mailbox"]'), - 'o-active', - "'mailbox' tab should be selected after click on mailbox tab" - ); - assert.hasClass( - document.querySelector(`.o_DiscussMobileMailboxSelectionItemView[data-mailbox-local-id="${ - messaging.inbox.localId - }"]`), - 'o-active', - "'Inbox' mailbox should be auto-selected after click on mailbox tab" - ); -}); - -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js b/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js index f3fc9444434..af1c323fd82 100644 --- a/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/chatter_tests.js @@ -1,390 +1,486 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; -import { ROUTES_TO_IGNORE } from '@mail/../tests/helpers/webclient_setup'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; +import { ROUTES_TO_IGNORE } from "@mail/../tests/helpers/webclient_setup"; -import testUtils from 'web.test_utils'; -import { patchWithCleanup, selectDropdownItem } from '@web/../tests/helpers/utils'; +import testUtils from "web.test_utils"; +import { patchWithCleanup, selectDropdownItem } from "@web/../tests/helpers/utils"; import { ListController } from "@web/views/list/list_controller"; -QUnit.module('mail', {}, function () { -QUnit.module('Chatter'); +QUnit.module("mail", {}, function () { + QUnit.module("Chatter"); -QUnit.test('list activity widget with no activity', async function (assert) { - assert.expect(4); + QUnit.test("list activity widget with no activity", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const views = { - 'res.users,false,list': '', - }; - const { openView } = await start({ - mockRPC: function (route, args) { - if ( - args.method !== 'get_views' && - !['/mail/init_messaging', '/mail/load_message_failures', '/bus/im_status', ...ROUTES_TO_IGNORE].includes(route) - ) { - assert.step(route); - } - }, - serverData: { views }, - session: { uid: pyEnv.currentUserId }, - }); - await openView({ - res_model: 'res.users', - views: [[false, 'list']], + const pyEnv = await startServer(); + const views = { + "res.users,false,list": + '', + }; + const { openView } = await start({ + mockRPC: function (route, args) { + if ( + args.method !== "get_views" && + ![ + "/mail/init_messaging", + "/mail/load_message_failures", + "/bus/im_status", + ...ROUTES_TO_IGNORE, + ].includes(route) + ) { + assert.step(route); + } + }, + serverData: { views }, + session: { uid: pyEnv.currentUserId }, + }); + await openView({ + res_model: "res.users", + views: [[false, "list"]], + }); + + assert.containsOnce(document.body, ".o_ActivityButtonView_icon.text-muted"); + assert.strictEqual( + document.querySelector(".o_ListFieldActivityView_summary").innerText, + "" + ); + + assert.verifySteps(["/web/dataset/call_kw/res.users/web_search_read"]); }); - assert.containsOnce(document.body, '.o_ActivityButtonView_icon.text-muted'); - assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, ''); + QUnit.test("list activity widget with activities", async function (assert) { + assert.expect(6); - assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']); -}); - -QUnit.test('list activity widget with activities', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [mailActivityId1, mailActivityId2] = pyEnv['mail.activity'].create([{}, {}]); - const [mailActivityTypeId1, mailActivityTypeId2] = pyEnv['mail.activity.type'].create( - [{ name: 'Type 1' }, { name: 'Type 2' }], - ); - pyEnv['res.users'].write([pyEnv.currentUserId], { - activity_ids: [mailActivityId1, mailActivityId2], - activity_state: 'today', - activity_summary: 'Call with Al', - activity_type_id: mailActivityTypeId1, - activity_type_icon: 'fa-phone', - }); - - pyEnv['res.users'].create({ - activity_ids: [mailActivityId2], - activity_state: 'planned', - activity_summary: false, - activity_type_id: mailActivityTypeId2, - }); - const views = { - 'res.users,false,list': '', - }; - - const { openView } = await start({ - mockRPC: function (route, args) { - if ( - args.method !== 'get_views' && - !['/mail/init_messaging', '/mail/load_message_failures', '/bus/im_status', ...ROUTES_TO_IGNORE].includes(route) - ) { - assert.step(route); - } - }, - serverData: { views }, - }); - await openView({ - res_model: 'res.users', - views: [[false, 'list']], - }); - - const firstRow = document.querySelector('.o_data_row'); - assert.containsOnce(firstRow, '.o_ActivityButtonView_icon.text-warning.fa-phone'); - assert.strictEqual(firstRow.querySelector('.o_ListFieldActivityView_summary').innerText, 'Call with Al'); - - const secondRow = document.querySelectorAll('.o_data_row')[1]; - assert.containsOnce(secondRow, '.o_ActivityButtonView_icon.text-success.fa-clock-o'); - assert.strictEqual(secondRow.querySelector('.o_ListFieldActivityView_summary').innerText, 'Type 2'); - - assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']); -}); - -QUnit.test('list activity widget with exception', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailActivityId1 = pyEnv['mail.activity'].create({}); - const mailActivityTypeId1 = pyEnv['mail.activity.type'].create({}); - pyEnv['res.users'].write([pyEnv.currentUserId], { - activity_ids: [mailActivityId1], - activity_state: 'today', - activity_summary: 'Call with Al', - activity_type_id: mailActivityTypeId1, - activity_exception_decoration: 'warning', - activity_exception_icon: 'fa-warning', - }); - - const views = { - 'res.users,false,list': '', - }; - const { openView } = await start({ - mockRPC: function (route, args) { - if ( - args.method !== 'get_views' && - !['/mail/init_messaging', '/mail/load_message_failures', '/bus/im_status', ...ROUTES_TO_IGNORE].includes(route) - ) { - assert.step(route); - } - }, - serverData: { views }, - }); - await openView({ - res_model: 'res.users', - views: [[false, 'list']], - }); - - assert.containsOnce(document.body, '.o_ActivityButtonView_icon.text-warning.fa-warning'); - assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, 'Warning'); - - assert.verifySteps(['/web/dataset/call_kw/res.users/web_search_read']); -}); - -QUnit.test('list activity widget: open dropdown', async function (assert) { - assert.expect(9); - - const pyEnv = await startServer(); - const [mailActivityTypeId1, mailActivityTypeId2] = pyEnv['mail.activity.type'].create([{}, {}]); - const [mailActivityId1, mailActivityId2] = pyEnv['mail.activity'].create([ - { - display_name: "Call with Al", - date_deadline: moment().format("YYYY-MM-DD"), // now - can_write: true, - state: "today", - user_id: pyEnv.currentUserId, - create_uid: pyEnv.currentUserId, + const pyEnv = await startServer(); + const [mailActivityId1, mailActivityId2] = pyEnv["mail.activity"].create([{}, {}]); + const [mailActivityTypeId1, mailActivityTypeId2] = pyEnv["mail.activity.type"].create([ + { name: "Type 1" }, + { name: "Type 2" }, + ]); + pyEnv["res.users"].write([pyEnv.currentUserId], { + activity_ids: [mailActivityId1, mailActivityId2], + activity_state: "today", + activity_summary: "Call with Al", activity_type_id: mailActivityTypeId1, - }, - { - display_name: "Meet FP", - date_deadline: moment().add(1, 'day').format("YYYY-MM-DD"), // tomorrow - can_write: true, - state: "planned", - user_id: pyEnv.currentUserId, - create_uid: pyEnv.currentUserId, + activity_type_icon: "fa-phone", + }); + + pyEnv["res.users"].create({ + activity_ids: [mailActivityId2], + activity_state: "planned", + activity_summary: false, activity_type_id: mailActivityTypeId2, - } - ]); - pyEnv['res.users'].write([pyEnv.currentUserId], { - activity_ids: [mailActivityId1, mailActivityId2], - activity_state: 'today', - activity_summary: 'Call with Al', - activity_type_id: mailActivityTypeId2, + }); + const views = { + "res.users,false,list": + '', + }; + + const { openView } = await start({ + mockRPC: function (route, args) { + if ( + args.method !== "get_views" && + ![ + "/mail/init_messaging", + "/mail/load_message_failures", + "/bus/im_status", + ...ROUTES_TO_IGNORE, + ].includes(route) + ) { + assert.step(route); + } + }, + serverData: { views }, + }); + await openView({ + res_model: "res.users", + views: [[false, "list"]], + }); + + const firstRow = document.querySelector(".o_data_row"); + assert.containsOnce(firstRow, ".o_ActivityButtonView_icon.text-warning.fa-phone"); + assert.strictEqual( + firstRow.querySelector(".o_ListFieldActivityView_summary").innerText, + "Call with Al" + ); + + const secondRow = document.querySelectorAll(".o_data_row")[1]; + assert.containsOnce(secondRow, ".o_ActivityButtonView_icon.text-success.fa-clock-o"); + assert.strictEqual( + secondRow.querySelector(".o_ListFieldActivityView_summary").innerText, + "Type 2" + ); + + assert.verifySteps(["/web/dataset/call_kw/res.users/web_search_read"]); }); - const views = { - 'res.users,false,list': '', - }; - const { click, openView } = await start({ - mockRPC: function (route, args) { - if ( - args.method !== 'get_views' && - !['/mail/init_messaging', '/mail/load_message_failures', '/bus/im_status', ...ROUTES_TO_IGNORE].includes(route) - ) { - assert.step(args.method || route); - } - if (args.method === 'action_feedback') { - pyEnv['res.users'].write([pyEnv.currentUserId], { - activity_ids: [mailActivityId2], - activity_state: 'planned', - activity_summary: 'Meet FP', - activity_type_id: mailActivityTypeId1, - }); - // random value returned in order for the mock server to know that this route is implemented. - return true; - } - }, - serverData: { views }, - }); + QUnit.test("list activity widget with exception", async function (assert) { + assert.expect(4); - patchWithCleanup(ListController.prototype, { - setup() { - this._super(); - const selectRecord = this.props.selectRecord; - this.props.selectRecord = (...args) => { - assert.step(`select_record ${JSON.stringify(args)}`); - return selectRecord(...args); - }; - } - }); - - await openView({ - res_model: 'res.users', - views: [[false, 'list']], - }); - - assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, 'Call with Al'); - - await click('.o_ActivityButtonView'); // open the popover - await click('.o_ActivityListViewItem_markAsDone'); // mark the first activity as done - await click('.o_ActivityMarkDonePopoverContentView_doneButton'); // confirm - - assert.strictEqual(document.querySelector('.o_ListFieldActivityView_summary').innerText, 'Meet FP'); - - assert.verifySteps([ - 'web_search_read', - 'activity_format', - 'action_feedback', - '/mail/thread/messages', - '/mail/thread/data', - 'web_search_read', - ]); -}); - -QUnit.test('list activity exception widget with activity', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const [mailActivityTypeId1, mailActivityTypeId2] = pyEnv['mail.activity.type'].create([{}, {}]); - const [mailActivityId1, mailActivityId2] = pyEnv['mail.activity'].create([ - { - display_name: "An activity", - date_deadline: moment().format("YYYY-MM-DD"), // now - can_write: true, - state: "today", - user_id: pyEnv.currentUserId, - create_uid: pyEnv.currentUserId, + const pyEnv = await startServer(); + const mailActivityId1 = pyEnv["mail.activity"].create({}); + const mailActivityTypeId1 = pyEnv["mail.activity.type"].create({}); + pyEnv["res.users"].write([pyEnv.currentUserId], { + activity_ids: [mailActivityId1], + activity_state: "today", + activity_summary: "Call with Al", activity_type_id: mailActivityTypeId1, - }, - { - display_name: "An exception activity", - date_deadline: moment().format("YYYY-MM-DD"), // now - can_write: true, - state: "today", - user_id: pyEnv.currentUserId, - create_uid: pyEnv.currentUserId, - activity_type_id: mailActivityTypeId2, - } - ]); + activity_exception_decoration: "warning", + activity_exception_icon: "fa-warning", + }); - pyEnv['res.users'].write([pyEnv.currentUserId], { activity_ids: [mailActivityId1] }); - pyEnv['res.users'].create({ - message_attachment_count: 3, - display_name: "second partner", - message_follower_ids: [], - message_ids: [], - activity_ids: [mailActivityId2], - activity_exception_decoration: 'warning', - activity_exception_icon: 'fa-warning', + const views = { + "res.users,false,list": + '', + }; + const { openView } = await start({ + mockRPC: function (route, args) { + if ( + args.method !== "get_views" && + ![ + "/mail/init_messaging", + "/mail/load_message_failures", + "/bus/im_status", + ...ROUTES_TO_IGNORE, + ].includes(route) + ) { + assert.step(route); + } + }, + serverData: { views }, + }); + await openView({ + res_model: "res.users", + views: [[false, "list"]], + }); + + assert.containsOnce(document.body, ".o_ActivityButtonView_icon.text-warning.fa-warning"); + assert.strictEqual( + document.querySelector(".o_ListFieldActivityView_summary").innerText, + "Warning" + ); + + assert.verifySteps(["/web/dataset/call_kw/res.users/web_search_read"]); }); - const views = { - 'res.users,false,list': - ` + + QUnit.test("list activity widget: open dropdown", async function (assert) { + assert.expect(9); + + const pyEnv = await startServer(); + const [mailActivityTypeId1, mailActivityTypeId2] = pyEnv["mail.activity.type"].create([ + {}, + {}, + ]); + const [mailActivityId1, mailActivityId2] = pyEnv["mail.activity"].create([ + { + display_name: "Call with Al", + date_deadline: moment().format("YYYY-MM-DD"), // now + can_write: true, + state: "today", + user_id: pyEnv.currentUserId, + create_uid: pyEnv.currentUserId, + activity_type_id: mailActivityTypeId1, + }, + { + display_name: "Meet FP", + date_deadline: moment().add(1, "day").format("YYYY-MM-DD"), // tomorrow + can_write: true, + state: "planned", + user_id: pyEnv.currentUserId, + create_uid: pyEnv.currentUserId, + activity_type_id: mailActivityTypeId2, + }, + ]); + pyEnv["res.users"].write([pyEnv.currentUserId], { + activity_ids: [mailActivityId1, mailActivityId2], + activity_state: "today", + activity_summary: "Call with Al", + activity_type_id: mailActivityTypeId2, + }); + + const views = { + "res.users,false,list": + '', + }; + const { click, openView } = await start({ + mockRPC: function (route, args) { + if ( + args.method !== "get_views" && + ![ + "/mail/init_messaging", + "/mail/load_message_failures", + "/bus/im_status", + ...ROUTES_TO_IGNORE, + ].includes(route) + ) { + assert.step(args.method || route); + } + if (args.method === "action_feedback") { + pyEnv["res.users"].write([pyEnv.currentUserId], { + activity_ids: [mailActivityId2], + activity_state: "planned", + activity_summary: "Meet FP", + activity_type_id: mailActivityTypeId1, + }); + // random value returned in order for the mock server to know that this route is implemented. + return true; + } + }, + serverData: { views }, + }); + + patchWithCleanup(ListController.prototype, { + setup() { + this._super(); + const selectRecord = this.props.selectRecord; + this.props.selectRecord = (...args) => { + assert.step(`select_record ${JSON.stringify(args)}`); + return selectRecord(...args); + }; + }, + }); + + await openView({ + res_model: "res.users", + views: [[false, "list"]], + }); + + assert.strictEqual( + document.querySelector(".o_ListFieldActivityView_summary").innerText, + "Call with Al" + ); + + await click(".o_ActivityButtonView"); // open the popover + await click(".o_ActivityListViewItem_markAsDone"); // mark the first activity as done + await click(".o_ActivityMarkDonePopoverContentView_doneButton"); // confirm + + assert.strictEqual( + document.querySelector(".o_ListFieldActivityView_summary").innerText, + "Meet FP" + ); + + assert.verifySteps([ + "web_search_read", + "activity_format", + "action_feedback", + "/mail/thread/messages", + "/mail/thread/data", + "web_search_read", + ]); + }); + + QUnit.test("list activity exception widget with activity", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const [mailActivityTypeId1, mailActivityTypeId2] = pyEnv["mail.activity.type"].create([ + {}, + {}, + ]); + const [mailActivityId1, mailActivityId2] = pyEnv["mail.activity"].create([ + { + display_name: "An activity", + date_deadline: moment().format("YYYY-MM-DD"), // now + can_write: true, + state: "today", + user_id: pyEnv.currentUserId, + create_uid: pyEnv.currentUserId, + activity_type_id: mailActivityTypeId1, + }, + { + display_name: "An exception activity", + date_deadline: moment().format("YYYY-MM-DD"), // now + can_write: true, + state: "today", + user_id: pyEnv.currentUserId, + create_uid: pyEnv.currentUserId, + activity_type_id: mailActivityTypeId2, + }, + ]); + + pyEnv["res.users"].write([pyEnv.currentUserId], { activity_ids: [mailActivityId1] }); + pyEnv["res.users"].create({ + message_attachment_count: 3, + display_name: "second partner", + message_follower_ids: [], + message_ids: [], + activity_ids: [mailActivityId2], + activity_exception_decoration: "warning", + activity_exception_icon: "fa-warning", + }); + const views = { + "res.users,false,list": ` `, - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.users', - views: [[false, 'list']], + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.users", + views: [[false, "list"]], + }); + + assert.containsN(document.body, ".o_data_row", 2, "should have two records"); + assert.containsNone( + document.querySelectorAll(".o_data_row .o_activity_exception_cell")[0], + ".o_ActivityException", + "there is no any exception activity on record" + ); + assert.containsOnce( + document.querySelectorAll(".o_data_row .o_activity_exception_cell")[1], + ".o_ActivityException", + "there is an exception on a record" + ); }); - assert.containsN(document.body, '.o_data_row', 2, "should have two records"); - assert.containsNone(document.querySelectorAll('.o_data_row .o_activity_exception_cell')[0], '.o_ActivityException', "there is no any exception activity on record"); - assert.containsOnce(document.querySelectorAll('.o_data_row .o_activity_exception_cell')[1], '.o_ActivityException', "there is an exception on a record"); -}); + QUnit.module("FieldMany2ManyTagsEmail"); -QUnit.module('FieldMany2ManyTagsEmail'); + QUnit.test("fieldmany2many tags email (edition)", async function (assert) { + assert.expect(17); -QUnit.test('fieldmany2many tags email (edition)', async function (assert) { - assert.expect(17); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([ - { name: "gold", email: 'coucou@petite.perruche' }, - { name: "silver", email: '' }, - ]); - const mailMessageId1 = pyEnv['mail.message'].create({ - partner_ids: [resPartnerId1], - }); - const views = { - 'mail.message,false,form': - '
' + - '' + - '' + - '' + - '' + - '
', - 'res.partner,false,form': '
', - }; - var { openView } = await start({ - serverData: { views }, - mockRPC: function (route, args) { - if (args.method === 'read' && args.model === 'res.partner') { - assert.step(JSON.stringify(args.args[0])); - assert.ok(args.args[1].includes('email'), "should read the email"); - } else if (args.method === "get_formview_id") { - return false; + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { name: "gold", email: "coucou@petite.perruche" }, + { name: "silver", email: "" }, + ]); + const mailMessageId1 = pyEnv["mail.message"].create({ + partner_ids: [resPartnerId1], + }); + const views = { + "mail.message,false,form": + '
' + + "" + + '' + + '' + + "" + + "
", + "res.partner,false,form": + '
', + }; + var { openView } = await start({ + serverData: { views }, + mockRPC: function (route, args) { + if (args.method === "read" && args.model === "res.partner") { + assert.step(JSON.stringify(args.args[0])); + assert.ok(args.args[1].includes("email"), "should read the email"); + } else if (args.method === "get_formview_id") { + return false; + } + }, + }); + await openView( + { + res_id: mailMessageId1, + res_model: "mail.message", + views: [[false, "form"]], + }, + { + mode: "edit", } - }, + ); + + assert.verifySteps([`[${resPartnerId1}]`]); + assert.containsOnce( + document.body, + '.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0', + "should contain one tag" + ); + + // add an other existing tag + await selectDropdownItem(document.body, "partner_ids", "silver"); + + assert.strictEqual( + document.querySelectorAll(".modal-content .o_form_view").length, + 1, + "there should be one modal opened to edit the empty email" + ); + assert.strictEqual( + document.querySelector(".modal-content .o_form_view .o_input#name").value, + "silver", + "the opened modal in edit mode should be a form view dialog with the res.partner 14" + ); + assert.strictEqual( + document.querySelectorAll(".modal-content .o_form_view .o_input#email").length, + 1, + "there should be an email field in the modal" + ); + + // set the email and save the modal (will rerender the form view) + await testUtils.fields.editInput( + $(".modal-content .o_form_view .o_input#email"), + "coucou@petite.perruche" + ); + await testUtils.dom.click($(".modal-content .o_form_button_save")); + + assert.containsN( + document.body, + '.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0', + 2, + "should contain the second tag" + ); + const firstTag = document.querySelector( + '.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0' + ); + assert.strictEqual( + firstTag.querySelector(".o_badge_text").innerText, + "gold", + "tag should only show name" + ); + assert.hasAttrValue( + firstTag.querySelector(".o_badge_text"), + "title", + "coucou@petite.perruche", + "tag should show email address on mouse hover" + ); + // should have read resPartnerId2 three times: when opening the dropdown, when opening the modal, and + // after the save + assert.verifySteps([`[${resPartnerId2}]`, `[${resPartnerId2}]`, `[${resPartnerId2}]`]); }); - await openView( - { - res_id: mailMessageId1, - res_model: 'mail.message', - views: [[false, 'form']], - }, - { - mode: 'edit', - }, + + QUnit.test( + "many2many_tags_email widget can load more than 40 records", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const messagePartnerIds = []; + for (let i = 100; i < 200; i++) { + messagePartnerIds.push( + pyEnv["res.partner"].create({ display_name: `partner${i}` }) + ); + } + const mailMessageId1 = pyEnv["mail.message"].create({ + partner_ids: messagePartnerIds, + }); + const views = { + "mail.message,false,form": + '
', + }; + var { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_id: mailMessageId1, + res_model: "mail.message", + views: [[false, "form"]], + }); + + assert.strictEqual( + document.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length, + 100 + ); + + assert.containsOnce(document.body, ".o_form_editable"); + + // add a record to the relation + await selectDropdownItem(document.body, "partner_ids", "Public user"); + + assert.strictEqual( + document.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length, + 101 + ); + } ); - - assert.verifySteps([`[${resPartnerId1}]`]); - assert.containsOnce(document.body, '.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0', - "should contain one tag"); - - // add an other existing tag - await selectDropdownItem(document.body, 'partner_ids', "silver"); - - assert.strictEqual(document.querySelectorAll('.modal-content .o_form_view').length, 1, - "there should be one modal opened to edit the empty email"); - assert.strictEqual(document.querySelector(".modal-content .o_form_view .o_input#name").value, "silver", - "the opened modal in edit mode should be a form view dialog with the res.partner 14"); - assert.strictEqual(document.querySelectorAll(".modal-content .o_form_view .o_input#email").length, 1, - "there should be an email field in the modal"); - - // set the email and save the modal (will rerender the form view) - await testUtils.fields.editInput($('.modal-content .o_form_view .o_input#email'), 'coucou@petite.perruche'); - await testUtils.dom.click($('.modal-content .o_form_button_save')); - - assert.containsN(document.body, '.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0', 2, - "should contain the second tag"); - const firstTag = document.querySelector('.o_field_many2many_tags_email[name="partner_ids"] .badge.o_tag_color_0'); - assert.strictEqual(firstTag.querySelector('.o_badge_text').innerText, "gold", - "tag should only show name"); - assert.hasAttrValue(firstTag.querySelector('.o_badge_text'), 'title', "coucou@petite.perruche", - "tag should show email address on mouse hover"); - // should have read resPartnerId2 three times: when opening the dropdown, when opening the modal, and - // after the save - assert.verifySteps([`[${resPartnerId2}]`, `[${resPartnerId2}]`, `[${resPartnerId2}]`]); -}); - -QUnit.test('many2many_tags_email widget can load more than 40 records', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const messagePartnerIds = []; - for (let i = 100; i < 200; i++) { - messagePartnerIds.push(pyEnv['res.partner'].create({ display_name: `partner${i}` })); - } - const mailMessageId1 = pyEnv['mail.message'].create({ - partner_ids: messagePartnerIds, - }); - const views = { - 'mail.message,false,form': '
', - }; - var { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_id: mailMessageId1, - res_model: 'mail.message', - views: [[false, 'form']], - }); - - assert.strictEqual(document.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length, 100); - - assert.containsOnce(document.body, '.o_form_editable'); - - // add a record to the relation - await selectDropdownItem(document.body, 'partner_ids', "Public user"); - - assert.strictEqual(document.querySelectorAll('.o_field_widget[name="partner_ids"] .badge').length, 101); -}); - }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/activity_mark_done_popover_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/activity_mark_done_popover_tests.js index c89326cc495..ef1f617f749 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/activity_mark_done_popover_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/activity_mark_done_popover_tests.js @@ -1,295 +1,324 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('activity_mark_done_popover_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("activity_mark_done_popover_tests.js"); -QUnit.test('activity mark done popover simplest layout', async function (assert) { - assert.expect(6); + QUnit.test("activity mark done popover simplest layout", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - activity_category: 'not_upload_file', - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - await click('.o_ActivityView_markDoneButton'); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + activity_category: "not_upload_file", + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + await click(".o_ActivityView_markDoneButton"); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView', - "Popover component should be present" - ); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView_feedback', - "Popover component should contain the feedback textarea" - ); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView_buttons', - "Popover component should contain the action buttons" - ); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView_doneScheduleNextButton', - "Popover component should contain the done & schedule next button" - ); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView_doneButton', - "Popover component should contain the done button" - ); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView_discardButton', - "Popover component should contain the discard button" - ); -}); - -QUnit.test('activity with force next mark done popover simplest layout', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - activity_category: 'not_upload_file', - can_write: true, - chaining_type: 'trigger', - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - await click('.o_ActivityView_markDoneButton'); - - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView', - "Popover component should be present" - ); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView_feedback', - "Popover component should contain the feedback textarea" - ); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView_buttons', - "Popover component should contain the action buttons" - ); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView_doneScheduleNextButton', - "Popover component should contain the done & schedule next button" - ); - assert.containsNone( - document.body, - '.o_ActivityMarkDonePopoverContentView_doneButton', - "Popover component should NOT contain the done button" - ); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView_discardButton', - "Popover component should contain the discard button" - ); -}); - -QUnit.test('activity mark done popover mark done without feedback', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailActivityId1 = pyEnv['mail.activity'].create({ - activity_category: 'not_upload_file', - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start({ - async mockRPC(route, args) { - if (route === '/web/dataset/call_kw/mail.activity/action_feedback') { - assert.step('action_feedback'); - assert.strictEqual(args.args.length, 1); - assert.strictEqual(args.args[0].length, 1); - assert.strictEqual(args.args[0][0], mailActivityId1); - assert.strictEqual(args.kwargs.attachment_ids.length, 0); - assert.notOk(args.kwargs.feedback); - // random value returned in order for the mock server to know that this route is implemented. - return true; - } - if (route === '/web/dataset/call_kw/mail.activity/unlink') { - // 'unlink' on non-existing record raises a server crash - throw new Error("'unlink' RPC on activity must not be called (already unlinked from mark as done)"); - } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - await click('.o_ActivityView_markDoneButton'); - await click('.o_ActivityMarkDonePopoverContentView_doneButton'); - assert.verifySteps( - ['action_feedback'], - "Mark done and schedule next button should call the right rpc" - ); -}); - -QUnit.test('activity mark done popover mark done with feedback', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailActivityId1 = pyEnv['mail.activity'].create({ - activity_category: 'not_upload_file', - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start({ - async mockRPC(route, args) { - if (route === '/web/dataset/call_kw/mail.activity/action_feedback') { - assert.step('action_feedback'); - assert.strictEqual(args.args.length, 1); - assert.strictEqual(args.args[0].length, 1); - assert.strictEqual(args.args[0][0], mailActivityId1); - assert.strictEqual(args.kwargs.attachment_ids.length, 0); - assert.strictEqual(args.kwargs.feedback, 'This task is done'); - // random value returned in order for the mock server to know that this route is implemented. - return true; - } - if (route === '/web/dataset/call_kw/mail.activity/unlink') { - // 'unlink' on non-existing record raises a server crash - throw new Error("'unlink' RPC on activity must not be called (already unlinked from mark as done)"); - } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - await click('.o_ActivityView_markDoneButton'); - - let feedbackTextarea = document.querySelector('.o_ActivityMarkDonePopoverContentView_feedback'); - feedbackTextarea.focus(); - document.execCommand('insertText', false, 'This task is done'); - document.querySelector('.o_ActivityMarkDonePopoverContentView_doneButton').click(); - assert.verifySteps( - ['action_feedback'], - "Mark done and schedule next button should call the right rpc" - ); -}); - -QUnit.test('activity mark done popover mark done and schedule next', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailActivityId1 = pyEnv['mail.activity'].create({ - activity_category: 'not_upload_file', - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, env, openView } = await start({ - async mockRPC(route, args) { - if (route === '/web/dataset/call_kw/mail.activity/action_feedback_schedule_next') { - assert.step('action_feedback_schedule_next'); - assert.strictEqual(args.args.length, 1); - assert.strictEqual(args.args[0].length, 1); - assert.strictEqual(args.args[0][0], mailActivityId1); - assert.strictEqual(args.kwargs.feedback, 'This task is done'); - return false; - } - if (route === '/web/dataset/call_kw/mail.activity/unlink') { - // 'unlink' on non-existing record raises a server crash - throw new Error("'unlink' RPC on activity must not be called (already unlinked from mark as done)"); - } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction() { - assert.step('activity_action'); - throw new Error("The do-action event should not be triggered when the route doesn't return an action"); - }, - }); - await click('.o_ActivityView_markDoneButton'); - - let feedbackTextarea = document.querySelector('.o_ActivityMarkDonePopoverContentView_feedback'); - feedbackTextarea.focus(); - document.execCommand('insertText', false, 'This task is done'); - await click('.o_ActivityMarkDonePopoverContentView_doneScheduleNextButton'); - assert.verifySteps( - ['action_feedback_schedule_next'], - "Mark done and schedule next button should call the right rpc and not trigger an action" - ); -}); - -QUnit.test('[technical] activity mark done & schedule next with new action', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - activity_category: 'not_upload_file', - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, env, openView } = await start({ - async mockRPC(route, args) { - if (route === '/web/dataset/call_kw/mail.activity/action_feedback_schedule_next') { - return { type: 'ir.actions.act_window' }; - } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.step('activity_action'); - assert.deepEqual( - action, - { type: 'ir.actions.act_window' }, - "The content of the action should be correct" + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView", + "Popover component should be present" ); - }, + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView_feedback", + "Popover component should contain the feedback textarea" + ); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView_buttons", + "Popover component should contain the action buttons" + ); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView_doneScheduleNextButton", + "Popover component should contain the done & schedule next button" + ); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView_doneButton", + "Popover component should contain the done button" + ); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView_discardButton", + "Popover component should contain the discard button" + ); + }); + + QUnit.test( + "activity with force next mark done popover simplest layout", + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + activity_category: "not_upload_file", + can_write: true, + chaining_type: "trigger", + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + await click(".o_ActivityView_markDoneButton"); + + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView", + "Popover component should be present" + ); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView_feedback", + "Popover component should contain the feedback textarea" + ); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView_buttons", + "Popover component should contain the action buttons" + ); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView_doneScheduleNextButton", + "Popover component should contain the done & schedule next button" + ); + assert.containsNone( + document.body, + ".o_ActivityMarkDonePopoverContentView_doneButton", + "Popover component should NOT contain the done button" + ); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView_discardButton", + "Popover component should contain the discard button" + ); + } + ); + + QUnit.test( + "activity mark done popover mark done without feedback", + async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailActivityId1 = pyEnv["mail.activity"].create({ + activity_category: "not_upload_file", + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, openView } = await start({ + async mockRPC(route, args) { + if (route === "/web/dataset/call_kw/mail.activity/action_feedback") { + assert.step("action_feedback"); + assert.strictEqual(args.args.length, 1); + assert.strictEqual(args.args[0].length, 1); + assert.strictEqual(args.args[0][0], mailActivityId1); + assert.strictEqual(args.kwargs.attachment_ids.length, 0); + assert.notOk(args.kwargs.feedback); + // random value returned in order for the mock server to know that this route is implemented. + return true; + } + if (route === "/web/dataset/call_kw/mail.activity/unlink") { + // 'unlink' on non-existing record raises a server crash + throw new Error( + "'unlink' RPC on activity must not be called (already unlinked from mark as done)" + ); + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + await click(".o_ActivityView_markDoneButton"); + await click(".o_ActivityMarkDonePopoverContentView_doneButton"); + assert.verifySteps( + ["action_feedback"], + "Mark done and schedule next button should call the right rpc" + ); + } + ); + + QUnit.test("activity mark done popover mark done with feedback", async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailActivityId1 = pyEnv["mail.activity"].create({ + activity_category: "not_upload_file", + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, openView } = await start({ + async mockRPC(route, args) { + if (route === "/web/dataset/call_kw/mail.activity/action_feedback") { + assert.step("action_feedback"); + assert.strictEqual(args.args.length, 1); + assert.strictEqual(args.args[0].length, 1); + assert.strictEqual(args.args[0][0], mailActivityId1); + assert.strictEqual(args.kwargs.attachment_ids.length, 0); + assert.strictEqual(args.kwargs.feedback, "This task is done"); + // random value returned in order for the mock server to know that this route is implemented. + return true; + } + if (route === "/web/dataset/call_kw/mail.activity/unlink") { + // 'unlink' on non-existing record raises a server crash + throw new Error( + "'unlink' RPC on activity must not be called (already unlinked from mark as done)" + ); + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + await click(".o_ActivityView_markDoneButton"); + + const feedbackTextarea = document.querySelector( + ".o_ActivityMarkDonePopoverContentView_feedback" + ); + feedbackTextarea.focus(); + document.execCommand("insertText", false, "This task is done"); + document.querySelector(".o_ActivityMarkDonePopoverContentView_doneButton").click(); + assert.verifySteps( + ["action_feedback"], + "Mark done and schedule next button should call the right rpc" + ); + }); + + QUnit.test( + "activity mark done popover mark done and schedule next", + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailActivityId1 = pyEnv["mail.activity"].create({ + activity_category: "not_upload_file", + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, env, openView } = await start({ + async mockRPC(route, args) { + if ( + route === + "/web/dataset/call_kw/mail.activity/action_feedback_schedule_next" + ) { + assert.step("action_feedback_schedule_next"); + assert.strictEqual(args.args.length, 1); + assert.strictEqual(args.args[0].length, 1); + assert.strictEqual(args.args[0][0], mailActivityId1); + assert.strictEqual(args.kwargs.feedback, "This task is done"); + return false; + } + if (route === "/web/dataset/call_kw/mail.activity/unlink") { + // 'unlink' on non-existing record raises a server crash + throw new Error( + "'unlink' RPC on activity must not be called (already unlinked from mark as done)" + ); + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction() { + assert.step("activity_action"); + throw new Error( + "The do-action event should not be triggered when the route doesn't return an action" + ); + }, + }); + await click(".o_ActivityView_markDoneButton"); + + const feedbackTextarea = document.querySelector( + ".o_ActivityMarkDonePopoverContentView_feedback" + ); + feedbackTextarea.focus(); + document.execCommand("insertText", false, "This task is done"); + await click(".o_ActivityMarkDonePopoverContentView_doneScheduleNextButton"); + assert.verifySteps( + ["action_feedback_schedule_next"], + "Mark done and schedule next button should call the right rpc and not trigger an action" + ); + } + ); + + QUnit.test( + "[technical] activity mark done & schedule next with new action", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + activity_category: "not_upload_file", + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, env, openView } = await start({ + async mockRPC(route, args) { + if ( + route === + "/web/dataset/call_kw/mail.activity/action_feedback_schedule_next" + ) { + return { type: "ir.actions.act_window" }; + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.step("activity_action"); + assert.deepEqual( + action, + { type: "ir.actions.act_window" }, + "The content of the action should be correct" + ); + }, + }); + await click(".o_ActivityView_markDoneButton"); + + await click(".o_ActivityMarkDonePopoverContentView_doneScheduleNextButton"); + assert.verifySteps( + ["activity_action"], + "The action returned by the route should be executed" + ); + } + ); }); - await click('.o_ActivityView_markDoneButton'); - - await click('.o_ActivityMarkDonePopoverContentView_doneScheduleNextButton'); - assert.verifySteps( - ['activity_action'], - "The action returned by the route should be executed" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js index f1812293122..96c9506867f 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/activity_tests.js @@ -1,1233 +1,1266 @@ /** @odoo-module **/ -import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -import { date_to_str } from 'web.time'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { date_to_str } from "web.time"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('activity_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("activity_tests.js"); -QUnit.test('activity simplest layout', async function (assert) { - assert.expect(12); + QUnit.test("activity simplest layout", async function (assert) { + assert.expect(12); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_sidebar').length, - 1, - "should have activity sidebar" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_core').length, - 1, - "should have activity core" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_user').length, - 1, - "should have activity user" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_info').length, - 1, - "should have activity info" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_note').length, - 0, - "should not have activity note" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_details').length, - 0, - "should not have activity details" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_mailTemplates').length, - 0, - "should not have activity mail templates" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_editButton').length, - 0, - "should not have activity Edit button" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_cancelButton').length, - 0, - "should not have activity Cancel button" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_markDoneButton').length, - 0, - "should not have activity Mark as Done button" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_uploadButton').length, - 0, - "should not have activity Upload button" - ); -}); - -QUnit.test('activity with note layout', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - note: 'There is no good or bad note', - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_note').length, - 1, - "should have activity note" - ); - assert.strictEqual( - document.querySelector('.o_ActivityView_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); - - const today = new Date(); - const fiveDaysFromNow = new Date(); - fiveDaysFromNow.setDate(today.getDate() + 5); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - date_deadline: date_to_str(fiveDaysFromNow), - res_id: resPartnerId1, - res_model: 'res.partner', - state: 'planned', - }); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_dueDateText').length, - 1, - "should have activity delay" - ); - assert.ok( - document.querySelector('.o_ActivityView_dueDateText').classList.contains('text-success'), - "activity delay should have the right color modifier class (text-success)" - ); - assert.strictEqual( - document.querySelector('.o_ActivityView_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); - - const today = new Date(); - const tomorrow = new Date(); - tomorrow.setDate(today.getDate() + 1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - date_deadline: date_to_str(tomorrow), - res_id: resPartnerId1, - res_model: 'res.partner', - state: 'planned', - }); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_dueDateText').length, - 1, - "should have activity delay" - ); - assert.ok( - document.querySelector('.o_ActivityView_dueDateText').classList.contains('text-success'), - "activity delay should have the right color modifier class (text-success)" - ); - assert.strictEqual( - document.querySelector('.o_ActivityView_dueDateText').textContent, - 'Tomorrow:', - "activity delay should have 'Tomorrow:' as label" - ); -}); - -QUnit.test('activity info layout when planned today', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - date_deadline: date_to_str(new Date()), - res_id: resPartnerId1, - res_model: 'res.partner', - state: 'today', - }); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_dueDateText').length, - 1, - "should have activity delay" - ); - assert.ok( - document.querySelector('.o_ActivityView_dueDateText').classList.contains('text-warning'), - "activity delay should have the right color modifier class (text-warning)" - ); - assert.strictEqual( - document.querySelector('.o_ActivityView_dueDateText').textContent, - "Today:", - "activity delay should have 'Today:' as label" - ); -}); - -QUnit.test('activity info layout when planned yesterday', async function (assert) { - assert.expect(4); - - const today = new Date(); - const yesterday = new Date(); - yesterday.setDate(today.getDate() - 1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - date_deadline: date_to_str(yesterday), - res_id: resPartnerId1, - res_model: 'res.partner', - state: 'overdue', - }); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_dueDateText').length, - 1, - "should have activity delay" - ); - assert.ok( - document.querySelector('.o_ActivityView_dueDateText').classList.contains('text-danger'), - "activity delay should have the right color modifier class (text-danger)" - ); - assert.strictEqual( - document.querySelector('.o_ActivityView_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); - - const today = new Date(); - const fiveDaysBeforeNow = new Date(); - fiveDaysBeforeNow.setDate(today.getDate() - 5); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - date_deadline: date_to_str(fiveDaysBeforeNow), - res_id: resPartnerId1, - res_model: 'res.partner', - state: 'overdue', - }); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_dueDateText').length, - 1, - "should have activity delay" - ); - assert.ok( - document.querySelector('.o_ActivityView_dueDateText').classList.contains('text-danger'), - "activity delay should have the right color modifier class (text-danger)" - ); - assert.strictEqual( - document.querySelector('.o_ActivityView_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); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - res_id: resPartnerId1, - res_model: 'res.partner', - summary: 'test summary', - }); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_summary').length, - 1, - "should have activity summary" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_type').length, - 0, - "should not have the activity type as summary" - ); - assert.strictEqual( - document.querySelector('.o_ActivityView_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); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.activity'].create({ - activity_type_id: 1, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_type').length, - 1, - "activity details should have an activity type section" - ); - assert.strictEqual( - document.querySelector('.o_ActivityView_type').textContent.trim(), - "Email", - "activity details should have the activity type display name in type section" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_summary.o_ActivityView_type').length, - 1, - "should have activity type as summary" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_summary:not(.o_ActivityView_type)').length, - 0, - "should not have a specific summary" - ); -}); - -QUnit.test('activity details toggle', async function (assert) { - assert.expect(5); - - const today = new Date(); - const tomorrow = new Date(); - tomorrow.setDate(today.getDate() + 1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const resUsersId1 = pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['mail.activity'].create({ - create_date: date_to_str(today), - create_uid: resUsersId1, - date_deadline: date_to_str(tomorrow), - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_details').length, - 0, - "activity details should not be visible by default" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_detailsButton').length, - 1, - "activity should have a details button" - ); - - await click('.o_ActivityView_detailsButton'); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_details').length, - 1, - "activity details should be visible after clicking on details button" - ); - - await click('.o_ActivityView_detailsButton'); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_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); - - const today = new Date(); - const tomorrow = new Date(); - tomorrow.setDate(today.getDate() + 1); - const pyEnv = await startServer(); - const resUsersId1 = pyEnv['res.users'].create({ name: 'Pauvre pomme' }); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - pyEnv['mail.activity'].create({ - activity_type_id: emailActivityTypeId, - create_date: date_to_str(today), - create_uid: resUsersId1, - date_deadline: date_to_str(tomorrow), - res_id: resPartnerId1, - res_model: 'res.partner', - state: 'planned', - user_id: resUsersId1, - }); - const { click, openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_userAvatar').length, - 1, - "should have activity user avatar" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_detailsButton').length, - 1, - "activity should have a details button" - ); - - await click('.o_ActivityView_detailsButton'); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_details').length, - 1, - "activity details should be visible after clicking on details button" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_details .o_ActivityView_type').length, - 1, - "activity details should have type" - ); - assert.strictEqual( - document.querySelector('.o_ActivityView_details .o_ActivityView_type').textContent, - "Email", - "activity details type should be 'Email'" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_detailsCreation').length, - 1, - "activity details should have creation date " - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_detailsCreator').length, - 1, - "activity details should have creator" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_detailsAssignation').length, - 1, - "activity details should have assignation information" - ); - assert.strictEqual( - document.querySelector('.o_ActivityView_detailsAssignation').textContent.indexOf('Pauvre pomme'), - 0, - "activity details assignation information should contain creator display name" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_detailsAssignationUserAvatar').length, - 1, - "activity details should have user avatar" - ); -}); - -QUnit.test('activity with mail template layout', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailTemplateId1 = pyEnv['mail.template'].create({ name: "Dummy mail template" }); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - pyEnv['mail.activity'].create({ - activity_type_id: emailActivityTypeId, - mail_template_ids: [mailTemplateId1], - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_sidebar').length, - 1, - "should have activity sidebar" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_mailTemplates').length, - 1, - "should have activity mail templates" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_mailTemplate').length, - 1, - "should have activity mail template" - ); - assert.strictEqual( - document.querySelectorAll('.o_MailTemplateView_name').length, - 1, - "should have activity mail template name" - ); - assert.strictEqual( - document.querySelector('.o_MailTemplateView_name').textContent, - "Dummy mail template", - "should have activity mail template name" - ); - assert.strictEqual( - document.querySelectorAll('.o_MailTemplateView_preview').length, - 1, - "should have activity mail template name preview button" - ); - assert.strictEqual( - document.querySelectorAll('.o_MailTemplateView_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 pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailTemplateId1 = pyEnv['mail.template'].create({ name: "Dummy mail template" }); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - pyEnv['mail.activity'].create({ - activity_type_id: emailActivityTypeId, - mail_template_ids: [mailTemplateId1], - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { env, openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.step('do_action'); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); assert.strictEqual( - action.context.default_res_id, - resPartnerId1, - 'Action should have the activity res id as default res id in context' + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" ); assert.strictEqual( - action.context.default_model, - 'res.partner', - 'Action should have the activity res model as default model in context' + document.querySelectorAll(".o_ActivityView_sidebar").length, + 1, + "should have activity sidebar" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_core").length, + 1, + "should have activity core" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_user").length, + 1, + "should have activity user" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_info").length, + 1, + "should have activity info" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_note").length, + 0, + "should not have activity note" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_details").length, + 0, + "should not have activity details" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_mailTemplates").length, + 0, + "should not have activity mail templates" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_editButton").length, + 0, + "should not have activity Edit button" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_cancelButton").length, + 0, + "should not have activity Cancel button" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_markDoneButton").length, + 0, + "should not have activity Mark as Done button" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_uploadButton").length, + 0, + "should not have activity Upload button" + ); + }); + + QUnit.test("activity with note layout", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + note: "There is no good or bad note", + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_note").length, + 1, + "should have activity note" + ); + assert.strictEqual( + document.querySelector(".o_ActivityView_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); + + const today = new Date(); + const fiveDaysFromNow = new Date(); + fiveDaysFromNow.setDate(today.getDate() + 5); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + date_deadline: date_to_str(fiveDaysFromNow), + res_id: resPartnerId1, + res_model: "res.partner", + state: "planned", + }); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_dueDateText").length, + 1, + "should have activity delay" ); assert.ok( - action.context.default_use_template, - 'Action should have true as default use_template in context' + document + .querySelector(".o_ActivityView_dueDateText") + .classList.contains("text-success"), + "activity delay should have the right color modifier class (text-success)" ); assert.strictEqual( - action.context.default_template_id, - mailTemplateId1, - 'Action should have the selected mail template id as default template id in context' + document.querySelector(".o_ActivityView_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); + + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + date_deadline: date_to_str(tomorrow), + res_id: resPartnerId1, + res_model: "res.partner", + state: "planned", + }); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" ); assert.strictEqual( - action.type, - "ir.actions.act_window", - 'Action should be of type "ir.actions.act_window"' + document.querySelectorAll(".o_ActivityView_dueDateText").length, + 1, + "should have activity delay" + ); + assert.ok( + document + .querySelector(".o_ActivityView_dueDateText") + .classList.contains("text-success"), + "activity delay should have the right color modifier class (text-success)" ); assert.strictEqual( - action.res_model, - "mail.compose.message", - 'Action should have "mail.compose.message" as res_model' + document.querySelector(".o_ActivityView_dueDateText").textContent, + "Tomorrow:", + "activity delay should have 'Tomorrow:' as label" ); - }, - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_MailTemplateView_preview').length, - 1, - "should have activity mail template name preview button" - ); + }); - document.querySelector('.o_MailTemplateView_preview').click(); - assert.verifySteps( - ['do_action'], - "should have called 'compose email' action correctly" - ); -}); + QUnit.test("activity info layout when planned today", async function (assert) { + assert.expect(4); -QUnit.test('activity with mail template: send mail', async function (assert) { - assert.expect(7); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + date_deadline: date_to_str(new Date()), + res_id: resPartnerId1, + res_model: "res.partner", + state: "today", + }); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_dueDateText").length, + 1, + "should have activity delay" + ); + assert.ok( + document + .querySelector(".o_ActivityView_dueDateText") + .classList.contains("text-warning"), + "activity delay should have the right color modifier class (text-warning)" + ); + assert.strictEqual( + document.querySelector(".o_ActivityView_dueDateText").textContent, + "Today:", + "activity delay should have 'Today:' as label" + ); + }); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailTemplateId1 = pyEnv['mail.template'].create({ name: "Dummy mail template" }); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - pyEnv['mail.activity'].create({ - activity_type_id: emailActivityTypeId, - mail_template_ids: [mailTemplateId1], - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { openView } = await 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], resPartnerId1); - assert.strictEqual(args.args[1], mailTemplateId1); - // random value returned in order for the mock server to know that this route is implemented. - return true; + QUnit.test("activity info layout when planned yesterday", async function (assert) { + assert.expect(4); + + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate(today.getDate() - 1); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + date_deadline: date_to_str(yesterday), + res_id: resPartnerId1, + res_model: "res.partner", + state: "overdue", + }); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_dueDateText").length, + 1, + "should have activity delay" + ); + assert.ok( + document + .querySelector(".o_ActivityView_dueDateText") + .classList.contains("text-danger"), + "activity delay should have the right color modifier class (text-danger)" + ); + assert.strictEqual( + document.querySelector(".o_ActivityView_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); + + const today = new Date(); + const fiveDaysBeforeNow = new Date(); + fiveDaysBeforeNow.setDate(today.getDate() - 5); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + date_deadline: date_to_str(fiveDaysBeforeNow), + res_id: resPartnerId1, + res_model: "res.partner", + state: "overdue", + }); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_dueDateText").length, + 1, + "should have activity delay" + ); + assert.ok( + document + .querySelector(".o_ActivityView_dueDateText") + .classList.contains("text-danger"), + "activity delay should have the right color modifier class (text-danger)" + ); + assert.strictEqual( + document.querySelector(".o_ActivityView_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); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + res_id: resPartnerId1, + res_model: "res.partner", + summary: "test summary", + }); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_summary").length, + 1, + "should have activity summary" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_type").length, + 0, + "should not have the activity type as summary" + ); + assert.strictEqual( + document.querySelector(".o_ActivityView_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); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.activity"].create({ + activity_type_id: 1, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_type").length, + 1, + "activity details should have an activity type section" + ); + assert.strictEqual( + document.querySelector(".o_ActivityView_type").textContent.trim(), + "Email", + "activity details should have the activity type display name in type section" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_summary.o_ActivityView_type").length, + 1, + "should have activity type as summary" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_summary:not(.o_ActivityView_type)") + .length, + 0, + "should not have a specific summary" + ); + }); + + QUnit.test("activity details toggle", async function (assert) { + assert.expect(5); + + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const resUsersId1 = pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["mail.activity"].create({ + create_date: date_to_str(today), + create_uid: resUsersId1, + date_deadline: date_to_str(tomorrow), + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_details").length, + 0, + "activity details should not be visible by default" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_detailsButton").length, + 1, + "activity should have a details button" + ); + + await click(".o_ActivityView_detailsButton"); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_details").length, + 1, + "activity details should be visible after clicking on details button" + ); + + await click(".o_ActivityView_detailsButton"); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_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); + + const today = new Date(); + const tomorrow = new Date(); + tomorrow.setDate(today.getDate() + 1); + const pyEnv = await startServer(); + const resUsersId1 = pyEnv["res.users"].create({ name: "Pauvre pomme" }); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + pyEnv["mail.activity"].create({ + activity_type_id: emailActivityTypeId, + create_date: date_to_str(today), + create_uid: resUsersId1, + date_deadline: date_to_str(tomorrow), + res_id: resPartnerId1, + res_model: "res.partner", + state: "planned", + user_id: resUsersId1, + }); + const { click, openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_userAvatar").length, + 1, + "should have activity user avatar" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_detailsButton").length, + 1, + "activity should have a details button" + ); + + await click(".o_ActivityView_detailsButton"); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_details").length, + 1, + "activity details should be visible after clicking on details button" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_details .o_ActivityView_type").length, + 1, + "activity details should have type" + ); + assert.strictEqual( + document.querySelector(".o_ActivityView_details .o_ActivityView_type").textContent, + "Email", + "activity details type should be 'Email'" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_detailsCreation").length, + 1, + "activity details should have creation date " + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_detailsCreator").length, + 1, + "activity details should have creator" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_detailsAssignation").length, + 1, + "activity details should have assignation information" + ); + assert.strictEqual( + document + .querySelector(".o_ActivityView_detailsAssignation") + .textContent.indexOf("Pauvre pomme"), + 0, + "activity details assignation information should contain creator display name" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_detailsAssignationUserAvatar").length, + 1, + "activity details should have user avatar" + ); + }); + + QUnit.test("activity with mail template layout", async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailTemplateId1 = pyEnv["mail.template"].create({ name: "Dummy mail template" }); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + pyEnv["mail.activity"].create({ + activity_type_id: emailActivityTypeId, + mail_template_ids: [mailTemplateId1], + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_sidebar").length, + 1, + "should have activity sidebar" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_mailTemplates").length, + 1, + "should have activity mail templates" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_mailTemplate").length, + 1, + "should have activity mail template" + ); + assert.strictEqual( + document.querySelectorAll(".o_MailTemplateView_name").length, + 1, + "should have activity mail template name" + ); + assert.strictEqual( + document.querySelector(".o_MailTemplateView_name").textContent, + "Dummy mail template", + "should have activity mail template name" + ); + assert.strictEqual( + document.querySelectorAll(".o_MailTemplateView_preview").length, + 1, + "should have activity mail template name preview button" + ); + assert.strictEqual( + document.querySelectorAll(".o_MailTemplateView_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 pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailTemplateId1 = pyEnv["mail.template"].create({ name: "Dummy mail template" }); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + pyEnv["mail.activity"].create({ + activity_type_id: emailActivityTypeId, + mail_template_ids: [mailTemplateId1], + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { env, openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.step("do_action"); + assert.strictEqual( + action.context.default_res_id, + resPartnerId1, + "Action should have the activity res id as default res id in context" + ); + assert.strictEqual( + action.context.default_model, + "res.partner", + "Action should have the activity res model as default model in context" + ); + assert.ok( + action.context.default_use_template, + "Action should have true as default use_template in context" + ); + assert.strictEqual( + action.context.default_template_id, + mailTemplateId1, + "Action should have the selected mail template id as default template id in context" + ); + assert.strictEqual( + action.type, + "ir.actions.act_window", + 'Action should be of type "ir.actions.act_window"' + ); + assert.strictEqual( + action.res_model, + "mail.compose.message", + 'Action should have "mail.compose.message" as res_model' + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_MailTemplateView_preview").length, + 1, + "should have activity mail template name preview button" + ); + + document.querySelector(".o_MailTemplateView_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); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailTemplateId1 = pyEnv["mail.template"].create({ name: "Dummy mail template" }); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + pyEnv["mail.activity"].create({ + activity_type_id: emailActivityTypeId, + mail_template_ids: [mailTemplateId1], + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { openView } = await 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], resPartnerId1); + assert.strictEqual(args.args[1], mailTemplateId1); + // random value returned in order for the mock server to know that this route is implemented. + return true; + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_MailTemplateView_send").length, + 1, + "should have activity mail template name send button" + ); + + document.querySelector(".o_MailTemplateView_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); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const uploadActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Upload Document"], + ])[0]; + pyEnv["mail.activity"].create({ + activity_category: "upload_file", + activity_type_id: uploadActivityTypeId, + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { messaging, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_uploadButton").length, + 1, + "should have activity upload button" + ); + assert.ok( + messaging.models["Chatter"].all()[0].activityViews[0].fileUploader, + "should have a file uploader" + ); + }); + + QUnit.test("activity click on mark as done", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + pyEnv["mail.activity"].create({ + activity_category: "default", + activity_type_id: emailActivityTypeId, + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" + ); + assert.strictEqual( + document.querySelectorAll(".o_ActivityView_markDoneButton").length, + 1, + "should have activity Mark as Done button" + ); + + await click(".o_ActivityView_markDoneButton"); + assert.strictEqual( + document.querySelectorAll(".o_ActivityMarkDonePopoverContentView").length, + 1, + "should have opened the mark done popover" + ); + + await click(".o_ActivityView_markDoneButton"); + assert.strictEqual( + document.querySelectorAll(".o_ActivityMarkDonePopoverContentView").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); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + pyEnv["mail.activity"].create({ + activity_category: "default", + activity_type_id: emailActivityTypeId, + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ActivityView", + "should have activity component" + ); + assert.containsOnce( + document.body, + ".o_ActivityView_markDoneButton", + "should have activity Mark as Done button" + ); + + await click(".o_ActivityView_markDoneButton"); + assert.strictEqual( + document.querySelector(".o_ActivityMarkDonePopoverContentView_feedback"), + document.activeElement, + "the popover textarea should have the focus" + ); } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_MailTemplateView_send').length, - 1, - "should have activity mail template name send button" - ); + ); - document.querySelector('.o_MailTemplateView_send').click(); - assert.verifySteps( - ['activity_send_mail'], - "should have called activity_send_mail rpc" - ); -}); + QUnit.test("activity click on edit", async function (assert) { + assert.expect(9); -QUnit.test('activity upload document is available', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const uploadActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Upload Document']])[0]; - pyEnv['mail.activity'].create({ - activity_category: 'upload_file', - activity_type_id: uploadActivityTypeId, - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { messaging, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_uploadButton').length, - 1, - "should have activity upload button" - ); - assert.ok( - messaging.models['Chatter'].all()[0].activityViews[0].fileUploader, - "should have a file uploader" - ); -}); - -QUnit.test('activity click on mark as done', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - pyEnv['mail.activity'].create({ - activity_category: 'default', - activity_type_id: emailActivityTypeId, - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_markDoneButton').length, - 1, - "should have activity Mark as Done button" - ); - - await click('.o_ActivityView_markDoneButton'); - assert.strictEqual( - document.querySelectorAll('.o_ActivityMarkDonePopoverContentView').length, - 1, - "should have opened the mark done popover" - ); - - await click('.o_ActivityView_markDoneButton'); - assert.strictEqual( - document.querySelectorAll('.o_ActivityMarkDonePopoverContentView').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); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - pyEnv['mail.activity'].create({ - activity_category: 'default', - activity_type_id: emailActivityTypeId, - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ActivityView', - "should have activity component" - ); - assert.containsOnce( - document.body, - '.o_ActivityView_markDoneButton', - "should have activity Mark as Done button" - ); - - await click('.o_ActivityView_markDoneButton'); - assert.strictEqual( - document.querySelector('.o_ActivityMarkDonePopoverContentView_feedback'), - document.activeElement, - "the popover textarea should have the focus" - ); -}); - -QUnit.test('activity click on edit', async function (assert) { - assert.expect(9); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailTemplateId1 = pyEnv['mail.template'].create({ name: "Dummy mail template" }); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - const mailActivityId1 = pyEnv['mail.activity'].create({ - activity_type_id: emailActivityTypeId, - can_write: true, - mail_template_ids: [mailTemplateId1], - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, env, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.step('do_action'); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailTemplateId1 = pyEnv["mail.template"].create({ name: "Dummy mail template" }); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + const mailActivityId1 = pyEnv["mail.activity"].create({ + activity_type_id: emailActivityTypeId, + can_write: true, + mail_template_ids: [mailTemplateId1], + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, env, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.step("do_action"); + assert.strictEqual( + action.context.default_res_id, + resPartnerId1, + "Action should have the activity res id as default res id in context" + ); + assert.strictEqual( + action.context.default_res_model, + "res.partner", + "Action should have the activity res model as default res model in context" + ); + assert.strictEqual( + action.type, + "ir.actions.act_window", + 'Action should be of type "ir.actions.act_window"' + ); + assert.strictEqual( + action.res_model, + "mail.activity", + 'Action should have "mail.activity" as res_model' + ); + assert.strictEqual( + action.res_id, + mailActivityId1, + "Action should have activity id as res_id" + ); + return this._super(...arguments); + }, + }); assert.strictEqual( - action.context.default_res_id, - resPartnerId1, - 'Action should have the activity res id as default res id in context' + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" ); assert.strictEqual( - action.context.default_res_model, - 'res.partner', - 'Action should have the activity res model as default res model in context' + document.querySelectorAll(".o_ActivityView_editButton").length, + 1, + "should have activity edit button" ); - assert.strictEqual( - action.type, - "ir.actions.act_window", - 'Action should be of type "ir.actions.act_window"' - ); - assert.strictEqual( - action.res_model, - "mail.activity", - 'Action should have "mail.activity" as res_model' - ); - assert.strictEqual( - action.res_id, - mailActivityId1, - 'Action should have activity id as res_id' - ); - return this._super(...arguments); - }, - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_editButton').length, - 1, - "should have activity edit button" - ); - await click('.o_ActivityView_editButton'); - assert.verifySteps( - ['do_action'], - "should have called 'schedule activity' action correctly" - ); -}); + await click(".o_ActivityView_editButton"); + assert.verifySteps( + ["do_action"], + "should have called 'schedule activity' action correctly" + ); + }); -QUnit.test('activity edition', async function (assert) { - assert.expect(14); + QUnit.test("activity edition", async function (assert) { + assert.expect(14); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailActivityId1 = pyEnv['mail.activity'].create({ - can_write: true, - icon: 'fa-times', - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, env, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action, options) { - assert.step('do_action'); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailActivityId1 = pyEnv["mail.activity"].create({ + can_write: true, + icon: "fa-times", + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, env, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action, options) { + assert.step("do_action"); + assert.strictEqual( + action.context.default_res_id, + resPartnerId1, + "Action should have the activity res id as default res id in context" + ); + assert.strictEqual( + action.context.default_res_model, + "res.partner", + "Action should have the activity res model as default res model in context" + ); + assert.strictEqual( + action.type, + "ir.actions.act_window", + 'Action should be of type "ir.actions.act_window"' + ); + assert.strictEqual( + action.res_model, + "mail.activity", + 'Action should have "mail.activity" as res_model' + ); + assert.strictEqual( + action.res_id, + mailActivityId1, + "Action should have activity id as res_id" + ); + pyEnv["mail.activity"].write([mailActivityId1], { icon: "fa-check" }); + options.onClose(); + }, + }); + assert.containsOnce(document.body, ".o_ActivityView", "should have activity component"); + assert.containsOnce( + document.body, + ".o_ActivityView_editButton", + "should have activity edit button" + ); + assert.containsOnce(document.body, ".o_ActivityView_icon", "should have activity icon"); + assert.containsOnce( + document.body, + ".o_ActivityView_icon.fa-times", + "should have initial activity icon" + ); + assert.containsNone( + document.body, + ".o_ActivityView_icon.fa-check", + "should not have new activity icon when not edited yet" + ); + + await click(".o_ActivityView_editButton"); + assert.verifySteps( + ["do_action"], + "should have called 'schedule activity' action correctly" + ); + assert.containsNone( + document.body, + ".o_ActivityView_icon.fa-times", + "should no more have initial activity icon once edited" + ); + assert.containsOnce( + document.body, + ".o_ActivityView_icon.fa-check", + "should now have new activity icon once edited" + ); + }); + + QUnit.test("activity click on cancel", async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + const mailActivityId1 = pyEnv["mail.activity"].create({ + activity_type_id: emailActivityTypeId, + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, openView } = await 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], mailActivityId1); + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); assert.strictEqual( - action.context.default_res_id, - resPartnerId1, - 'Action should have the activity res id as default res id in context' + document.querySelectorAll(".o_ActivityView").length, + 1, + "should have activity component" ); assert.strictEqual( - action.context.default_res_model, - 'res.partner', - 'Action should have the activity res model as default res model in context' + document.querySelectorAll(".o_ActivityView_cancelButton").length, + 1, + "should have activity cancel button" + ); + + await click(".o_ActivityView_cancelButton"); + assert.verifySteps( + ["unlink"], + "should have called unlink rpc after clicking on cancel" ); assert.strictEqual( - action.type, - 'ir.actions.act_window', - 'Action should be of type "ir.actions.act_window"' + document.querySelectorAll(".o_ActivityView").length, + 0, + "should no longer display activity after clicking on cancel" ); - assert.strictEqual( - action.res_model, - 'mail.activity', - 'Action should have "mail.activity" as res_model' + }); + + 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); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + pyEnv["mail.activity"].create({ + activity_category: "default", + activity_type_id: emailActivityTypeId, + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + await click(".o_ActivityView_markDoneButton"); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView", + "Popover component should be present" ); - assert.strictEqual( - action.res_id, - mailActivityId1, - 'Action should have activity id as res_id' + + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ActivityMarkDonePopoverContentView`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_ActivityMarkDonePopoverContentView", + "ESCAPE pressed should have closed the mark done popover" ); - pyEnv['mail.activity'].write([mailActivityId1], { icon: 'fa-check' }); - options.onClose(); - }, - }); - assert.containsOnce( - document.body, - '.o_ActivityView', - "should have activity component" - ); - assert.containsOnce( - document.body, - '.o_ActivityView_editButton', - "should have activity edit button" - ); - assert.containsOnce( - document.body, - '.o_ActivityView_icon', - "should have activity icon" - ); - assert.containsOnce( - document.body, - '.o_ActivityView_icon.fa-times', - "should have initial activity icon" - ); - assert.containsNone( - document.body, - '.o_ActivityView_icon.fa-check', - "should not have new activity icon when not edited yet" - ); + }); - await click('.o_ActivityView_editButton'); - assert.verifySteps( - ['do_action'], - "should have called 'schedule activity' action correctly" - ); - assert.containsNone( - document.body, - '.o_ActivityView_icon.fa-times', - "should no more have initial activity icon once edited" - ); - assert.containsOnce( - document.body, - '.o_ActivityView_icon.fa-check', - "should now have new activity icon once edited" - ); -}); + 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); -QUnit.test('activity click on cancel', async function (assert) { - assert.expect(7); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + pyEnv["mail.activity"].create({ + activity_category: "default", + activity_type_id: emailActivityTypeId, + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - const mailActivityId1 = pyEnv['mail.activity'].create({ - activity_type_id: emailActivityTypeId, - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await 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], mailActivityId1); + await click(".o_ActivityView_markDoneButton"); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView", + "Popover component should be present" + ); + assert.containsOnce( + document.body, + ".o_ActivityMarkDonePopoverContentView_discardButton", + "Popover component should contain the discard button" + ); + await click(".o_ActivityMarkDonePopoverContentView_discardButton"); + assert.containsNone( + document.body, + ".o_ActivityMarkDonePopoverContentView", + "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 pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + pyEnv["mail.activity"].create({ + activity_category: "default", + activity_type_id: emailActivityTypeId, + can_write: true, + note: `

some.model_250

`, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { env, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.strictEqual( + action.type, + "ir.actions.act_window", + "action should open view" + ); + assert.strictEqual( + action.res_model, + "some.model", + "action should open view on 'some.model' model" + ); + assert.strictEqual(action.res_id, 250, "action should open view on 250"); + assert.step("do-action:openFormView_some.model_250"); + }, + }); + assert.containsOnce( + document.body, + ".o_ActivityView_note", + "activity should have a note" + ); + assert.containsOnce( + document.querySelector(".o_ActivityView_note"), + "a", + "activity note should have a link" + ); + + document.querySelector(`.o_ActivityView_note a`).click(); + assert.verifySteps( + ["do-action:openFormView_some.model_250"], + "should have open form view on related record after click on link" + ); + }); + + QUnit.test( + 'button related to file uploading is replaced when updating activity type from "Upload Document" to "Email"', + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const uploadActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Upload document"], + ])[0]; + const emailActivityTypeId = pyEnv["mail.activity.type"].search([ + ["name", "=", "Email"], + ])[0]; + const mailActivityId1 = pyEnv["mail.activity"].create({ + activity_category: "upload_file", + activity_type_id: uploadActivityTypeId, + can_write: true, + res_id: resPartnerId1, + res_model: "res.partner", + }); + const { messaging, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + // Update the record server side then fetch updated data in order to + // emulate what happens when using the form view. + pyEnv["mail.activity"].write([mailActivityId1], { + activity_category: "default", + activity_type_id: emailActivityTypeId, + }); + await afterNextRender(async () => { + const activity = messaging.models["Activity"].findFromIdentifyingData({ + id: mailActivityId1, + }); + await activity.fetchAndUpdate(); + }); + assert.containsOnce( + document.body, + ".o_ActivityView_markDoneButton", + "should have a mark done button when changing activity type from 'Upload Document' to 'Email'" + ); + assert.containsNone( + document.body, + ".o_ActivityView_uploadButton", + "should not have an upload button after changing the activity type from 'Upload Document' to 'Email'" + ); } - }, + ); }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').length, - 1, - "should have activity component" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView_cancelButton').length, - 1, - "should have activity cancel button" - ); - - await click('.o_ActivityView_cancelButton'); - assert.verifySteps( - ['unlink'], - "should have called unlink rpc after clicking on cancel" - ); - assert.strictEqual( - document.querySelectorAll('.o_ActivityView').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); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - pyEnv['mail.activity'].create({ - activity_category: 'default', - activity_type_id: emailActivityTypeId, - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - await click('.o_ActivityView_markDoneButton'); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView', - "Popover component should be present" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ActivityMarkDonePopoverContentView`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_ActivityMarkDonePopoverContentView', - "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); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - pyEnv['mail.activity'].create({ - activity_category: 'default', - activity_type_id: emailActivityTypeId, - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - await click('.o_ActivityView_markDoneButton'); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView', - "Popover component should be present" - ); - assert.containsOnce( - document.body, - '.o_ActivityMarkDonePopoverContentView_discardButton', - "Popover component should contain the discard button" - ); - await click('.o_ActivityMarkDonePopoverContentView_discardButton'); - assert.containsNone( - document.body, - '.o_ActivityMarkDonePopoverContentView', - "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 pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - pyEnv['mail.activity'].create({ - activity_category: 'default', - activity_type_id: emailActivityTypeId, - can_write: true, - note: `

some.model_250

`, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { env, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.strictEqual( - action.type, - 'ir.actions.act_window', - "action should open view" - ); - assert.strictEqual( - action.res_model, - 'some.model', - "action should open view on 'some.model' model" - ); - assert.strictEqual( - action.res_id, - 250, - "action should open view on 250" - ); - assert.step('do-action:openFormView_some.model_250'); - }, - }); - assert.containsOnce( - document.body, - '.o_ActivityView_note', - "activity should have a note" - ); - assert.containsOnce( - document.querySelector('.o_ActivityView_note'), - 'a', - "activity note should have a link" - ); - - document.querySelector(`.o_ActivityView_note a`).click(); - assert.verifySteps( - ['do-action:openFormView_some.model_250'], - "should have open form view on related record after click on link" - ); -}); - -QUnit.test('button related to file uploading is replaced when updating activity type from "Upload Document" to "Email"', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const uploadActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Upload document']])[0]; - const emailActivityTypeId = pyEnv['mail.activity.type'].search([['name', '=', 'Email']])[0]; - const mailActivityId1 = pyEnv['mail.activity'].create({ - activity_category: 'upload_file', - activity_type_id: uploadActivityTypeId, - can_write: true, - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { messaging, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - // Update the record server side then fetch updated data in order to - // emulate what happens when using the form view. - pyEnv['mail.activity'].write([mailActivityId1], { - activity_category: 'default', - activity_type_id: emailActivityTypeId, - }); - await afterNextRender(async () => { - const activity = messaging.models['Activity'].findFromIdentifyingData({ id: mailActivityId1 }); - await activity.fetchAndUpdate(); - }); - assert.containsOnce( - document.body, - '.o_ActivityView_markDoneButton', - "should have a mark done button when changing activity type from 'Upload Document' to 'Email'" - ); - assert.containsNone( - document.body, - '.o_ActivityView_uploadButton', - "should not have an upload button after changing the activity type from 'Upload Document' to 'Email'" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/attachment_box_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/attachment_box_tests.js index fb090d029ed..15ce8fa5676 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/attachment_box_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/attachment_box_tests.js @@ -1,241 +1,234 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('attachment_box_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("attachment_box_tests.js"); -QUnit.test('base empty rendering', async function (assert) { - assert.expect(4); + QUnit.test("base empty rendering", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const views = { - 'res.partner,false,form': - `
+ const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const views = { + "res.partner,false,form": `
`, - }; - const { messaging, openView } = await start({ serverData: { views } }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView`).length, - 1, - "should have an attachment box" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView_buttonAdd`).length, - 1, - "should have a button add" - ); - assert.ok( - messaging.models['Chatter'].all()[0].fileUploader, - "should have a file uploader" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView .o_AttachmentCard`).length, - 0, - "should not have any attachment" - ); -}); + }; + const { messaging, openView } = await start({ serverData: { views } }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView`).length, + 1, + "should have an attachment box" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView_buttonAdd`).length, + 1, + "should have a button add" + ); + assert.ok( + messaging.models["Chatter"].all()[0].fileUploader, + "should have a file uploader" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView .o_AttachmentCard`).length, + 0, + "should not have any attachment" + ); + }); -QUnit.test('base non-empty rendering', async function (assert) { - assert.expect(4); + QUnit.test("base non-empty rendering", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['ir.attachment'].create([ - { - mimetype: 'text/plain', - name: 'Blah.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - mimetype: 'text/plain', - name: 'Blu.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const views = { - 'res.partner,false,form': - `
+ const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["ir.attachment"].create([ + { + mimetype: "text/plain", + name: "Blah.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + mimetype: "text/plain", + name: "Blu.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const views = { + "res.partner,false,form": `
`, - }; - const { messaging, openView } = await start({ serverData: { views } }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView`).length, - 1, - "should have an attachment box" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView_buttonAdd`).length, - 1, - "should have a button add" - ); - assert.ok( - messaging.models['Chatter'].all()[0].fileUploader, - "should have a file uploader" - ); - assert.strictEqual( - document.querySelectorAll(`.o_attachmentBox_attachmentList`).length, - 1, - "should have an attachment list" - ); -}); + }; + const { messaging, openView } = await start({ serverData: { views } }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView`).length, + 1, + "should have an attachment box" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView_buttonAdd`).length, + 1, + "should have a button add" + ); + assert.ok( + messaging.models["Chatter"].all()[0].fileUploader, + "should have a file uploader" + ); + assert.strictEqual( + document.querySelectorAll(`.o_attachmentBox_attachmentList`).length, + 1, + "should have an attachment list" + ); + }); -QUnit.test('view attachments', async function (assert) { - assert.expect(7); + QUnit.test("view attachments", async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const [irAttachmentId1] = pyEnv['ir.attachment'].create([ - { - mimetype: 'text/plain', - name: 'Blah.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - mimetype: 'text/plain', - name: 'Blu.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const views = { - 'res.partner,false,form': - `
+ const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const [irAttachmentId1] = pyEnv["ir.attachment"].create([ + { + mimetype: "text/plain", + name: "Blah.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + mimetype: "text/plain", + name: "Blu.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const views = { + "res.partner,false,form": `
`, - }; - const { click, messaging, openView } = await start({ serverData: { views } }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - const firstAttachment = messaging.models['Attachment'].findFromIdentifyingData({ id: irAttachmentId1 }); + }; + const { click, messaging, openView } = await start({ serverData: { views } }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + const firstAttachment = messaging.models["Attachment"].findFromIdentifyingData({ + id: irAttachmentId1, + }); - await click(` + await click(` .o_AttachmentCard[data-id="${firstAttachment.localId}"] .o_AttachmentCard_image `); - assert.containsOnce( - document.body, - '.o_Dialog', - "a dialog should have been opened once attachment image is clicked", - ); - assert.containsOnce( - document.body, - '.o_AttachmentViewer', - "an attachment viewer should have been opened once attachment image is clicked", - ); - assert.strictEqual( - document.querySelector('.o_AttachmentViewer_name').textContent, - 'Blah.txt', - "attachment viewer iframe should point to clicked attachment", - ); - assert.containsOnce( - document.body, - '.o_AttachmentViewer_buttonNavigationNext', - "attachment viewer should allow to see next attachment", - ); + assert.containsOnce( + document.body, + ".o_Dialog", + "a dialog should have been opened once attachment image is clicked" + ); + assert.containsOnce( + document.body, + ".o_AttachmentViewer", + "an attachment viewer should have been opened once attachment image is clicked" + ); + assert.strictEqual( + document.querySelector(".o_AttachmentViewer_name").textContent, + "Blah.txt", + "attachment viewer iframe should point to clicked attachment" + ); + assert.containsOnce( + document.body, + ".o_AttachmentViewer_buttonNavigationNext", + "attachment viewer should allow to see next attachment" + ); - await click('.o_AttachmentViewer_buttonNavigationNext'); - assert.strictEqual( - document.querySelector('.o_AttachmentViewer_name').textContent, - 'Blu.txt', - "attachment viewer iframe should point to next attachment of attachment box", - ); - assert.containsOnce( - document.body, - '.o_AttachmentViewer_buttonNavigationNext', - "attachment viewer should allow to see next attachment", - ); + await click(".o_AttachmentViewer_buttonNavigationNext"); + assert.strictEqual( + document.querySelector(".o_AttachmentViewer_name").textContent, + "Blu.txt", + "attachment viewer iframe should point to next attachment of attachment box" + ); + assert.containsOnce( + document.body, + ".o_AttachmentViewer_buttonNavigationNext", + "attachment viewer should allow to see next attachment" + ); - await click('.o_AttachmentViewer_buttonNavigationNext'); - assert.strictEqual( - document.querySelector('.o_AttachmentViewer_name').textContent, - 'Blah.txt', - "attachment viewer iframe should point anew to first attachment", - ); -}); + await click(".o_AttachmentViewer_buttonNavigationNext"); + assert.strictEqual( + document.querySelector(".o_AttachmentViewer_name").textContent, + "Blah.txt", + "attachment viewer iframe should point anew to first attachment" + ); + }); -QUnit.test('remove attachment should ask for confirmation', async function (assert) { - assert.expect(5); + QUnit.test("remove attachment should ask for confirmation", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['ir.attachment'].create({ - mimetype: 'text/plain', - name: 'Blah.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const views = { - 'res.partner,false,form': - `
+ const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["ir.attachment"].create({ + mimetype: "text/plain", + name: "Blah.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }); + const views = { + "res.partner,false,form": `
`, - }; - const { click, openView } = await start({ serverData: { views } }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], + }; + const { click, openView } = await start({ serverData: { views } }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce(document.body, ".o_AttachmentCard", "should have an attachment"); + assert.containsOnce( + document.body, + ".o_AttachmentCard_asideItemUnlink", + "attachment should have a delete button" + ); + + await click(".o_AttachmentCard_asideItemUnlink"); + assert.containsOnce( + document.body, + ".o_AttachmentDeleteConfirmView", + "A confirmation dialog should have been opened" + ); + assert.strictEqual( + document.querySelector(".o_AttachmentDeleteConfirmView_mainText").textContent, + `Do you really want to delete "Blah.txt"?`, + "Confirmation dialog should contain the attachment delete confirmation text" + ); + + // Confirm the deletion + await click(".o_AttachmentDeleteConfirmView_confirmButton"); + assert.containsNone( + document.body, + ".o_AttachmentCard", + "should no longer have an attachment" + ); + }); }); - assert.containsOnce( - document.body, - '.o_AttachmentCard', - "should have an attachment", - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard_asideItemUnlink', - "attachment should have a delete button" - ); - - await click('.o_AttachmentCard_asideItemUnlink'); - assert.containsOnce( - document.body, - '.o_AttachmentDeleteConfirmView', - "A confirmation dialog should have been opened" - ); - assert.strictEqual( - document.querySelector('.o_AttachmentDeleteConfirmView_mainText').textContent, - `Do you really want to delete "Blah.txt"?`, - "Confirmation dialog should contain the attachment delete confirmation text" - ); - - // Confirm the deletion - await click('.o_AttachmentDeleteConfirmView_confirmButton'); - assert.containsNone( - document.body, - '.o_AttachmentCard', - "should no longer have an attachment", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/attachment_image_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/attachment_image_tests.js index 9e5f4353401..2e6489f3585 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/attachment_image_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/attachment_image_tests.js @@ -1,51 +1,50 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('attachment_image_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("attachment_image_tests.js"); -QUnit.test('auto layout with image', async function (assert) { - assert.expect(3); + QUnit.test("auto layout with image", async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.png", + mimetype: "image/png", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentImage img`).length, + 1, + "attachment should have an image part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentImage_imageOverlay`).length, + 1, + "attachment should have an image overlay part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentImage_aside`).length, + 0, + "attachment should not have an aside element" + ); + }); }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.png", - mimetype: 'image/png', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentImage img`).length, - 1, - "attachment should have an image part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentImage_imageOverlay`).length, - 1, - "attachment should have an image overlay part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentImage_aside`).length, - 0, - "attachment should not have an aside element" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js index 70b59031dd9..8f154837098 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/attachment_list_tests.js @@ -1,492 +1,496 @@ /** @odoo-module **/ -import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('attachment_list_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("attachment_list_tests.js"); -QUnit.test('simplest layout', async function (assert) { - assert.expect(8); + QUnit.test("simplest layout", async function (assert) { + assert.expect(8); - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.txt", - mimetype: 'text/plain', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.txt", + mimetype: "text/plain", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll('.o_AttachmentList').length, - 1, - "should have attachment list component in DOM" - ); - const attachmentEl = document.querySelector('.o_AttachmentList .o_AttachmentCard'); - assert.strictEqual( - attachmentEl.dataset.id, - messaging.models['Attachment'].findFromIdentifyingData({ id: messageAttachmentId }).localId, - "attachment component should be linked to attachment store model" - ); - assert.strictEqual( - attachmentEl.title, - "test.txt", - "attachment should have filename as title attribute" - ); + assert.strictEqual( + document.querySelectorAll(".o_AttachmentList").length, + 1, + "should have attachment list component in DOM" + ); + const attachmentEl = document.querySelector(".o_AttachmentList .o_AttachmentCard"); + assert.strictEqual( + attachmentEl.dataset.id, + messaging.models["Attachment"].findFromIdentifyingData({ id: messageAttachmentId }) + .localId, + "attachment component should be linked to attachment store model" + ); + assert.strictEqual( + attachmentEl.title, + "test.txt", + "attachment should have filename as title attribute" + ); - assert.strictEqual( - attachmentEl.querySelectorAll(`:scope .o_AttachmentCard_image`).length, - 1, - "attachment should have an image part" - ); - const attachmentImage = document.querySelector(`.o_AttachmentCard_image`); - assert.ok( - attachmentImage.classList.contains('o_image'), - "attachment should have o_image classname (required for mimetype.scss style)" - ); - assert.strictEqual( - attachmentImage.dataset.mimetype, - 'text/plain', - "attachment should have data-mimetype set (required for mimetype.scss style)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentList_details`).length, - 0, - "attachment should not have a details part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentList_aside`).length, - 0, - "attachment should not have an aside part" - ); -}); + assert.strictEqual( + attachmentEl.querySelectorAll(`:scope .o_AttachmentCard_image`).length, + 1, + "attachment should have an image part" + ); + const attachmentImage = document.querySelector(`.o_AttachmentCard_image`); + assert.ok( + attachmentImage.classList.contains("o_image"), + "attachment should have o_image classname (required for mimetype.scss style)" + ); + assert.strictEqual( + attachmentImage.dataset.mimetype, + "text/plain", + "attachment should have data-mimetype set (required for mimetype.scss style)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentList_details`).length, + 0, + "attachment should not have a details part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentList_aside`).length, + 0, + "attachment should not have an aside part" + ); + }); -QUnit.test('simplest layout + editable', async function (assert) { - assert.expect(7); + QUnit.test("simplest layout + editable", async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.txt", - mimetype: 'text/plain', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - async mockRPC(route, args) { - if (route.includes('web/image/750')) { - assert.ok( - route.includes('/200x200'), - "should fetch image with 200x200 pixels ratio"); - assert.step('fetch_image'); + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.txt", + mimetype: "text/plain", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + async mockRPC(route, args) { + if (route.includes("web/image/750")) { + assert.ok( + route.includes("/200x200"), + "should fetch image with 200x200 pixels ratio" + ); + assert.step("fetch_image"); + } + }, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelectorAll(".o_AttachmentList").length, + 1, + "should have attachment component in DOM" + ); + + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_image`).length, + 1, + "attachment should have an image part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_details`).length, + 1, + "attachment should not have a details part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_aside`).length, + 1, + "attachment should have an aside part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_asideItem`).length, + 2, + "attachment should have two aside item" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_asideItemUnlink`).length, + 1, + "attachment should have a delete button" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_asideItemDownload`).length, + 1, + "attachment should have a download button" + ); + }); + + QUnit.test("layout with card details and filename and extension", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.txt", + mimetype: "text/plain", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_details`).length, + 1, + "attachment should have a details part" + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentCard_extension`).length, + 1, + "attachment should have its extension shown" + ); + }); + + QUnit.test("view attachment", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.png", + mimetype: "image/png", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_AttachmentImage img", + "attachment should have an image part" + ); + await click(".o_AttachmentImage"); + assert.containsOnce( + document.body, + ".o_Dialog", + "a dialog should have been opened once attachment image is clicked" + ); + assert.containsOnce( + document.body, + ".o_AttachmentViewer", + "an attachment viewer should have been opened once attachment image is clicked" + ); + }); + + QUnit.test("close attachment viewer", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.png", + mimetype: "image/png", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_AttachmentImage img", + "attachment should have an image part" + ); + + await click(".o_AttachmentImage"); + assert.containsOnce( + document.body, + ".o_AttachmentViewer", + "an attachment viewer should have been opened once attachment image is clicked" + ); + + await click(".o_AttachmentViewer_headerItemButtonClose"); + assert.containsNone( + document.body, + ".o_Dialog", + "attachment viewer should be closed after clicking on close button" + ); + }); + + QUnit.test( + "clicking on the delete attachment button multiple times should do the rpc only once", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.txt", + mimetype: "text/plain", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + async mockRPC(route, args) { + if (route === "/mail/attachment/delete") { + assert.step("attachment_unlink"); + } + }, + }); + await openDiscuss(); + + await click(".o_AttachmentCard_asideItemUnlink"); + + await afterNextRender(() => { + document.querySelector(".o_AttachmentDeleteConfirmView_confirmButton").click(); + document.querySelector(".o_AttachmentDeleteConfirmView_confirmButton").click(); + document.querySelector(".o_AttachmentDeleteConfirmView_confirmButton").click(); + }); + assert.verifySteps(["attachment_unlink"], "The unlink method must be called once"); } - }, - }); - await openDiscuss(); + ); - assert.strictEqual( - document.querySelectorAll('.o_AttachmentList').length, - 1, - "should have attachment component in DOM" - ); + QUnit.test( + "[technical] does not crash when the viewer is closed before image load", + async function (assert) { + /** + * When images are displayed using `src` attribute for the 1st time, it fetches the resource. + * In this case, images are actually displayed (fully fetched and rendered on screen) when + * `` intercepts `load` event. + * + * Current code needs to be aware of load state of image, to display spinner when loading + * and actual image when loaded. This test asserts no crash from mishandling image becoming + * loaded from being viewed for 1st time, but viewer being closed while image is loading. + */ + assert.expect(1); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentCard_image`).length, - 1, - "attachment should have an image part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentCard_details`).length, - 1, - "attachment should not have a details part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentCard_aside`).length, - 1, - "attachment should have an aside part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentCard_asideItem`).length, - 2, - "attachment should have two aside item" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentCard_asideItemUnlink`).length, - 1, - "attachment should have a delete button" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentCard_asideItemDownload`).length, - 1, - "attachment should have a download button" - ); -}); - -QUnit.test('layout with card details and filename and extension', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.txt", - mimetype: 'text/plain', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentCard_details`).length, - 1, - "attachment should have a details part" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentCard_extension`).length, - 1, - "attachment should have its extension shown" - ); -}); - -QUnit.test('view attachment', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.png", - mimetype: 'image/png', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_AttachmentImage img', - "attachment should have an image part" - ); - await click('.o_AttachmentImage'); - assert.containsOnce( - document.body, - '.o_Dialog', - 'a dialog should have been opened once attachment image is clicked', - ); - assert.containsOnce( - document.body, - '.o_AttachmentViewer', - 'an attachment viewer should have been opened once attachment image is clicked', - ); -}); - -QUnit.test('close attachment viewer', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.png", - mimetype: 'image/png', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_AttachmentImage img', - "attachment should have an image part" - ); - - await click('.o_AttachmentImage'); - assert.containsOnce( - document.body, - '.o_AttachmentViewer', - "an attachment viewer should have been opened once attachment image is clicked", - ); - - await click('.o_AttachmentViewer_headerItemButtonClose'); - assert.containsNone( - document.body, - '.o_Dialog', - "attachment viewer should be closed after clicking on close button" - ); -}); - -QUnit.test('clicking on the delete attachment button multiple times should do the rpc only once', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.txt", - mimetype: 'text/plain', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - async mockRPC(route, args) { - if (route === '/mail/attachment/delete') { - assert.step('attachment_unlink'); + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.png", + mimetype: "image/png", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + await click(".o_AttachmentImage"); + const imageEl = document.querySelector(".o_AttachmentViewer_viewImage"); + await click(".o_AttachmentViewer_headerItemButtonClose"); + // Simulate image becoming loaded. + let successfulLoad; + try { + imageEl.dispatchEvent(new Event("load", { bubbles: true })); + successfulLoad = true; + } catch { + successfulLoad = false; + } finally { + assert.ok(successfulLoad, "should not crash when the image is loaded"); + } } - }, - }); - await openDiscuss(); + ); - await click('.o_AttachmentCard_asideItemUnlink'); + QUnit.test("plain text file is viewable", async function (assert) { + assert.expect(1); - await afterNextRender(() => { - document.querySelector('.o_AttachmentDeleteConfirmView_confirmButton').click(); - document.querySelector('.o_AttachmentDeleteConfirmView_confirmButton').click(); - document.querySelector('.o_AttachmentDeleteConfirmView_confirmButton').click(); + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.txt", + mimetype: "text/plain", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + + assert.hasClass( + document.querySelector(".o_AttachmentCard"), + "o-viewable", + "should be viewable" + ); + }); + + QUnit.test("HTML file is viewable", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.html", + mimetype: "text/html", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + assert.hasClass( + document.querySelector(".o_AttachmentCard"), + "o-viewable", + "should be viewable" + ); + }); + + QUnit.test("ODT file is not viewable", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.odt", + mimetype: "application/vnd.oasis.opendocument.text", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + assert.doesNotHaveClass( + document.querySelector(".o_AttachmentCard"), + "o-viewable", + "should not be viewable" + ); + }); + + QUnit.test("DOCX file is not viewable", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "channel1", + }); + const messageAttachmentId = pyEnv["ir.attachment"].create({ + name: "test.docx", + mimetype: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }); + pyEnv["mail.message"].create({ + attachment_ids: [messageAttachmentId], + body: "

Test

", + model: "mail.channel", + res_id: channelId, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: channelId }, + }, + }); + await openDiscuss(); + assert.doesNotHaveClass( + document.querySelector(".o_AttachmentCard"), + "o-viewable", + "should not be viewable" + ); + }); }); - assert.verifySteps( - ['attachment_unlink'], - "The unlink method must be called once" - ); -}); - -QUnit.test('[technical] does not crash when the viewer is closed before image load', async function (assert) { - /** - * When images are displayed using `src` attribute for the 1st time, it fetches the resource. - * In this case, images are actually displayed (fully fetched and rendered on screen) when - * `` intercepts `load` event. - * - * Current code needs to be aware of load state of image, to display spinner when loading - * and actual image when loaded. This test asserts no crash from mishandling image becoming - * loaded from being viewed for 1st time, but viewer being closed while image is loading. - */ - assert.expect(1); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.png", - mimetype: 'image/png', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - await click('.o_AttachmentImage'); - const imageEl = document.querySelector('.o_AttachmentViewer_viewImage'); - await click('.o_AttachmentViewer_headerItemButtonClose'); - // Simulate image becoming loaded. - let successfulLoad; - try { - imageEl.dispatchEvent(new Event('load', { bubbles: true })); - successfulLoad = true; - } catch { - successfulLoad = false; - } finally { - assert.ok(successfulLoad, 'should not crash when the image is loaded'); - } -}); - -QUnit.test('plain text file is viewable', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.txt", - mimetype: 'text/plain', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - - assert.hasClass( - document.querySelector('.o_AttachmentCard'), - 'o-viewable', - "should be viewable", - ); -}); - -QUnit.test('HTML file is viewable', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.html", - mimetype: 'text/html', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - assert.hasClass( - document.querySelector('.o_AttachmentCard'), - 'o-viewable', - "should be viewable", - ); -}); - -QUnit.test('ODT file is not viewable', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.odt", - mimetype: 'application/vnd.oasis.opendocument.text', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - assert.doesNotHaveClass( - document.querySelector('.o_AttachmentCard'), - 'o-viewable', - "should not be viewable", - ); -}); - -QUnit.test('DOCX file is not viewable', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channelId = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: 'channel1', - }); - const messageAttachmentId = pyEnv['ir.attachment'].create({ - name: "test.docx", - mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - }); - pyEnv['mail.message'].create({ - attachment_ids: [messageAttachmentId], - body: "

Test

", - model: 'mail.channel', - res_id: channelId - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: channelId }, - }, - }); - await openDiscuss(); - assert.doesNotHaveClass( - document.querySelector('.o_AttachmentCard'), - 'o-viewable', - "should not be viewable", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/call_main_view_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/call_main_view_tests.js index 38884a4f881..385d944cc72 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/call_main_view_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/call_main_view_tests.js @@ -1,77 +1,65 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('call_main_view_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("call_main_view_tests.js"); -QUnit.test('Join a call', async function (assert) { - assert.expect(4); + QUnit.test("Join a call", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_callButton"); + + assert.containsOnce(document.body, ".o_CallView", "Should have a call view"); + assert.containsOnce( + document.body, + ".o_CallParticipantCard", + "Should have a call participant card" + ); + assert.containsOnce( + document.body, + ".o_CallMainView_controls", + "Should have call controls" + ); + assert.containsNone( + document.body, + ".o_ThreadViewTopbar_callButton", + "Should not have a join call button anymore" + ); + }); + + QUnit.test("Leave a call", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_callButton"); + assert.containsOnce( + document.body, + ".o_CallActionListView_callToggle", + "Should have a button to leave the call" + ); + await click(".o_CallActionListView_callToggle"); + assert.containsNone(document.body, ".o_CallView", "Should not have a call view"); + }); }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_callButton'); - - assert.containsOnce( - document.body, - '.o_CallView', - "Should have a call view" - ); - assert.containsOnce( - document.body, - '.o_CallParticipantCard', - "Should have a call participant card" - ); - assert.containsOnce( - document.body, - '.o_CallMainView_controls', - "Should have call controls" - ); - assert.containsNone( - document.body, - '.o_ThreadViewTopbar_callButton', - "Should not have a join call button anymore" - ); -}); - -QUnit.test('Leave a call', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_callButton'); - assert.containsOnce( - document.body, - '.o_CallActionListView_callToggle', - "Should have a button to leave the call" - ); - await click('.o_CallActionListView_callToggle'); - assert.containsNone( - document.body, - '.o_CallView', - "Should not have a call view" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/call_settings_menu_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/call_settings_menu_tests.js index ca4cb8f7152..181bc5950ab 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/call_settings_menu_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/call_settings_menu_tests.js @@ -1,170 +1,175 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; -import { browser } from '@web/core/browser/browser'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; +import { browser } from "@web/core/browser/browser"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('call_settings_menu_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("call_settings_menu_tests.js"); -QUnit.test('Renders the call settings', async function (assert) { - assert.expect(9); + QUnit.test("Renders the call settings", async function (assert) { + assert.expect(9); - patchWithCleanup(browser, { - navigator: { - ...browser.navigator, - mediaDevices: { - enumerateDevices: () => Promise.resolve([ - { deviceId: 'mockAudioDeviceId', kind: 'audioinput', label: 'mockAudioDeviceLabel' }, - { deviceId: 'mockVideoDeviceId', kind: 'videoinput', label: 'mockVideoDeviceLabel' }, - ]), - }, - } + patchWithCleanup(browser, { + navigator: { + ...browser.navigator, + mediaDevices: { + enumerateDevices: () => + Promise.resolve([ + { + deviceId: "mockAudioDeviceId", + kind: "audioinput", + label: "mockAudioDeviceLabel", + }, + { + deviceId: "mockVideoDeviceId", + kind: "videoinput", + label: "mockVideoDeviceLabel", + }, + ]), + }, + }, + }); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_openCallSettingsButton"); + + assert.containsOnce( + document.body, + ".o_CallSettingsMenu", + "Should have a call settings menu" + ); + assert.containsN( + document.body, + ".o_CallSettingsMenu_option", + 5, + "Should have five options" + ); + + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_optionDeviceSelect", + "should have an audio device selection" + ); + assert.containsOnce( + document.body, + "option[value=mockAudioDeviceId]", + "should have an option to select an audio input device" + ); + assert.containsNone( + document.body, + "option[value=mockVideoDeviceId]", + "should not have an option to select a video input device" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_pushToTalkOption", + "should have an option to toggle push-to-talk" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_voiceThresholdOption", + "should have an option to set the voice detection threshold" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_showOnlyVideoOption", + "should have an option to filter participants who have no video" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_blurOption", + "should have an option to toggle the background blur feature" + ); + }); + + QUnit.test("activate push to talk", async function (assert) { + assert.expect(3); + + patchWithCleanup(browser, { + navigator: { + ...browser.navigator, + mediaDevices: { + enumerateDevices: () => Promise.resolve([]), + }, + }, + }); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_openCallSettingsButton"); + await click(".o_CallSettingsMenu_pushToTalkOption"); + + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_pushToTalkKeyOption", + "should have an option set the push to talk shortcut key" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_pushToTalkDelayOption", + "should have an option to set the push-to-talk delay" + ); + assert.containsNone( + document.body, + ".o_CallSettingsMenu_voiceThresholdOption", + "should not have an option to set the voice detection threshold" + ); + }); + + QUnit.test("activate blur", async function (assert) { + assert.expect(2); + + patchWithCleanup(browser, { + navigator: { + ...browser.navigator, + mediaDevices: { + enumerateDevices: () => Promise.resolve([]), + }, + }, + }); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_openCallSettingsButton"); + await click(".o_CallSettingsMenu_blurOption"); + + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_backgroundBlurIntensityOption", + "should have an option set the background blur intensity" + ); + assert.containsOnce( + document.body, + ".o_CallSettingsMenu_edgeBlurIntensityOption", + "should have an option to set the edge blur intensity" + ); + }); }); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_openCallSettingsButton'); - - assert.containsOnce( - document.body, - '.o_CallSettingsMenu', - "Should have a call settings menu" - ); - assert.containsN( - document.body, - '.o_CallSettingsMenu_option', - 5, - "Should have five options", - ); - - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_optionDeviceSelect', - "should have an audio device selection", - ); - assert.containsOnce( - document.body, - 'option[value=mockAudioDeviceId]', - "should have an option to select an audio input device", - ); - assert.containsNone( - document.body, - 'option[value=mockVideoDeviceId]', - "should not have an option to select a video input device", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_pushToTalkOption', - "should have an option to toggle push-to-talk", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_voiceThresholdOption', - "should have an option to set the voice detection threshold", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_showOnlyVideoOption', - "should have an option to filter participants who have no video", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_blurOption', - "should have an option to toggle the background blur feature", - ); -}); - -QUnit.test('activate push to talk', async function (assert) { - assert.expect(3); - - patchWithCleanup(browser, { - navigator: { - ...browser.navigator, - mediaDevices: { - enumerateDevices: () => Promise.resolve([]), - }, - } - }); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_openCallSettingsButton'); - await click('.o_CallSettingsMenu_pushToTalkOption'); - - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_pushToTalkKeyOption', - "should have an option set the push to talk shortcut key", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_pushToTalkDelayOption', - "should have an option to set the push-to-talk delay", - ); - assert.containsNone( - document.body, - '.o_CallSettingsMenu_voiceThresholdOption', - "should not have an option to set the voice detection threshold", - ); -}); - -QUnit.test('activate blur', async function (assert) { - assert.expect(2); - - patchWithCleanup(browser, { - navigator: { - ...browser.navigator, - mediaDevices: { - enumerateDevices: () => Promise.resolve([]), - }, - } - }); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_openCallSettingsButton'); - await click('.o_CallSettingsMenu_blurOption'); - - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_backgroundBlurIntensityOption', - "should have an option set the background blur intensity", - ); - assert.containsOnce( - document.body, - '.o_CallSettingsMenu_edgeBlurIntensityOption', - "should have an option to set the edge blur intensity", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/channel_invitation_form_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/channel_invitation_form_tests.js index abb1bdb3235..764bc2e0825 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/channel_invitation_form_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/channel_invitation_form_tests.js @@ -1,160 +1,167 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('channel_invitation_form_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("channel_invitation_form_tests.js"); -QUnit.test('should display the channel invitation form after clicking on the invite button of a chat', async function (assert) { - assert.expect(1); + QUnit.test( + "should display the channel invitation form after clicking on the invite button of a chat", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "testpartner@odoo.com", - name: "TestPartner", + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await click(`.o_ThreadViewTopbar_inviteButton`); + assert.containsOnce( + document.body, + ".o_ChannelInvitationForm", + "should display the channel invitation form after clicking on the invite button of a chat" + ); + } + ); + + QUnit.test( + "should be able to search for a new user to invite from an existing chat", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const resPartnerId2 = pyEnv["res.partner"].create({ + email: "testpartner2@odoo.com", + name: "TestPartner2", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["res.users"].create({ partner_id: resPartnerId2 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await click(`.o_ThreadViewTopbar_inviteButton`); + await insertText(".o_ChannelInvitationForm_searchInput", "TestPartner2"); + assert.strictEqual( + document.querySelector(`.o_ChannelInvitationFormSelectablePartnerView_name`) + .textContent, + "TestPartner2", + "should display 'TestPartner2' as it matches search term" + ); + } + ); + + QUnit.test( + "should be able to create a new group chat from an existing chat", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const resPartnerId2 = pyEnv["res.partner"].create({ + email: "testpartner2@odoo.com", + name: "TestPartner2", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["res.users"].create({ partner_id: resPartnerId2 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + + await click(`.o_ThreadViewTopbar_inviteButton`); + await insertText(".o_ChannelInvitationForm_searchInput", "TestPartner2"); + await click(`.o_ChannelInvitationFormSelectablePartnerView_checkbox`); + await click(`.o_ChannelInvitationForm_inviteButton`); + assert.strictEqual( + document.querySelector(`.o_ThreadViewTopbar_threadName`).textContent, + "Mitchell Admin, TestPartner, TestPartner2", + "should have created a new group chat with the existing chat members and the selected user" + ); + } + ); + + QUnit.test( + "Invitation form should display channel group restriction", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const resGroupId1 = pyEnv["res.groups"].create({ + name: "testGroup", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [[0, 0, { partner_id: pyEnv.currentPartnerId }]], + channel_type: "channel", + group_public_id: resGroupId1, + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + + await click(`.o_ThreadViewTopbar_inviteButton`); + assert.containsOnce( + document.body, + ".o_ChannelInvitationForm_accessRestrictedToGroup", + "should display the channel restriction warning" + ); + } + ); }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await click(`.o_ThreadViewTopbar_inviteButton`); - assert.containsOnce( - document.body, - '.o_ChannelInvitationForm', - "should display the channel invitation form after clicking on the invite button of a chat" - ); -}); - -QUnit.test('should be able to search for a new user to invite from an existing chat', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "testpartner@odoo.com", - name: "TestPartner", - }); - const resPartnerId2 = pyEnv['res.partner'].create({ - email: "testpartner2@odoo.com", - name: "TestPartner2", - }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['res.users'].create({ partner_id: resPartnerId2 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await click(`.o_ThreadViewTopbar_inviteButton`); - await insertText('.o_ChannelInvitationForm_searchInput', "TestPartner2"); - assert.strictEqual( - document.querySelector(`.o_ChannelInvitationFormSelectablePartnerView_name`).textContent, - "TestPartner2", - "should display 'TestPartner2' as it matches search term", - ); -}); - -QUnit.test('should be able to create a new group chat from an existing chat', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "testpartner@odoo.com", - name: "TestPartner", - }); - const resPartnerId2 = pyEnv['res.partner'].create({ - email: "testpartner2@odoo.com", - name: "TestPartner2", - }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['res.users'].create({ partner_id: resPartnerId2 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - - await click(`.o_ThreadViewTopbar_inviteButton`); - await insertText('.o_ChannelInvitationForm_searchInput', "TestPartner2"); - await click(`.o_ChannelInvitationFormSelectablePartnerView_checkbox`); - await click(`.o_ChannelInvitationForm_inviteButton`); - assert.strictEqual( - document.querySelector(`.o_ThreadViewTopbar_threadName`).textContent, - 'Mitchell Admin, TestPartner, TestPartner2', - "should have created a new group chat with the existing chat members and the selected user", - ); -}); - -QUnit.test('Invitation form should display channel group restriction', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "testpartner@odoo.com", - name: "TestPartner", - }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const resGroupId1 = pyEnv['res.groups'].create({ - name: "testGroup", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - ], - channel_type: 'channel', - group_public_id: resGroupId1, - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - - await click(`.o_ThreadViewTopbar_inviteButton`); - assert.containsOnce( - document.body, - '.o_ChannelInvitationForm_accessRestrictedToGroup', - "should display the channel restriction warning" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/channel_member_list_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/channel_member_list_tests.js index 5395a918618..da4e29e5aeb 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/channel_member_list_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/channel_member_list_tests.js @@ -1,227 +1,237 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('channel_member_list_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("channel_member_list_tests.js"); -QUnit.test('there should be a button to show member list in the thread view topbar initially', async function (assert) { - assert.expect(1); + QUnit.test( + "there should be a button to show member list in the thread view topbar initially", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'group', + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "group", + }); + const { openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_ThreadViewTopbar_showMemberListButton", + "there should be a button to show member list in the thread view topbar initially" + ); + } + ); + + QUnit.test( + "should show member list when clicking on show member list button in thread view topbar", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "group", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_showMemberListButton"); + assert.containsOnce( + document.body, + ".o_ChannelMemberListView", + "should show member list when clicking on show member list button in thread view topbar" + ); + } + ); + + QUnit.test("should have correct members in member list", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "group", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_showMemberListButton"); + assert.containsN( + document.body, + ".o_ChannelMemberView", + 2, + "should have 2 members in member list" + ); + assert.containsOnce( + document.body, + `.o_ChannelMemberView[data-partner-id="${pyEnv.currentPartnerId}"]`, + "should have current partner in member list (current partner is a member)" + ); + assert.containsOnce( + document.body, + `.o_ChannelMemberView[data-partner-id="${resPartnerId1}"]`, + "should have 'Demo' in member list ('Demo' is a member)" + ); + }); + + QUnit.test( + "there should be a button to hide member list in the thread view topbar when the member list is visible", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "group", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_showMemberListButton"); + assert.containsOnce( + document.body, + ".o_ThreadViewTopbar_hideMemberListButton", + "there should be a button to hide member list in the thread view topbar when the member list is visible" + ); + } + ); + + QUnit.test( + "should show a button to load more members if they are not all loaded", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const channel_member_ids = [[0, 0, { partner_id: pyEnv.currentPartnerId }]]; + for (let i = 0; i < 101; i++) { + const resPartnerId = pyEnv["res.partner"].create({ name: "name" + i }); + channel_member_ids.push([0, 0, { partner_id: resPartnerId }]); + } + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids, + channel_type: "channel", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_showMemberListButton"); + assert.containsOnce( + document.body, + ".o_ChannelMemberListView_loadMoreButton", + "should have a load more button because 100 members were fetched initially and there are 102 members in total" + ); + } + ); + + QUnit.test("Load more button should load more members", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const channel_member_ids = [[0, 0, { partner_id: pyEnv.currentPartnerId }]]; + for (let i = 0; i < 101; i++) { + const resPartnerId = pyEnv["res.partner"].create({ name: "name" + i }); + channel_member_ids.push([0, 0, { partner_id: resPartnerId }]); + } + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids, + channel_type: "channel", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + await click(".o_ThreadViewTopbar_showMemberListButton"); + await click(".o_ChannelMemberListView_loadMoreButton"); + assert.containsN( + document.body, + ".o_ChannelMemberView", + 102, + "should load all the members because 100 members were fetched initially, and load more fetched the remaining 2 members" + ); + }); + + QUnit.test( + "chat with member should be opened after clicking on channel member", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "channel", + }); + const { click, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + + await click(".o_ThreadViewTopbar_showMemberListButton"); + await click(`.o_ChannelMemberView[data-partner-id="${resPartnerId1}"]`); + assert.containsOnce( + document.body, + `.o_ThreadView[data-correspondent-id="${resPartnerId1}"]`, + "Chat with member Demo should be opened" + ); + } + ); }); - const { openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_ThreadViewTopbar_showMemberListButton', - "there should be a button to show member list in the thread view topbar initially", - ); -}); - -QUnit.test('should show member list when clicking on show member list button in thread view topbar', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'group', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_showMemberListButton'); - assert.containsOnce( - document.body, - '.o_ChannelMemberListView', - "should show member list when clicking on show member list button in thread view topbar", - ); -}); - -QUnit.test('should have correct members in member list', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'group', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_showMemberListButton'); - assert.containsN( - document.body, - '.o_ChannelMemberView', - 2, - "should have 2 members in member list", - ); - assert.containsOnce( - document.body, - `.o_ChannelMemberView[data-partner-id="${pyEnv.currentPartnerId}"]`, - "should have current partner in member list (current partner is a member)", - ); - assert.containsOnce( - document.body, - `.o_ChannelMemberView[data-partner-id="${resPartnerId1}"]`, - "should have 'Demo' in member list ('Demo' is a member)", - ); -}); - -QUnit.test('there should be a button to hide member list in the thread view topbar when the member list is visible', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'group', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_showMemberListButton'); - assert.containsOnce( - document.body, - '.o_ThreadViewTopbar_hideMemberListButton', - "there should be a button to hide member list in the thread view topbar when the member list is visible", - ); -}); - -QUnit.test('should show a button to load more members if they are not all loaded', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channel_member_ids = [[0, 0, { partner_id: pyEnv.currentPartnerId }]]; - for (let i = 0; i < 101; i++) { - const resPartnerId = pyEnv['res.partner'].create({ name: "name" + i }); - channel_member_ids.push([0, 0, { partner_id: resPartnerId }]); - } - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids, - channel_type: 'channel', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_showMemberListButton'); - assert.containsOnce( - document.body, - '.o_ChannelMemberListView_loadMoreButton', - "should have a load more button because 100 members were fetched initially and there are 102 members in total", - ); -}); - - -QUnit.test('Load more button should load more members', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const channel_member_ids = [[0, 0, { partner_id: pyEnv.currentPartnerId }]]; - for (let i = 0; i < 101; i++) { - const resPartnerId = pyEnv['res.partner'].create({ name: "name" + i }); - channel_member_ids.push([0, 0, { partner_id: resPartnerId }]); - } - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids, - channel_type: 'channel', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - await click('.o_ThreadViewTopbar_showMemberListButton'); - await click('.o_ChannelMemberListView_loadMoreButton'); - assert.containsN( - document.body, - '.o_ChannelMemberView', - 102, - "should load all the members because 100 members were fetched initially, and load more fetched the remaining 2 members", - ); -}); - -QUnit.test('chat with member should be opened after clicking on channel member', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'channel', - }); - const { click, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - - await click('.o_ThreadViewTopbar_showMemberListButton'); - await click(`.o_ChannelMemberView[data-partner-id="${resPartnerId1}"]`); - assert.containsOnce( - document.body, - `.o_ThreadView[data-correspondent-id="${resPartnerId1}"]`, - "Chat with member Demo should be opened", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/channel_preview_view_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/channel_preview_view_tests.js index 1080b6ccafe..80f6ae75099 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/channel_preview_view_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/channel_preview_view_tests.js @@ -1,69 +1,78 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('channel_preview_view_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("channel_preview_view_tests.js"); -QUnit.test('mark as read', async function (assert) { - assert.expect(6); + QUnit.test("mark as read", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 1, // mandatory for good working of test, but ideally should be deduced by other server data - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { - partner_id: resPartnerId1, - }], - ], + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 1, // mandatory for good working of test, but ideally should be deduced by other server data + partner_id: pyEnv.currentPartnerId, + }, + ], + [ + 0, + 0, + { + partner_id: resPartnerId1, + }, + ], + ], + }); + const [mailMessageId1] = pyEnv["mail.message"].create([ + { author_id: resPartnerId1, model: "mail.channel", res_id: mailChannelId1 }, + { author_id: resPartnerId1, model: "mail.channel", res_id: mailChannelId1 }, + ]); + const [mailChannelMemberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId], { + seen_message_id: mailMessageId1, + }); + + const { click } = await start({ + async mockRPC(route, args) { + if (route.includes("set_last_seen_message")) { + assert.step("set_last_seen_message"); + } + }, + }); + await click(".o_MessagingMenu_toggler"); + assert.containsOnce( + document.body, + ".o_ChannelPreviewView_markAsRead", + "should have the mark as read button" + ); + + await click(".o_ChannelPreviewView_markAsRead"); + assert.verifySteps(["set_last_seen_message"], "should have marked the thread as seen"); + assert.hasClass( + document.querySelector(".o_ChannelPreviewView"), + "o-muted", + "should be muted once marked as read" + ); + assert.containsNone( + document.body, + ".o_ChannelPreviewView_markAsRead", + "should no longer have the mark as read button" + ); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have opened the thread" + ); + }); }); - const [mailMessageId1] = pyEnv['mail.message'].create([ - { author_id: resPartnerId1, model: 'mail.channel', res_id: mailChannelId1 }, - { author_id: resPartnerId1, model: 'mail.channel', res_id: mailChannelId1 }, - ]); - const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: mailMessageId1 }); - - const { click } = await start({ - async mockRPC(route, args) { - if (route.includes('set_last_seen_message')) { - assert.step('set_last_seen_message'); - } - }, - }); - await click('.o_MessagingMenu_toggler'); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_markAsRead', - "should have the mark as read button" - ); - - await click('.o_ChannelPreviewView_markAsRead'); - assert.verifySteps( - ['set_last_seen_message'], - "should have marked the thread as seen" - ); - assert.hasClass( - document.querySelector('.o_ChannelPreviewView'), - 'o-muted', - "should be muted once marked as read" - ); - assert.containsNone( - document.body, - '.o_ChannelPreviewView_markAsRead', - "should no longer have the mark as read button" - ); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should not have opened the thread" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js index 820452c47c4..a4214ca4f01 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chat_window_manager_tests.js @@ -1,1986 +1,2261 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; -import { patchUiSize, SIZES } from '@mail/../tests/helpers/patch_ui_size'; +import { makeDeferred } from "@mail/utils/deferred"; +import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size"; import { afterNextRender, isScrolledToBottom, nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -import { file, dom } from 'web.test_utils'; +import { file, dom } from "web.test_utils"; const { createFile, inputFiles } = file; const { triggerEvent } = dom; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('chat_window_manager_tests.js'); - -QUnit.test('[technical] messaging not created', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - */ - assert.expect(1); - - const messagingBeforeCreationDeferred = makeDeferred(); - const { afterNextRender } = await start({ - messagingBeforeCreationDeferred, - waitUntilMessagingCondition: 'none', - }); - - // simulate messaging being created - await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); - - assert.containsOnce( - document.body, - '.o_ChatWindowManager', - "should contain chat window manager after messaging has been created" - ); -}); - -QUnit.test('initial mount', async function (assert) { - assert.expect(1); - - await start(); - assert.containsOnce( - document.body, - '.o_ChatWindowManager', - "should have chat window manager" - ); -}); - -QUnit.test('chat window new message: basic rendering', async function (assert) { - assert.expect(10); - - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open a chat window" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_header`).length, - 1, - "should have a header" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeaderView_name`).length, - 1, - "should have name part in header" - ); - assert.strictEqual( - document.querySelector(`.o_ChatWindow_header .o_ChatWindowHeaderView_name`).textContent, - "New message", - "should display 'new message' in the header" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeaderView_command`).length, - 1, - "should have 1 command in header" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeaderView_commandClose`).length, - 1, - "should have command to close chat window" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_newMessageForm`).length, - 1, - "should have a new message chat window container" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_newMessageFormLabel`).length, - 1, - "should have a part in selection with label" - ); - assert.strictEqual( - document.querySelector(`.o_ChatWindow_newMessageFormLabel`).textContent.trim(), - "To:", - "should have label 'To:' in selection" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow_newMessageFormInput`).length, - 1, - "should have an input in selection" - ); -}); - -QUnit.test('chat window new message: focused on open [REQUIRE FOCUS]', async function (assert) { - assert.expect(2); - - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - assert.ok( - document.querySelector(`.o_ChatWindow`).classList.contains('o-focused'), - "chat window should be focused" - ); - assert.strictEqual( - document.activeElement, - document.querySelector(`.o_ChatWindow_newMessageFormInput`), - "chat window focused = selection input focused" - ); -}); - -QUnit.test('chat window new message: close', async function (assert) { - assert.expect(1); - - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - await click(`.o_ChatWindow_header .o_ChatWindowHeaderView_commandClose`); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 0, - "chat window should be closed" - ); -}); - -QUnit.test('chat window new message: fold', async function (assert) { - assert.expect(6); - - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - assert.doesNotHaveClass( - document.querySelector(`.o_ChatWindow`), - 'o-folded', - "chat window should not be folded by default" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow_newMessageForm', - "chat window should have new message form" - ); - - await click(`.o_ChatWindow_header`); - assert.hasClass( - document.querySelector(`.o_ChatWindow`), - 'o-folded', - "chat window should become folded" - ); - assert.containsNone( - document.body, - '.o_ChatWindow_newMessageForm', - "chat window should not have new message form" - ); - - await click(`.o_ChatWindow_header`); - assert.doesNotHaveClass( - document.querySelector(`.o_ChatWindow`), - 'o-folded', - "chat window should become unfolded" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow_newMessageForm', - "chat window should have new message form" - ); -}); - -QUnit.test('open chat from "new message" chat window should open chat in place of this "new message" chat window', async function (assert) { - /** - * InnerWith computation uses following info: - * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) - * (others: @see mail/static/src/models/chat_window_manager.js:visual) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1920px - * - * Enough space for 3 visible chat windows: - * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 1920 - */ - assert.expect(12); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Partner 131" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const [, mailChannelId2] = pyEnv['mail.channel'].create([ - { - name: 'channel-1', - channel_member_ids: [ - [0, 0, { - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - { - name: 'channel-2', - channel_member_ids: [ - [0, 0, { - is_minimized: false, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - ]); - const imSearchDef = makeDeferred(); - patchUiSize({ width: 1920 }); - const { click, insertText } = await start({ - mockRPC(route, args) { - if (args.method === 'im_search') { - imSearchDef.resolve(); - } - } - }); - assert.containsN( - document.body, - '.o_ChatWindow', - 1, - "should have 1 chat window initially" - ); - assert.containsNone( - document.body, - '.o_ChatWindow.o-new-message', - "should not have any 'new message' chat window initially" - ); - - // open "new message" chat window - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - assert.containsOnce( - document.body, - '.o_ChatWindow.o-new-message', - "should have 'new message' chat window after clicking 'new message' in messaging menu" - ); - assert.containsN( - document.body, - '.o_ChatWindow', - 2, - "should have 2 chat windows after opening 'new message' chat window", - ); - assert.containsOnce( - document.body, - '.o_ChatWindow_newMessageFormInput', - "'new message' chat window should have new message form input" - ); - assert.hasClass( - document.querySelector('.o_ChatWindow[data-visible-index="1"]'), - 'o-new-message', - "'new message' chat window should be the last chat window initially", - ); - - // open channel-2 - await click(`.o_MessagingMenu_toggler`); - await click(`.o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]`); - assert.containsN( - document.body, - '.o_ChatWindow', - 3, - "should have 3 chat windows after opening chat window of 'channel-2'", - ); - assert.hasClass( - document.querySelector('.o_ChatWindow[data-visible-index="1"]'), - 'o-new-message', - "'new message' chat window should be in the middle after opening chat window of 'channel-2'", - ); - - // search for a user in "new message" autocomplete - await insertText('.o_ChatWindow_newMessageFormInput', "131"); - // Wait for search RPC to be resolved. The following await lines are - // necessary because autocomplete is an external lib therefore it is not - // possible to use `afterNextRender`. - await imSearchDef; - await nextAnimationFrame(); - const link = document.querySelector('.ui-autocomplete .ui-menu-item a'); - assert.ok( - link, - "should have autocomplete suggestion after typing on 'new message' input" - ); - assert.strictEqual( - link.textContent, - "Partner 131", - "autocomplete suggestion should target the partner matching search term" - ); - - await afterNextRender(() => link.click()); - assert.containsNone( - document.body, - '.o_ChatWindow.o-new-message', - "should have removed the 'new message' chat window after selecting a partner" - ); - assert.strictEqual( - document.querySelector('.o_ChatWindow[data-visible-index="1"] .o_ChatWindowHeaderView_name').textContent, - "Partner 131", - "chat window with selected partner should be opened in position where 'new message' chat window was, which is in the middle" - ); -}); - -QUnit.test('new message chat window should close on selecting the user if chat with the user is already open', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Partner 131" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - name: "Partner 131", - }); - const { afterEvent, click } = await start(); - - // open "new message" chat window - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - - // search for a user in "new message" autocomplete - await afterEvent({ - eventName: 'o-AutocompleteInput-source', - func: () => { - document.execCommand('insertText', false, "131"); - document.querySelector(`.o_ChatWindow_newMessageFormInput`) - .dispatchEvent(new window.KeyboardEvent('keydown')); - document.querySelector(`.o_ChatWindow_newMessageFormInput`) - .dispatchEvent(new window.KeyboardEvent('keyup')); - }, - message: "should wait until autocomplete ui sourced its data", - predicate: () => true, - }); - await nextAnimationFrame(); - const link = document.querySelector('.ui-autocomplete .ui-menu-item a'); - - await afterNextRender(() => link.click()); - assert.containsNone( - document.body, - '.o_ChatWindow_newMessageFormInput', - "'new message' chat window should not be there" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have only one chat window after selecting user whose chat is already open", - ); -}); - -QUnit.test('new message autocomplete should automatically select first result', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Partner 131" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const imSearchDef = makeDeferred(); - const { click } = await start({ - mockRPC(route, args) { - if (args.method === 'im_search') { - imSearchDef.resolve(); - } - }, - }); - - // open "new message" chat window - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); - - // search for a user in "new message" autocomplete - document.execCommand('insertText', false, "131"); - document.querySelector(`.o_ChatWindow_newMessageFormInput`) - .dispatchEvent(new window.KeyboardEvent('keydown')); - document.querySelector(`.o_ChatWindow_newMessageFormInput`) - .dispatchEvent(new window.KeyboardEvent('keyup')); - // Wait for search RPC to be resolved. The following await lines are - // necessary because autocomplete is an external lib therefore it is not - // possible to use `afterNextRender`. - await imSearchDef; - await nextAnimationFrame(); - assert.hasClass( - document.querySelector('.ui-autocomplete .ui-menu-item a'), - 'ui-state-active', - "first autocomplete result should be automatically selected", - ); -}); - -QUnit.test('chat window: basic rendering', async function (assert) { - assert.expect(15); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" }); - const { click } = await start(); - - await click(`.o_MessagingMenu_toggler`); - await click(`.o_NotificationListView_preview`); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open a chat window" - ); - const chatWindow = document.querySelector(`.o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`); - assert.ok( - chatWindow, - 'should have open a chat window of channel' - ); - assert.strictEqual( - chatWindow.querySelectorAll(`:scope .o_ChatWindow_header`).length, - 1, - "should have header part" - ); - const chatWindowHeader = chatWindow.querySelector(`:scope .o_ChatWindow_header`); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ThreadIconView`).length, - 1, - "should have thread icon in header part" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_name`).length, - 1, - "should have thread name in header part" - ); - assert.strictEqual( - chatWindowHeader.querySelector(`:scope .o_ChatWindowHeaderView_name`).textContent, - "General", - "should have correct thread name in header part" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_command`).length, - 6, - "should have 6 commands in header part" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandPhone`).length, - 1, - "should have command to start an RTC call in audio mode" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandCamera`).length, - 1, - "should have command to start an RTC call in video mode" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandShowMemberList`).length, - 1, - "should have command to show the member list" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandShowCallSettingsMenu`).length, - 1, - "should have command to show the call settings menu" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandExpand`).length, - 1, - "should have command to expand thread in discuss" - ); - assert.strictEqual( - chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandClose`).length, - 1, - "should have command to close chat window" - ); - assert.strictEqual( - chatWindow.querySelectorAll(`:scope .o_ChatWindow_thread`).length, - 1, - "should have part to display thread content inside chat window" - ); - assert.ok( - chatWindow.querySelector(`:scope .o_ChatWindow_thread`).classList.contains('o_ThreadView'), - "thread part should use component ThreadView" - ); -}); - -QUnit.test('chat window: fold', async function (assert) { - assert.expect(9); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - const { click } = await start({ - mockRPC(route, args) { - if (args.method === 'channel_fold') { - assert.step(`rpc:${args.method}/${args.kwargs.state}`); - } - }, - }); - // Open Thread - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window should have a thread" - ); - assert.verifySteps( - ['rpc:channel_fold/open'], - "should sync fold state 'open' with server after opening chat window" - ); - - // Fold chat window - await click(`.o_ChatWindow_header`); - assert.verifySteps( - ['rpc:channel_fold/folded'], - "should sync fold state 'folded' with server after folding chat window" - ); - assert.containsNone( - document.body, - '.o_ChatWindow_thread', - "chat window should not have any thread" - ); - - // Unfold chat window - await click(`.o_ChatWindow_header`); - assert.verifySteps( - ['rpc:channel_fold/open'], - "should sync fold state 'open' with server after unfolding chat window" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window should have a thread" - ); -}); - -QUnit.test('chat window: open / close', async function (assert) { - assert.expect(10); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - const { click } = await start({ - mockRPC(route, args) { - if (args.method === 'channel_fold') { - assert.step(`rpc:channel_fold/${args.kwargs.state}`); - } - }, - }); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should not have a chat window initially" - ); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have a chat window after clicking on thread preview" - ); - assert.verifySteps( - ['rpc:channel_fold/open'], - "should sync fold state 'open' with server after opening chat window" - ); - - // Close chat window - await click(`.o_ChatWindowHeaderView_commandClose`); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should not have a chat window after closing it" - ); - assert.verifySteps( - ['rpc:channel_fold/closed'], - "should sync fold state 'closed' with server after closing chat window" - ); - - // Reopen chat window - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have a chat window again after clicking on thread preview again" - ); - assert.verifySteps( - ['rpc:channel_fold/open'], - "should sync fold state 'open' with server after opening chat window again" - ); -}); - -QUnit.test('Mobile: opening a chat window should not update channel state on the server', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - fold_state: 'closed', - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - patchUiSize({ size: SIZES.SM }); - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_NotificationListView_preview`); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have a chat window after clicking on thread preview" - ); - const [member] = pyEnv['mail.channel.member'].searchRead([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - assert.strictEqual( - member.fold_state, - 'closed', - 'opening a chat window in mobile should not update channel state on the server', - ); -}); - -QUnit.test('Mobile: closing a chat window should not update channel state on the server', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - patchUiSize({ size: SIZES.SM }); - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_NotificationListView_preview`); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have a chat window after clicking on thread preview" - ); - // Close chat window - await click(`.o_ChatWindowHeaderView_commandClose`); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should not have a chat window after closing it" - ); - const [member] = pyEnv['mail.channel.member'].searchRead([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - assert.strictEqual( - member.fold_state, - 'open', - 'closing the chat window should not update channel state on the server', - ); -}); - -QUnit.test("Mobile: chat window shouldn't open automatically after receiving a new message", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const resUsersId1 = pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['mail.channel'].records = [ - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - id: resPartnerId1, - uuid: 'channel-10-uuid', - }, - ]; - patchUiSize({ size: SIZES.SM }); - const { messaging } = await start(); - - // simulate receiving a message - messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: 'channel-10-uuid', - }, - }); - await nextAnimationFrame(); - assert.containsNone( - document.body, - '.o_ChatWindow', - "On mobile, the chat window shouldn't open automatically after receiving a new message" - ); -}); - -QUnit.test('chat window: close on ESCAPE', async function (assert) { - assert.expect(10); - - const pyEnv = await startServer(); - pyEnv['res.partner'].create({ name: "TestPartner" }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - const { click, insertText } = await start({ - mockRPC(route, args) { - if (args.method === 'channel_fold') { - assert.step(`rpc:channel_fold/${args.kwargs.state}`); - } - }, - }); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "chat window should be opened initially" - ); - - await click(`.o_ComposerView_buttonEmojis`); - assert.containsOnce( - document.body, - '.o_EmojiPickerView', - "emoji list should be opened after click on emojis button" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerView_buttonEmojis`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_EmojiPickerView', - "emoji list should be closed after pressing escape on emojis button" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "chat window should still be opened after pressing escape on emojis button" - ); - - await insertText('.o_ComposerTextInputView_textarea', "@"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionListView_list'), - 'show', - "should display mention suggestions on typing '@'" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "mention suggestion should be closed after pressing escape on mention suggestion" - ); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "chat window should still be opened after pressing escape on mention suggestion" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_ChatWindow', - "chat window should be closed after pressing escape if there was no other priority escape handler" - ); - assert.verifySteps(['rpc:channel_fold/closed']); -}); - -QUnit.test('focus next visible chat window when closing current chat window with ESCAPE [REQUIRE FOCUS]', async function (assert) { - /** - * computation uses following info: - * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) - * (others: @see mail/static/src/models/chat_window_manager.js:visual) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1920px - * - * Enough space for 2 visible chat windows: - * 10 + 340 + 5 + 340 + 10 = 705 < 1920 - */ - assert.expect(4); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - ]); - patchUiSize({ width: 1920 }); - await start(); - assert.containsN( - document.body, - '.o_ChatWindow .o_ComposerTextInputView_textarea', - 2, - "2 chat windows should be present initially" - ); - assert.containsNone( - document.body, - '.o_ChatWindow.o-folded', - "both chat windows should be open" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: 'Escape' }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(ev); - }); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "only one chat window should remain after pressing escape on first chat window" - ); - assert.hasClass( - document.querySelector('.o_ChatWindow'), - 'o-focused', - "next visible chat window should be focused after pressing escape on first chat window" - ); -}); - -QUnit.test('chat window: composer state conservation on toggle discuss', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - const { click, insertText, messaging, openDiscuss, openView } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); - // Set content of the composer of the chat window - await insertText('.o_ComposerTextInputView_textarea', 'XDU for the win !'); - assert.containsNone( - document.body, - '.o_ComposerView .o_AttachmentCard', - "composer should have no attachment initially" - ); - // Set attachments of the composer - const files = [ - await createFile({ - name: 'text state conservation on toggle home menu.txt', - content: 'hello, world', - contentType: 'text/plain', - }), - await createFile({ - name: 'text2 state conservation on toggle home menu.txt', - content: 'hello, xdu is da best man', - contentType: 'text/plain', - }) - ]; - await afterNextRender(() => - inputFiles( - messaging.chatWindowManager.chatWindows[0].threadView.composerView.fileUploader.fileInput, - files - ) - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "XDU for the win !", - "chat window composer initial text input should contain 'XDU for the win !'" - ); - assert.containsN( - document.body, - '.o_ComposerView .o_AttachmentCard', - 2, - "composer should have 2 total attachments after adding 2 attachments" - ); - - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.containsNone(document.body, '.o_ChatWindow', "should not have any chat window after opening discuss"); - - await openView({ - res_id: mailChannelId, - res_model: 'mail.channel', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "XDU for the win !", - "chat window composer should still have the same input after closing discuss" - ); - assert.containsN( - document.body, - '.o_ComposerView .o_AttachmentCard', - 2, - "Chat window composer should have 2 attachments after closing discuss" - ); -}); - -QUnit.test('chat window: scroll conservation on toggle discuss', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i < 100; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("chat_window_manager_tests.js"); + + QUnit.test("[technical] messaging not created", async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(1); + + const messagingBeforeCreationDeferred = makeDeferred(); + const { afterNextRender } = await start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: "none", + }); + + // simulate messaging being created + await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); + + assert.containsOnce( + document.body, + ".o_ChatWindowManager", + "should contain chat window manager after messaging has been created" + ); }); - } - const { afterEvent, click, openDiscuss, openView } = await start(); - await click(`.o_MessagingMenu_toggler`); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector('.o_NotificationListView_preview').click(), - message: "should wait until channel scrolled to its last message after opening it from the messaging menu", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector('.o_ThreadView_messageList'); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) + + QUnit.test("initial mount", async function (assert) { + assert.expect(1); + + await start(); + assert.containsOnce( + document.body, + ".o_ChatWindowManager", + "should have chat window manager" ); - }, - }); - // Set a scroll position to chat window - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142; - }, - message: "should wait until channel scrolled to 142 after setting this value manually", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 + }); + + QUnit.test("chat window new message: basic rendering", async function (assert) { + assert.expect(10); + + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" ); - }, - }); - - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.containsNone(document.body, '.o_ChatWindow', "should not have any chat window after opening discuss"); - - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => openView({ - res_id: mailChannelId1, - res_model: 'mail.channel', - views: [[false, 'list']], - }), - message: "should wait until channel restored its scroll to 142 after closing discuss", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_header`).length, + 1, + "should have a header" ); - }, - }); - assert.strictEqual( - document.querySelector(`.o_ThreadView_messageList`).scrollTop, - 142, - "chat window scrollTop should still be the same after closing discuss" - ); -}); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeaderView_name`) + .length, + 1, + "should have name part in header" + ); + assert.strictEqual( + document.querySelector(`.o_ChatWindow_header .o_ChatWindowHeaderView_name`) + .textContent, + "New message", + "should display 'new message' in the header" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_header .o_ChatWindowHeaderView_command`) + .length, + 1, + "should have 1 command in header" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_ChatWindow_header .o_ChatWindowHeaderView_commandClose` + ).length, + 1, + "should have command to close chat window" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_newMessageForm`).length, + 1, + "should have a new message chat window container" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_newMessageFormLabel`).length, + 1, + "should have a part in selection with label" + ); + assert.strictEqual( + document.querySelector(`.o_ChatWindow_newMessageFormLabel`).textContent.trim(), + "To:", + "should have label 'To:' in selection" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow_newMessageFormInput`).length, + 1, + "should have an input in selection" + ); + }); -QUnit.test('open 2 different chat windows: enough screen width [REQUIRE FOCUS]', async function (assert) { - /** - * computation uses following info: - * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) - * (others: @see mail/static/src/models/chat_window_manager.js:visual) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1920px - * - * Enough space for 2 visible chat windows: - * 10 + 340 + 5 + 340 + 10 = 705 < 1920 - */ - assert.expect(8); + QUnit.test( + "chat window new message: focused on open [REQUIRE FOCUS]", + async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }]); - patchUiSize({ width: 1920 }); // enough to fit at least 2 chat windows - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(` + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + assert.ok( + document.querySelector(`.o_ChatWindow`).classList.contains("o-focused"), + "chat window should be focused" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ChatWindow_newMessageFormInput`), + "chat window focused = selection input focused" + ); + } + ); + + QUnit.test("chat window new message: close", async function (assert) { + assert.expect(1); + + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + await click(`.o_ChatWindow_header .o_ChatWindowHeaderView_commandClose`); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 0, + "chat window should be closed" + ); + }); + + QUnit.test("chat window new message: fold", async function (assert) { + assert.expect(6); + + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + assert.doesNotHaveClass( + document.querySelector(`.o_ChatWindow`), + "o-folded", + "chat window should not be folded by default" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow_newMessageForm", + "chat window should have new message form" + ); + + await click(`.o_ChatWindow_header`); + assert.hasClass( + document.querySelector(`.o_ChatWindow`), + "o-folded", + "chat window should become folded" + ); + assert.containsNone( + document.body, + ".o_ChatWindow_newMessageForm", + "chat window should not have new message form" + ); + + await click(`.o_ChatWindow_header`); + assert.doesNotHaveClass( + document.querySelector(`.o_ChatWindow`), + "o-folded", + "chat window should become unfolded" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow_newMessageForm", + "chat window should have new message form" + ); + }); + + QUnit.test( + 'open chat from "new message" chat window should open chat in place of this "new message" chat window', + async function (assert) { + /** + * InnerWith computation uses following info: + * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager.js:visual) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1920px + * + * Enough space for 3 visible chat windows: + * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 1920 + */ + assert.expect(12); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Partner 131" }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const [, mailChannelId2] = pyEnv["mail.channel"].create([ + { + name: "channel-1", + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + { + name: "channel-2", + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + ]); + const imSearchDef = makeDeferred(); + patchUiSize({ width: 1920 }); + const { click, insertText } = await start({ + mockRPC(route, args) { + if (args.method === "im_search") { + imSearchDef.resolve(); + } + }, + }); + assert.containsN( + document.body, + ".o_ChatWindow", + 1, + "should have 1 chat window initially" + ); + assert.containsNone( + document.body, + ".o_ChatWindow.o-new-message", + "should not have any 'new message' chat window initially" + ); + + // open "new message" chat window + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + assert.containsOnce( + document.body, + ".o_ChatWindow.o-new-message", + "should have 'new message' chat window after clicking 'new message' in messaging menu" + ); + assert.containsN( + document.body, + ".o_ChatWindow", + 2, + "should have 2 chat windows after opening 'new message' chat window" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow_newMessageFormInput", + "'new message' chat window should have new message form input" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow[data-visible-index="1"]'), + "o-new-message", + "'new message' chat window should be the last chat window initially" + ); + + // open channel-2 + await click(`.o_MessagingMenu_toggler`); + await click(`.o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]`); + assert.containsN( + document.body, + ".o_ChatWindow", + 3, + "should have 3 chat windows after opening chat window of 'channel-2'" + ); + assert.hasClass( + document.querySelector('.o_ChatWindow[data-visible-index="1"]'), + "o-new-message", + "'new message' chat window should be in the middle after opening chat window of 'channel-2'" + ); + + // search for a user in "new message" autocomplete + await insertText(".o_ChatWindow_newMessageFormInput", "131"); + // Wait for search RPC to be resolved. The following await lines are + // necessary because autocomplete is an external lib therefore it is not + // possible to use `afterNextRender`. + await imSearchDef; + await nextAnimationFrame(); + const link = document.querySelector(".ui-autocomplete .ui-menu-item a"); + assert.ok( + link, + "should have autocomplete suggestion after typing on 'new message' input" + ); + assert.strictEqual( + link.textContent, + "Partner 131", + "autocomplete suggestion should target the partner matching search term" + ); + + await afterNextRender(() => link.click()); + assert.containsNone( + document.body, + ".o_ChatWindow.o-new-message", + "should have removed the 'new message' chat window after selecting a partner" + ); + assert.strictEqual( + document.querySelector( + '.o_ChatWindow[data-visible-index="1"] .o_ChatWindowHeaderView_name' + ).textContent, + "Partner 131", + "chat window with selected partner should be opened in position where 'new message' chat window was, which is in the middle" + ); + } + ); + + QUnit.test( + "new message chat window should close on selecting the user if chat with the user is already open", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Partner 131" }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + name: "Partner 131", + }); + const { afterEvent, click } = await start(); + + // open "new message" chat window + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + + // search for a user in "new message" autocomplete + await afterEvent({ + eventName: "o-AutocompleteInput-source", + func: () => { + document.execCommand("insertText", false, "131"); + document + .querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent("keydown")); + document + .querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent("keyup")); + }, + message: "should wait until autocomplete ui sourced its data", + predicate: () => true, + }); + await nextAnimationFrame(); + const link = document.querySelector(".ui-autocomplete .ui-menu-item a"); + + await afterNextRender(() => link.click()); + assert.containsNone( + document.body, + ".o_ChatWindow_newMessageFormInput", + "'new message' chat window should not be there" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have only one chat window after selecting user whose chat is already open" + ); + } + ); + + QUnit.test( + "new message autocomplete should automatically select first result", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Partner 131" }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const imSearchDef = makeDeferred(); + const { click } = await start({ + mockRPC(route, args) { + if (args.method === "im_search") { + imSearchDef.resolve(); + } + }, + }); + + // open "new message" chat window + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); + + // search for a user in "new message" autocomplete + document.execCommand("insertText", false, "131"); + document + .querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent("keydown")); + document + .querySelector(`.o_ChatWindow_newMessageFormInput`) + .dispatchEvent(new window.KeyboardEvent("keyup")); + // Wait for search RPC to be resolved. The following await lines are + // necessary because autocomplete is an external lib therefore it is not + // possible to use `afterNextRender`. + await imSearchDef; + await nextAnimationFrame(); + assert.hasClass( + document.querySelector(".ui-autocomplete .ui-menu-item a"), + "ui-state-active", + "first autocomplete result should be automatically selected" + ); + } + ); + + QUnit.test("chat window: basic rendering", async function (assert) { + assert.expect(15); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "General" }); + const { click } = await start(); + + await click(`.o_MessagingMenu_toggler`); + await click(`.o_NotificationListView_preview`); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + const chatWindow = document.querySelector( + `.o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]` + ); + assert.ok(chatWindow, "should have open a chat window of channel"); + assert.strictEqual( + chatWindow.querySelectorAll(`:scope .o_ChatWindow_header`).length, + 1, + "should have header part" + ); + const chatWindowHeader = chatWindow.querySelector(`:scope .o_ChatWindow_header`); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ThreadIconView`).length, + 1, + "should have thread icon in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_name`).length, + 1, + "should have thread name in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelector(`:scope .o_ChatWindowHeaderView_name`).textContent, + "General", + "should have correct thread name in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_command`).length, + 6, + "should have 6 commands in header part" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandPhone`) + .length, + 1, + "should have command to start an RTC call in audio mode" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandCamera`) + .length, + 1, + "should have command to start an RTC call in video mode" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll( + `:scope .o_ChatWindowHeaderView_commandShowMemberList` + ).length, + 1, + "should have command to show the member list" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll( + `:scope .o_ChatWindowHeaderView_commandShowCallSettingsMenu` + ).length, + 1, + "should have command to show the call settings menu" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandExpand`) + .length, + 1, + "should have command to expand thread in discuss" + ); + assert.strictEqual( + chatWindowHeader.querySelectorAll(`:scope .o_ChatWindowHeaderView_commandClose`) + .length, + 1, + "should have command to close chat window" + ); + assert.strictEqual( + chatWindow.querySelectorAll(`:scope .o_ChatWindow_thread`).length, + 1, + "should have part to display thread content inside chat window" + ); + assert.ok( + chatWindow + .querySelector(`:scope .o_ChatWindow_thread`) + .classList.contains("o_ThreadView"), + "thread part should use component ThreadView" + ); + }); + + QUnit.test("chat window: fold", async function (assert) { + assert.expect(9); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + const { click } = await start({ + mockRPC(route, args) { + if (args.method === "channel_fold") { + assert.step(`rpc:${args.method}/${args.kwargs.state}`); + } + }, + }); + // Open Thread + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window should have a thread" + ); + assert.verifySteps( + ["rpc:channel_fold/open"], + "should sync fold state 'open' with server after opening chat window" + ); + + // Fold chat window + await click(`.o_ChatWindow_header`); + assert.verifySteps( + ["rpc:channel_fold/folded"], + "should sync fold state 'folded' with server after folding chat window" + ); + assert.containsNone( + document.body, + ".o_ChatWindow_thread", + "chat window should not have any thread" + ); + + // Unfold chat window + await click(`.o_ChatWindow_header`); + assert.verifySteps( + ["rpc:channel_fold/open"], + "should sync fold state 'open' with server after unfolding chat window" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window should have a thread" + ); + }); + + QUnit.test("chat window: open / close", async function (assert) { + assert.expect(10); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + const { click } = await start({ + mockRPC(route, args) { + if (args.method === "channel_fold") { + assert.step(`rpc:channel_fold/${args.kwargs.state}`); + } + }, + }); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have a chat window initially" + ); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have a chat window after clicking on thread preview" + ); + assert.verifySteps( + ["rpc:channel_fold/open"], + "should sync fold state 'open' with server after opening chat window" + ); + + // Close chat window + await click(`.o_ChatWindowHeaderView_commandClose`); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have a chat window after closing it" + ); + assert.verifySteps( + ["rpc:channel_fold/closed"], + "should sync fold state 'closed' with server after closing chat window" + ); + + // Reopen chat window + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have a chat window again after clicking on thread preview again" + ); + assert.verifySteps( + ["rpc:channel_fold/open"], + "should sync fold state 'open' with server after opening chat window again" + ); + }); + + QUnit.test( + "Mobile: opening a chat window should not update channel state on the server", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "closed", + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + patchUiSize({ size: SIZES.SM }); + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_NotificationListView_preview`); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have a chat window after clicking on thread preview" + ); + const [member] = pyEnv["mail.channel.member"].searchRead([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + assert.strictEqual( + member.fold_state, + "closed", + "opening a chat window in mobile should not update channel state on the server" + ); + } + ); + + QUnit.test( + "Mobile: closing a chat window should not update channel state on the server", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + patchUiSize({ size: SIZES.SM }); + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_NotificationListView_preview`); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have a chat window after clicking on thread preview" + ); + // Close chat window + await click(`.o_ChatWindowHeaderView_commandClose`); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have a chat window after closing it" + ); + const [member] = pyEnv["mail.channel.member"].searchRead([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + assert.strictEqual( + member.fold_state, + "open", + "closing the chat window should not update channel state on the server" + ); + } + ); + + QUnit.test( + "Mobile: chat window shouldn't open automatically after receiving a new message", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const resUsersId1 = pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["mail.channel"].records = [ + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + id: resPartnerId1, + uuid: "channel-10-uuid", + }, + ]; + patchUiSize({ size: SIZES.SM }); + const { messaging } = await start(); + + // simulate receiving a message + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: "channel-10-uuid", + }, + }); + await nextAnimationFrame(); + assert.containsNone( + document.body, + ".o_ChatWindow", + "On mobile, the chat window shouldn't open automatically after receiving a new message" + ); + } + ); + + QUnit.test("chat window: close on ESCAPE", async function (assert) { + assert.expect(10); + + const pyEnv = await startServer(); + pyEnv["res.partner"].create({ name: "TestPartner" }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + const { click, insertText } = await start({ + mockRPC(route, args) { + if (args.method === "channel_fold") { + assert.step(`rpc:channel_fold/${args.kwargs.state}`); + } + }, + }); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "chat window should be opened initially" + ); + + await click(`.o_ComposerView_buttonEmojis`); + assert.containsOnce( + document.body, + ".o_EmojiPickerView", + "emoji list should be opened after click on emojis button" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerView_buttonEmojis`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_EmojiPickerView", + "emoji list should be closed after pressing escape on emojis button" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "chat window should still be opened after pressing escape on emojis button" + ); + + await insertText(".o_ComposerTextInputView_textarea", "@"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionListView_list"), + "show", + "should display mention suggestions on typing '@'" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "mention suggestion should be closed after pressing escape on mention suggestion" + ); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "chat window should still be opened after pressing escape on mention suggestion" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_ChatWindow", + "chat window should be closed after pressing escape if there was no other priority escape handler" + ); + assert.verifySteps(["rpc:channel_fold/closed"]); + }); + + QUnit.test( + "focus next visible chat window when closing current chat window with ESCAPE [REQUIRE FOCUS]", + async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager.js:visual) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1920px + * + * Enough space for 2 visible chat windows: + * 10 + 340 + 5 + 340 + 10 = 705 < 1920 + */ + assert.expect(4); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + ]); + patchUiSize({ width: 1920 }); + await start(); + assert.containsN( + document.body, + ".o_ChatWindow .o_ComposerTextInputView_textarea", + 2, + "2 chat windows should be present initially" + ); + assert.containsNone( + document.body, + ".o_ChatWindow.o-folded", + "both chat windows should be open" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { + bubbles: true, + key: "Escape", + }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(ev); + }); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "only one chat window should remain after pressing escape on first chat window" + ); + assert.hasClass( + document.querySelector(".o_ChatWindow"), + "o-focused", + "next visible chat window should be focused after pressing escape on first chat window" + ); + } + ); + + QUnit.test( + "chat window: composer state conservation on toggle discuss", + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + const { click, insertText, messaging, openDiscuss, openView } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); + // Set content of the composer of the chat window + await insertText(".o_ComposerTextInputView_textarea", "XDU for the win !"); + assert.containsNone( + document.body, + ".o_ComposerView .o_AttachmentCard", + "composer should have no attachment initially" + ); + // Set attachments of the composer + const files = [ + await createFile({ + name: "text state conservation on toggle home menu.txt", + content: "hello, world", + contentType: "text/plain", + }), + await createFile({ + name: "text2 state conservation on toggle home menu.txt", + content: "hello, xdu is da best man", + contentType: "text/plain", + }), + ]; + await afterNextRender(() => + inputFiles( + messaging.chatWindowManager.chatWindows[0].threadView.composerView + .fileUploader.fileInput, + files + ) + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "XDU for the win !", + "chat window composer initial text input should contain 'XDU for the win !'" + ); + assert.containsN( + document.body, + ".o_ComposerView .o_AttachmentCard", + 2, + "composer should have 2 total attachments after adding 2 attachments" + ); + + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have any chat window after opening discuss" + ); + + await openView({ + res_id: mailChannelId, + res_model: "mail.channel", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "XDU for the win !", + "chat window composer should still have the same input after closing discuss" + ); + assert.containsN( + document.body, + ".o_ComposerView .o_AttachmentCard", + 2, + "Chat window composer should have 2 attachments after closing discuss" + ); + } + ); + + QUnit.test("chat window: scroll conservation on toggle discuss", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i < 100; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, click, openDiscuss, openView } = await start(); + await click(`.o_MessagingMenu_toggler`); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => document.querySelector(".o_NotificationListView_preview").click(), + message: + "should wait until channel scrolled to its last message after opening it from the messaging menu", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(".o_ThreadView_messageList"); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + // Set a scroll position to chat window + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142; + }, + message: + "should wait until channel scrolled to 142 after setting this value manually", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }); + + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have any chat window after opening discuss" + ); + + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => + openView({ + res_id: mailChannelId1, + res_model: "mail.channel", + views: [[false, "list"]], + }), + message: + "should wait until channel restored its scroll to 142 after closing discuss", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same after closing discuss" + ); + }); + + QUnit.test( + "open 2 different chat windows: enough screen width [REQUIRE FOCUS]", + async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager.js:visual) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1920px + * + * Enough space for 2 visible chat windows: + * 10 + 340 + 5 + 340 + 10 = 705 < 1920 + */ + assert.expect(8); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + ]); + patchUiSize({ width: 1920 }); // enough to fit at least 2 chat windows + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open a chat window" - ); - assert.strictEqual( - document.querySelectorAll(` + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + assert.strictEqual( + document.querySelectorAll(` .o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] `).length, - 1, - "chat window of chat should be open" - ); - assert.ok( - document.querySelector(` + 1, + "chat window of chat should be open" + ); + assert.ok( + document + .querySelector( + ` .o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] - `).classList.contains('o-focused'), - "chat window of chat should have focus" - ); + ` + ) + .classList.contains("o-focused"), + "chat window of chat should have focus" + ); - await click(`.o_MessagingMenu_toggler`); - await click(` + await click(`.o_MessagingMenu_toggler`); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 2, - "should have open a new chat window" - ); - assert.strictEqual( - document.querySelectorAll(` + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 2, + "should have open a new chat window" + ); + assert.strictEqual( + document.querySelectorAll(` .o_ChatWindow[data-thread-id="${mailChannelId2}"][data-thread-model="mail.channel"] `).length, - 1, - "chat window of channel should be open" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "chat window of channel should be open" + ); + assert.strictEqual( + document.querySelectorAll(` .o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] `).length, - 1, - "chat window of chat should still be open" - ); - assert.ok( - document.querySelector(` + 1, + "chat window of chat should still be open" + ); + assert.ok( + document + .querySelector( + ` .o_ChatWindow[data-thread-id="${mailChannelId2}"][data-thread-model="mail.channel"] - `).classList.contains('o-focused'), - "chat window of channel should have focus" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-focused"), + "chat window of channel should have focus" + ); + assert.notOk( + document + .querySelector( + ` .o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] - `).classList.contains('o-focused'), - "chat window of chat should no longer have focus" - ); -}); + ` + ) + .classList.contains("o-focused"), + "chat window of chat should no longer have focus" + ); + } + ); -QUnit.test('open 3 different chat windows: not enough screen width', async function (assert) { - /** - * computation uses following info: - * ([mocked] global window width: 900px) - * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1080px - * - * Enough space for 2 visible chat windows, and one hidden chat window: - * 3 visible chat windows: - * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 900 - * 2 visible chat windows + hidden menu: - * 10 + 340 + 5 + 340 + 10 + 170 + 5 = 880 < 900 - */ - assert.expect(12); + QUnit.test( + "open 3 different chat windows: not enough screen width", + async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: 900px) + * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1080px + * + * Enough space for 2 visible chat windows, and one hidden chat window: + * 3 visible chat windows: + * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 900 + * 2 visible chat windows + hidden menu: + * 10 + 340 + 5 + 340 + 10 + 170 + 5 = 880 < 900 + */ + assert.expect(12); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv['mail.channel'].create([ - { name: 'mailChannel1' }, - { name: 'mailChannel2' }, - { name: 'mailChannel3' }, - ]); - patchUiSize({ width: 900 }); // enough to fit 2 chat windows but not 3 - const { click } = await start(); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv[ + "mail.channel" + ].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + { name: "mailChannel3" }, + ]); + patchUiSize({ width: 900 }); // enough to fit 2 chat windows but not 3 + const { click } = await start(); - // open, from systray menu, chat windows of channels with Id 1, 2, then 3 - await click(`.o_MessagingMenu_toggler`); - await click(` + // open, from systray menu, chat windows of channels with Id 1, 2, then 3 + await click(`.o_MessagingMenu_toggler`); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open 1 visible chat window" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, - 0, - "should not have hidden menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, - 0, - "messaging menu should be hidden" - ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open 1 visible chat window" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, + 0, + "should not have hidden menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "messaging menu should be hidden" + ); - await click(`.o_MessagingMenu_toggler`); - await click(` + await click(`.o_MessagingMenu_toggler`); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 2, - "should have open 2 visible chat windows" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, - 0, - "should not have hidden menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, - 0, - "messaging menu should be hidden" - ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 2, + "should have open 2 visible chat windows" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, + 0, + "should not have hidden menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "messaging menu should be hidden" + ); - await click(`.o_MessagingMenu_toggler`); - await click(` + await click(`.o_MessagingMenu_toggler`); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId3}"] `); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 2, - "should have open 2 visible chat windows" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, - 1, - "should have hidden menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, - 0, - "messaging menu should be hidden" - ); - assert.strictEqual( - document.querySelectorAll(` + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 2, + "should have open 2 visible chat windows" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindowManager_hiddenMenu`).length, + 1, + "should have hidden menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "messaging menu should be hidden" + ); + assert.strictEqual( + document.querySelectorAll(` .o_ChatWindow[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] `).length, - 1, - "chat window of channel 1 should be open" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "chat window of channel 1 should be open" + ); + assert.strictEqual( + document.querySelectorAll(` .o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"] `).length, - 1, - "chat window of channel 3 should be open" - ); - assert.ok( - document.querySelector(` + 1, + "chat window of channel 3 should be open" + ); + assert.ok( + document + .querySelector( + ` .o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"] - `).classList.contains('o-focused'), - "chat window of channel 3 should have focus" - ); -}); - -QUnit.test('chat window: switch on TAB', async function (assert) { - assert.expect(10); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'channel1' }, { name: 'channel2' }]); - const { click } = await start(); - - await click(`.o_MessagingMenu_toggler`); - await click(` - .o_MessagingMenu_dropdownMenu - .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]` - ); - - assert.containsOnce(document.body, '.o_ChatWindow', "Only 1 chatWindow must be opened"); - const chatWindow = document.querySelector('.o_ChatWindow'); - assert.strictEqual( - chatWindow.querySelector('.o_ChatWindowHeaderView_name').textContent, - 'channel1', - "The name of the only chatWindow should be 'channel1' (channel with ID 1)" - ); - assert.strictEqual( - chatWindow.querySelector('.o_ComposerTextInputView_textarea'), - document.activeElement, - "The chatWindow composer must have focus" - ); - - await afterNextRender(() => - triggerEvent( - chatWindow.querySelector('.o_ChatWindow .o_ComposerTextInputView_textarea'), - 'keydown', - { key: 'Tab' }, - ) - ); - assert.strictEqual( - chatWindow.querySelector('.o_ChatWindow .o_ComposerTextInputView_textarea'), - document.activeElement, - "The chatWindow composer still has focus" - ); - - await click(`.o_MessagingMenu_toggler`); - await click(` - .o_MessagingMenu_dropdownMenu - .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]` - ); - - assert.containsN(document.body, '.o_ChatWindow', 2, "2 chatWindows must be opened"); - const chatWindows = document.querySelectorAll('.o_ChatWindow'); - assert.strictEqual( - chatWindows[0].querySelector('.o_ChatWindowHeaderView_name').textContent, - 'channel1', - "The name of the 1st chatWindow should be 'channel1' (channel with ID 1)" - ); - assert.strictEqual( - chatWindows[1].querySelector('.o_ChatWindowHeaderView_name').textContent, - 'channel2', - "The name of the 2nd chatWindow should be 'channel2' (channel with ID 2)" - ); - assert.strictEqual( - chatWindows[1].querySelector('.o_ComposerTextInputView_textarea'), - document.activeElement, - "The 2nd chatWindow composer must have focus (channel with ID 2)" - ); - - await afterNextRender(() => - triggerEvent( - chatWindows[1].querySelector('.o_ComposerTextInputView_textarea'), - 'keydown', - { key: 'Tab' }, - ) - ); - assert.containsN(document.body, '.o_ChatWindow', 2, "2 chatWindows should still be opened"); - assert.strictEqual( - chatWindows[0].querySelector('.o_ComposerTextInputView_textarea'), - document.activeElement, - "The 1st chatWindow composer must have focus (channel with ID 1)" - ); -}); - -QUnit.test('chat window: TAB cycle with 3 open chat windows [REQUIRE FOCUS]', async function (assert) { - /** - * InnerWith computation uses following info: - * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) - * (others: @see mail/static/src/models/chat_window_manager.js:visual) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1920px - * - * Enough space for 3 visible chat windows: - * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 1920 - */ - assert.expect(6); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }, - ]); - patchUiSize({ width: 1920 }); - await start(); - assert.containsN( - document.body, - '.o_ChatWindow .o_ComposerTextInputView_textarea', - 3, - "initialy, 3 chat windows should be present" - ); - assert.containsNone( - document.body, - '.o_ChatWindow.o-folded', - "all 3 chat windows should be open" - ); - - await afterNextRender(() => { - document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea").focus(); - }); - assert.strictEqual( - document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea"), - document.activeElement, - "The chatWindow with visible-index 2 should have the focus" - ); - - await afterNextRender(() => - triggerEvent( - document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea"), - 'keydown', - { key: 'Tab' }, - ) - ); - assert.strictEqual( - document.querySelector(".o_ChatWindow[data-visible-index='1'] .o_ComposerTextInputView_textarea"), - document.activeElement, - "after pressing tab on the chatWindow with visible-index 2, the chatWindow with visible-index 1 should have focus" - ); - - await afterNextRender(() => - triggerEvent( - document.querySelector(".o_ChatWindow[data-visible-index='1'] .o_ComposerTextInputView_textarea"), - 'keydown', - { key: 'Tab' }, - ) - ); - assert.strictEqual( - document.querySelector(".o_ChatWindow[data-visible-index='0'] .o_ComposerTextInputView_textarea"), - document.activeElement, - "after pressing tab on the chat window with visible-index 1, the chatWindow with visible-index 0 should have focus" - ); - - await afterNextRender(() => - triggerEvent( - document.querySelector(".o_ChatWindow[data-visible-index='0'] .o_ComposerTextInputView_textarea"), - 'keydown', - { key: 'Tab' }, - ) - ); - assert.strictEqual( - document.querySelector(".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea"), - document.activeElement, - "the chatWindow with visible-index 2 should have the focus after pressing tab on the chatWindow with visible-index 0" - ); -}); - -QUnit.test('chat window with a thread: keep scroll position in message list on folded', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i < 100; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, - }); - } - const { afterEvent, click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector('.o_NotificationListView_preview').click(), - message: "should wait until channel scrolled to its last message after opening it from the messaging menu", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector('.o_ThreadView_messageList'); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) - ); - }, - }); - // Set a scroll position to chat window - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142; - }, - message: "should wait until channel scrolled to 142 after setting this value manually", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 - ); - }, - }); - assert.strictEqual( - document.querySelector(`.o_ThreadView_messageList`).scrollTop, - 142, - "verify chat window initial scrollTop" - ); - - // fold chat window - await click('.o_ChatWindow_header'); - assert.containsNone( - document.body, - ".o_ThreadView", - "chat window should be folded so no ThreadView should be present" - ); - - // unfold chat window - await afterNextRender(() => afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector('.o_ChatWindow_header').click(), - message: "should wait until channel restored its scroll position to 142", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 - ); - }, - })); - assert.strictEqual( - document.querySelector(`.o_ThreadView_messageList`).scrollTop, - 142, - "chat window scrollTop should still be the same when chat window is unfolded" - ); -}); - -QUnit.test('chat window should scroll to the newly posted message just after posting it', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - for (let i = 0; i < 10; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, - }); - } - const { insertText } = await start(); - - // Set content of the composer of the chat window - await insertText('.o_ComposerTextInputView_textarea', 'WOLOLO'); - // Send a new message in the chatwindow to trigger the scroll - await afterNextRender(() => - triggerEvent( - document.querySelector('.o_ChatWindow .o_ComposerTextInputView_textarea'), - 'keydown', - { key: 'Enter' }, - ) - ); - const messageList = document.querySelector('.o_MessageListView'); - assert.ok( - isScrolledToBottom(messageList), - "chat window should scroll to the newly posted message just after posting it" - ); -}); - -QUnit.test('chat window: post message on non-mailing channel with "CTRL-Enter" keyboard shortcut for small screen size', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - patchUiSize({ size: SIZES.SM }); - const { click, insertText } = await start(); - - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); - // insert some HTML in editable - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await afterNextRender(() => { - const kevt = new window.KeyboardEvent('keydown', { ctrlKey: true, key: "Enter" }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should now have single message in channel after posting message from pressing 'CTRL-Enter' in text input of composer for small screen" - ); -}); - -QUnit.test('chat window with a thread: keep scroll position in message list on toggle discuss when folded', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i < 100; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, - }); - } - const { afterEvent, click, openDiscuss, openView } = await start(); - await click(`.o_MessagingMenu_toggler`); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector('.o_NotificationListView_preview').click(), - message: "should wait until channel scrolled to its last message after opening it from the messaging menu", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector('.o_ThreadView_messageList'); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) - ); - }, - }); - // Set a scroll position to chat window - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142, - message: "should wait until channel scrolled to 142 after setting this value manually", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 - ); - }, - }); - // fold chat window - await click('.o_ChatWindow_header'); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.containsNone(document.body, '.o_ChatWindow', "should not have any chat window after opening discuss"); - - await openView({ - res_id: mailChannelId1, - res_model: 'mail.channel', - views: [[false, 'list']], - }); - // unfold chat window - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector('.o_ChatWindow_header').click(), - message: "should wait until channel restored its scroll position to the last saved value (142)", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 142 - ); - }, - }); - assert.strictEqual( - document.querySelector(`.o_ThreadView_messageList`).scrollTop, - 142, - "chat window scrollTop should still be the same after closing discuss" - ); -}); - -QUnit.test('chat window does not fetch messages if hidden', async function (assert) { - /** - * computation uses following info: - * ([mocked] global window width: 900px) - * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1080px - * - * Enough space for 2 visible chat windows, and one hidden chat window: - * 3 visible chat windows: - * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 > 900 - * 2 visible chat windows + hidden menu: - * 10 + 340 + 5 + 340 + 10 + 170 + 5 = 880 < 900 - */ - assert.expect(11); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }] - ], - name: "Channel #10", - }, - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - name: "Channel #11", - }, - { - channel_member_ids: [ - [0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }], - ], - name: "Channel #12", - }, - ]); - patchUiSize({ width: 900 }); - const { click } = await start({ - mockRPC(route, args) { - if (route === '/mail/channel/messages') { - const { channel_id } = args; - assert.step(`rpc:/mail/channel/messages:${channel_id}`); + ` + ) + .classList.contains("o-focused"), + "chat window of channel 3 should have focus" + ); } - }, - }); + ); - assert.containsN( - document.body, - '.o_ChatWindow', - 2, - "2 chat windows should be visible" - ); - assert.containsNone( - document.body, - `.o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"]`, - "chat window for Channel #12 should be hidden" - ); - assert.containsOnce( - document.body, - '.o_ChatWindowHiddenMenuView', - "chat window hidden menu should be displayed" - ); - assert.verifySteps( - [`rpc:/mail/channel/messages:${mailChannelId1}`, `rpc:/mail/channel/messages:${mailChannelId2}`], - "messages should be fetched for the two visible chat windows" - ); + QUnit.test("chat window: switch on TAB", async function (assert) { + assert.expect(10); - await click('.o_ChatWindowHiddenMenuView_dropdownToggle'); - assert.containsOnce( - document.body, - '.o_ChatWindowHiddenMenuItemView', - "1 hidden chat window should be listed in hidden menu" - ); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "channel1" }, + { name: "channel2" }, + ]); + const { click } = await start(); - await click('.o_ChatWindowHiddenMenuItemView_chatWindowHeader'); - assert.containsN( - document.body, - '.o_ChatWindow', - 2, - "2 chat windows should still be visible" - ); - assert.containsOnce( - document.body, - `.o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"]`, - "chat window for Channel #12 should now be visible" - ); - assert.verifySteps( - [`rpc:/mail/channel/messages:${mailChannelId3}`], - "messages should now be fetched for Channel #12" - ); -}); + await click(`.o_MessagingMenu_toggler`); + await click(` + .o_MessagingMenu_dropdownMenu + .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"]`); -QUnit.test('new message separator is shown in a chat window of a chat on receiving new message if there is a history of conversation', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Foreigner user", partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_minimized: true, - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - uuid: 'channel-10-uuid', - }); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - const { messaging } = await start(); - - // simulate receiving a message - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: 'channel-10-uuid', - }, - })); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "a chat window should be visible after receiving a new message from a chat" - ); - assert.containsN( - document.body, - '.o_MessageView', - 2, - "chat window should have 2 messages" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "should display 'new messages' separator in the conversation, from reception of new messages" - ); -}); - -QUnit.test('new message separator is not shown in a chat window of a chat on receiving new message if there is no history of conversation', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Foreigner user", partner_id: resPartnerId1 }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - uuid: 'channel-10-uuid', - }); - const { messaging } = await start(); - - // simulate receiving a message - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: 'channel-10-uuid', - }, - })); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should not display 'new messages' separator in the conversation of a chat on receiving new message if there is no history of conversation" - ); -}); - -QUnit.test('focusing a chat window of a chat should make new message separator disappear [REQUIRE FOCUS]', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Foreigner user", partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_minimized: true, - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - uuid: 'channel-10-uuid', - }); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - const { afterEvent, messaging } = await start(); - - // simulate receiving a message - await afterNextRender(() => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: 'channel-10-uuid', - }, - })); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "should display 'new messages' separator in the conversation, from reception of new messages" - ); - - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => document.querySelector('.o_ComposerTextInputView_textarea').focus(), - message: "should wait until last seen by current partner message id changed", - predicate: ({ thread }) => { - return ( - thread.id === mailChannelId1 && - thread.model === 'mail.channel' + assert.containsOnce(document.body, ".o_ChatWindow", "Only 1 chatWindow must be opened"); + const chatWindow = document.querySelector(".o_ChatWindow"); + assert.strictEqual( + chatWindow.querySelector(".o_ChatWindowHeaderView_name").textContent, + "channel1", + "The name of the only chatWindow should be 'channel1' (channel with ID 1)" + ); + assert.strictEqual( + chatWindow.querySelector(".o_ComposerTextInputView_textarea"), + document.activeElement, + "The chatWindow composer must have focus" ); - }, - })); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "new message separator should no longer be shown, after focus on composer text input of chat window" - ); -}); -QUnit.test('chat window should open when receiving a new DM', async function (assert) { - assert.expect(1); + await afterNextRender(() => + triggerEvent( + chatWindow.querySelector(".o_ChatWindow .o_ComposerTextInputView_textarea"), + "keydown", + { key: "Tab" } + ) + ); + assert.strictEqual( + chatWindow.querySelector(".o_ChatWindow .o_ComposerTextInputView_textarea"), + document.activeElement, + "The chatWindow composer still has focus" + ); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const resUsersId1 = pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - uuid: 'channel11uuid', - }); - const { messaging } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(` + .o_MessagingMenu_dropdownMenu + .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"]`); - // simulate receiving the first message on channel 11 - await afterNextRender(() => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "new message", - uuid: 'channel11uuid', - }, - })); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "a chat window should be open now that current user received a new message" - ); -}); + assert.containsN(document.body, ".o_ChatWindow", 2, "2 chatWindows must be opened"); + const chatWindows = document.querySelectorAll(".o_ChatWindow"); + assert.strictEqual( + chatWindows[0].querySelector(".o_ChatWindowHeaderView_name").textContent, + "channel1", + "The name of the 1st chatWindow should be 'channel1' (channel with ID 1)" + ); + assert.strictEqual( + chatWindows[1].querySelector(".o_ChatWindowHeaderView_name").textContent, + "channel2", + "The name of the 2nd chatWindow should be 'channel2' (channel with ID 2)" + ); + assert.strictEqual( + chatWindows[1].querySelector(".o_ComposerTextInputView_textarea"), + document.activeElement, + "The 2nd chatWindow composer must have focus (channel with ID 2)" + ); -QUnit.test('chat window should remain folded when new message is received', async function (assert) { - assert.expect(1); + await afterNextRender(() => + triggerEvent( + chatWindows[1].querySelector(".o_ComposerTextInputView_textarea"), + "keydown", + { key: "Tab" } + ) + ); + assert.containsN( + document.body, + ".o_ChatWindow", + 2, + "2 chatWindows should still be opened" + ); + assert.strictEqual( + chatWindows[0].querySelector(".o_ComposerTextInputView_textarea"), + document.activeElement, + "The 1st chatWindow composer must have focus (channel with ID 1)" + ); + }); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Foreigner user", partner_id: resPartnerId1 }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - fold_state: 'folded', - is_minimized: true, - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { - partner_id: resPartnerId1, - }], - ], - channel_type: "chat", - uuid: 'channel-10-uuid', - }); + QUnit.test( + "chat window: TAB cycle with 3 open chat windows [REQUIRE FOCUS]", + async function (assert) { + /** + * InnerWith computation uses following info: + * ([mocked] global window width: @see `mail/static/tests/helpers/test_utils.js:start()` method) + * (others: @see mail/static/src/models/chat_window_manager.js:visual) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1920px + * + * Enough space for 3 visible chat windows: + * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 < 1920 + */ + assert.expect(6); - const { messaging } = await start(); - // simulate receiving a new message - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "New Message 2", - uuid: 'channel-10-uuid', - }, - })); - assert.hasClass( - document.querySelector(`.o_ChatWindow`), - 'o-folded', - "chat window should remain folded" - ); -}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }, + ]); + patchUiSize({ width: 1920 }); + await start(); + assert.containsN( + document.body, + ".o_ChatWindow .o_ComposerTextInputView_textarea", + 3, + "initialy, 3 chat windows should be present" + ); + assert.containsNone( + document.body, + ".o_ChatWindow.o-folded", + "all 3 chat windows should be open" + ); -QUnit.test('should not have chat window hidden menu in mobile (transition from 3 chat windows in desktop to mobile)', async function (assert) { - /** - * computation uses following info: - * ([mocked] global window width: 900px) - * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) - * - * - chat window width: 340px - * - start/end/between gap width: 10px/10px/5px - * - hidden menu width: 170px - * - global width: 1080px - * - * Not enough space for 3 visible chat windows: - * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 > 900 - * Enough space for 2 visible chat windows + hidden menu: - * 10 + 340 + 5 + 340 + 5 + 170 + 10 = 880 < 900 - */ - assert.expect(1); + await afterNextRender(() => { + document + .querySelector( + ".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea" + ) + .focus(); + }); + assert.strictEqual( + document.querySelector( + ".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea" + ), + document.activeElement, + "The chatWindow with visible-index 2 should have the focus" + ); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }, { name: 'mailChannel3' }]); - patchUiSize({ width: 900 }); // enough to fit 2 chat windows + hidden menu - const { click, messaging } = await start(); - // open, from systray menu, chat windows of channels with id 1, 2, 3 - await click('.o_MessagingMenu_toggler'); - await click(` + await afterNextRender(() => + triggerEvent( + document.querySelector( + ".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea" + ), + "keydown", + { key: "Tab" } + ) + ); + assert.strictEqual( + document.querySelector( + ".o_ChatWindow[data-visible-index='1'] .o_ComposerTextInputView_textarea" + ), + document.activeElement, + "after pressing tab on the chatWindow with visible-index 2, the chatWindow with visible-index 1 should have focus" + ); + + await afterNextRender(() => + triggerEvent( + document.querySelector( + ".o_ChatWindow[data-visible-index='1'] .o_ComposerTextInputView_textarea" + ), + "keydown", + { key: "Tab" } + ) + ); + assert.strictEqual( + document.querySelector( + ".o_ChatWindow[data-visible-index='0'] .o_ComposerTextInputView_textarea" + ), + document.activeElement, + "after pressing tab on the chat window with visible-index 1, the chatWindow with visible-index 0 should have focus" + ); + + await afterNextRender(() => + triggerEvent( + document.querySelector( + ".o_ChatWindow[data-visible-index='0'] .o_ComposerTextInputView_textarea" + ), + "keydown", + { key: "Tab" } + ) + ); + assert.strictEqual( + document.querySelector( + ".o_ChatWindow[data-visible-index='2'] .o_ComposerTextInputView_textarea" + ), + document.activeElement, + "the chatWindow with visible-index 2 should have the focus after pressing tab on the chatWindow with visible-index 0" + ); + } + ); + + QUnit.test( + "chat window with a thread: keep scroll position in message list on folded", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i < 100; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => document.querySelector(".o_NotificationListView_preview").click(), + message: + "should wait until channel scrolled to its last message after opening it from the messaging menu", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(".o_ThreadView_messageList"); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + // Set a scroll position to chat window + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142; + }, + message: + "should wait until channel scrolled to 142 after setting this value manually", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "verify chat window initial scrollTop" + ); + + // fold chat window + await click(".o_ChatWindow_header"); + assert.containsNone( + document.body, + ".o_ThreadView", + "chat window should be folded so no ThreadView should be present" + ); + + // unfold chat window + await afterNextRender(() => + afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => document.querySelector(".o_ChatWindow_header").click(), + message: "should wait until channel restored its scroll position to 142", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }) + ); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same when chat window is unfolded" + ); + } + ); + + QUnit.test( + "chat window should scroll to the newly posted message just after posting it", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + for (let i = 0; i < 10; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { insertText } = await start(); + + // Set content of the composer of the chat window + await insertText(".o_ComposerTextInputView_textarea", "WOLOLO"); + // Send a new message in the chatwindow to trigger the scroll + await afterNextRender(() => + triggerEvent( + document.querySelector(".o_ChatWindow .o_ComposerTextInputView_textarea"), + "keydown", + { key: "Enter" } + ) + ); + const messageList = document.querySelector(".o_MessageListView"); + assert.ok( + isScrolledToBottom(messageList), + "chat window should scroll to the newly posted message just after posting it" + ); + } + ); + + QUnit.test( + 'chat window: post message on non-mailing channel with "CTRL-Enter" keyboard shortcut for small screen size', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + patchUiSize({ size: SIZES.SM }); + const { click, insertText } = await start(); + + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_NotificationListView_preview`); + // insert some HTML in editable + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent("keydown", { + ctrlKey: true, + key: "Enter", + }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should now have single message in channel after posting message from pressing 'CTRL-Enter' in text input of composer for small screen" + ); + } + ); + + QUnit.test( + "chat window with a thread: keep scroll position in message list on toggle discuss when folded", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i < 100; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, click, openDiscuss, openView } = await start(); + await click(`.o_MessagingMenu_toggler`); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => document.querySelector(".o_NotificationListView_preview").click(), + message: + "should wait until channel scrolled to its last message after opening it from the messaging menu", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(".o_ThreadView_messageList"); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + // Set a scroll position to chat window + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => + (document.querySelector(`.o_ThreadView_messageList`).scrollTop = 142), + message: + "should wait until channel scrolled to 142 after setting this value manually", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }); + // fold chat window + await click(".o_ChatWindow_header"); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have any chat window after opening discuss" + ); + + await openView({ + res_id: mailChannelId1, + res_model: "mail.channel", + views: [[false, "list"]], + }); + // unfold chat window + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => document.querySelector(".o_ChatWindow_header").click(), + message: + "should wait until channel restored its scroll position to the last saved value (142)", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 142 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_ThreadView_messageList`).scrollTop, + 142, + "chat window scrollTop should still be the same after closing discuss" + ); + } + ); + + QUnit.test("chat window does not fetch messages if hidden", async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: 900px) + * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1080px + * + * Enough space for 2 visible chat windows, and one hidden chat window: + * 3 visible chat windows: + * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 > 900 + * 2 visible chat windows + hidden menu: + * 10 + 340 + 5 + 340 + 10 + 170 + 5 = 880 < 900 + */ + assert.expect(11); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + name: "Channel #10", + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + name: "Channel #11", + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + name: "Channel #12", + }, + ]); + patchUiSize({ width: 900 }); + const { click } = await start({ + mockRPC(route, args) { + if (route === "/mail/channel/messages") { + const { channel_id } = args; + assert.step(`rpc:/mail/channel/messages:${channel_id}`); + } + }, + }); + + assert.containsN(document.body, ".o_ChatWindow", 2, "2 chat windows should be visible"); + assert.containsNone( + document.body, + `.o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"]`, + "chat window for Channel #12 should be hidden" + ); + assert.containsOnce( + document.body, + ".o_ChatWindowHiddenMenuView", + "chat window hidden menu should be displayed" + ); + assert.verifySteps( + [ + `rpc:/mail/channel/messages:${mailChannelId1}`, + `rpc:/mail/channel/messages:${mailChannelId2}`, + ], + "messages should be fetched for the two visible chat windows" + ); + + await click(".o_ChatWindowHiddenMenuView_dropdownToggle"); + assert.containsOnce( + document.body, + ".o_ChatWindowHiddenMenuItemView", + "1 hidden chat window should be listed in hidden menu" + ); + + await click(".o_ChatWindowHiddenMenuItemView_chatWindowHeader"); + assert.containsN( + document.body, + ".o_ChatWindow", + 2, + "2 chat windows should still be visible" + ); + assert.containsOnce( + document.body, + `.o_ChatWindow[data-thread-id="${mailChannelId3}"][data-thread-model="mail.channel"]`, + "chat window for Channel #12 should now be visible" + ); + assert.verifySteps( + [`rpc:/mail/channel/messages:${mailChannelId3}`], + "messages should now be fetched for Channel #12" + ); + }); + + QUnit.test( + "new message separator is shown in a chat window of a chat on receiving new message if there is a history of conversation", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: true, + is_pinned: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + uuid: "channel-10-uuid", + }); + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { messaging } = await start(); + + // simulate receiving a message + await afterNextRender(async () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: "channel-10-uuid", + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "a chat window should be visible after receiving a new message from a chat" + ); + assert.containsN( + document.body, + ".o_MessageView", + 2, + "chat window should have 2 messages" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "should display 'new messages' separator in the conversation, from reception of new messages" + ); + } + ); + + QUnit.test( + "new message separator is not shown in a chat window of a chat on receiving new message if there is no history of conversation", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + uuid: "channel-10-uuid", + }); + const { messaging } = await start(); + + // simulate receiving a message + await afterNextRender(async () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: "channel-10-uuid", + }, + }) + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should not display 'new messages' separator in the conversation of a chat on receiving new message if there is no history of conversation" + ); + } + ); + + QUnit.test( + "focusing a chat window of a chat should make new message separator disappear [REQUIRE FOCUS]", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_minimized: true, + is_pinned: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + uuid: "channel-10-uuid", + }); + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { afterEvent, messaging } = await start(); + + // simulate receiving a message + await afterNextRender(() => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: "channel-10-uuid", + }, + }) + ); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "should display 'new messages' separator in the conversation, from reception of new messages" + ); + + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => + document.querySelector(".o_ComposerTextInputView_textarea").focus(), + message: + "should wait until last seen by current partner message id changed", + predicate: ({ thread }) => { + return thread.id === mailChannelId1 && thread.model === "mail.channel"; + }, + }) + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "new message separator should no longer be shown, after focus on composer text input of chat window" + ); + } + ); + + QUnit.test("chat window should open when receiving a new DM", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const resUsersId1 = pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_pinned: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + uuid: "channel11uuid", + }); + const { messaging } = await start(); + + // simulate receiving the first message on channel 11 + await afterNextRender(() => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "new message", + uuid: "channel11uuid", + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "a chat window should be open now that current user received a new message" + ); + }); + + QUnit.test( + "chat window should remain folded when new message is received", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "folded", + is_minimized: true, + is_pinned: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + [ + 0, + 0, + { + partner_id: resPartnerId1, + }, + ], + ], + channel_type: "chat", + uuid: "channel-10-uuid", + }); + + const { messaging } = await start(); + // simulate receiving a new message + await afterNextRender(async () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "New Message 2", + uuid: "channel-10-uuid", + }, + }) + ); + assert.hasClass( + document.querySelector(`.o_ChatWindow`), + "o-folded", + "chat window should remain folded" + ); + } + ); + + QUnit.test( + "should not have chat window hidden menu in mobile (transition from 3 chat windows in desktop to mobile)", + async function (assert) { + /** + * computation uses following info: + * ([mocked] global window width: 900px) + * (others: @see `mail/static/src/models/chat_window_manager.js:visual`) + * + * - chat window width: 340px + * - start/end/between gap width: 10px/10px/5px + * - hidden menu width: 170px + * - global width: 1080px + * + * Not enough space for 3 visible chat windows: + * 10 + 340 + 5 + 340 + 5 + 340 + 10 = 1050 > 900 + * Enough space for 2 visible chat windows + hidden menu: + * 10 + 340 + 5 + 340 + 5 + 170 + 10 = 880 < 900 + */ + assert.expect(1); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv[ + "mail.channel" + ].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + { name: "mailChannel3" }, + ]); + patchUiSize({ width: 900 }); // enough to fit 2 chat windows + hidden menu + const { click, messaging } = await start(); + // open, from systray menu, chat windows of channels with id 1, 2, 3 + await click(".o_MessagingMenu_toggler"); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `); - await click('.o_MessagingMenu_toggler'); - await click(` + await click(".o_MessagingMenu_toggler"); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `); - await click('.o_MessagingMenu_toggler'); - await click(` + await click(".o_MessagingMenu_toggler"); + await click(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId3}"] `); - // simulate resize to go into mobile - await afterNextRender( - () => messaging.device.update({ - globalWindowInnerWidth: 300, - isMobileDevice: true, - isSmall: true, - sizeClass: 0, // XS - }), - ); - assert.containsNone( - document.body, - '.o_ChatWindowManager_hiddenMenu', - "should not have any chat window hidden menu in mobile (transition from desktop having 3 chat windows (2 visible, 1 hidden)", - ); -}); - -}); + // simulate resize to go into mobile + await afterNextRender(() => + messaging.device.update({ + globalWindowInnerWidth: 300, + isMobileDevice: true, + isSmall: true, + sizeClass: 0, // XS + }) + ); + assert.containsNone( + document.body, + ".o_ChatWindowManager_hiddenMenu", + "should not have any chat window hidden menu in mobile (transition from desktop having 3 chat windows (2 visible, 1 hidden)" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js index ac5531fba52..dd89e90f213 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chatter_suggested_recipient_tests.js @@ -1,260 +1,320 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('chatter_suggested_recipients_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("chatter_suggested_recipients_tests.js"); -QUnit.test("suggest recipient on 'Send message' composer", async function (assert) { - assert.expect(1); + QUnit.test("suggest recipient on 'Send message' composer", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" }); - const resFakeId1 = pyEnv['res.fake'].create({ email_cc: "john@test.be", partner_ids: [resPartnerId1] }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.containsOnce( - document.body, - '.o_ComposerSuggestedRecipientListView', - "Should display a list of suggested recipients after opening the composer from 'Send message' button" - ); -}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "John Jane", + email: "john@jane.be", + }); + const resFakeId1 = pyEnv["res.fake"].create({ + email_cc: "john@test.be", + partner_ids: [resPartnerId1], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.containsOnce( + document.body, + ".o_ComposerSuggestedRecipientListView", + "Should display a list of suggested recipients after opening the composer from 'Send message' button" + ); + }); -QUnit.test("with 3 or less suggested recipients: no 'show more' button", async function (assert) { - assert.expect(1); + QUnit.test( + "with 3 or less suggested recipients: no 'show more' button", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" }); - const resFakeId1 = pyEnv['res.fake'].create({ email_cc: "john@test.be", partner_ids: [resPartnerId1] }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.containsNone( - document.body, - '.o_ComposerSuggestedRecipientListView_showMore', - "should not display 'show more' button with 3 or less suggested recipients" - ); -}); - -QUnit.test("display reason for suggested recipient on mouse over", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" }); - const resFakeId1 = pyEnv['res.fake'].create({ partner_ids: [resPartnerId1] }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonSendMessage`); - const partnerTitle = document.querySelector(`.o_ComposerSuggestedRecipientView[data-partner-id="${resPartnerId1}"]`).getAttribute('title'); - assert.strictEqual( - partnerTitle, - "Add as recipient and follower (reason: Email partner)", - "must display reason for suggested recipient on mouse over", - ); -}); - -QUnit.test("suggested recipient without partner are unchecked by default", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resFakeId1 = pyEnv['res.fake'].create({ email_cc: "john@test.be" }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonSendMessage`); - const checkboxUnchecked = document.querySelector('.o_ComposerSuggestedRecipientView:not([data-partner-id]) input[type=checkbox]'); - assert.notOk( - checkboxUnchecked.checked, - "suggested recipient without partner must be unchecked by default", - ); -}); - -QUnit.test("suggested recipient with partner are checked by default", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" }); - const resFakeId1 = pyEnv['res.fake'].create({ partner_ids: [resPartnerId1] }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonSendMessage`); - const checkboxChecked = document.querySelector(`.o_ComposerSuggestedRecipientView[data-partner-id="${resPartnerId1}"] input[type=checkbox]`); - assert.ok( - checkboxChecked.checked, - "suggested recipient with partner must be checked by default", - ); -}); - -QUnit.test("more than 3 suggested recipients: display only 3 and 'show more' button", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([ - { display_name: "John Jane", email: "john@jane.be" }, - { display_name: "Jack Jone", email: "jack@jone.be" }, - { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, - { display_name: "jolly Roger", email: "Roger@skullflag.com" }, - ]); - const resFakeId1 = pyEnv['res.fake'].create({ - partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], - }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.containsOnce( - document.body, - '.o_ComposerSuggestedRecipientListView_showMore', - "more than 3 suggested recipients display 'show more' button" - ); -}); - -QUnit.test("more than 3 suggested recipients: show all of them on click 'show more' button", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([ - { display_name: "John Jane", email: "john@jane.be" }, - { display_name: "Jack Jone", email: "jack@jone.be" }, - { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, - { display_name: "jolly Roger", email: "Roger@skullflag.com" }, - ]); - const resFakeId1 = pyEnv['res.fake'].create({ - partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], - }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - await click(`.o_ComposerSuggestedRecipientListView_showMore`); - assert.containsN( - document.body, - '.o_ComposerSuggestedRecipientView', - 4, - "more than 3 suggested recipients: show all of them on click 'show more' button" - ); -}); - -QUnit.test("more than 3 suggested recipients -> click 'show more' -> 'show less' button", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([ - { display_name: "John Jane", email: "john@jane.be" }, - { display_name: "Jack Jone", email: "jack@jone.be" }, - { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, - { display_name: "jolly Roger", email: "Roger@skullflag.com" }, - ]); - const resFakeId1 = pyEnv['res.fake'].create({ - partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], - }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - await click(`.o_ComposerSuggestedRecipientListView_showMore`); - assert.containsOnce( - document.body, - '.o_ComposerSuggestedRecipientListView_showLess', - "more than 3 suggested recipients -> click 'show more' -> 'show less' button" - ); -}); - -QUnit.test("suggested recipients list display 3 suggested recipient and 'show more' button when 'show less' button is clicked", async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv['res.partner'].create([ - { display_name: "John Jane", email: "john@jane.be" }, - { display_name: "Jack Jone", email: "jack@jone.be" }, - { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, - { display_name: "jolly Roger", email: "Roger@skullflag.com" }, - ]); - const resFakeId1 = pyEnv['res.fake'].create({ - partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], - }); - const { click, openView } = await start(); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - await click(`.o_ComposerSuggestedRecipientListView_showMore`); - await click(`.o_ComposerSuggestedRecipientListView_showLess`); - assert.containsN( - document.body, - '.o_ComposerSuggestedRecipientView', - 3, - "suggested recipient list should display 3 suggested recipients after clicking on 'show less'." - ); - assert.containsOnce( - document.body, - '.o_ComposerSuggestedRecipientListView_showMore', - "suggested recipient list should containt a 'show More' button after clicking on 'show less'." - ); -}); - -QUnit.test("suggested recipients should not be notified when posting an internal note", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "John Jane", email: "john@jane.be" }); - const resFakeId1 = pyEnv['res.fake'].create({ partner_ids: [resPartnerId1] }); - const { click, insertText, openView } = await start({ - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.strictEqual( - args.post_data.partner_ids.length, - 0, - "post data should not contain suggested recipients when posting an internal note" + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "John Jane", + email: "john@jane.be", + }); + const resFakeId1 = pyEnv["res.fake"].create({ + email_cc: "john@test.be", + partner_ids: [resPartnerId1], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.containsNone( + document.body, + ".o_ComposerSuggestedRecipientListView_showMore", + "should not display 'show more' button with 3 or less suggested recipients" ); } - }, - }); - await openView({ - res_id: resFakeId1, - res_model: 'res.fake', - views: [[false, 'form']], - }); - await click(`.o_ChatterTopbar_buttonLogNote`); - await insertText('.o_ComposerTextInputView_textarea', "Dummy Message"); - await click('.o_ComposerView_buttonSend'); -}); + ); -}); + QUnit.test("display reason for suggested recipient on mouse over", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "John Jane", + email: "john@jane.be", + }); + const resFakeId1 = pyEnv["res.fake"].create({ partner_ids: [resPartnerId1] }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonSendMessage`); + const partnerTitle = document + .querySelector( + `.o_ComposerSuggestedRecipientView[data-partner-id="${resPartnerId1}"]` + ) + .getAttribute("title"); + assert.strictEqual( + partnerTitle, + "Add as recipient and follower (reason: Email partner)", + "must display reason for suggested recipient on mouse over" + ); + }); + + QUnit.test( + "suggested recipient without partner are unchecked by default", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resFakeId1 = pyEnv["res.fake"].create({ email_cc: "john@test.be" }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonSendMessage`); + const checkboxUnchecked = document.querySelector( + ".o_ComposerSuggestedRecipientView:not([data-partner-id]) input[type=checkbox]" + ); + assert.notOk( + checkboxUnchecked.checked, + "suggested recipient without partner must be unchecked by default" + ); + } + ); + + QUnit.test( + "suggested recipient with partner are checked by default", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "John Jane", + email: "john@jane.be", + }); + const resFakeId1 = pyEnv["res.fake"].create({ partner_ids: [resPartnerId1] }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonSendMessage`); + const checkboxChecked = document.querySelector( + `.o_ComposerSuggestedRecipientView[data-partner-id="${resPartnerId1}"] input[type=checkbox]` + ); + assert.ok( + checkboxChecked.checked, + "suggested recipient with partner must be checked by default" + ); + } + ); + + QUnit.test( + "more than 3 suggested recipients: display only 3 and 'show more' button", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv[ + "res.partner" + ].create([ + { display_name: "John Jane", email: "john@jane.be" }, + { display_name: "Jack Jone", email: "jack@jone.be" }, + { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, + { display_name: "jolly Roger", email: "Roger@skullflag.com" }, + ]); + const resFakeId1 = pyEnv["res.fake"].create({ + partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.containsOnce( + document.body, + ".o_ComposerSuggestedRecipientListView_showMore", + "more than 3 suggested recipients display 'show more' button" + ); + } + ); + + QUnit.test( + "more than 3 suggested recipients: show all of them on click 'show more' button", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv[ + "res.partner" + ].create([ + { display_name: "John Jane", email: "john@jane.be" }, + { display_name: "Jack Jone", email: "jack@jone.be" }, + { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, + { display_name: "jolly Roger", email: "Roger@skullflag.com" }, + ]); + const resFakeId1 = pyEnv["res.fake"].create({ + partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + await click(`.o_ComposerSuggestedRecipientListView_showMore`); + assert.containsN( + document.body, + ".o_ComposerSuggestedRecipientView", + 4, + "more than 3 suggested recipients: show all of them on click 'show more' button" + ); + } + ); + + QUnit.test( + "more than 3 suggested recipients -> click 'show more' -> 'show less' button", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv[ + "res.partner" + ].create([ + { display_name: "John Jane", email: "john@jane.be" }, + { display_name: "Jack Jone", email: "jack@jone.be" }, + { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, + { display_name: "jolly Roger", email: "Roger@skullflag.com" }, + ]); + const resFakeId1 = pyEnv["res.fake"].create({ + partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + await click(`.o_ComposerSuggestedRecipientListView_showMore`); + assert.containsOnce( + document.body, + ".o_ComposerSuggestedRecipientListView_showLess", + "more than 3 suggested recipients -> click 'show more' -> 'show less' button" + ); + } + ); + + QUnit.test( + "suggested recipients list display 3 suggested recipient and 'show more' button when 'show less' button is clicked", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4] = pyEnv[ + "res.partner" + ].create([ + { display_name: "John Jane", email: "john@jane.be" }, + { display_name: "Jack Jone", email: "jack@jone.be" }, + { display_name: "jack sparrow", email: "jsparrow@blackpearl.bb" }, + { display_name: "jolly Roger", email: "Roger@skullflag.com" }, + ]); + const resFakeId1 = pyEnv["res.fake"].create({ + partner_ids: [resPartnerId1, resPartnerId2, resPartnerId3, resPartnerId4], + }); + const { click, openView } = await start(); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + await click(`.o_ComposerSuggestedRecipientListView_showMore`); + await click(`.o_ComposerSuggestedRecipientListView_showLess`); + assert.containsN( + document.body, + ".o_ComposerSuggestedRecipientView", + 3, + "suggested recipient list should display 3 suggested recipients after clicking on 'show less'." + ); + assert.containsOnce( + document.body, + ".o_ComposerSuggestedRecipientListView_showMore", + "suggested recipient list should containt a 'show More' button after clicking on 'show less'." + ); + } + ); + + QUnit.test( + "suggested recipients should not be notified when posting an internal note", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "John Jane", + email: "john@jane.be", + }); + const resFakeId1 = pyEnv["res.fake"].create({ partner_ids: [resPartnerId1] }); + const { click, insertText, openView } = await start({ + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.strictEqual( + args.post_data.partner_ids.length, + 0, + "post data should not contain suggested recipients when posting an internal note" + ); + } + }, + }); + await openView({ + res_id: resFakeId1, + res_model: "res.fake", + views: [[false, "form"]], + }); + await click(`.o_ChatterTopbar_buttonLogNote`); + await insertText(".o_ComposerTextInputView_textarea", "Dummy Message"); + await click(".o_ComposerView_buttonSend"); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chatter_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chatter_tests.js index 9eedbd02f69..ca6fa36e807 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chatter_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chatter_tests.js @@ -6,537 +6,560 @@ import { dropFiles, nextAnimationFrame, start, - startServer -} from '@mail/../tests/helpers/test_utils'; + startServer, +} from "@mail/../tests/helpers/test_utils"; -import { file } from 'web.test_utils'; +import { file } from "web.test_utils"; const { createFile } = file; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('chatter', {}, function () { -QUnit.module('chatter_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("chatter", {}, function () { + QUnit.module("chatter_tests.js"); -QUnit.test('base rendering when chatter has no attachment', async function (assert) { - assert.expect(6); + QUnit.test("base rendering when chatter has no attachment", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - for (let i = 0; i < 60; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - res_id: resPartnerId1, + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + for (let i = 0; i < 60; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + res_id: resPartnerId1, + }); + } + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "should have a chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_attachmentBox`).length, + 0, + "should not have an attachment box in the chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_thread`).length, + 1, + "should have a thread in the chatter" + ); + assert.containsOnce( + document.body, + `.o_Chatter_thread[data-thread-id="${resPartnerId1}"][data-thread-model="res.partner"]`, + "chatter should have the right thread." + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 30, + "the first 30 messages of thread should be loaded" + ); + }); + + QUnit.test("base rendering when chatter has no record", async function (assert) { + assert.expect(9); + + const { click, openView } = await start(); + await openView({ + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "should have a chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_attachmentBox`).length, + 0, + "should not have an attachment box in the chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_thread`).length, + 1, + "should have a thread in the chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 1, + "should have a message" + ); + assert.strictEqual( + document.querySelector(`.o_MessageView_content`).textContent, + "Creating a new record...", + "should have the 'Creating a new record ...' message" + ); + assert.containsNone( + document.body, + ".o_MessageListView_loadMore", + "should not have the 'load more' button" + ); + + await click(".o_MessageView"); + assert.strictEqual( + document.querySelectorAll(`.o_MessageActionList`).length, + 1, + "should action list in message" + ); + assert.containsNone( + document.body, + ".o_MessageActionView", + "should not have any action in action list of message" + ); + }); + + QUnit.test("base rendering when chatter has attachments", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["ir.attachment"].create([ + { + mimetype: "text/plain", + name: "Blah.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + mimetype: "text/plain", + name: "Blu.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "should have a chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_attachmentBox`).length, + 0, + "should not have an attachment box in the chatter" + ); + }); + + QUnit.test("show attachment box", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["ir.attachment"].create([ + { + mimetype: "text/plain", + name: "Blah.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + mimetype: "text/plain", + name: "Blu.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "should have a chatter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonToggleAttachments`).length, + 1, + "should have an attachments button in chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, + 1, + "attachments button should have a counter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_attachmentBox`).length, + 0, + "should not have an attachment box in the chatter" + ); + + await click(`.o_ChatterTopbar_buttonToggleAttachments`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_attachmentBox`).length, + 1, + "should have an attachment box in the chatter" + ); + }); + + QUnit.test("chatter: drop attachments", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { afterEvent, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + let files = [ + await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }), + await createFile({ + content: "hello, worlduh", + contentType: "text/plain", + name: "text2.txt", + }), + ]; + await afterNextRender(() => dragenterFiles(document.querySelector(".o_Chatter"))); + assert.ok(document.querySelector(".o_Chatter_dropZone"), "should have a drop zone"); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView`).length, + 0, + "should have no attachment before files are dropped" + ); + + await afterNextRender(() => + afterEvent({ + eventName: "o-file-uploader-upload", + func: () => dropFiles(document.querySelector(".o_Chatter_dropZone"), files), + message: "should wait until files are uploaded", + predicate: ({ files: uploadedFiles }) => uploadedFiles === files, + }) + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView .o_AttachmentCard`).length, + 2, + "should have 2 attachments in the attachment box after files dropped" + ); + + await afterNextRender(() => dragenterFiles(document.querySelector(".o_Chatter"))); + files = [ + await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text3.txt", + }), + ]; + await afterNextRender(() => + afterEvent({ + eventName: "o-file-uploader-upload", + func: () => dropFiles(document.querySelector(".o_Chatter_dropZone"), files), + message: "should wait until files are uploaded", + predicate: ({ files: uploadedFiles }) => uploadedFiles === files, + }) + ); + assert.strictEqual( + document.querySelectorAll(`.o_AttachmentBoxView .o_AttachmentCard`).length, + 3, + "should have 3 attachments in the attachment box after files dropped" + ); + }); + + QUnit.test( + "composer show/hide on log note/send message [REQUIRE FOCUS]", + async function (assert) { + assert.expect(10); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonSendMessage`).length, + 1, + "should have a send message button" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonLogNote`).length, + 1, + "should have a log note button" + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 0, + "should not have a composer" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 1, + "should have a composer" + ); + assert.hasClass( + document.querySelector(".o_Chatter_composer"), + "o-focused", + "composer 'send message' in chatter should have focus just after being displayed" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 1, + "should still have a composer" + ); + assert.hasClass( + document.querySelector(".o_Chatter_composer"), + "o-focused", + "composer 'log note' in chatter should have focus just after being displayed" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 0, + "should have no composer anymore" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 1, + "should have a composer" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter_composer`).length, + 0, + "should have no composer anymore" + ); + } + ); + + QUnit.test( + "should display subject when subject is not the same as the thread name", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + res_id: resPartnerId1, + subject: "Salutations, voyageur", + }); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.containsOnce( + document.body, + ".o_MessageView_subject", + "should display subject of the message" + ); + assert.strictEqual( + document.querySelector(".o_MessageView_subject").textContent, + "Subject: Salutations, voyageur", + "Subject of the message should be 'Salutations, voyageur'" + ); + } + ); + + QUnit.test( + "should not display subject when subject is the same as the thread name", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Salutations, voyageur", + }); + pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + res_id: resPartnerId1, + subject: "Salutations, voyageur", + }); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.containsNone( + document.body, + ".o_MessageView_subject", + "should not display subject of the message" + ); + } + ); + + QUnit.test( + "should not display user notification messages in chatter", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["mail.message"].create({ + message_type: "user_notification", + model: "res.partner", + res_id: resPartnerId1, + }); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.containsNone( + document.body, + ".o_MessageView", + "should display no messages" + ); + } + ); + + QUnit.test('post message with "CTRL-Enter" keyboard shortcut', async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, insertText, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsNone( + document.body, + ".o_MessageView", + "should not have any message initially in chatter" + ); + + await click(".o_ChatterTopbar_buttonSendMessage"); + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent("keydown", { + ctrlKey: true, + key: "Enter", + }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should now have single message in chatter after posting message from pressing 'CTRL-Enter' in text input of composer" + ); + }); + + QUnit.test('post message with "META-Enter" keyboard shortcut', async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, insertText, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsNone( + document.body, + ".o_MessageView", + "should not have any message initially in chatter" + ); + + await click(".o_ChatterTopbar_buttonSendMessage"); + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent("keydown", { + key: "Enter", + metaKey: true, + }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should now have single message in channel after posting message from pressing 'META-Enter' in text input of composer" + ); + }); + + QUnit.test( + 'do not post message with "Enter" keyboard shortcut', + async function (assert) { + // Note that test doesn't assert Enter makes a newline, because this + // default browser cannot be simulated with just dispatching + // programmatically crafted events... + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, insertText, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsNone( + document.body, + ".o_MessageView", + "should not have any message initially in chatter" + ); + + await click(".o_ChatterTopbar_buttonSendMessage"); + await insertText(".o_ComposerTextInputView_textarea", "Test"); + const kevt = new window.KeyboardEvent("keydown", { key: "Enter" }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + await nextAnimationFrame(); + assert.containsNone( + document.body, + ".o_MessageView", + "should still not have any message in mailing channel after pressing 'Enter' in text input of composer" + ); + } + ); }); - } - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], }); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "should have a chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_attachmentBox`).length, - 0, - "should not have an attachment box in the chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_thread`).length, - 1, - "should have a thread in the chatter" - ); - assert.containsOnce( - document.body, - `.o_Chatter_thread[data-thread-id="${resPartnerId1}"][data-thread-model="res.partner"]`, - "chatter should have the right thread." - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 30, - "the first 30 messages of thread should be loaded" - ); -}); - -QUnit.test('base rendering when chatter has no record', async function (assert) { - assert.expect(9); - - const { click, openView } = await start(); - await openView({ - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "should have a chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_attachmentBox`).length, - 0, - "should not have an attachment box in the chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_thread`).length, - 1, - "should have a thread in the chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 1, - "should have a message" - ); - assert.strictEqual( - document.querySelector(`.o_MessageView_content`).textContent, - "Creating a new record...", - "should have the 'Creating a new record ...' message" - ); - assert.containsNone( - document.body, - '.o_MessageListView_loadMore', - "should not have the 'load more' button" - ); - - await click('.o_MessageView'); - assert.strictEqual( - document.querySelectorAll(`.o_MessageActionList`).length, - 1, - "should action list in message" - ); - assert.containsNone( - document.body, - '.o_MessageActionView', - "should not have any action in action list of message" - ); -}); - -QUnit.test('base rendering when chatter has attachments', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['ir.attachment'].create([ - { - mimetype: 'text/plain', - name: 'Blah.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - mimetype: 'text/plain', - name: 'Blu.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "should have a chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_attachmentBox`).length, - 0, - "should not have an attachment box in the chatter" - ); -}); - -QUnit.test('show attachment box', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['ir.attachment'].create([ - { - mimetype: 'text/plain', - name: 'Blah.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - mimetype: 'text/plain', - name: 'Blu.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "should have a chatter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonToggleAttachments`).length, - 1, - "should have an attachments button in chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, - 1, - "attachments button should have a counter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_attachmentBox`).length, - 0, - "should not have an attachment box in the chatter" - ); - - await click(`.o_ChatterTopbar_buttonToggleAttachments`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_attachmentBox`).length, - 1, - "should have an attachment box in the chatter" - ); -}); - -QUnit.test('chatter: drop attachments', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { afterEvent, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - let files = [ - await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }), - await createFile({ - content: 'hello, worlduh', - contentType: 'text/plain', - name: 'text2.txt', - }), - ]; - await afterNextRender(() => dragenterFiles(document.querySelector('.o_Chatter'))); - assert.ok( - document.querySelector('.o_Chatter_dropZone'), - "should have a drop zone" - ); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView`).length, - 0, - "should have no attachment before files are dropped" - ); - - await afterNextRender(() => afterEvent({ - eventName: 'o-file-uploader-upload', - func: () => dropFiles(document.querySelector('.o_Chatter_dropZone'), files), - message: "should wait until files are uploaded", - predicate: ({ files: uploadedFiles }) => uploadedFiles === files, - })); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView .o_AttachmentCard`).length, - 2, - "should have 2 attachments in the attachment box after files dropped" - ); - - await afterNextRender(() => dragenterFiles(document.querySelector('.o_Chatter'))); - files = [ - await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text3.txt', - }) - ]; - await afterNextRender(() => afterEvent({ - eventName: 'o-file-uploader-upload', - func: () => dropFiles(document.querySelector('.o_Chatter_dropZone'), files), - message: "should wait until files are uploaded", - predicate: ({ files: uploadedFiles }) => uploadedFiles === files, - })); - assert.strictEqual( - document.querySelectorAll(`.o_AttachmentBoxView .o_AttachmentCard`).length, - 3, - "should have 3 attachments in the attachment box after files dropped" - ); -}); - -QUnit.test('composer show/hide on log note/send message [REQUIRE FOCUS]', async function (assert) { - assert.expect(10); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonSendMessage`).length, - 1, - "should have a send message button" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonLogNote`).length, - 1, - "should have a log note button" - ); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 0, - "should not have a composer" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 1, - "should have a composer" - ); - assert.hasClass( - document.querySelector('.o_Chatter_composer'), - 'o-focused', - "composer 'send message' in chatter should have focus just after being displayed" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 1, - "should still have a composer" - ); - assert.hasClass( - document.querySelector('.o_Chatter_composer'), - 'o-focused', - "composer 'log note' in chatter should have focus just after being displayed" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 0, - "should have no composer anymore" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 1, - "should have a composer" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter_composer`).length, - 0, - "should have no composer anymore" - ); -}); - -QUnit.test('should display subject when subject is not the same as the thread name', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - res_id: resPartnerId1, - subject: "Salutations, voyageur", - }); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.containsOnce( - document.body, - '.o_MessageView_subject', - "should display subject of the message" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_subject').textContent, - "Subject: Salutations, voyageur", - "Subject of the message should be 'Salutations, voyageur'" - ); -}); - -QUnit.test('should not display subject when subject is the same as the thread name', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Salutations, voyageur" }); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - res_id: resPartnerId1, - subject: "Salutations, voyageur", - }); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.containsNone( - document.body, - '.o_MessageView_subject', - "should not display subject of the message" - ); -}); - -QUnit.test('should not display user notification messages in chatter', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['mail.message'].create({ - message_type: 'user_notification', - model: 'res.partner', - res_id: resPartnerId1, - }); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.containsNone( - document.body, - '.o_MessageView', - "should display no messages" - ); -}); - -QUnit.test('post message with "CTRL-Enter" keyboard shortcut', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, insertText, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsNone( - document.body, - '.o_MessageView', - "should not have any message initially in chatter" - ); - - await click('.o_ChatterTopbar_buttonSendMessage'); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await afterNextRender(() => { - const kevt = new window.KeyboardEvent('keydown', { ctrlKey: true, key: "Enter" }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should now have single message in chatter after posting message from pressing 'CTRL-Enter' in text input of composer" - ); -}); - -QUnit.test('post message with "META-Enter" keyboard shortcut', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, insertText, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsNone( - document.body, - '.o_MessageView', - "should not have any message initially in chatter" - ); - - await click('.o_ChatterTopbar_buttonSendMessage'); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await afterNextRender(() => { - const kevt = new window.KeyboardEvent('keydown', { key: "Enter", metaKey: true }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should now have single message in channel after posting message from pressing 'META-Enter' in text input of composer" - ); -}); - -QUnit.test('do not post message with "Enter" keyboard shortcut', async function (assert) { - // Note that test doesn't assert Enter makes a newline, because this - // default browser cannot be simulated with just dispatching - // programmatically crafted events... - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, insertText, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsNone( - document.body, - '.o_MessageView', - "should not have any message initially in chatter" - ); - - await click('.o_ChatterTopbar_buttonSendMessage'); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - const kevt = new window.KeyboardEvent('keydown', { key: "Enter" }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - await nextAnimationFrame(); - assert.containsNone( - document.body, - '.o_MessageView', - "should still not have any message in mailing channel after pressing 'Enter' in text input of composer" - ); -}); - -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js index c33e5e8bad0..795c1adea5f 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/chatter_topbar_tests.js @@ -1,567 +1,579 @@ /** @odoo-module **/ -import { afterNextRender, nextAnimationFrame, start, startServer } from '@mail/../tests/helpers/test_utils'; +import { + afterNextRender, + nextAnimationFrame, + start, + startServer, +} from "@mail/../tests/helpers/test_utils"; -import { makeTestPromise } from 'web.test_utils'; +import { makeTestPromise } from "web.test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('chatter_topbar_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("chatter_topbar_tests.js"); -QUnit.test('base rendering', async function (assert) { - assert.expect(7); + QUnit.test("base rendering", async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonSendMessage`).length, - 1, - "should have a send message button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonLogNote`).length, - 1, - "should have a log note button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonScheduleActivity`).length, - 1, - "should have a schedule activity button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 0, - "attachments button should not have a loader" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_followerListMenu`).length, - 1, - "should have a follower menu" - ); -}); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonSendMessage`).length, + 1, + "should have a send message button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonLogNote`).length, + 1, + "should have a log note button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonScheduleActivity`).length, + 1, + "should have a schedule activity button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, + 0, + "attachments button should not have a loader" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_followerListMenu`).length, + 1, + "should have a follower menu" + ); + }); -QUnit.test('base disabled rendering', async function (assert) { - assert.expect(6); + QUnit.test("base disabled rendering", async function (assert) { + assert.expect(6); - const { openView } = await start(); - await openView({ - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled, - "send message button should be disabled" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled, - "log note button should be disabled" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonScheduleActivity`).disabled, - "schedule activity should be disabled" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).disabled, - "attachments button should be disabled" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 0, - "attachments button should not have a loader" - ); -}); + const { openView } = await start(); + await openView({ + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled, + "send message button should be disabled" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled, + "log note button should be disabled" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonScheduleActivity`).disabled, + "schedule activity should be disabled" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).disabled, + "attachments button should be disabled" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, + 0, + "attachments button should not have a loader" + ); + }); -QUnit.test('attachment loading is delayed', async function (assert) { - assert.expect(4); + QUnit.test("attachment loading is delayed", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { advanceTime, openView } = await start({ - hasTimeControl: true, - loadingBaseDelayDuration: 100, - async mockRPC(route) { - if (route.includes('/mail/thread/data')) { - await makeTestPromise(); // simulate long loading + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { advanceTime, openView } = await start({ + hasTimeControl: true, + loadingBaseDelayDuration: 100, + async mockRPC(route) { + if (route.includes("/mail/thread/data")) { + await makeTestPromise(); // simulate long loading + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, + 0, + "attachments button should not have a loader yet" + ); + + await afterNextRender(async () => advanceTime(100)); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, + 1, + "attachments button should now have a loader" + ); + }); + + QUnit.test("attachment counter while loading attachments", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { openView } = await start({ + async mockRPC(route) { + if (route.includes("/mail/thread/data")) { + await makeTestPromise(); // simulate long loading + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, + 1, + "attachments button should have a loader" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, + 0, + "attachments button should not have a counter" + ); + }); + + QUnit.test( + "attachment counter transition when attachments become loaded)", + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const attachmentPromise = makeTestPromise(); + const { openView } = await start({ + async mockRPC(route) { + if (route.includes("/mail/thread/data")) { + await attachmentPromise; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`) + .length, + 1, + "attachments button should have a loader" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, + 0, + "attachments button should not have a counter" + ); + + await afterNextRender(() => attachmentPromise.resolve()); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`) + .length, + 0, + "attachments button should not have a loader" + ); } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); + ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 0, - "attachments button should not have a loader yet" - ); + QUnit.test("attachment counter without attachments", async function (assert) { + assert.expect(2); - await afterNextRender(async () => advanceTime(100)); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 1, - "attachments button should now have a loader" - ); -}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); -QUnit.test('attachment counter while loading attachments', async function (assert) { - assert.expect(4); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + }); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { openView } = await start({ - async mockRPC(route) { - if (route.includes('/mail/thread/data')) { - await makeTestPromise(); // simulate long loading + QUnit.test("attachment counter with attachments", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["ir.attachment"].create([ + { + mimetype: "text/plain", + name: "Blah.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + mimetype: "text/plain", + name: "Blu.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const { openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar`).length, + 1, + "should have a chatter topbar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonToggleAttachments`).length, + 1, + "should have an attachments button in chatter menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, + 1, + "attachments button should have a counter" + ); + assert.strictEqual( + document.querySelector(`.o_ChatterTopbar_buttonAttachmentsCount`).textContent, + "2", + 'attachment counter content should contain "2 files"' + ); + }); + + QUnit.test( + "composer state conserved when clicking on another topbar button", + async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.containsOnce( + document.body, + `.o_ChatterTopbar`, + "should have a chatter topbar" + ); + assert.containsOnce( + document.body, + `.o_ChatterTopbar_buttonSendMessage`, + "should have a send message button in chatter menu" + ); + assert.containsOnce( + document.body, + `.o_ChatterTopbar_buttonLogNote`, + "should have a log note button in chatter menu" + ); + assert.containsOnce( + document.body, + `.o_ChatterTopbar_buttonAddAttachments`, + "should have an attachments button in chatter menu" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.containsOnce( + document.body, + `.o_ChatterTopbar_buttonLogNote.o-active`, + "log button should now be active" + ); + assert.containsNone( + document.body, + `.o_ChatterTopbar_buttonSendMessage.o-active`, + "send message button should not be active" + ); + + document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).click(); + await nextAnimationFrame(); + assert.containsOnce( + document.body, + `.o_ChatterTopbar_buttonLogNote.o-active`, + "log button should still be active" + ); + assert.containsNone( + document.body, + `.o_ChatterTopbar_buttonSendMessage.o-active`, + "send message button should still be not active" + ); } - } + ); + + QUnit.test("rendering with multiple partner followers", async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv["res.partner"].create([ + { name: "resPartner1" }, + { name: "resPartner2" }, + { message_follower_ids: [1, 2] }, + ]); + pyEnv["mail.followers"].create([ + { + name: "Jean Michang", + partner_id: resPartnerId2, + res_id: resPartnerId3, + res_model: "res.partner", + }, + { + name: "Eden Hazard", + partner_id: resPartnerId1, + res_id: resPartnerId3, + res_model: "res.partner", + }, + ]); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId3, + res_model: "res.partner", + views: [[false, "form"]], + }); + + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "should have followers menu component" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_buttonFollowers", + "should have followers button" + ); + + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should be opened" + ); + assert.containsN( + document.body, + ".o_FollowerView", + 2, + "exactly two followers should be listed" + ); + assert.containsN( + document.body, + ".o_FollowerView_name", + 2, + "exactly two follower names should be listed" + ); + assert.strictEqual( + document.querySelectorAll(".o_FollowerView_name")[0].textContent.trim(), + "Jean Michang", + "first follower is 'Jean Michang'" + ); + assert.strictEqual( + document.querySelectorAll(".o_FollowerView_name")[1].textContent.trim(), + "Eden Hazard", + "second follower is 'Eden Hazard'" + ); + }); + + QUnit.test("log note/send message switching", async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonSendMessage", + "should have a 'Send Message' button" + ); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should not be active" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonLogNote", + "should have a 'Log Note' button" + ); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should not be active" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.hasClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should be active" + ); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should not be active" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should not be active" + ); + assert.hasClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should be active" + ); + }); + + QUnit.test("log note toggling", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonLogNote", + "should have a 'Log Note' button" + ); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should not be active" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.hasClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should be active" + ); + + await click(`.o_ChatterTopbar_buttonLogNote`); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonLogNote"), + "o-active", + "'Log Note' button should not be active" + ); + }); + + QUnit.test("send message toggling", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonSendMessage", + "should have a 'Send Message' button" + ); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should not be active" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.hasClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should be active" + ); + + await click(`.o_ChatterTopbar_buttonSendMessage`); + assert.doesNotHaveClass( + document.querySelector(".o_ChatterTopbar_buttonSendMessage"), + "o-active", + "'Send Message' button should not be active" + ); + }); }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 1, - "attachments button should have a loader" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, - 0, - "attachments button should not have a counter" - ); -}); - -QUnit.test('attachment counter transition when attachments become loaded)', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const attachmentPromise = makeTestPromise(); - const { openView } = await start({ - async mockRPC(route) { - if (route.includes('/mail/thread/data')) { - await attachmentPromise; - } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 1, - "attachments button should have a loader" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, - 0, - "attachments button should not have a counter" - ); - - await afterNextRender(() => attachmentPromise.resolve()); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCountLoader`).length, - 0, - "attachments button should not have a loader" - ); -}); - -QUnit.test('attachment counter without attachments', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAddAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); -}); - -QUnit.test('attachment counter with attachments', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['ir.attachment'].create([ - { - mimetype: 'text/plain', - name: 'Blah.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - mimetype: 'text/plain', - name: 'Blu.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const { openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar`).length, - 1, - "should have a chatter topbar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonToggleAttachments`).length, - 1, - "should have an attachments button in chatter menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ChatterTopbar_buttonAttachmentsCount`).length, - 1, - "attachments button should have a counter" - ); - assert.strictEqual( - document.querySelector(`.o_ChatterTopbar_buttonAttachmentsCount`).textContent, - '2', - 'attachment counter content should contain "2 files"' - ); -}); - -QUnit.test('composer state conserved when clicking on another topbar button', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.containsOnce( - document.body, - `.o_ChatterTopbar`, - "should have a chatter topbar" - ); - assert.containsOnce( - document.body, - `.o_ChatterTopbar_buttonSendMessage`, - "should have a send message button in chatter menu" - ); - assert.containsOnce( - document.body, - `.o_ChatterTopbar_buttonLogNote`, - "should have a log note button in chatter menu" - ); - assert.containsOnce( - document.body, - `.o_ChatterTopbar_buttonAddAttachments`, - "should have an attachments button in chatter menu" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.containsOnce( - document.body, - `.o_ChatterTopbar_buttonLogNote.o-active`, - "log button should now be active" - ); - assert.containsNone( - document.body, - `.o_ChatterTopbar_buttonSendMessage.o-active`, - "send message button should not be active" - ); - - document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).click(); - await nextAnimationFrame(); - assert.containsOnce( - document.body, - `.o_ChatterTopbar_buttonLogNote.o-active`, - "log button should still be active" - ); - assert.containsNone( - document.body, - `.o_ChatterTopbar_buttonSendMessage.o-active`, - "send message button should still be not active" - ); -}); - -QUnit.test('rendering with multiple partner followers', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([ - { name: 'resPartner1' }, - { name: 'resPartner2' }, - { message_follower_ids: [1, 2] }, - ]); - pyEnv['mail.followers'].create([ - { - name: "Jean Michang", - partner_id: resPartnerId2, - res_id: resPartnerId3, - res_model: 'res.partner', - }, - { - name: "Eden Hazard", - partner_id: resPartnerId1, - res_id: resPartnerId3, - res_model: 'res.partner', - }, - ]); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId3, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "should have followers menu component" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_buttonFollowers', - "should have followers button" - ); - - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should be opened" - ); - assert.containsN( - document.body, - '.o_FollowerView', - 2, - "exactly two followers should be listed" - ); - assert.containsN( - document.body, - '.o_FollowerView_name', - 2, - "exactly two follower names should be listed" - ); - assert.strictEqual( - document.querySelectorAll('.o_FollowerView_name')[0].textContent.trim(), - "Jean Michang", - "first follower is 'Jean Michang'" - ); - assert.strictEqual( - document.querySelectorAll('.o_FollowerView_name')[1].textContent.trim(), - "Eden Hazard", - "second follower is 'Eden Hazard'" - ); -}); - -QUnit.test('log note/send message switching', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonSendMessage', - "should have a 'Send Message' button" - ); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should not be active" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonLogNote', - "should have a 'Log Note' button" - ); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should not be active" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.hasClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should be active" - ); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should not be active" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should not be active" - ); - assert.hasClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should be active" - ); -}); - -QUnit.test('log note toggling', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonLogNote', - "should have a 'Log Note' button" - ); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should not be active" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.hasClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should be active" - ); - - await click(`.o_ChatterTopbar_buttonLogNote`); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonLogNote'), - 'o-active', - "'Log Note' button should not be active" - ); -}); - -QUnit.test('send message toggling', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonSendMessage', - "should have a 'Send Message' button" - ); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should not be active" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.hasClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should be active" - ); - - await click(`.o_ChatterTopbar_buttonSendMessage`); - assert.doesNotHaveClass( - document.querySelector('.o_ChatterTopbar_buttonSendMessage'), - 'o-active', - "'Send Message' button should not be active" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_canned_response_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_canned_response_tests.js index bb17690c57d..9bb3ae79b55 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_canned_response_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_canned_response_tests.js @@ -1,100 +1,99 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_suggestion_canned_response_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("composer_suggestion_canned_response_tests.js"); -QUnit.test('canned response suggestion displayed', async function (assert) { - assert.expect(1); + QUnit.test("canned response suggestion displayed", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ - source: 'hello', - substitution: "Hello, how are you?", + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello, how are you?", + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", ":hello"); + assert.containsOnce( + document.body, + `.o_ComposerSuggestionView`, + "Canned response suggestion should be present" + ); + }); + + QUnit.test("canned response suggestion correct data", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello, how are you?", + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", ":hello"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part1", + "Canned response source should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, + "hello", + "Canned response source should be displayed" + ); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part2", + "Canned response substitution should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestionView_part2`).textContent, + "Hello, how are you?", + "Canned response substitution should be displayed" + ); + }); + + QUnit.test("canned response suggestion active", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello, how are you?", + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", ":hello"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionView"), + "active", + "should be active initially" + ); + }); }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', ":hello"); - assert.containsOnce( - document.body, - `.o_ComposerSuggestionView`, - "Canned response suggestion should be present" - ); -}); - -QUnit.test('canned response suggestion correct data', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ - source: 'hello', - substitution: "Hello, how are you?", - }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', ":hello"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part1', - "Canned response source should be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, - "hello", - "Canned response source should be displayed" - ); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part2', - "Canned response substitution should be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerSuggestionView_part2`).textContent, - "Hello, how are you?", - "Canned response substitution should be displayed" - ); -}); - -QUnit.test('canned response suggestion active', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ - source: 'hello', - substitution: "Hello, how are you?", - }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', ":hello"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionView'), - 'active', - "should be active initially" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_channel_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_channel_tests.js index 5aed5ceff3d..353621894e4 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_channel_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_channel_tests.js @@ -1,78 +1,77 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_suggestion_channel_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("composer_suggestion_channel_tests.js"); -QUnit.test('channel mention suggestion displayed', async function (assert) { - assert.expect(1); + QUnit.test("channel mention suggestion displayed", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "my-channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "#my-channel"); + assert.containsOnce( + document.body, + `.o_ComposerSuggestionView`, + "Channel mention suggestion should be present" + ); + }); + + QUnit.test("channel mention suggestion correct data", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "General" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "#General"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part1", + "Channel name should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, + "General", + "Channel name should be displayed" + ); + }); + + QUnit.test("channel mention suggestion active", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "my-channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "#my-channel"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionView"), + "active", + "should be active initially" + ); + }); }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "#my-channel"); - assert.containsOnce( - document.body, - `.o_ComposerSuggestionView`, - "Channel mention suggestion should be present" - ); -}); - -QUnit.test('channel mention suggestion correct data', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "#General"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part1', - "Channel name should be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, - "General", - "Channel name should be displayed" - ); -}); - -QUnit.test('channel mention suggestion active', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "#my-channel"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionView'), - 'active', - "should be active initially" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_command_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_command_tests.js index d2c420d2e02..8033276fb99 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_command_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_command_tests.js @@ -1,88 +1,87 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_suggestion_command_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("composer_suggestion_command_tests.js"); -QUnit.test('command suggestion displayed', async function (assert) { - assert.expect(1); + QUnit.test("command suggestion displayed", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "my-channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "/who"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "Command suggestion should be present" + ); + }); + + QUnit.test("command suggestion correct data", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "my-channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "/who"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part1", + "Command name should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, + "who", + "Command name should be displayed" + ); + assert.containsOnce( + document.querySelector(".o_ComposerSuggestionView"), + ".o_ComposerSuggestionView_part2", + "Command help should be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerSuggestionView_part2`).textContent, + "List users in the current channel", + "Command help should be displayed" + ); + }); + + QUnit.test("command suggestion active", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "my-channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "/who"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionView"), + "active", + "1st suggestion should be active initially" + ); + }); }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "/who"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "Command suggestion should be present", - ); -}); - -QUnit.test('command suggestion correct data', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "/who"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part1', - "Command name should be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerSuggestionView_part1`).textContent, - "who", - "Command name should be displayed" - ); - assert.containsOnce( - document.querySelector('.o_ComposerSuggestionView'), - '.o_ComposerSuggestionView_part2', - "Command help should be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerSuggestionView_part2`).textContent, - "List users in the current channel", - "Command help should be displayed" - ); -}); - -QUnit.test('command suggestion active', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "/who"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionView'), - 'active', - "1st suggestion should be active initially" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_partner_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_partner_tests.js index 90cb4319879..d668e0f4c44 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_partner_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_suggestion_partner_tests.js @@ -1,123 +1,122 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_suggestion_partner_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("composer_suggestion_partner_tests.js"); -QUnit.test('partner mention suggestion displayed', async function (assert) { - assert.expect(1); + QUnit.test("partner mention suggestion displayed", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ - email: "demo_user@odoo.com", - im_status: 'online', - name: 'Demo User', + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "demo_user@odoo.com", + im_status: "online", + name: "Demo User", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "@demo"); + assert.containsOnce( + document.body, + `.o_ComposerSuggestionView`, + "Partner mention suggestion should be present" + ); + }); + + QUnit.test("partner mention suggestion correct data", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "demo_user@odoo.com", + im_status: "online", + name: "Demo User", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "@demo"); + assert.containsOnce( + document.querySelector(".o_ComposerSuggestionView"), + ".o_PersonaImStatusIconView", + "Partner's im_status should be displayed" + ); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part1", + "Partner's name should be present" + ); + assert.strictEqual( + document.querySelector(".o_ComposerSuggestionView_part1").textContent, + "Demo User", + "Partner's name should be displayed" + ); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView_part2", + "Partner's email should be present" + ); + assert.strictEqual( + document.querySelector(".o_ComposerSuggestionView_part2").textContent, + "(demo_user@odoo.com)", + "Partner's email should be displayed" + ); + }); + + QUnit.test("partner mention suggestion active", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "demo_user@odoo.com", + im_status: "online", + name: "Demo User", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "@demo"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionView"), + "active", + "should be active initially" + ); + }); }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "@demo"); - assert.containsOnce( - document.body, - `.o_ComposerSuggestionView`, - "Partner mention suggestion should be present" - ); -}); - -QUnit.test('partner mention suggestion correct data', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ - email: "demo_user@odoo.com", - im_status: 'online', - name: 'Demo User', - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "@demo"); - assert.containsOnce( - document.querySelector('.o_ComposerSuggestionView'), - '.o_PersonaImStatusIconView', - "Partner's im_status should be displayed" - ); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part1', - "Partner's name should be present" - ); - assert.strictEqual( - document.querySelector('.o_ComposerSuggestionView_part1').textContent, - "Demo User", - "Partner's name should be displayed" - ); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView_part2', - "Partner's email should be present" - ); - assert.strictEqual( - document.querySelector('.o_ComposerSuggestionView_part2').textContent, - "(demo_user@odoo.com)", - "Partner's email should be displayed" - ); -}); - -QUnit.test('partner mention suggestion active', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ - email: "demo_user@odoo.com", - im_status: 'online', - name: 'Demo User', - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "@demo"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionView'), - 'active', - "should be active initially" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js index d4cbb91f882..5dd155b7441 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/composer_tests.js @@ -8,1593 +8,1725 @@ import { pasteFiles, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -import { - file, - makeTestPromise, -} from 'web.test_utils'; +import { file, makeTestPromise } from "web.test_utils"; const { createFile, inputFiles } = file; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('composer_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("composer_tests.js"); -QUnit.test('composer text input: basic rendering when posting a message', async function (assert) { - assert.expect(5); + QUnit.test( + "composer text input: basic rendering when posting a message", + async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const { click, openFormView } = await start(); - await openFormView({ - res_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - }); - await click('.o_ChatterTopbar_buttonSendMessage'); + const pyEnv = await startServer(); + const { click, openFormView } = await start(); + await openFormView({ + res_id: pyEnv.currentPartnerId, + res_model: "res.partner", + }); + await click(".o_ChatterTopbar_buttonSendMessage"); - assert.strictEqual( - document.querySelectorAll('.o_ComposerView').length, - 1, - "should have composer in discuss thread" - ); - assert.strictEqual( - document.querySelectorAll('.o_ComposerView_textInput').length, - 1, - "should have text input inside discuss thread composer" - ); - assert.ok( - document.querySelector('.o_ComposerView_textInput').classList.contains('o_ComposerTextInputView'), - "composer text input of composer should be a ComposerTextIput component" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, - 1, - "should have editable part inside composer text input" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, - "Send a message to followers...", - "should have 'Send a message to followers...' as placeholder composer text input" - ); -}); - -QUnit.test('composer text input: basic rendering when logging note', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const { click, openFormView } = await start(); - await openFormView({ - res_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - }); - await click('.o_ChatterTopbar_buttonLogNote'); - - assert.strictEqual( - document.querySelectorAll('.o_ComposerView').length, - 1, - "should have composer in discuss thread" - ); - assert.strictEqual( - document.querySelectorAll('.o_ComposerView_textInput').length, - 1, - "should have text input inside discuss thread composer" - ); - assert.ok( - document.querySelector('.o_ComposerView_textInput').classList.contains('o_ComposerTextInputView'), - "composer text input of composer should be a ComposerTextIput component" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, - 1, - "should have editable part inside composer text input" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, - "Log an internal note...", - "should have 'Log an internal note...' as placeholder in composer text input if composer is log" - ); -}); - -QUnit.test('composer text input: basic rendering when linked thread is a mail.channel', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll('.o_ComposerView').length, - 1, - "should have composer in discuss thread" - ); - assert.strictEqual( - document.querySelectorAll('.o_ComposerView_textInput').length, - 1, - "should have text input inside discuss thread composer" - ); - assert.ok( - document.querySelector('.o_ComposerView_textInput').classList.contains('o_ComposerTextInputView'), - "composer text input of composer should be a ComposerTextIput component" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, - 1, - "should have editable part inside composer text input" - ); -}); - -QUnit.test('composer text input placeholder should contain channel name when thread does not have specific correspondent', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ channel_type: 'channel', name: 'General' }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, - "Message #General...", - "should have 'Message #General...' as placeholder for composer text input when thread does not have specific correspondent" - ); -}); - -QUnit.test('composer text input placeholder should contain correspondent name when thread has exactly one correspondent', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Marc Demo' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, - "Message Marc Demo...", - "should have 'Message Marc Demo...' as placeholder for composer text input when thread has exactly one correspondent" - ); -}); - -QUnit.test('add an emoji', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "😊", - "emoji should be inserted in the composer text input" - ); -}); - -QUnit.test('add an emoji after some text', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "Blabla"); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "Blabla", - "composer text input should have text only initially" - ); - - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "Blabla😊", - "emoji should be inserted after the text" - ); -}); - -QUnit.test('add emoji replaces (keyboard) text selection', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1, }, - }, - }); - await openDiscuss(); - const composerTextInputTextArea = document.querySelector(`.o_ComposerTextInputView_textarea`); - await insertText('.o_ComposerTextInputView_textarea', "Blabla"); - assert.strictEqual( - composerTextInputTextArea.value, - "Blabla", - "composer text input should have text only initially" - ); - - // simulate selection of all the content by keyboard - composerTextInputTextArea.setSelectionRange(0, composerTextInputTextArea.value.length); - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "😊", - "whole text selection should have been replaced by emoji" - ); -}); - -QUnit.test('display canned response suggestions on typing ":"', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ source: "hello", substitution: "Hello! How are you?" }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "Canned responses suggestions list should not be present" - ); - await insertText('.o_ComposerTextInputView_textarea', ':'); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionListView_list'), - 'show', - "should display canned response suggestions on typing ':'" - ); -}); - -QUnit.test('use a canned response', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ source: "hello", substitution: "Hello! How are you?" }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "canned response suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', ':'); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a canned response suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "Hello! How are you? ", - "text content of composer should have canned response + additional whitespace afterwards" - ); -}); - -QUnit.test('use a canned response some text', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ source: "hello", substitution: "Hello! How are you?" }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "canned response suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "bluhbluh "); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "bluhbluh ", - "text content of composer should have content" - ); - await insertText('.o_ComposerTextInputView_textarea', ':'); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a canned response suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "bluhbluh Hello! How are you? ", - "text content of composer should have previous content + canned response substitution + additional whitespace afterwards" - ); -}); - -QUnit.test('add an emoji after a canned response', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.shortcode'].create({ source: "hello", substitution: "Hello! How are you?" }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "canned response suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', ':'); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a canned response suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "Hello! How are you? ", - "text content of composer should have previous content + canned response substitution + additional whitespace afterwards" - ); - - // select emoji - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "Hello! How are you? 😊", - "text content of composer should have previous canned response substitution and selected emoji just after" - ); -}); - -QUnit.test('display channel mention suggestions on typing "#"', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ name: "General", channel_type: 'channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "channel mention suggestions list should not be present" - ); - await insertText('.o_ComposerTextInputView_textarea', "#"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionListView_list'), - 'show', - "should display channel mention suggestions on typing '#'" - ); -}); - -QUnit.test('mention a channel', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ name: "General", channel_type: 'channel' }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "channel mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "#"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a channel mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "#General ", - "text content of composer should have mentioned channel + additional whitespace afterwards" - ); -}); - -QUnit.test('mention a channel after some text', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ name: "General", channel_type: 'channel' }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "channel mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "bluhbluh "); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "bluhbluh ", - "text content of composer should have content" - ); - await insertText('.o_ComposerTextInputView_textarea', "#"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a channel mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "bluhbluh #General ", - "text content of composer should have previous content + mentioned channel + additional whitespace afterwards" - ); -}); - -QUnit.test('add an emoji after a channel mention', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ name: "General", channel_type: 'channel' }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "#"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a channel mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "#General ", - "text content of composer should have previous content + mentioned channel + additional whitespace afterwards" - ); - - // select emoji - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "#General 😊", - "text content of composer should have previous channel mention and selected emoji just after" - ); -}); - -QUnit.test('display command suggestions on typing "/"', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ channel_type: 'channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "command suggestions list should not be present" - ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionListView_list'), - 'show', - "should display command suggestions on typing '/'" - ); -}); - -QUnit.test('do not send typing notification on typing "/" command', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView").length, + 1, + "should have composer in discuss thread" + ); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView_textInput").length, + 1, + "should have text input inside discuss thread composer" + ); + assert.ok( + document + .querySelector(".o_ComposerView_textInput") + .classList.contains("o_ComposerTextInputView"), + "composer text input of composer should be a ComposerTextIput component" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, + 1, + "should have editable part inside composer text input" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, + "Send a message to followers...", + "should have 'Send a message to followers...' as placeholder composer text input" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - assert.verifySteps([], "No rpc done"); -}); + QUnit.test( + "composer text input: basic rendering when logging note", + async function (assert) { + assert.expect(5); -QUnit.test('do not send typing notification on typing after selecting suggestion from "/" command', async function (assert) { - assert.expect(1); + const pyEnv = await startServer(); + const { click, openFormView } = await start(); + await openFormView({ + res_id: pyEnv.currentPartnerId, + res_model: "res.partner", + }); + await click(".o_ChatterTopbar_buttonLogNote"); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView").length, + 1, + "should have composer in discuss thread" + ); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView_textInput").length, + 1, + "should have text input inside discuss thread composer" + ); + assert.ok( + document + .querySelector(".o_ComposerView_textInput") + .classList.contains("o_ComposerTextInputView"), + "composer text input of composer should be a ComposerTextIput component" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, + 1, + "should have editable part inside composer text input" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, + "Log an internal note...", + "should have 'Log an internal note...' as placeholder in composer text input if composer is log" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - await click('.o_ComposerSuggestionView'); - await insertText('.o_ComposerTextInputView_textarea', " is user?"); - assert.verifySteps([], "No rpc done"); -}); + QUnit.test( + "composer text input: basic rendering when linked thread is a mail.channel", + async function (assert) { + assert.expect(4); -QUnit.test('use a command for a specific channel type', async function (assert) { - assert.expect(3); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView").length, + 1, + "should have composer in discuss thread" + ); + assert.strictEqual( + document.querySelectorAll(".o_ComposerView_textInput").length, + 1, + "should have text input inside discuss thread composer" + ); + assert.ok( + document + .querySelector(".o_ComposerView_textInput") + .classList.contains("o_ComposerTextInputView"), + "composer text input of composer should be a ComposerTextIput component" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerTextInputView_textarea`).length, + 1, + "should have editable part inside composer text input" + ); + } + ); - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ channel_type: 'chat' }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); + QUnit.test( + "composer text input placeholder should contain channel name when thread does not have specific correspondent", + async function (assert) { + assert.expect(1); - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "command suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "/who ", - "text content of composer should have used command + additional whitespace afterwards" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, + "Message #General...", + "should have 'Message #General...' as placeholder for composer text input when thread does not have specific correspondent" + ); + } + ); -QUnit.test('command suggestion should only open if command is the first character', async function (assert) { - assert.expect(4); + QUnit.test( + "composer text input placeholder should contain correspondent name when thread has exactly one correspondent", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ channel_type: 'channel' }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "command suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "bluhbluh "); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "bluhbluh ", - "text content of composer should have content" - ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "should not have a command suggestion" - ); -}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Marc Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).placeholder, + "Message Marc Demo...", + "should have 'Message Marc Demo...' as placeholder for composer text input when thread has exactly one correspondent" + ); + } + ); -QUnit.test('add an emoji after a command', async function (assert) { - assert.expect(4); + QUnit.test("add an emoji", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChanelId1 = pyEnv['mail.channel'].create({ channel_type: 'channel' }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChanelId1 }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "😊", + "emoji should be inserted in the composer text input" + ); + }); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "command suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "/"); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "/who ", - "text content of composer should have previous content + used command + additional whitespace afterwards" - ); + QUnit.test("add an emoji after some text", async function (assert) { + assert.expect(2); - // select emoji - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "/who 😊", - "text content of composer should have previous command and selected emoji just after" - ); -}); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "Blabla"); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "Blabla", + "composer text input should have text only initially" + ); -QUnit.test('display partner mention suggestions on typing "@"', async function (assert) { - assert.expect(3); + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "Blabla😊", + "emoji should be inserted after the text" + ); + }); - const pyEnv = await startServer(); + QUnit.test("add emoji replaces (keyboard) text selection", async function (assert) { + assert.expect(2); - const resPartnerId1 = pyEnv['res.partner'].create({ email: "testpartner@odoo.com", name: "TestPartner" }); - const resPartnerId2 = pyEnv['res.partner'].create({ email: "testpartner2@odoo.com", name: "TestPartner2" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + const composerTextInputTextArea = document.querySelector( + `.o_ComposerTextInputView_textarea` + ); + await insertText(".o_ComposerTextInputView_textarea", "Blabla"); + assert.strictEqual( + composerTextInputTextArea.value, + "Blabla", + "composer text input should have text only initially" + ); - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "mention suggestions list should not be present" - ); - await insertText('.o_ComposerTextInputView_textarea', "@"); - assert.hasClass( - document.querySelector('.o_ComposerSuggestionListView_list'), - 'show', - "should display mention suggestions on typing '@'" - ); - assert.containsOnce( - document.body, - '.dropdown-divider', - "should have a separator" - ); -}); + // simulate selection of all the content by keyboard + composerTextInputTextArea.setSelectionRange(0, composerTextInputTextArea.value.length); + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "😊", + "whole text selection should have been replaced by emoji" + ); + }); -QUnit.test('mention a partner', async function (assert) { - assert.expect(4); + QUnit.test('display canned response suggestions on typing ":"', async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ email: "testpartner@odoo.com", name: "TestPartner" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); - assert.containsNone( - document.body, - '.o_ComposerSuggestionListView_list', - "mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', '@Te'); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "@TestPartner ", - "text content of composer should have mentioned partner + additional whitespace afterwards" - ); -}); + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "Canned responses suggestions list should not be present" + ); + await insertText(".o_ComposerTextInputView_textarea", ":"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionListView_list"), + "show", + "should display canned response suggestions on typing ':'" + ); + }); -QUnit.test('mention a partner after some text', async function (assert) { - assert.expect(5); + QUnit.test("use a canned response", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ email: "testpartner@odoo.com", name: "TestPartner" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "bluhbluh "); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "bluhbluh ", - "text content of composer should have content" - ); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "bluhbluh @TestPartner ", - "text content of composer should have previous content + mentioned partner + additional whitespace afterwards" - ); -}); + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "canned response suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", ":"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a canned response suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "Hello! How are you? ", + "text content of composer should have canned response + additional whitespace afterwards" + ); + }); -QUnit.test('add an emoji after a partner mention', async function (assert) { - assert.expect(5); + QUnit.test("use a canned response some text", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ email: "testpartner@odoo.com", name: "TestPartner" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "mention suggestions list should not be present" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "text content of composer should be empty initially" - ); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "should have a mention suggestion" - ); - await click('.o_ComposerSuggestionView'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "@TestPartner ", - "text content of composer should have previous content + mentioned partner + additional whitespace afterwards" - ); + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "canned response suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "bluhbluh "); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "bluhbluh ", + "text content of composer should have content" + ); + await insertText(".o_ComposerTextInputView_textarea", ":"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a canned response suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "bluhbluh Hello! How are you? ", + "text content of composer should have previous content + canned response substitution + additional whitespace afterwards" + ); + }); - // select emoji - await click('.o_ComposerView_buttonEmojis'); - await click('.o_EmojiView[data-codepoints="😊"]'); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value.replace(/\s/, " "), - "@TestPartner 😊", - "text content of composer should have previous mention and selected emoji just after" - ); -}); + QUnit.test("add an emoji after a canned response", async function (assert) { + assert.expect(5); -QUnit.test('composer: add an attachment', async function (assert) { - assert.expect(2); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "canned response suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", ":"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a canned response suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "Hello! How are you? ", + "text content of composer should have previous content + canned response substitution + additional whitespace afterwards" + ); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => - inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) - ); - assert.ok( - document.querySelector('.o_ComposerView_attachmentList'), - "should have an attachment list" - ); - assert.ok( - document.querySelector(`.o_ComposerView .o_AttachmentCard`), - "should have an attachment" - ); -}); + // select emoji + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "Hello! How are you? 😊", + "text content of composer should have previous canned response substitution and selected emoji just after" + ); + }); -QUnit.test('composer: drop attachments', async function (assert) { - assert.expect(4); + QUnit.test('display channel mention suggestions on typing "#"', async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1, }, - }, - }); - await openDiscuss(); - const files = [ - await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }), - await createFile({ - content: 'hello, worlduh', - contentType: 'text/plain', - name: 'text2.txt', - }), - ]; - await afterNextRender(() => dragenterFiles(document.querySelector('.o_ComposerView'))); - assert.ok( - document.querySelector('.o_ComposerView_dropZone'), - "should have a drop zone" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 0, - "should have no attachment before files are dropped" - ); + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ + name: "General", + channel_type: "channel", + }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); - await afterNextRender(() => - dropFiles(document.querySelector('.o_ComposerView_dropZone'), files) - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 2, - "should have 2 attachments in the composer after files dropped" - ); + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "channel mention suggestions list should not be present" + ); + await insertText(".o_ComposerTextInputView_textarea", "#"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionListView_list"), + "show", + "should display channel mention suggestions on typing '#'" + ); + }); - await afterNextRender(() => dragenterFiles(document.querySelector('.o_ComposerView'))); - await afterNextRender(async () => - dropFiles( - document.querySelector('.o_ComposerView_dropZone'), - [ + QUnit.test("mention a channel", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ + name: "General", + channel_type: "channel", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "channel mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "#"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a channel mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "#General ", + "text content of composer should have mentioned channel + additional whitespace afterwards" + ); + }); + + QUnit.test("mention a channel after some text", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ + name: "General", + channel_type: "channel", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "channel mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "bluhbluh "); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "bluhbluh ", + "text content of composer should have content" + ); + await insertText(".o_ComposerTextInputView_textarea", "#"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a channel mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "bluhbluh #General ", + "text content of composer should have previous content + mentioned channel + additional whitespace afterwards" + ); + }); + + QUnit.test("add an emoji after a channel mention", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ + name: "General", + channel_type: "channel", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "#"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a channel mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "#General ", + "text content of composer should have previous content + mentioned channel + additional whitespace afterwards" + ); + + // select emoji + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "#General 😊", + "text content of composer should have previous channel mention and selected emoji just after" + ); + }); + + QUnit.test('display command suggestions on typing "/"', async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ channel_type: "channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "command suggestions list should not be present" + ); + await insertText(".o_ComposerTextInputView_textarea", "/"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionListView_list"), + "show", + "should display command suggestions on typing '/'" + ); + }); + + QUnit.test( + 'do not send typing notification on typing "/" command', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); + + await insertText(".o_ComposerTextInputView_textarea", "/"); + assert.verifySteps([], "No rpc done"); + } + ); + + QUnit.test( + 'do not send typing notification on typing after selecting suggestion from "/" command', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); + + await insertText(".o_ComposerTextInputView_textarea", "/"); + await click(".o_ComposerSuggestionView"); + await insertText(".o_ComposerTextInputView_textarea", " is user?"); + assert.verifySteps([], "No rpc done"); + } + ); + + QUnit.test("use a command for a specific channel type", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ channel_type: "chat" }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "command suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "/"); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "/who ", + "text content of composer should have used command + additional whitespace afterwards" + ); + }); + + QUnit.test( + "command suggestion should only open if command is the first character", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ channel_type: "channel" }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "command suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "bluhbluh "); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "bluhbluh ", + "text content of composer should have content" + ); + await insertText(".o_ComposerTextInputView_textarea", "/"); + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "should not have a command suggestion" + ); + } + ); + + QUnit.test("add an emoji after a command", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChanelId1 = pyEnv["mail.channel"].create({ channel_type: "channel" }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChanelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "command suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "/"); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "/who ", + "text content of composer should have previous content + used command + additional whitespace afterwards" + ); + + // select emoji + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "/who 😊", + "text content of composer should have previous command and selected emoji just after" + ); + }); + + QUnit.test('display partner mention suggestions on typing "@"', async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const resPartnerId2 = pyEnv["res.partner"].create({ + email: "testpartner2@odoo.com", + name: "TestPartner2", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "mention suggestions list should not be present" + ); + await insertText(".o_ComposerTextInputView_textarea", "@"); + assert.hasClass( + document.querySelector(".o_ComposerSuggestionListView_list"), + "show", + "should display mention suggestions on typing '@'" + ); + assert.containsOnce(document.body, ".dropdown-divider", "should have a separator"); + }); + + QUnit.test("mention a partner", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionListView_list", + "mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "@TestPartner ", + "text content of composer should have mentioned partner + additional whitespace afterwards" + ); + }); + + QUnit.test("mention a partner after some text", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "bluhbluh "); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "bluhbluh ", + "text content of composer should have content" + ); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "bluhbluh @TestPartner ", + "text content of composer should have previous content + mentioned partner + additional whitespace afterwards" + ); + }); + + QUnit.test("add an emoji after a partner mention", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "mention suggestions list should not be present" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "text content of composer should be empty initially" + ); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "should have a mention suggestion" + ); + await click(".o_ComposerSuggestionView"); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "@TestPartner ", + "text content of composer should have previous content + mentioned partner + additional whitespace afterwards" + ); + + // select emoji + await click(".o_ComposerView_buttonEmojis"); + await click('.o_EmojiView[data-codepoints="😊"]'); + assert.strictEqual( + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .value.replace(/\s/, " "), + "@TestPartner 😊", + "text content of composer should have previous mention and selected emoji just after" + ); + }); + + QUnit.test("composer: add an attachment", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) + ); + assert.ok( + document.querySelector(".o_ComposerView_attachmentList"), + "should have an attachment list" + ); + assert.ok( + document.querySelector(`.o_ComposerView .o_AttachmentCard`), + "should have an attachment" + ); + }); + + QUnit.test("composer: drop attachments", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + const files = [ await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text3.txt', - }) - ] - ) - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 3, - "should have 3 attachments in the box after files dropped" - ); -}); + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }), + await createFile({ + content: "hello, worlduh", + contentType: "text/plain", + name: "text2.txt", + }), + ]; + await afterNextRender(() => dragenterFiles(document.querySelector(".o_ComposerView"))); + assert.ok( + document.querySelector(".o_ComposerView_dropZone"), + "should have a drop zone" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 0, + "should have no attachment before files are dropped" + ); -QUnit.test('composer: paste attachments', async function (assert) { - assert.expect(2); + await afterNextRender(() => + dropFiles(document.querySelector(".o_ComposerView_dropZone"), files) + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 2, + "should have 2 attachments in the composer after files dropped" + ); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1, }, - }, - }); - await openDiscuss(); - const files = [ - await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }) - ]; - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 0, - "should not have any attachment in the composer before paste" - ); + await afterNextRender(() => dragenterFiles(document.querySelector(".o_ComposerView"))); + await afterNextRender(async () => + dropFiles(document.querySelector(".o_ComposerView_dropZone"), [ + await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text3.txt", + }), + ]) + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 3, + "should have 3 attachments in the box after files dropped" + ); + }); - await afterNextRender(() => - pasteFiles(document.querySelector('.o_ComposerTextInputView'), files) - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 1, - "should have 1 attachment in the composer after paste" - ); -}); + QUnit.test("composer: paste attachments", async function (assert) { + assert.expect(2); -QUnit.test('composer text input cleared on message post', async function (assert) { - assert.expect(4); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + const files = [ + await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }), + ]; + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 0, + "should not have any attachment in the composer before paste" + ); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1, }, - }, - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('message_post'); + await afterNextRender(() => + pasteFiles(document.querySelector(".o_ComposerTextInputView"), files) + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 1, + "should have 1 attachment in the composer after paste" + ); + }); + + QUnit.test("composer text input cleared on message post", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("message_post"); + } + }, + }); + await openDiscuss(); + // Type message + await insertText(".o_ComposerTextInputView_textarea", "test message"); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "test message", + "should have inserted text content in editable" + ); + + // Send message + await click(".o_ComposerView_buttonSend"); + assert.verifySteps(["message_post"]); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "should have no content in composer input after posting message" + ); + }); + + QUnit.test("composer with thread typing notification status", async function (assert) { + assert.expect(2); + + // channel that is expected to be rendered + // with a random unique id that will be referenced in the test + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_ComposerView_threadTextualTypingStatus", + "Composer should have a thread textual typing status bar" + ); + assert.strictEqual( + document.body.querySelector(".o_ComposerView_threadTextualTypingStatus") + .textContent, + "", + "By default, thread textual typing status bar should be empty" + ); + }); + + QUnit.test( + "current partner notify is typing to other thread members", + async function (assert) { + assert.expect(2); + + // channel that is expected to be rendered + // with a random unique id that will be referenced in the test + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); + + await insertText(".o_ComposerTextInputView_textarea", "a"); + assert.verifySteps( + ["notify_typing:true"], + "should have notified current partner typing status" + ); } - }, - }); - await openDiscuss(); - // Type message - await insertText('.o_ComposerTextInputView_textarea', "test message"); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "test message", - "should have inserted text content in editable" - ); + ); - // Send message - await click('.o_ComposerView_buttonSend'); - assert.verifySteps(['message_post']); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "should have no content in composer input after posting message" - ); -}); + QUnit.test( + "current partner is typing should not translate on textual typing status", + async function (assert) { + assert.expect(3); -QUnit.test('composer with thread typing notification status', async function (assert) { - assert.expect(2); + // channel that is expected to be rendered + // with a random unique id that will be referenced in the test + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + hasTimeControl: true, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); - // channel that is expected to be rendered - // with a random unique id that will be referenced in the test - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "a"); - assert.containsOnce( - document.body, - '.o_ComposerView_threadTextualTypingStatus', - "Composer should have a thread textual typing status bar" - ); - assert.strictEqual( - document.body.querySelector('.o_ComposerView_threadTextualTypingStatus').textContent, - "", - "By default, thread textual typing status bar should be empty" - ); -}); + assert.verifySteps( + ["notify_typing:true"], + "should have notified current partner typing status" + ); -QUnit.test('current partner notify is typing to other thread members', async function (assert) { - assert.expect(2); - - // channel that is expected to be rendered - // with a random unique id that will be referenced in the test - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + await nextAnimationFrame(); + assert.strictEqual( + document.body.querySelector(".o_ComposerView_threadTextualTypingStatus") + .textContent, + "", + "Thread textual typing status bar should not display current partner is typing" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', 'a'); - assert.verifySteps( - ['notify_typing:true'], - "should have notified current partner typing status" - ); -}); + QUnit.test( + "current partner notify no longer is typing to thread members after 5 seconds inactivity", + async function (assert) { + assert.expect(4); -QUnit.test('current partner is typing should not translate on textual typing status', async function (assert) { - assert.expect(3); + // channel that is expected to be rendered + // with a random unique id that will be referenced in the test + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { advanceTime, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + hasTimeControl: true, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); - // channel that is expected to be rendered - // with a random unique id that will be referenced in the test - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - hasTimeControl: true, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + await insertText(".o_ComposerTextInputView_textarea", "a"); + + assert.verifySteps( + ["notify_typing:true"], + "should have notified current partner is typing" + ); + + await advanceTime(5 * 1000); + assert.verifySteps( + ["notify_typing:false"], + "should have notified current partner no longer is typing (inactive for 5 seconds)" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', 'a'); + QUnit.test( + "current partner notify is typing again to other members every 50s of long continuous typing", + async function (assert) { + assert.expect(4); - assert.verifySteps( - ['notify_typing:true'], - "should have notified current partner typing status" - ); + // channel that is expected to be rendered + // with a random unique id that will be referenced in the test + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { advanceTime, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + hasTimeControl: true, + async mockRPC(route, args) { + if (route === "/mail/channel/notify_typing") { + assert.step(`notify_typing:${args.is_typing}`); + } + }, + }); + await openDiscuss(); - await nextAnimationFrame(); - assert.strictEqual( - document.body.querySelector('.o_ComposerView_threadTextualTypingStatus').textContent, - "", - "Thread textual typing status bar should not display current partner is typing" - ); -}); + await insertText(".o_ComposerTextInputView_textarea", "a"); + assert.verifySteps( + ["notify_typing:true"], + "should have notified current partner is typing" + ); -QUnit.test('current partner notify no longer is typing to thread members after 5 seconds inactivity', async function (assert) { - assert.expect(4); + // simulate current partner typing a character every 2.5 seconds for 50 seconds straight. + let totalTimeElapsed = 0; + const elapseTickTime = 2.5 * 1000; + while (totalTimeElapsed < 50 * 1000) { + await insertText(".o_ComposerTextInputView_textarea", "a"); + totalTimeElapsed += elapseTickTime; + await advanceTime(elapseTickTime); + } - // channel that is expected to be rendered - // with a random unique id that will be referenced in the test - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { advanceTime, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - hasTimeControl: true, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + assert.verifySteps( + ["notify_typing:true"], + "should have notified current partner is still typing after 50s of straight typing" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', 'a'); + QUnit.test( + "composer: send button is disabled if attachment upload is not finished", + async function (assert) { + assert.expect(8); - assert.verifySteps( - ['notify_typing:true'], - "should have notified current partner is typing" - ); + const pyEnv = await startServer(); + const attachmentUploadedPromise = makeTestPromise(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route) { + if (route === "/mail/attachment/upload") { + await attachmentUploadedPromise; + } + }, + }); + await openDiscuss(); + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [ + file, + ]) + ); + assert.containsOnce( + document.body, + ".o_AttachmentCard", + "should have an attachment after a file has been input" + ); + assert.containsOnce( + document.body, + ".o_AttachmentCard.o-isUploading", + "attachment displayed is being uploaded" + ); + assert.containsOnce( + document.body, + ".o_ComposerView_buttonSend", + "composer send button should be displayed" + ); + assert.ok( + !!document.querySelector(".o_ComposerView_buttonSend").attributes.disabled, + "composer send button should be disabled as attachment is not yet uploaded" + ); - await advanceTime(5 * 1000); - assert.verifySteps( - ['notify_typing:false'], - "should have notified current partner no longer is typing (inactive for 5 seconds)" - ); -}); - -QUnit.test('current partner notify is typing again to other members every 50s of long continuous typing', async function (assert) { - assert.expect(4); - - // channel that is expected to be rendered - // with a random unique id that will be referenced in the test - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { advanceTime, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - hasTimeControl: true, - async mockRPC(route, args) { - if (route === '/mail/channel/notify_typing') { - assert.step(`notify_typing:${args.is_typing}`); + // simulates attachment finishes uploading + await afterNextRender(() => attachmentUploadedPromise.resolve()); + assert.containsOnce( + document.body, + ".o_AttachmentCard", + "should have only one attachment" + ); + assert.containsNone( + document.body, + ".o_AttachmentCard.o-isUploading", + "attachment displayed should be uploaded" + ); + assert.containsOnce( + document.body, + ".o_ComposerView_buttonSend", + "composer send button should still be present" + ); + assert.ok( + !document.querySelector(".o_ComposerView_buttonSend").attributes.disabled, + "composer send button should be enabled as attachment is now uploaded" + ); } - }, - }); - await openDiscuss(); + ); - await insertText('.o_ComposerTextInputView_textarea', "a"); - assert.verifySteps( - ['notify_typing:true'], - "should have notified current partner is typing" - ); + QUnit.test( + "remove an attachment from composer does not need any confirmation", + async function (assert) { + assert.expect(3); - // simulate current partner typing a character every 2.5 seconds for 50 seconds straight. - let totalTimeElapsed = 0; - const elapseTickTime = 2.5 * 1000; - while (totalTimeElapsed < 50 * 1000) { - await insertText('.o_ComposerTextInputView_textarea', 'a'); - totalTimeElapsed += elapseTickTime; - await advanceTime(elapseTickTime); - } + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { afterEvent, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + afterEvent({ + eventName: "o-file-uploader-upload", + func() { + inputFiles( + messaging.discuss.threadView.composerView.fileUploader.fileInput, + [file] + ); + }, + message: "should wait until files are uploaded", + predicate: ({ files: uploadedFiles }) => uploadedFiles[0] === file, + }) + ); + assert.containsOnce( + document.body, + ".o_ComposerView_attachmentList", + "should have an attachment list" + ); + assert.containsOnce( + document.body, + ".o_ComposerView .o_AttachmentCard", + "should have only one attachment" + ); - assert.verifySteps( - ['notify_typing:true'], - "should have notified current partner is still typing after 50s of straight typing" - ); -}); + const attachmentLocalId = document.querySelector(".o_AttachmentCard").dataset.id; + await afterNextRender(() => + afterEvent({ + eventName: "o-attachment-deleted", + func() { + document.querySelector(".o_AttachmentCard_asideItemUnlink").click(); + }, + message: "should wait until attachment is deleted", + predicate: ({ attachment }) => { + return attachment.localId === attachmentLocalId; + }, + }) + ); -QUnit.test('composer: send button is disabled if attachment upload is not finished', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const attachmentUploadedPromise = makeTestPromise(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1, }, - }, - async mockRPC(route) { - if (route === '/mail/attachment/upload') { - await attachmentUploadedPromise; + assert.containsNone( + document.body, + ".o_ComposerView .o_AttachmentCard", + "should not have any attachment left after unlinking the only one" + ); } - } - }); - await openDiscuss(); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => - inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard', - "should have an attachment after a file has been input" - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard.o-isUploading', - "attachment displayed is being uploaded" - ); - assert.containsOnce( - document.body, - '.o_ComposerView_buttonSend', - "composer send button should be displayed" - ); - assert.ok( - !!document.querySelector('.o_ComposerView_buttonSend').attributes.disabled, - "composer send button should be disabled as attachment is not yet uploaded" - ); + ); - // simulates attachment finishes uploading - await afterNextRender(() => attachmentUploadedPromise.resolve()); - assert.containsOnce( - document.body, - '.o_AttachmentCard', - "should have only one attachment" - ); - assert.containsNone( - document.body, - '.o_AttachmentCard.o-isUploading', - "attachment displayed should be uploaded" - ); - assert.containsOnce( - document.body, - '.o_ComposerView_buttonSend', - "composer send button should still be present" - ); - assert.ok( - !document.querySelector('.o_ComposerView_buttonSend').attributes.disabled, - "composer send button should be enabled as attachment is now uploaded" - ); -}); + QUnit.test("remove an uploading attachment", async function (assert) { + assert.expect(4); -QUnit.test('remove an attachment from composer does not need any confirmation', async function (assert) { - assert.expect(3); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, openDiscuss, messaging } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route) { + if (route === "/mail/attachment/upload") { + // simulates uploading indefinitely + await new Promise(() => {}); + } + }, + }); + await openDiscuss(); + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) + ); + assert.containsOnce( + document.body, + ".o_ComposerView_attachmentList", + "should have an attachment list" + ); + assert.containsOnce( + document.body, + ".o_ComposerView .o_AttachmentCard", + "should have only one attachment" + ); + assert.containsOnce( + document.body, + ".o_AttachmentCard.o-isUploading", + "should have an uploading attachment" + ); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { afterEvent, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => afterEvent({ - eventName: 'o-file-uploader-upload', - func() { - inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]); - }, - message: 'should wait until files are uploaded', - predicate: ({ files: uploadedFiles }) => uploadedFiles[0] === file, - })); - assert.containsOnce( - document.body, - '.o_ComposerView_attachmentList', - "should have an attachment list" - ); - assert.containsOnce( - document.body, - '.o_ComposerView .o_AttachmentCard', - "should have only one attachment" - ); + await click(".o_AttachmentCard_asideItemUnlink"); + assert.containsNone( + document.body, + ".o_ComposerView .o_AttachmentCard", + "should not have any attachment left after unlinking uploading one" + ); + }); - const attachmentLocalId = document.querySelector('.o_AttachmentCard').dataset.id; - await afterNextRender(() => afterEvent({ - eventName: 'o-attachment-deleted', - func() { - document.querySelector('.o_AttachmentCard_asideItemUnlink').click(); - }, - message: "should wait until attachment is deleted", - predicate: ({ attachment }) => { - return attachment.localId === attachmentLocalId; - }, - })); + QUnit.test("remove an uploading attachment aborts upload", async function (assert) { + assert.expect(1); - assert.containsNone( - document.body, - '.o_ComposerView .o_AttachmentCard', - "should not have any attachment left after unlinking the only one" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { afterEvent, openDiscuss, messaging } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route) { + if (route === "/mail/attachment/upload") { + // simulates uploading indefinitely + await new Promise(() => {}); + } + }, + }); + await openDiscuss(); + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) + ); + assert.containsOnce(document.body, ".o_AttachmentCard", "should contain an attachment"); + const attachmentLocalId = document.querySelector(".o_AttachmentCard").dataset.id; -QUnit.test('remove an uploading attachment', async function (assert) { - assert.expect(4); + await afterEvent({ + eventName: "o-attachment-upload-abort", + func: () => { + document.querySelector(".o_AttachmentCard_asideItemUnlink").click(); + }, + message: "attachment upload request should have been aborted", + predicate: ({ attachment }) => { + return attachment.localId === attachmentLocalId; + }, + }); + }); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, openDiscuss, messaging } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route) { - if (route === '/mail/attachment/upload') { - // simulates uploading indefinitely - await new Promise(() => {}); + QUnit.test( + "Show a default status in the recipient status text when the thread doesn't have a name.", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + await click(".o_ChatterTopbar_buttonSendMessage"); + assert.strictEqual( + document + .querySelector(".o_ComposerView_followers") + .textContent.replace(/\s+/g, ""), + "To:Followersofthisdocument", + 'Composer should display "To: Followers of this document" if the thread as no name.' + ); } - } - }); - await openDiscuss(); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => - inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) - ); - assert.containsOnce( - document.body, - '.o_ComposerView_attachmentList', - "should have an attachment list" - ); - assert.containsOnce( - document.body, - '.o_ComposerView .o_AttachmentCard', - "should have only one attachment" - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard.o-isUploading', - "should have an uploading attachment" - ); + ); - await click('.o_AttachmentCard_asideItemUnlink'); - assert.containsNone( - document.body, - '.o_ComposerView .o_AttachmentCard', - "should not have any attachment left after unlinking uploading one" - ); -}); + QUnit.test("Show a thread name in the recipient status text.", async function (assert) { + assert.expect(1); -QUnit.test('remove an uploading attachment aborts upload', async function (assert) { - assert.expect(1); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "test name" }); + const { click, messaging, openView } = await start(); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + // hack: provide awareness of name (not received in usual chatter flow) + messaging.models["Thread"].insert({ + id: resPartnerId1, + model: "res.partner", + name: "test name", + }); + await click(".o_ChatterTopbar_buttonSendMessage"); + assert.strictEqual( + document.querySelector(".o_ComposerView_followers").textContent.replace(/\s+/g, ""), + 'To:Followersof"testname"', + "basic rendering when sending a message to the followers and thread does have a name" + ); + }); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { afterEvent, openDiscuss, messaging } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route) { - if (route === '/mail/attachment/upload') { - // simulates uploading indefinitely - await new Promise(() => {}); + QUnit.test( + "send message only once when button send is clicked twice quickly", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("message_post"); + } + }, + }); + await openDiscuss(); + // Type message + await insertText(".o_ComposerTextInputView_textarea", "test message"); + + await afterNextRender(() => { + document.querySelector(`.o_ComposerView_buttonSend`).click(); + document.querySelector(`.o_ComposerView_buttonSend`).click(); + }); + assert.verifySteps(["message_post"], "The message has been posted only once"); } - } - }); - await openDiscuss(); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => - inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [file]) - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard', - "should contain an attachment" - ); - const attachmentLocalId = document.querySelector('.o_AttachmentCard').dataset.id; + ); - await afterEvent({ - eventName: 'o-attachment-upload-abort', - func: () => { - document.querySelector('.o_AttachmentCard_asideItemUnlink').click(); - }, - message: "attachment upload request should have been aborted", - predicate: ({ attachment }) => { - return attachment.localId === attachmentLocalId; - }, - }); -}); + QUnit.test( + "[technical] does not crash when an attachment is removed before its upload starts", + async function (assert) { + // Uploading multiple files uploads attachments one at a time, this test + // ensures that there is no crash when an attachment is destroyed before its + // upload started. + assert.expect(1); -QUnit.test("Show a default status in the recipient status text when the thread doesn't have a name.", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - await click('.o_ChatterTopbar_buttonSendMessage'); - assert.strictEqual( - document.querySelector('.o_ComposerView_followers').textContent.replace(/\s+/g, ''), - "To:Followersofthisdocument", - "Composer should display \"To: Followers of this document\" if the thread as no name." - ); -}); - -QUnit.test("Show a thread name in the recipient status text.", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "test name" }); - const { click, messaging, openView } = await start(); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - // hack: provide awareness of name (not received in usual chatter flow) - messaging.models['Thread'].insert({ id: resPartnerId1, model: 'res.partner', name: "test name" }); - await click('.o_ChatterTopbar_buttonSendMessage'); - assert.strictEqual( - document.querySelector('.o_ComposerView_followers').textContent.replace(/\s+/g, ''), - "To:Followersof\"testname\"", - "basic rendering when sending a message to the followers and thread does have a name" - ); -}); - -QUnit.test('send message only once when button send is clicked twice quickly', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('message_post'); + const pyEnv = await startServer(); + // Promise to block attachment uploading + const uploadPromise = makeTestPromise(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route) { + if (route === "/mail/attachment/upload") { + await uploadPromise; + } + }, + }); + await openDiscuss(); + const file1 = await createFile({ + name: "text1.txt", + content: "hello, world", + contentType: "text/plain", + }); + const file2 = await createFile({ + name: "text2.txt", + content: "hello, world", + contentType: "text/plain", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [ + file1, + file2, + ]) + ); + await afterNextRender(() => { + Array.from(document.querySelectorAll("div")) + .find((el) => el.textContent === "text2.txt") + .closest(".o_AttachmentCard") + .querySelector(".o_AttachmentCard_asideItemUnlink") + .click(); + }); + // Simulates the completion of the upload of the first attachment + await afterNextRender(() => uploadPromise.resolve()); + assert.containsOnce( + document.body, + '.o_AttachmentCard:contains("text1.txt")', + "should only have the first attachment after cancelling the second attachment" + ); } - }, - }); - await openDiscuss(); - // Type message - await insertText('.o_ComposerTextInputView_textarea', "test message"); + ); - await afterNextRender(() => { - document.querySelector(`.o_ComposerView_buttonSend`).click(); - document.querySelector(`.o_ComposerView_buttonSend`).click(); - }); - assert.verifySteps( - ['message_post'], - "The message has been posted only once" - ); -}); + QUnit.test( + 'send button on mail.channel should have "Send" as label', + async function (assert) { + assert.expect(1); -QUnit.test('[technical] does not crash when an attachment is removed before its upload starts', async function (assert) { - // Uploading multiple files uploads attachments one at a time, this test - // ensures that there is no crash when an attachment is destroyed before its - // upload started. - assert.expect(1); - - const pyEnv = await startServer(); - // Promise to block attachment uploading - const uploadPromise = makeTestPromise(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route) { - if (route === '/mail/attachment/upload') { - await uploadPromise; + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelector(".o_ComposerView_buttonSend").textContent, + "Send", + "Send button of mail.channel composer should have 'Send' as label" + ); } - }, + ); }); - await openDiscuss(); - const file1 = await createFile({ - name: 'text1.txt', - content: 'hello, world', - contentType: 'text/plain', - }); - const file2 = await createFile({ - name: 'text2.txt', - content: 'hello, world', - contentType: 'text/plain', - }); - await afterNextRender(() => - inputFiles( - messaging.discuss.threadView.composerView.fileUploader.fileInput, - [file1, file2] - ) - ); - await afterNextRender(() => { - Array.from(document.querySelectorAll('div')) - .find(el => el.textContent === 'text2.txt') - .closest('.o_AttachmentCard') - .querySelector('.o_AttachmentCard_asideItemUnlink') - .click(); - } - ); - // Simulates the completion of the upload of the first attachment - await afterNextRender(() => uploadPromise.resolve()); - assert.containsOnce( - document.body, - '.o_AttachmentCard:contains("text1.txt")', - "should only have the first attachment after cancelling the second attachment" - ); -}); - -QUnit.test('send button on mail.channel should have "Send" as label', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelector('.o_ComposerView_buttonSend').textContent, - "Send", - "Send button of mail.channel composer should have 'Send' as label", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/dialog_manager_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/dialog_manager_tests.js index 945ebc35024..2e814fb978d 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/dialog_manager_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/dialog_manager_tests.js @@ -1,49 +1,44 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; -import { start } from '@mail/../tests/helpers/test_utils'; +import { makeDeferred } from "@mail/utils/deferred"; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('dialog_manager_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("dialog_manager_tests.js"); -QUnit.test('[technical] messaging not created', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - */ - assert.expect(1); + QUnit.test("[technical] messaging not created", async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(1); - const messagingBeforeCreationDeferred = makeDeferred(); - const { afterNextRender } = await start({ - messagingBeforeCreationDeferred, - waitUntilMessagingCondition: 'none', + const messagingBeforeCreationDeferred = makeDeferred(); + const { afterNextRender } = await start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: "none", + }); + + // simulate messaging being created + await afterNextRender(messagingBeforeCreationDeferred.resolve); + + assert.containsOnce( + document.body, + ".o_DialogManager", + "should contain dialog manager after messaging has been created" + ); + }); + + QUnit.test("initial mount", async function (assert) { + assert.expect(1); + + await start(); + assert.containsOnce(document.body, ".o_DialogManager", "should have dialog manager"); + }); }); - - // simulate messaging being created - await afterNextRender(messagingBeforeCreationDeferred.resolve); - - assert.containsOnce( - document.body, - '.o_DialogManager', - "should contain dialog manager after messaging has been created" - ); -}); - -QUnit.test('initial mount', async function (assert) { - assert.expect(1); - - await start(); - assert.containsOnce( - document.body, - '.o_DialogManager', - "should have dialog manager" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js index 57faeaff2c7..92aa7aae24d 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_inbox_tests.js @@ -5,907 +5,925 @@ import { nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_inbox_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_inbox_tests.js"); -QUnit.test('reply: discard on pressing escape', async function (assert) { - assert.expect(9); + QUnit.test("reply: discard on pressing escape", async function (assert) { + assert.expect(9); - const pyEnv = await startServer(); - // partner expected to be found by mention - pyEnv['res.partner'].create({ - email: "testpartnert@odoo.com", - name: "TestPartner", - }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: 20, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, insertText, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + const pyEnv = await startServer(); + // partner expected to be found by mention + pyEnv["res.partner"].create({ + email: "testpartnert@odoo.com", + name: "TestPartner", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: 20, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, insertText, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); + assert.containsOnce( + document.body, + ".o_ComposerView", + "should have composer after clicking on reply to message" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.containsOnce( - document.body, - '.o_ComposerView', - "should have composer after clicking on reply to message" - ); - await click(`.o_ComposerView_buttonEmojis`); - assert.containsOnce( - document.body, - '.o_EmojiPickerView', - "emoji list should be opened after click on emojis button" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerView_buttonEmojis`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_EmojiPickerView', - "emoji list should be closed after pressing escape on emojis button" - ); - assert.containsOnce( - document.body, - '.o_ComposerView', - "reply composer should still be opened after pressing escape on emojis button" - ); - - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - assert.containsOnce( - document.body, - '.o_ComposerSuggestionView', - "mention suggestion should be opened after typing @" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_ComposerSuggestionView', - "mention suggestion should be closed after pressing escape on mention suggestion" - ); - assert.containsOnce( - document.body, - '.o_ComposerView', - "reply composer should still be opened after pressing escape on mention suggestion" - ); - - await afterNextRender(() => { - const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); - }); - assert.containsNone( - document.body, - '.o_ComposerView', - "reply composer should be closed after pressing escape if there was no other priority escape handler" - ); -}); - -QUnit.test('reply: discard on discard button click', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + await click(`.o_ComposerView_buttonEmojis`); + assert.containsOnce( + document.body, + ".o_EmojiPickerView", + "emoji list should be opened after click on emojis button" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.containsOnce( - document.body, - '.o_ComposerView', - "should have composer after clicking on reply to message" - ); - assert.containsOnce( - document.body, - '.o_ComposerView_buttonDiscard', - "composer should have a discard button" - ); - - await click(`.o_ComposerView_buttonDiscard`); - assert.containsNone( - document.body, - '.o_ComposerView', - "reply composer should be closed after clicking on discard" - ); -}); - -QUnit.test('reply: discard on reply button toggle', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerView_buttonEmojis`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_EmojiPickerView", + "emoji list should be closed after pressing escape on emojis button" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.containsOnce( - document.body, - '.o_ComposerView', - "should have composer after clicking on reply to message" - ); - await click(`.o_MessageActionView_actionReplyTo`); - assert.containsNone( - document.body, - '.o_ComposerView', - "reply composer should be closed after clicking on reply button again" - ); -}); - -QUnit.test('reply: discard on click away', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + assert.containsOnce( + document.body, + ".o_ComposerView", + "reply composer should still be opened after pressing escape on emojis button" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.containsOnce( - document.body, - '.o_ComposerView', - "should have composer after clicking on reply to message" - ); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + assert.containsOnce( + document.body, + ".o_ComposerSuggestionView", + "mention suggestion should be opened after typing @" + ); - document.querySelector(`.o_ComposerTextInputView_textarea`).click(); - await nextAnimationFrame(); // wait just in case, but nothing is supposed to happen - assert.containsOnce( - document.body, - '.o_ComposerView', - "reply composer should still be there after clicking inside itself" - ); + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_ComposerSuggestionView", + "mention suggestion should be closed after pressing escape on mention suggestion" + ); + assert.containsOnce( + document.body, + ".o_ComposerView", + "reply composer should still be opened after pressing escape on mention suggestion" + ); - await click(`.o_ComposerView_buttonEmojis`); - assert.containsOnce( - document.body, - '.o_EmojiPickerView', - "emoji list should be opened after clicking on emojis button" - ); + await afterNextRender(() => { + const ev = new window.KeyboardEvent("keydown", { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInputView_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + ".o_ComposerView", + "reply composer should be closed after pressing escape if there was no other priority escape handler" + ); + }); - await click(`.o_EmojiView`); - assert.containsNone( - document.body, - '.o_EmojiPickerView', - "emoji list should be closed after selecting an emoji" - ); - assert.containsOnce( - document.body, - '.o_ComposerView', - "reply composer should still be there after selecting an emoji (even though it is technically a click away, it should be considered inside)" - ); + QUnit.test("reply: discard on discard button click", async function (assert) { + assert.expect(4); - await click(`.o_MessageView`); - assert.containsNone( - document.body, - '.o_ComposerView', - "reply composer should be closed after clicking away" - ); -}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + await click(".o_MessageView"); -QUnit.test('"reply to" composer should log note if message replied to is a note', async function (assert) { - assert.expect(6); + await click(".o_MessageActionView_actionReplyTo"); + assert.containsOnce( + document.body, + ".o_ComposerView", + "should have composer after clicking on reply to message" + ); + assert.containsOnce( + document.body, + ".o_ComposerView_buttonDiscard", + "composer should have a discard button" + ); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - is_discussion: false, - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, insertText, messaging, openDiscuss } = await start({ - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('/mail/message/post'); + await click(`.o_ComposerView_buttonDiscard`); + assert.containsNone( + document.body, + ".o_ComposerView", + "reply composer should be closed after clicking on discard" + ); + }); + + QUnit.test("reply: discard on reply button toggle", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); + assert.containsOnce( + document.body, + ".o_ComposerView", + "should have composer after clicking on reply to message" + ); + await click(`.o_MessageActionView_actionReplyTo`); + assert.containsNone( + document.body, + ".o_ComposerView", + "reply composer should be closed after clicking on reply button again" + ); + }); + + QUnit.test("reply: discard on click away", async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); + assert.containsOnce( + document.body, + ".o_ComposerView", + "should have composer after clicking on reply to message" + ); + + document.querySelector(`.o_ComposerTextInputView_textarea`).click(); + await nextAnimationFrame(); // wait just in case, but nothing is supposed to happen + assert.containsOnce( + document.body, + ".o_ComposerView", + "reply composer should still be there after clicking inside itself" + ); + + await click(`.o_ComposerView_buttonEmojis`); + assert.containsOnce( + document.body, + ".o_EmojiPickerView", + "emoji list should be opened after clicking on emojis button" + ); + + await click(`.o_EmojiView`); + assert.containsNone( + document.body, + ".o_EmojiPickerView", + "emoji list should be closed after selecting an emoji" + ); + assert.containsOnce( + document.body, + ".o_ComposerView", + "reply composer should still be there after selecting an emoji (even though it is technically a click away, it should be considered inside)" + ); + + await click(`.o_MessageView`); + assert.containsNone( + document.body, + ".o_ComposerView", + "reply composer should be closed after clicking away" + ); + }); + + QUnit.test( + '"reply to" composer should log note if message replied to is a note', + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + is_discussion: false, + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, insertText, messaging, openDiscuss } = await start({ + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("/mail/message/post"); + assert.strictEqual( + args.post_data.message_type, + "comment", + "should set message type as 'comment'" + ); + assert.strictEqual( + args.post_data.subtype_xmlid, + "mail.mt_note", + "should set subtype_xmlid as 'note'" + ); + } + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a single message" + ); + + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); assert.strictEqual( - args.post_data.message_type, - "comment", - "should set message type as 'comment'" + document.querySelector(".o_ComposerView_buttonSend").textContent.trim(), + "Log", + "Send button text should be 'Log'" + ); + + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await click(".o_ComposerView_buttonSend"); + assert.verifySteps(["/mail/message/post"]); + } + ); + + QUnit.test( + '"reply to" composer should send message if message replied to is not a note', + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + is_discussion: true, + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, insertText, messaging, openDiscuss } = await start({ + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("/mail/message/post"); + assert.strictEqual( + args.post_data.message_type, + "comment", + "should set message type as 'comment'" + ); + assert.strictEqual( + args.post_data.subtype_xmlid, + "mail.mt_comment", + "should set subtype_xmlid as 'comment'" + ); + } + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a single message" + ); + + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); + assert.strictEqual( + document.querySelector(".o_ComposerView_buttonSend").textContent.trim(), + "Send", + "Send button text should be 'Send'" + ); + + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await click(".o_ComposerView_buttonSend"); + assert.verifySteps(["/mail/message/post"]); + } + ); + + QUnit.test("error notifications should not be shown in Inbox", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, // id of related message + notification_status: "exception", + notification_type: "email", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + assert.containsOnce( + document.body, + ".o_MessageView_originThreadLink", + "should display origin thread link" + ); + assert.containsNone( + document.body, + ".o_MessageView_notificationIcon", + "should not display any notification icon in Inbox" + ); + }); + + QUnit.test("show subject of message in Inbox", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], // not needed, for consistency + subject: "Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + assert.containsOnce( + document.body, + ".o_MessageView_subject", + "should display subject of the message" + ); + assert.strictEqual( + document.querySelector(".o_MessageView_subject").textContent, + "Subject: Salutations, voyageur", + "Subject of the message should be 'Salutations, voyageur'" + ); + }); + + QUnit.test("show subject of message in history", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + history_partner_ids: [3], // not needed, for consistency + model: "mail.channel", + subject: "Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + is_read: true, + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: "mail.box_history", + }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until history displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.history.thread + ); + }, + }); + assert.containsOnce(document.body, ".o_MessageView", "should display a single message"); + assert.containsOnce( + document.body, + ".o_MessageView_subject", + "should display subject of the message" + ); + assert.strictEqual( + document.querySelector(".o_MessageView_subject").textContent, + "Subject: Salutations, voyageur", + "Subject of the message should be 'Salutations, voyageur'" + ); + }); + + QUnit.test( + "click on (non-channel/non-partner) origin thread link should redirect to form view", + async function (assert) { + assert.expect(9); + + const pyEnv = await startServer(); + const resFakeId1 = pyEnv["res.fake"].create({ name: "Some record" }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.fake", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resFakeId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, env, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + patchWithCleanup(env.services.action, { + doAction(action) { + // Callback of doing an action (action manager). + // Expected to be called on click on origin thread link, + // which redirects to form view of record related to origin thread + assert.step("do-action"); + assert.strictEqual( + action.type, + "ir.actions.act_window", + "action should open a view" + ); + assert.deepEqual( + action.views, + [[false, "form"]], + "action should open form view" + ); + assert.strictEqual( + action.res_model, + "res.fake", + "action should open view with model 'res.fake' (model of message origin thread)" + ); + assert.strictEqual( + action.res_id, + resFakeId1, + "action should open view with id of resFake1 (id of message origin thread)" + ); + return Promise.resolve(); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a single message" + ); + assert.containsOnce( + document.body, + ".o_MessageView_originThreadLink", + "should display origin thread link" ); assert.strictEqual( - args.post_data.subtype_xmlid, - "mail.mt_note", - "should set subtype_xmlid as 'note'" + document.querySelector(".o_MessageView_originThreadLink").textContent, + "Some record", + "origin thread link should display record name" + ); + + document.querySelector(".o_MessageView_originThreadLink").click(); + assert.verifySteps( + ["do-action"], + "should have made an action on click on origin thread (to open form view)" ); } - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); + ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.strictEqual( - document.querySelector('.o_ComposerView_buttonSend').textContent.trim(), - "Log", - "Send button text should be 'Log'" - ); + QUnit.test( + "subject should not be shown when subject is the same as the thread name", + async function (assert) { + assert.expect(1); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await click('.o_ComposerView_buttonSend'); - assert.verifySteps(['/mail/message/post']); -}); - -QUnit.test('"reply to" composer should send message if message replied to is not a note', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - is_discussion: true, - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, insertText, messaging, openDiscuss } = await start({ - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('/mail/message/post'); - assert.strictEqual( - args.post_data.message_type, - "comment", - "should set message type as 'comment'" - ); - assert.strictEqual( - args.post_data.subtype_xmlid, - "mail.mt_comment", - "should set subtype_xmlid as 'comment'" + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "subject should not be shown when subject is the same as the thread name" ); } - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); + ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.strictEqual( - document.querySelector('.o_ComposerView_buttonSend').textContent.trim(), - "Send", - "Send button text should be 'Send'" - ); + QUnit.test( + "subject should not be shown when subject is the same as the thread name and both have the same prefix", + async function (assert) { + assert.expect(1); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await click('.o_ComposerView_buttonSend'); - assert.verifySteps(['/mail/message/post']); -}); - -QUnit.test('error notifications should not be shown in Inbox', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, // id of related message - notification_status: 'exception', - notification_type: 'email', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_originThreadLink', - "should display origin thread link" - ); - assert.containsNone( - document.body, - '.o_MessageView_notificationIcon', - "should not display any notification icon in Inbox" - ); -}); - -QUnit.test('show subject of message in Inbox', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], // not needed, for consistency - subject: "Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_subject', - "should display subject of the message" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_subject').textContent, - "Subject: Salutations, voyageur", - "Subject of the message should be 'Salutations, voyageur'" - ); -}); - -QUnit.test('show subject of message in history', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - history_partner_ids: [3], // not needed, for consistency - model: 'mail.channel', - subject: "Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - is_read: true, - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: 'mail.box_history', - }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until history displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.history.thread - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_subject', - "should display subject of the message" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_subject').textContent, - "Subject: Salutations, voyageur", - "Subject of the message should be 'Salutations, voyageur'" - ); -}); - -QUnit.test('click on (non-channel/non-partner) origin thread link should redirect to form view', async function (assert) { - assert.expect(9); - - const pyEnv = await startServer(); - const resFakeId1 = pyEnv['res.fake'].create({ name: 'Some record' }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.fake', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resFakeId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, env, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - patchWithCleanup(env.services.action, { - doAction(action) { - // Callback of doing an action (action manager). - // Expected to be called on click on origin thread link, - // which redirects to form view of record related to origin thread - assert.step('do-action'); - assert.strictEqual( - action.type, - 'ir.actions.act_window', - "action should open a view" - ); - assert.deepEqual( - action.views, - [[false, 'form']], - "action should open form view" - ); - assert.strictEqual( - action.res_model, - 'res.fake', - "action should open view with model 'res.fake' (model of message origin thread)" - ); - assert.strictEqual( - action.res_id, - resFakeId1, - "action should open view with id of resFake1 (id of message origin thread)" - ); - return Promise.resolve(); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_originThreadLink', - "should display origin thread link" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_originThreadLink').textContent, - "Some record", - "origin thread link should display record name" - ); - - document.querySelector('.o_MessageView_originThreadLink').click(); - assert.verifySteps(['do-action'], "should have made an action on click on origin thread (to open form view)"); -}); - -QUnit.test('subject should not be shown when subject is the same as the thread name', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "subject should not be shown when subject is the same as the thread name" - ); -}); - -QUnit.test('subject should not be shown when subject is the same as the thread name and both have the same prefix', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Re: Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Re: Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "subject should not be shown when subject is the same as the thread name and both have the same prefix" - ); -}); - -QUnit.test('subject should not be shown when subject differs from thread name only by the "Re:" prefix', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Re: Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "should not display subject when subject differs from thread name only by the 'Re:' prefix" - ); -}); - -QUnit.test('subject should not be shown when subject differs from thread name only by the "Fw:" and "Re:" prefix', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Fw: Re: Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "should not display subject when subject differs from thread name only by the 'Fw:' and Re:' prefix" - ); -}); - -QUnit.test('subject should be shown when the thread name has an extra prefix compared to subject', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Re: Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView_subject', - "subject should be shown when the thread name has an extra prefix compared to subject" - ); -}); - -QUnit.test('subject should not be shown when subject differs from thread name only by the "fw:" prefix and both contain another common prefix', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Re: Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "fw: re: Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "subject should not be shown when subject differs from thread name only by the 'fw:' prefix and both contain another common prefix" - ); -}); - -QUnit.test('subject should not be shown when subject differs from thread name only by the "Re: Re:" prefix', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "Salutations, voyageur" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - needaction: true, - subject: "Re: Re: Salutations, voyageur", - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread - ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView_subject', - "should not display subject when subject differs from thread name only by the 'Re: Re:'' prefix" - ); -}); - -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Re: Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Re: Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "subject should not be shown when subject is the same as the thread name and both have the same prefix" + ); + } + ); + + QUnit.test( + 'subject should not be shown when subject differs from thread name only by the "Re:" prefix', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Re: Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "should not display subject when subject differs from thread name only by the 'Re:' prefix" + ); + } + ); + + QUnit.test( + 'subject should not be shown when subject differs from thread name only by the "Fw:" and "Re:" prefix', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Fw: Re: Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "should not display subject when subject differs from thread name only by the 'Fw:' and Re:' prefix" + ); + } + ); + + QUnit.test( + "subject should be shown when the thread name has an extra prefix compared to subject", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Re: Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView_subject", + "subject should be shown when the thread name has an extra prefix compared to subject" + ); + } + ); + + QUnit.test( + 'subject should not be shown when subject differs from thread name only by the "fw:" prefix and both contain another common prefix', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Re: Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "fw: re: Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "subject should not be shown when subject differs from thread name only by the 'fw:' prefix and both contain another common prefix" + ); + } + ); + + QUnit.test( + 'subject should not be shown when subject differs from thread name only by the "Re: Re:" prefix', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "Salutations, voyageur", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + needaction: true, + subject: "Re: Re: Salutations, voyageur", + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView_subject", + "should not display subject when subject differs from thread name only by the 'Re: Re:'' prefix" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_message_edit_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_message_edit_tests.js index d6dbd703a6c..51bc22d88fa 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_message_edit_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_message_edit_tests.js @@ -1,34 +1,40 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_message_edit_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_message_edit_tests.js"); -QUnit.test('click on message edit button should open edit composer', async function (assert) { - assert.expect(1); + QUnit.test( + "click on message edit button should open edit composer", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - message_type: 'comment', - model: 'mail.channel', - res_id: mailChannelId1, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + await click(".o_MessageView"); + await click(".o_MessageActionView_actionEdit"); + assert.containsOnce( + document.body, + ".o_MessageView_composer", + "click on message edit button should open edit composer" + ); + } + ); }); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionEdit'); - assert.containsOnce(document.body, '.o_MessageView_composer', 'click on message edit button should open edit composer'); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js index 977c6c9baaf..99d630ac019 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_pinned_tests.js @@ -1,170 +1,183 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_pinned_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_pinned_tests.js"); -QUnit.test('sidebar: pinned channel 1: init with one pinned channel', async function (assert) { - assert.expect(2); + QUnit.test( + "sidebar: pinned channel 1: init with one pinned channel", + async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.containsOnce( - document.body, - `.o_DiscussView_thread[data-thread-id="${messaging.inbox.thread.id}"][data-thread-model="mail.box"]`, - "The Inbox is opened in discuss" - ); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "should have the only channel of which user is member in discuss sidebar" - ); -}); - -QUnit.test('sidebar: pinned channel 2: open pinned channel', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, openDiscuss } = await start(); - await openDiscuss(); - - await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); - assert.containsOnce( - document.body, - `.o_DiscussView_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`, - "The channel #General is displayed in discuss" - ); -}); - -QUnit.test('sidebar: pinned channel 3: open channel and leave it', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [[0, 0, { - fold_state: 'open', - is_minimized: true, - partner_id: pyEnv.currentPartnerId, - }]], - }); - const { click, openDiscuss } = await start({ - async mockRPC(route, args) { - if (args.method === 'action_unfollow') { - assert.step('action_unfollow'); - assert.deepEqual(args.args[0], [mailChannelId1], - "The right id is sent to the server to remove" + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.containsOnce( + document.body, + `.o_DiscussView_thread[data-thread-id="${messaging.inbox.thread.id}"][data-thread-model="mail.box"]`, + "The Inbox is opened in discuss" + ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "should have the only channel of which user is member in discuss sidebar" ); } - }, - }); - await openDiscuss(); + ); - await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); - assert.verifySteps([], "action_unfollow is not called yet"); + QUnit.test("sidebar: pinned channel 2: open pinned channel", async function (assert) { + assert.expect(1); - await click('.o_DiscussSidebarCategoryItem_commandLeave'); - assert.verifySteps( - [ - 'action_unfollow' - ], - "action_unfollow has been called when leaving a channel" - ); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "The channel must have been removed from discuss sidebar" - ); - assert.containsOnce( - document.body, - '.o_DiscussView_noThread', - "should have no thread opened in discuss" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, openDiscuss } = await start(); + await openDiscuss(); -QUnit.test('sidebar: unpin channel from bus', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_DiscussView_thread[data-thread-id="${messaging.inbox.thread.id}"][data-thread-model="mail.box"]`, - "The Inbox is opened in discuss" - ); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "1 channel is present in discuss sidebar and it is 'general'" - ); - - await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); - assert.containsOnce( - document.body, - `.o_DiscussView_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`, - "The channel #General is opened in discuss" - ); - - // Simulate receiving a leave channel notification - // (e.g. from user interaction from another device or browser tab) - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.channel/unpin', { - 'id': mailChannelId1, + await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); + assert.containsOnce( + document.body, + `.o_DiscussView_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`, + "The channel #General is displayed in discuss" + ); }); + + QUnit.test("sidebar: pinned channel 3: open channel and leave it", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + fold_state: "open", + is_minimized: true, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + const { click, openDiscuss } = await start({ + async mockRPC(route, args) { + if (args.method === "action_unfollow") { + assert.step("action_unfollow"); + assert.deepEqual( + args.args[0], + [mailChannelId1], + "The right id is sent to the server to remove" + ); + } + }, + }); + await openDiscuss(); + + await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); + assert.verifySteps([], "action_unfollow is not called yet"); + + await click(".o_DiscussSidebarCategoryItem_commandLeave"); + assert.verifySteps( + ["action_unfollow"], + "action_unfollow has been called when leaving a channel" + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "The channel must have been removed from discuss sidebar" + ); + assert.containsOnce( + document.body, + ".o_DiscussView_noThread", + "should have no thread opened in discuss" + ); + }); + + QUnit.test("sidebar: unpin channel from bus", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + + assert.containsOnce( + document.body, + `.o_DiscussView_thread[data-thread-id="${messaging.inbox.thread.id}"][data-thread-model="mail.box"]`, + "The Inbox is opened in discuss" + ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "1 channel is present in discuss sidebar and it is 'general'" + ); + + await click(`.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`); + assert.containsOnce( + document.body, + `.o_DiscussView_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"]`, + "The channel #General is opened in discuss" + ); + + // Simulate receiving a leave channel notification + // (e.g. from user interaction from another device or browser tab) + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.channel/unpin", { + id: mailChannelId1, + }); + }); + assert.containsOnce( + document.body, + ".o_DiscussView_noThread", + "should have no thread opened in discuss" + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "The channel must have been removed from discuss sidebar" + ); + }); + + QUnit.test( + "[technical] sidebar: channel group_based_subscription: mandatorily pinned", + async function (assert) { + assert.expect(2); + + // FIXME: The following is admittedly odd. + // Fixing it should entail a deeper reflexion on the group_based_subscription + // and is_pinned functionalities, especially in python. + // task-2284357 + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + is_pinned: false, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + group_based_subscription: true, + }); + const { openDiscuss } = await start(); + await openDiscuss(); + + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "The channel #General is in discuss sidebar" + ); + assert.containsNone( + document.body, + "o_DiscussSidebarCategoryItem_commandLeave", + "The group_based_subscription channel is not unpinnable" + ); + } + ); }); - assert.containsOnce( - document.body, - '.o_DiscussView_noThread', - "should have no thread opened in discuss" - ); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "The channel must have been removed from discuss sidebar" - ); -}); - -QUnit.test('[technical] sidebar: channel group_based_subscription: mandatorily pinned', async function (assert) { - assert.expect(2); - - // FIXME: The following is admittedly odd. - // Fixing it should entail a deeper reflexion on the group_based_subscription - // and is_pinned functionalities, especially in python. - // task-2284357 - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [[0, 0, { - is_pinned: false, - partner_id: pyEnv.currentPartnerId, - }]], - group_based_subscription: true, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "The channel #General is in discuss sidebar" - ); - assert.containsNone( - document.body, - 'o_DiscussSidebarCategoryItem_commandLeave', - "The group_based_subscription channel is not unpinnable" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js index 96d4ccdcb9e..28ed7127a24 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_item_tests.js @@ -1,168 +1,177 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -import { datetime_to_str } from 'web.time'; +import { datetime_to_str } from "web.time"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_sidebar_category_item_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_sidebar_category_item_tests.js"); -QUnit.test('channel - avatar: should have correct avatar', async function (assert) { - assert.expect(2); + QUnit.test("channel - avatar: should have correct avatar", async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ avatarCacheKey: '100111' }); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ avatarCacheKey: "100111" }); - const { openDiscuss } = await start(); - await openDiscuss(); + const { openDiscuss } = await start(); + await openDiscuss(); - const channelItem = document.querySelector(` + const channelItem = document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `); - assert.strictEqual( - channelItem.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_image`).length, - 1, - "channel should have an avatar" - ); + assert.strictEqual( + channelItem.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_image`).length, + 1, + "channel should have an avatar" + ); - assert.strictEqual( - channelItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src, - `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=100111`, - 'should link to the correct picture source' - ); -}); - -QUnit.test('channel - avatar: should update avatar url from bus', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ avatarCacheKey: '101010' }); - - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - .o_DiscussSidebarCategoryItem_image`).dataset.src, - `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=101010`, - ); - - await afterNextRender(() => { - messaging.rpc({ - model: 'mail.channel', - method: 'write', - args: [[mailChannelId1], { image_128: 'This field does not matter' }], + assert.strictEqual( + channelItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src, + `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=100111`, + "should link to the correct picture source" + ); }); - }); - const result = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]]); - const newCacheKey = result[0]['avatarCacheKey']; - assert.strictEqual( - document.querySelector(` + QUnit.test("channel - avatar: should update avatar url from bus", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ avatarCacheKey: "101010" }); + + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] .o_DiscussSidebarCategoryItem_image`).dataset.src, - `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=${newCacheKey}`, - ); -}); + `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=101010` + ); -QUnit.test('chat - avatar: should have correct avatar', async function (assert) { - assert.expect(2); + await afterNextRender(() => { + messaging.rpc({ + model: "mail.channel", + method: "write", + args: [[mailChannelId1], { image_128: "This field does not matter" }], + }); + }); + const result = pyEnv["mail.channel"].searchRead([["id", "=", mailChannelId1]]); + const newCacheKey = result[0]["avatarCacheKey"]; - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo", im_status: 'offline' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { openDiscuss } = await start(); - await openDiscuss(); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] + .o_DiscussSidebarCategoryItem_image`).dataset.src, + `/web/image/mail.channel/${mailChannelId1}/avatar_128?unique=${newCacheKey}` + ); + }); - const chatItem = document.querySelector(` + QUnit.test("chat - avatar: should have correct avatar", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Demo", + im_status: "offline", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { openDiscuss } = await start(); + await openDiscuss(); + + const chatItem = document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `); - assert.strictEqual( - chatItem.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_image`).length, - 1, - "chat should have an avatar" - ); + assert.strictEqual( + chatItem.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_image`).length, + 1, + "chat should have an avatar" + ); - assert.strictEqual( - chatItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src, - `/web/image/res.partner/${resPartnerId1}/avatar_128`, - 'should link to the partner avatar' - ); -}); - -QUnit.test('chat - sorting: should be sorted by last activity time', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { - channel_member_ids: [[0, 0, { - last_interest_dt: datetime_to_str(new Date(2021, 0, 1)), - partner_id: pyEnv.currentPartnerId, - }]], - channel_type: 'chat', - }, - { - channel_member_ids: [[0, 0, { - last_interest_dt: datetime_to_str(new Date(2021, 0, 2)), - partner_id: pyEnv.currentPartnerId, - }]], - channel_type: 'chat', - }, - ]); - const { click, openDiscuss } = await start(); - await openDiscuss(); - - const initialChats = document.querySelectorAll('.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item'); - assert.strictEqual( - initialChats.length, - 2, - "should have 2 livechat items" - ); - assert.strictEqual( - Number(initialChats[0].dataset.channelId), - mailChannelId2, - "first livechat should be the one with the more recent last activity time" - ); - assert.strictEqual( - Number(initialChats[1].dataset.channelId), - mailChannelId1, - "second chat should be the one with the less recent last activity time" - ); - - // post a new message on the last channel - await afterNextRender(() => initialChats[1].click()); - await afterNextRender(() => document.execCommand('insertText', false, "Blabla")); - await click('.o_ComposerView_buttonSend'); - const newChats = document.querySelectorAll('.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item'); - assert.strictEqual( - newChats.length, - 2, - "should have 2 chat items" - ); - assert.strictEqual( - Number(newChats[0].dataset.channelId), - mailChannelId1, - "first chat should be the one with the more recent last activity time" - ); - assert.strictEqual( - Number(newChats[1].dataset.channelId), - mailChannelId2, - "second chat should be the one with the less recent last activity time" - ); -}); - -}); + assert.strictEqual( + chatItem.querySelector(`:scope .o_DiscussSidebarCategoryItem_image`).dataset.src, + `/web/image/res.partner/${resPartnerId1}/avatar_128`, + "should link to the partner avatar" + ); + }); + + QUnit.test( + "chat - sorting: should be sorted by last activity time", + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + last_interest_dt: datetime_to_str(new Date(2021, 0, 1)), + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + last_interest_dt: datetime_to_str(new Date(2021, 0, 2)), + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }, + ]); + const { click, openDiscuss } = await start(); + await openDiscuss(); + + const initialChats = document.querySelectorAll( + ".o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item" + ); + assert.strictEqual(initialChats.length, 2, "should have 2 livechat items"); + assert.strictEqual( + Number(initialChats[0].dataset.channelId), + mailChannelId2, + "first livechat should be the one with the more recent last activity time" + ); + assert.strictEqual( + Number(initialChats[1].dataset.channelId), + mailChannelId1, + "second chat should be the one with the less recent last activity time" + ); + + // post a new message on the last channel + await afterNextRender(() => initialChats[1].click()); + await afterNextRender(() => document.execCommand("insertText", false, "Blabla")); + await click(".o_ComposerView_buttonSend"); + const newChats = document.querySelectorAll( + ".o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item" + ); + assert.strictEqual(newChats.length, 2, "should have 2 chat items"); + assert.strictEqual( + Number(newChats[0].dataset.channelId), + mailChannelId1, + "first chat should be the one with the more recent last activity time" + ); + assert.strictEqual( + Number(newChats[1].dataset.channelId), + mailChannelId2, + "second chat should be the one with the less recent last activity time" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js index 101bba00f60..51bbbc224ac 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_category_tests.js @@ -1,740 +1,865 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_sidebar_category_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_sidebar_category_tests.js"); -QUnit.test('channel - counter: should not have a counter if the category is unfolded and without needaction messages', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - counter: should not have a counter if the category is unfolded and without needaction messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is unfolded and without unread messages" - ); -}); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is unfolded and without unread messages" + ); + } + ); -QUnit.test('channel - counter: should not have a counter if the category is unfolded and with needaction messagens', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - counter: should not have a counter if the category is unfolded and with needaction messagens", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }]); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - body: "message 1", - model: "mail.channel", - res_id: mailChannelId1, - }, - { - body: "message_2", - model: "mail.channel", - res_id: mailChannelId2, - }, - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - { - mail_message_id: mailMessageId2, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - ]); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is unfolded and with needaction messages", - ); -}); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + ]); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + body: "message 1", + model: "mail.channel", + res_id: mailChannelId1, + }, + { + body: "message_2", + model: "mail.channel", + res_id: mailChannelId2, + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + { + mail_message_id: mailMessageId2, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + ]); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is unfolded and with needaction messages" + ); + } + ); -QUnit.test('channel - counter: should not have a counter if category is folded and without needaction messages', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - counter: should not have a counter if category is folded and without needaction messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); - const { openDiscuss } = await start(); - await openDiscuss(); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is folded and without unread messages" - ); -}); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is folded and without unread messages" + ); + } + ); -QUnit.test('channel - counter: should have correct value of needaction threads if category is folded and with needaction messages', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - counter: should have correct value of needaction threads if category is folded and with needaction messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'mailChannel1' }, { name: 'mailChannel2' }]); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - body: "message 1", - model: "mail.channel", - res_id: mailChannelId1, - }, - { - body: "message_2", - model: "mail.channel", - res_id: mailChannelId2, - }, - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - { - mail_message_id: mailMessageId2, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - ]); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + ]); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + body: "message 1", + model: "mail.channel", + res_id: mailChannelId1, + }, + { + body: "message_2", + model: "mail.channel", + res_id: mailChannelId2, + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + { + mail_message_id: mailMessageId2, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + ]); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelector(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter`).textContent, - "2", - "should have correct value of needaction threads if category is folded and with needaction messages" - ); -}); + assert.strictEqual( + document.querySelector( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_counter` + ).textContent, + "2", + "should have correct value of needaction threads if category is folded and with needaction messages" + ); + } + ); -QUnit.test('channel - command: should have view command when category is unfolded', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - command: should have view command when category is unfolded", + async function (assert) { + assert.expect(1); - const { openDiscuss } = await start(); - await openDiscuss(); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandView`).length, - 1, - "should have view command when channel category is open" - ); -}); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandView` + ).length, + 1, + "should have view command when channel category is open" + ); + } + ); -QUnit.test('channel - command: should have view command when category is folded', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - command: should have view command when category is folded", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandView`).length, - 1, - "should have view command when channel category is closed" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandView` + ).length, + 1, + "should have view command when channel category is closed" + ); + } + ); -QUnit.test('channel - command: should have add command when category is unfolded', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - command: should have add command when category is unfolded", + async function (assert) { + assert.expect(1); - const { openDiscuss } = await start(); - await openDiscuss(); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length, - 1, - "should have add command when channel category is open" - ); -}); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd` + ).length, + 1, + "should have add command when channel category is open" + ); + } + ); -QUnit.test('channel - command: should not have add command when category is folded', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - command: should not have add command when category is folded", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length, - 0, - "should not have add command when channel category is closed" - ); -}); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd` + ).length, + 0, + "should not have add command when channel category is closed" + ); + } + ); -QUnit.test('channel - states: close manually by clicking the title', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - states: close manually by clicking the title", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: true, - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: true, + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category channel should be closed and the content should be invisible" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category channel should be closed and the content should be invisible" + ); + } + ); -QUnit.test('channel - states: open manually by clicking the title', async function (assert) { - assert.expect(1); + QUnit.test( + "channel - states: open manually by clicking the title", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category channel should be open and the content should be visible" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category channel should be open and the content should be visible" + ); + } + ); -QUnit.test('channel - states: close should update the value on the server', async function (assert) { - assert.expect(2); + QUnit.test( + "channel - states: close should update the value on the server", + async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: true, - }); - const currentUserId = pyEnv.currentUserId; - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: true, + }); + const currentUserId = pyEnv.currentUserId; + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); - const initalSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - initalSettings.is_discuss_sidebar_category_channel_open, - true, - "the server side value should be true" - ); + const initalSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + initalSettings.is_discuss_sidebar_category_channel_open, + true, + "the server side value should be true" + ); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - const newSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - newSettings.is_discuss_sidebar_category_channel_open, - false, - "the server side value should be false" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + const newSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + newSettings.is_discuss_sidebar_category_channel_open, + false, + "the server side value should be false" + ); + } + ); -QUnit.test('channel - states: open should update the value on the server', async function (assert) { - assert.expect(2); + QUnit.test( + "channel - states: open should update the value on the server", + async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const currentUserId = pyEnv.currentUserId; - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const currentUserId = pyEnv.currentUserId; + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); - const initalSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - initalSettings.is_discuss_sidebar_category_channel_open, - false, - "the server side value should be false" - ); + const initalSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + initalSettings.is_discuss_sidebar_category_channel_open, + false, + "the server side value should be false" + ); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - const newSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - newSettings.is_discuss_sidebar_category_channel_open, - true, - "the server side value should be false" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + const newSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + newSettings.is_discuss_sidebar_category_channel_open, + true, + "the server side value should be false" + ); + } + ); -QUnit.test('channel - states: close from the bus', async function (assert) { - assert.expect(1); + QUnit.test("channel - states: close from the bus", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resUsersSettingsId1 = pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: true, - }); - const { openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resUsersSettingsId1 = pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: true, + }); + const { openDiscuss } = await start(); + await openDiscuss(); - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.record/insert', { - 'res.users.settings': { - id: resUsersSettingsId1, - 'is_discuss_sidebar_category_channel_open': false, - }, + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: resUsersSettingsId1, + is_discuss_sidebar_category_channel_open: false, + }, + }); + }); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category channel should be closed and the content should be invisible" + ); }); - }); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category channel should be closed and the content should be invisible" - ); -}); -QUnit.test('channel - states: open from the bus', async function (assert) { - assert.expect(1); + QUnit.test("channel - states: open from the bus", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resUsersSettingsId1 = pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_channel_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resUsersSettingsId1 = pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_channel_open: false, + }); + const { openDiscuss } = await start(); + await openDiscuss(); - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.record/insert', { - 'res.users.settings': { - id: resUsersSettingsId1, - 'is_discuss_sidebar_category_channel_open': true, - }, + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: resUsersSettingsId1, + is_discuss_sidebar_category_channel_open: true, + }, + }); + }); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category channel should be open and the content should be visible" + ); }); - }); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category channel should be open and the content should be visible" - ); -}); -QUnit.test('channel - states: the active category item should be visible even if the category is closed', async function (assert) { - assert.expect(4); + QUnit.test( + "channel - states: the active category item should be visible even if the category is closed", + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` - ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` + ); - const channel = document.querySelector(`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`); - await afterNextRender(() => { - channel.click(); - }); - assert.ok(channel.classList.contains('o-active')); + const channel = document.querySelector( + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` + ); + await afterNextRender(() => { + channel.click(); + }); + assert.ok(channel.classList.contains("o-active")); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title`); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - 'the active channel item should remain even if the category is folded' - ); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_title` + ); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "the active channel item should remain even if the category is folded" + ); - await click(`.o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"]`); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "inactive item should be invisible if the category is folded" - ); -}); + await click( + `.o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"]` + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "inactive item should be invisible if the category is folded" + ); + } + ); -QUnit.test('chat - counter: should not have a counter if the category is unfolded and without unread messages', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - counter: should not have a counter if the category is unfolded and without unread messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 0, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 0, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is unfolded and without unread messages", - ); -}); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is unfolded and without unread messages" + ); + } + ); -QUnit.test('chat - counter: should not have a counter if the category is unfolded and with unread messagens', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - counter: should not have a counter if the category is unfolded and with unread messagens", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 10, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is unfolded and with unread messages", - ); -}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 10, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is unfolded and with unread messages" + ); + } + ); -QUnit.test('chat - counter: should not have a counter if category is folded and without unread messages', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - counter: should not have a counter if category is folded and without unread messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 0, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter`).length, - 0, - "should not have a counter if the category is folded and without unread messages" - ); -}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 0, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter` + ).length, + 0, + "should not have a counter if the category is folded and without unread messages" + ); + } + ); -QUnit.test('chat - counter: should have correct value of unread threads if category is folded and with unread messages', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - counter: should have correct value of unread threads if category is folded and with unread messages", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { - message_unread_counter: 10, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }, - { - channel_member_ids: [ - [0, 0, { - message_unread_counter: 20, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }, - ]); - const { click, openDiscuss } = await start(); - await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - assert.strictEqual( - document.querySelector(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter`).textContent, - "2", - "should have correct value of unread threads if category is folded and with unread messages" - ); -}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 10, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 20, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }, + ]); + const { click, openDiscuss } = await start(); + await openDiscuss(); + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + assert.strictEqual( + document.querySelector( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_counter` + ).textContent, + "2", + "should have correct value of unread threads if category is folded and with unread messages" + ); + } + ); -QUnit.test('chat - command: should have add command when category is unfolded', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - command: should have add command when category is unfolded", + async function (assert) { + assert.expect(1); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length, - 1, - "should have add command when chat category is open" - ); -}); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd` + ).length, + 1, + "should have add command when chat category is open" + ); + } + ); -QUnit.test('chat - command: should not have add command when category is folded', async function (assert) { - assert.expect(1); + QUnit.test( + "chat - command: should not have add command when category is folded", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); + const pyEnv = await startServer(); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: false, + }); + const { openDiscuss } = await start(); + await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd`).length, - 0, - "should not have add command when chat category is closed" - ); -}); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_commandAdd` + ).length, + 0, + "should not have add command when chat category is closed" + ); + } + ); -QUnit.test('chat - states: close manually by clicking the title', async function (assert) { - assert.expect(1); + QUnit.test("chat - states: close manually by clicking the title", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'chat', - }); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: true, - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category chat should be closed and the content should be invisible" - ); -}); - -QUnit.test('chat - states: open manually by clicking the title', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'chat', - }); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: false, - }); - const { click, openDiscuss } = await start(); - await openDiscuss(); - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category chat should be open and the content should be visible" - ); -}); - -QUnit.test('chat - states: close should call update server data', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: true, - }); - const currentUserId = pyEnv.currentUserId; - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - - const initalSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - initalSettings.is_discuss_sidebar_category_chat_open, - true, - "the value in server side should be true" - ); - - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - const newSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[currentUserId]], - }); - assert.strictEqual( - newSettings.is_discuss_sidebar_category_chat_open, - false, - "the value in server side should be false" - ); -}); - -QUnit.test('chat - states: open should call update server data', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: false, - }); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - - const initalSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[pyEnv.currentUserId]], - }); - assert.strictEqual( - initalSettings.is_discuss_sidebar_category_chat_open, - false, - "the value in server side should be false" - ); - - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - const newSettings = await messaging.rpc({ - model: 'res.users.settings', - method: '_find_or_create_for_user', - args: [[pyEnv.currentUserId]], - }); - assert.strictEqual( - newSettings.is_discuss_sidebar_category_chat_open, - true, - "the value in server side should be true" - ); -}); - -QUnit.test('chat - states: close from the bus', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'chat', - }); - const resUsersSettingsId1 = pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: true, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.record/insert', { - 'res.users.settings': { - id: resUsersSettingsId1, - 'is_discuss_sidebar_category_chat_open': false, - }, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "chat", + }); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: true, + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category chat should be closed and the content should be invisible" + ); }); - }); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category chat should be close and the content should be invisible" - ); -}); -QUnit.test('chat - states: open from the bus', async function (assert) { - assert.expect(1); + QUnit.test("chat - states: open manually by clicking the title", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'chat', - }); - const resUsersSettingsId1 = pyEnv['res.users.settings'].create({ - user_id: pyEnv.currentUserId, - is_discuss_sidebar_category_chat_open: false, - }); - const { openDiscuss } = await start(); - await openDiscuss(); - - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.record/insert', { - 'res.users.settings': { - id: resUsersSettingsId1, - 'is_discuss_sidebar_category_chat_open': true, - }, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "chat", + }); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: false, + }); + const { click, openDiscuss } = await start(); + await openDiscuss(); + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category chat should be open and the content should be visible" + ); }); + + QUnit.test("chat - states: close should call update server data", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: true, + }); + const currentUserId = pyEnv.currentUserId; + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + + const initalSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + initalSettings.is_discuss_sidebar_category_chat_open, + true, + "the value in server side should be true" + ); + + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + const newSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[currentUserId]], + }); + assert.strictEqual( + newSettings.is_discuss_sidebar_category_chat_open, + false, + "the value in server side should be false" + ); + }); + + QUnit.test("chat - states: open should call update server data", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: false, + }); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + + const initalSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[pyEnv.currentUserId]], + }); + assert.strictEqual( + initalSettings.is_discuss_sidebar_category_chat_open, + false, + "the value in server side should be false" + ); + + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + const newSettings = await messaging.rpc({ + model: "res.users.settings", + method: "_find_or_create_for_user", + args: [[pyEnv.currentUserId]], + }); + assert.strictEqual( + newSettings.is_discuss_sidebar_category_chat_open, + true, + "the value in server side should be true" + ); + }); + + QUnit.test("chat - states: close from the bus", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "chat", + }); + const resUsersSettingsId1 = pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: true, + }); + const { openDiscuss } = await start(); + await openDiscuss(); + + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: resUsersSettingsId1, + is_discuss_sidebar_category_chat_open: false, + }, + }); + }); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category chat should be close and the content should be invisible" + ); + }); + + QUnit.test("chat - states: open from the bus", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "chat", + }); + const resUsersSettingsId1 = pyEnv["res.users.settings"].create({ + user_id: pyEnv.currentUserId, + is_discuss_sidebar_category_chat_open: false, + }); + const { openDiscuss } = await start(); + await openDiscuss(); + + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { + "res.users.settings": { + id: resUsersSettingsId1, + is_discuss_sidebar_category_chat_open: true, + }, + }); + }); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "Category chat should be open and the content should be visible" + ); + }); + + QUnit.test( + "chat - states: the active category item should be visible even if the category is closed", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "chat", + }); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` + ); + + const chat = document.querySelector( + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` + ); + await afterNextRender(() => { + chat.click(); + }); + assert.ok(chat.classList.contains("o-active")); + + await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "the active chat item should remain even if the category is folded" + ); + + await click( + `.o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"]` + ); + assert.containsNone( + document.body, + `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, + "inactive item should be invisible if the category is folded" + ); + } + ); }); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "Category chat should be open and the content should be visible" - ); -}); - -QUnit.test('chat - states: the active category item should be visible even if the category is closed', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'chat', - }); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]` - ); - - const chat = document.querySelector(`.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`); - await afterNextRender(() => { - chat.click(); - }); - assert.ok(chat.classList.contains('o-active')); - - await click(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - 'the active chat item should remain even if the category is folded' - ); - - await click(`.o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"]`); - assert.containsNone( - document.body, - `.o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"]`, - "inactive item should be invisible if the category is folded" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js index 103a61feec4..d5302ff5558 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_sidebar_tests.js @@ -1,133 +1,143 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; -import { - nextAnimationFrame, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { makeDeferred } from "@mail/utils/deferred"; +import { nextAnimationFrame, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_sidebar_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_sidebar_tests.js"); -QUnit.test('sidebar find shows channels matching search term', async function (assert) { - assert.expect(3); + QUnit.test("sidebar find shows channels matching search term", async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [], - channel_type: 'channel', - group_public_id: false, - name: 'test', - }); - const searchReadDef = makeDeferred(); - const { click, openDiscuss } = await start({ - async mockRPC(route, args) { - if (args.method === 'search_read') { - searchReadDef.resolve(); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [], + channel_type: "channel", + group_public_id: false, + name: "test", + }); + const searchReadDef = makeDeferred(); + const { click, openDiscuss } = await start({ + async mockRPC(route, args) { + if (args.method === "search_read") { + searchReadDef.resolve(); + } + }, + }); + await openDiscuss(); + await click(`.o_DiscussSidebarCategory_commandAdd`); + document.querySelector(`.o_DiscussSidebarCategory_addingItem`).focus(); + document.execCommand("insertText", false, "test"); + document + .querySelector(`.o_DiscussSidebarCategory_addingItem`) + .dispatchEvent(new window.KeyboardEvent("keydown")); + document + .querySelector(`.o_DiscussSidebarCategory_addingItem`) + .dispatchEvent(new window.KeyboardEvent("keyup")); + + await searchReadDef; + await nextAnimationFrame(); // ensures search_read rpc is rendered. + const results = document.querySelectorAll(".ui-autocomplete .ui-menu-item a"); + assert.ok( + results, + "should have autocomplete suggestion after typing on 'find or create channel' input" + ); + assert.strictEqual( + results.length, + // When searching for a single existing channel, the results list will have at least 2 lines: + // One for the existing channel itself + // One for creating a channel with the search term + 2 + ); + assert.strictEqual( + results[0].textContent, + "test", + "autocomplete suggestion should target the channel matching search term" + ); + }); + + QUnit.test( + "sidebar find shows channels matching search term even when user is member", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [[0, 0, { partner_id: pyEnv.currentPartnerId }]], + channel_type: "channel", + group_public_id: false, + name: "test", + }); + const searchReadDef = makeDeferred(); + const { click, openDiscuss } = await start({ + async mockRPC(route, args) { + if (args.method === "search_read") { + searchReadDef.resolve(); + } + }, + }); + await openDiscuss(); + await click(`.o_DiscussSidebarCategory_commandAdd`); + document.querySelector(`.o_DiscussSidebarCategory_addingItem`).focus(); + document.execCommand("insertText", false, "test"); + document + .querySelector(`.o_DiscussSidebarCategory_addingItem`) + .dispatchEvent(new window.KeyboardEvent("keydown")); + document + .querySelector(`.o_DiscussSidebarCategory_addingItem`) + .dispatchEvent(new window.KeyboardEvent("keyup")); + + await searchReadDef; + await nextAnimationFrame(); + const results = document.querySelectorAll(".ui-autocomplete .ui-menu-item a"); + assert.ok( + results, + "should have autocomplete suggestion after typing on 'find or create channel' input" + ); + assert.strictEqual( + results.length, + // When searching for a single existing channel, the results list will have at least 2 lines: + // One for the existing channel itself + // One for creating a channel with the search term + 2 + ); + assert.strictEqual( + results[0].textContent, + "test", + "autocomplete suggestion should target the channel matching search term even if user is member" + ); } - }, - }); - await openDiscuss(); - await click(`.o_DiscussSidebarCategory_commandAdd`); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`).focus(); - document.execCommand('insertText', false, "test"); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`) - .dispatchEvent(new window.KeyboardEvent('keydown')); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`) - .dispatchEvent(new window.KeyboardEvent('keyup')); + ); - await searchReadDef; - await nextAnimationFrame(); // ensures search_read rpc is rendered. - const results = document.querySelectorAll('.ui-autocomplete .ui-menu-item a'); - assert.ok( - results, - "should have autocomplete suggestion after typing on 'find or create channel' input" - ); - assert.strictEqual( - results.length, - // When searching for a single existing channel, the results list will have at least 2 lines: - // One for the existing channel itself - // One for creating a channel with the search term - 2 - ); - assert.strictEqual( - results[0].textContent, - "test", - "autocomplete suggestion should target the channel matching search term" - ); -}); + QUnit.test( + "sidebar channels should be ordered case insensitive alphabetically", + async function (assert) { + assert.expect(1); -QUnit.test('sidebar find shows channels matching search term even when user is member', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - ], - channel_type: 'channel', - group_public_id: false, - name: 'test', - }); - const searchReadDef = makeDeferred(); - const { click, openDiscuss } = await start({ - async mockRPC(route, args) { - if (args.method === 'search_read') { - searchReadDef.resolve(); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create([ + { name: "Xyz" }, + { name: "abc" }, + { name: "Abc" }, + { name: "Xyz" }, + ]); + const { openDiscuss } = await start(); + await openDiscuss(); + const results = document.querySelectorAll( + ".o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategoryItem_name" + ); + assert.deepEqual( + [ + results[0].textContent, + results[1].textContent, + results[2].textContent, + results[3].textContent, + ], + ["abc", "Abc", "Xyz", "Xyz"], + "Channel name should be in case insensitive alphabetical order" + ); } - }, + ); }); - await openDiscuss(); - await click(`.o_DiscussSidebarCategory_commandAdd`); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`).focus(); - document.execCommand('insertText', false, "test"); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`) - .dispatchEvent(new window.KeyboardEvent('keydown')); - document.querySelector(`.o_DiscussSidebarCategory_addingItem`) - .dispatchEvent(new window.KeyboardEvent('keyup')); - - await searchReadDef; - await nextAnimationFrame(); - const results = document.querySelectorAll('.ui-autocomplete .ui-menu-item a'); - assert.ok( - results, - "should have autocomplete suggestion after typing on 'find or create channel' input" - ); - assert.strictEqual( - results.length, - // When searching for a single existing channel, the results list will have at least 2 lines: - // One for the existing channel itself - // One for creating a channel with the search term - 2 - ); - assert.strictEqual( - results[0].textContent, - "test", - "autocomplete suggestion should target the channel matching search term even if user is member" - ); -}); - -QUnit.test('sidebar channels should be ordered case insensitive alphabetically', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create([ - { name: "Xyz" }, - { name: "abc" }, - { name: "Abc" }, - { name: "Xyz" }, - ]); - const { openDiscuss } = await start(); - await openDiscuss(); - const results = document.querySelectorAll('.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategoryItem_name'); - assert.deepEqual( - [results[0].textContent, results[1].textContent, results[2].textContent, results[3].textContent], - ["abc", "Abc", "Xyz", "Xyz"], - "Channel name should be in case insensitive alphabetical order" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js index 8ea07247586..1e5d837ed71 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/discuss_tests.js @@ -6,3804 +6,4008 @@ import { nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; -import { destroy } from '@web/../tests/helpers/utils'; +import { destroy } from "@web/../tests/helpers/utils"; -import { makeTestPromise, file } from 'web.test_utils'; +import { makeTestPromise, file } from "web.test_utils"; -import { makeFakePresenceService } from '@bus/../tests/helpers/mock_services'; +import { makeFakePresenceService } from "@bus/../tests/helpers/mock_services"; const { createFile, inputFiles } = file; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('discuss_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("discuss_tests.js"); -QUnit.test('messaging not created', async function (assert) { - assert.expect(1); + QUnit.test("messaging not created", async function (assert) { + assert.expect(1); - const messagingBeforeCreationDeferred = makeTestPromise(); - const { openDiscuss } = await start({ - messagingBeforeCreationDeferred, - waitUntilMessagingCondition: 'none' - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.containsOnce(document.body, '.o_DiscussContainer_spinner', "should display messaging not initialized"); - messagingBeforeCreationDeferred.resolve(); -}); + const messagingBeforeCreationDeferred = makeTestPromise(); + const { openDiscuss } = await start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: "none", + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.containsOnce( + document.body, + ".o_DiscussContainer_spinner", + "should display messaging not initialized" + ); + messagingBeforeCreationDeferred.resolve(); + }); -QUnit.test('discuss should be marked as opened if the component is already rendered and messaging becomes created afterwards', async function (assert) { - assert.expect(1); + QUnit.test( + "discuss should be marked as opened if the component is already rendered and messaging becomes created afterwards", + async function (assert) { + assert.expect(1); - const messagingBeforeCreationDeferred = makeTestPromise(); - const { env, openDiscuss } = await start({ - messagingBeforeCreationDeferred, - waitUntilMessagingCondition: 'none', - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); + const messagingBeforeCreationDeferred = makeTestPromise(); + const { env, openDiscuss } = await start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: "none", + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); - await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); - const { messaging } = env.services.messaging.modelManager; - assert.ok( - messaging.discuss.discussView, - "discuss should be marked as opened if the component is already rendered and messaging becomes created afterwards" - ); -}); - -QUnit.test('discuss should be marked as closed when the component is unmounted', async function (assert) { - assert.expect(1); - - const { messaging, openDiscuss, webClient } = await start(); - await openDiscuss(); - - await afterNextRender(() => destroy(webClient)); - assert.notOk( - messaging.discuss.discussView, - "discuss should be marked as closed when the component is unmounted" - ); -}); - -QUnit.test('messaging not initialized', async function (assert) { - assert.expect(1); - - const messaginginitializedDeferred = makeTestPromise(); - const { openDiscuss } = await start({ - async mockRPC(route) { - if (route === '/mail/init_messaging') { - await messaginginitializedDeferred; // simulate messaging never initialized + await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); + const { messaging } = env.services.messaging.modelManager; + assert.ok( + messaging.discuss.discussView, + "discuss should be marked as opened if the component is already rendered and messaging becomes created afterwards" + ); } - }, - waitUntilMessagingCondition: 'created', - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.strictEqual( - document.querySelectorAll('.o_DiscussContainer_spinner').length, - 1, - "should display messaging not initialized" - ); - messaginginitializedDeferred.resolve(); // ensure proper teardown -}); + ); -QUnit.test('messaging becomes initialized', async function (assert) { - assert.expect(2); + QUnit.test( + "discuss should be marked as closed when the component is unmounted", + async function (assert) { + assert.expect(1); - const messagingInitializedProm = makeTestPromise(); + const { messaging, openDiscuss, webClient } = await start(); + await openDiscuss(); - const { openDiscuss } = await start({ - async mockRPC(route) { - if (route === '/mail/init_messaging') { - await messagingInitializedProm; + await afterNextRender(() => destroy(webClient)); + assert.notOk( + messaging.discuss.discussView, + "discuss should be marked as closed when the component is unmounted" + ); } - }, - waitUntilMessagingCondition: 'created', - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.strictEqual( - document.querySelectorAll('.o_DiscussContainer_spinner').length, - 1, - "should display messaging not initialized" - ); + ); - await afterNextRender(() => messagingInitializedProm.resolve()); - assert.strictEqual( - document.querySelectorAll('.o_DiscussContainer_spinner').length, - 0, - "should no longer display messaging not initialized" - ); -}); + QUnit.test("messaging not initialized", async function (assert) { + assert.expect(1); -QUnit.test('basic rendering', async function (assert) { - assert.expect(4); + const messaginginitializedDeferred = makeTestPromise(); + const { openDiscuss } = await start({ + async mockRPC(route) { + if (route === "/mail/init_messaging") { + await messaginginitializedDeferred; // simulate messaging never initialized + } + }, + waitUntilMessagingCondition: "created", + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.strictEqual( + document.querySelectorAll(".o_DiscussContainer_spinner").length, + 1, + "should display messaging not initialized" + ); + messaginginitializedDeferred.resolve(); // ensure proper teardown + }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll('.o_DiscussView_sidebar').length, - 1, - "should have a sidebar section" - ); - assert.strictEqual( - document.querySelectorAll('.o_DiscussView_content').length, - 1, - "should have content section" - ); - assert.strictEqual( - document.querySelectorAll('.o_DiscussView_thread').length, - 1, - "should have thread section inside content" - ); - assert.ok( - document.querySelector('.o_DiscussView_thread').classList.contains('o_ThreadView'), - "thread section should use ThreadView component" - ); -}); + QUnit.test("messaging becomes initialized", async function (assert) { + assert.expect(2); -QUnit.test('basic rendering: sidebar', async function (assert) { - assert.expect(18); + const messagingInitializedProm = makeTestPromise(); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_category`).length, - 3, - "should have 3 groups in sidebar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryMailbox`).length, - 1, - "should have group 'Mailbox' in sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` + const { openDiscuss } = await start({ + async mockRPC(route) { + if (route === "/mail/init_messaging") { + await messagingInitializedProm; + } + }, + waitUntilMessagingCondition: "created", + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.strictEqual( + document.querySelectorAll(".o_DiscussContainer_spinner").length, + 1, + "should display messaging not initialized" + ); + + await afterNextRender(() => messagingInitializedProm.resolve()); + assert.strictEqual( + document.querySelectorAll(".o_DiscussContainer_spinner").length, + 0, + "should no longer display messaging not initialized" + ); + }); + + QUnit.test("basic rendering", async function (assert) { + assert.expect(4); + + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(".o_DiscussView_sidebar").length, + 1, + "should have a sidebar section" + ); + assert.strictEqual( + document.querySelectorAll(".o_DiscussView_content").length, + 1, + "should have content section" + ); + assert.strictEqual( + document.querySelectorAll(".o_DiscussView_thread").length, + 1, + "should have thread section inside content" + ); + assert.ok( + document.querySelector(".o_DiscussView_thread").classList.contains("o_ThreadView"), + "thread section should use ThreadView component" + ); + }); + + QUnit.test("basic rendering: sidebar", async function (assert) { + assert.expect(18); + + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebarView_category`).length, + 3, + "should have 3 groups in sidebar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebarView_categoryMailbox`).length, + 1, + "should have group 'Mailbox' in sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryMailbox .o_DiscussSidebarMailboxView `).length, - 3, - "should have 3 mailbox items" - ); - assert.strictEqual( - document.querySelectorAll(` + 3, + "should have 3 mailbox items" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryMailbox - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `).length, - 1, - "should have inbox mailbox item" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have inbox mailbox item" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryMailbox - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] `).length, - 1, - "should have starred mailbox item" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have starred mailbox item" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryMailbox - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `).length, - 1, - "should have history mailbox item" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_sidebar .o_DiscussSidebarView_separator`).length, - 2, - "should have 2 separators (separating 'Start a meeting' button, mailboxes and channels, but that's not tested)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel`).length, - 1, - "should have group 'Channel' in sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have history mailbox item" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_sidebar .o_DiscussSidebarView_separator`) + .length, + 2, + "should have 2 separators (separating 'Start a meeting' button, mailboxes and channels, but that's not tested)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel`).length, + 1, + "should have group 'Channel' in sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header `).length, - 1, - "should have header in channel group" - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have header in channel group" + ); + assert.strictEqual( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_header .o_DiscussSidebarCategory_titleText - `).textContent.trim(), - "Channels" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_content`).length, - 1, - "channel category should list items" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 0, - "channel category should have no item by default" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat`).length, - 1, - "should have group 'Chat' in sidebar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header`).length, - 1, - "channel category should have a header" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title`).length, - 1, - "should have title in chat header" - ); - assert.strictEqual( - document.querySelector(` + ` + ) + .textContent.trim(), + "Channels" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_content` + ).length, + 1, + "channel category should list items" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 0, + "channel category should have no item by default" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussSidebarView_categoryChat`).length, + 1, + "should have group 'Chat' in sidebar" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_header` + ).length, + 1, + "channel category should have a header" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title` + ).length, + 1, + "should have title in chat header" + ); + assert.strictEqual( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_title .o_DiscussSidebarCategory_titleText - `).textContent.trim(), - "Direct Messages" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_content`).length, - 1, - "chat category should list items" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item`).length, - 0, - "chat category should have no item by default" - ); -}); + ` + ) + .textContent.trim(), + "Direct Messages" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_content` + ).length, + 1, + "chat category should list items" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item` + ).length, + 0, + "chat category should have no item by default" + ); + }); -QUnit.test('sidebar: basic mailbox rendering', async function (assert) { - assert.expect(5); + QUnit.test("sidebar: basic mailbox rendering", async function (assert) { + assert.expect(5); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - const inbox = document.querySelector(` + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + const inbox = document.querySelector(` .o_DiscussSidebarView_categoryMailbox - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `); - assert.strictEqual( - inbox.querySelectorAll(`:scope .o_ThreadIconView`).length, - 1, - "mailbox should have an icon" - ); - assert.strictEqual( - inbox.querySelectorAll(`:scope .o_ThreadIconView_mailboxInbox`).length, - 1, - "inbox should have 'inbox' icon" - ); - assert.strictEqual( - inbox.querySelectorAll(`:scope .o_DiscussSidebarMailboxView_name`).length, - 1, - "mailbox should have a name" - ); - assert.strictEqual( - inbox.querySelector(`:scope .o_DiscussSidebarMailboxView_name`).textContent, - "Inbox", - "inbox should have name 'Inbox'" - ); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + assert.strictEqual( + inbox.querySelectorAll(`:scope .o_ThreadIconView`).length, + 1, + "mailbox should have an icon" + ); + assert.strictEqual( + inbox.querySelectorAll(`:scope .o_ThreadIconView_mailboxInbox`).length, + 1, + "inbox should have 'inbox' icon" + ); + assert.strictEqual( + inbox.querySelectorAll(`:scope .o_DiscussSidebarMailboxView_name`).length, + 1, + "mailbox should have a name" + ); + assert.strictEqual( + inbox.querySelector(`:scope .o_DiscussSidebarMailboxView_name`).textContent, + "Inbox", + "inbox should have name 'Inbox'" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 0, - "should have no counter when equal to 0 (default value)" - ); -}); + 0, + "should have no counter when equal to 0 (default value)" + ); + }); -QUnit.test('sidebar: default active inbox', async function (assert) { - assert.expect(1); + QUnit.test("sidebar: default active inbox", async function (assert) { + assert.expect(1); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - const inbox = document.querySelector(` + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + const inbox = document.querySelector(` .o_DiscussSidebarView_categoryMailbox - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `); - assert.ok( - inbox.classList.contains('o-active'), - "inbox should be active by default" - ); -}); + assert.ok(inbox.classList.contains("o-active"), "inbox should be active by default"); + }); -QUnit.test('sidebar: change item', async function (assert) { - assert.expect(4); + QUnit.test("sidebar: change item", async function (assert) { + assert.expect(4); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox should be active by default" - ); - assert.notOk( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] - `).classList.contains('o-active'), - "starred should be inactive by default" - ); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox should be active by default" + ); + assert.notOk( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] + ` + ) + .classList.contains("o-active"), + "starred should be inactive by default" + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] `); - assert.notOk( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox mailbox should become inactive" - ); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] - `).classList.contains('o-active'), - "starred mailbox should become active"); -}); + assert.notOk( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox mailbox should become inactive" + ); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] + ` + ) + .classList.contains("o-active"), + "starred mailbox should become active" + ); + }); -QUnit.test('sidebar: inbox with counter', async function (assert) { - assert.expect(2); + QUnit.test("sidebar: inbox with counter", async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - pyEnv['mail.notification'].create({ notification_type: 'inbox', res_partner_id: pyEnv.currentPartnerId }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + const pyEnv = await startServer(); + pyEnv["mail.notification"].create({ + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 1, - "should display a counter (= have a counter when different from 0)" - ); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + 1, + "should display a counter (= have a counter when different from 0)" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - "1", - "should have counter value" - ); -}); + "1", + "should have counter value" + ); + }); -QUnit.test('sidebar: add channel', async function (assert) { - assert.expect(3); + QUnit.test("sidebar: add channel", async function (assert) { + assert.expect(3); - const { click, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` + const { click, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd `).length, - 1, - "should be able to add channel from header" - ); - assert.strictEqual( - document.querySelector(` + 1, + "should be able to add channel from header" + ); + assert.strictEqual( + document.querySelector(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd `).title, - "Add or join a channel"); + "Add or join a channel" + ); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd`); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_addingItemInput`).length, - 1, - "should have item to add a new channel" - ); -}); + await click( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd` + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_addingItemInput` + ).length, + 1, + "should have item to add a new channel" + ); + }); -QUnit.test('sidebar: basic channel rendering', async function (assert) { - assert.expect(12); + QUnit.test("sidebar: basic channel rendering", async function (assert) { + assert.expect(12); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" }); - const { click, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 1, - "should have one channel item"); - let channel = document.querySelector(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "General" }); + const { click, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 1, + "should have one channel item" + ); + let channel = document.querySelector(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `); - assert.ok( - channel, - 'channel 1 should be in sidebar' - ); - assert.notOk( - channel.classList.contains('o-active'), - "should not be active by default" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_name`).length, - 1, - "should have a name" - ); - assert.strictEqual( - channel.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, - "General", - "should have name value" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commands`).length, - 1, - "should have commands" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, - 2, - "should have 2 commands" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandSettings`).length, - 1, - "should have 'settings' command" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandLeave`).length, - 1, - "should have 'leave' command" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, - 0, - "should have a counter when equals 0 (default value)" - ); + assert.ok(channel, "channel 1 should be in sidebar"); + assert.notOk(channel.classList.contains("o-active"), "should not be active by default"); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_name`).length, + 1, + "should have a name" + ); + assert.strictEqual( + channel.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, + "General", + "should have name value" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commands`).length, + 1, + "should have commands" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, + 2, + "should have 2 commands" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandSettings`) + .length, + 1, + "should have 'settings' command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandLeave`) + .length, + 1, + "should have 'leave' command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, + 0, + "should have a counter when equals 0 (default value)" + ); - await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`); - channel = document.querySelector(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`); - assert.hasClass( - channel, - 'o-active', - "channel should become active" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_ThreadView_composer`).length, - 1, - "should have composer section inside thread content (can post message in channel)" - ); -}); + await click(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`); + channel = document.querySelector( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ); + assert.hasClass(channel, "o-active", "channel should become active"); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_ThreadView_composer`).length, + 1, + "should have composer section inside thread content (can post message in channel)" + ); + }); -QUnit.test('sidebar: channel rendering with needaction counter', async function (assert) { - assert.expect(5); + QUnit.test("sidebar: channel rendering with needaction counter", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1 - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }); - const { openDiscuss } = await start(); - await openDiscuss(); - const channel = document.querySelector(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, - 1, - "should have a counter when different from 0" - ); - assert.strictEqual( - channel.querySelector(`:scope .o_DiscussSidebarCategoryItem_counter`).textContent, - "1", - "should have counter value" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, - 1, - "should have single command" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandSettings`).length, - 1, - "should have 'settings' command" - ); - assert.strictEqual( - channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandLeave`).length, - 0, - "should not have 'leave' command" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }); + const { openDiscuss } = await start(); + await openDiscuss(); + const channel = document.querySelector( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, + 1, + "should have a counter when different from 0" + ); + assert.strictEqual( + channel.querySelector(`:scope .o_DiscussSidebarCategoryItem_counter`).textContent, + "1", + "should have counter value" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, + 1, + "should have single command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandSettings`) + .length, + 1, + "should have 'settings' command" + ); + assert.strictEqual( + channel.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandLeave`) + .length, + 0, + "should not have 'leave' command" + ); + }); -QUnit.test('sidebar: public channel rendering', async function (assert) { - assert.expect(3); + QUnit.test("sidebar: public channel rendering", async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create([ - { name: "channel1", channel_type: 'channel', group_public_id: false}, - ]); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 1, - "should have 1 channel items" - ); - assert.strictEqual( - document.querySelectorAll(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create([ + { name: "channel1", channel_type: "channel", group_public_id: false }, + ]); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 1, + "should have 1 channel items" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have channel 1" - ); - const channel1 = document.querySelector(` + 1, + "should have channel 1" + ); + const channel1 = document.querySelector(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `); - assert.ok( - channel1.querySelectorAll(`:scope .o_ThreadIconView_publicChannel`).length, - "channel1 (public) should have globe icon" - ); -}); + assert.ok( + channel1.querySelectorAll(`:scope .o_ThreadIconView_publicChannel`).length, + "channel1 (public) should have globe icon" + ); + }); -QUnit.test('sidebar: basic chat rendering', async function (assert) { - assert.expect(9); + QUnit.test("sidebar: basic chat rendering", async function (assert) { + assert.expect(9); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', // testing a chat is the goal of the test - }); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item`).length, - 1, - "should have one chat item" - ); - const chat = document.querySelector(` + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", // testing a chat is the goal of the test + }); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item` + ).length, + 1, + "should have one chat item" + ); + const chat = document.querySelector(` .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `); - assert.ok(chat, "should have channel 1 in the sidebar"); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_ThreadIconView`).length, - 1, - "should have an icon" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_name`).length, - 1, - "should have a name" - ); - assert.strictEqual( - chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, - "Demo", - "should have correspondent name as name" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commands`).length, - 1, - "should have commands" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, - 1, - "should have 1 command" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandUnpin`).length, - 1, - "should have 'unpin' command" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, - 0, - "should have a counter when equals 0 (default value)" - ); -}); + assert.ok(chat, "should have channel 1 in the sidebar"); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_ThreadIconView`).length, + 1, + "should have an icon" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_name`).length, + 1, + "should have a name" + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, + "Demo", + "should have correspondent name as name" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commands`).length, + 1, + "should have commands" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, + 1, + "should have 1 command" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandUnpin`).length, + 1, + "should have 'unpin' command" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, + 0, + "should have a counter when equals 0 (default value)" + ); + }); -QUnit.test('sidebar: chat rendering with unread counter', async function (assert) { - assert.expect(4); + QUnit.test("sidebar: chat rendering with unread counter", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 100, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'chat', - }); - const { openDiscuss } = await start(); - await openDiscuss(); - const chat = document.querySelector(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item`); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, - 1, - "should have a counter when different from 0" - ); - assert.strictEqual( - chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_counter`).textContent, - "100", - "should have counter value" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, - 0, - "should have no command" - ); - assert.strictEqual( - chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandUnpin`).length, - 0, - "should not have 'unpin' command" - ); -}); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 100, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "chat", + }); + const { openDiscuss } = await start(); + await openDiscuss(); + const chat = document.querySelector( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item` + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_counter`).length, + 1, + "should have a counter when different from 0" + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_counter`).textContent, + "100", + "should have counter value" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_command`).length, + 0, + "should have no command" + ); + assert.strictEqual( + chat.querySelectorAll(`:scope .o_DiscussSidebarCategoryItem_commandUnpin`).length, + 0, + "should not have 'unpin' command" + ); + }); -QUnit.test('sidebar: chat im_status rendering', async function (assert) { - assert.expect(7); + QUnit.test("sidebar: chat im_status rendering", async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([ - { im_status: 'offline', name: "Partner1" }, - { im_status: 'online', name: "Partner2" }, - { im_status: 'away', name: "Partner3" }, - ]); - const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }, - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', - }, - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId3 }], - ], - channel_type: 'chat', - } - ]); - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item`).length, - 3, - "should have 3 chat items" - ); - assert.strictEqual( - document.querySelectorAll(` + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv["res.partner"].create([ + { im_status: "offline", name: "Partner1" }, + { im_status: "online", name: "Partner2" }, + { im_status: "away", name: "Partner3" }, + ]); + const [mailChannelId1, mailChannelId2, mailChannelId3] = pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }, + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", + }, + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId3 }], + ], + channel_type: "chat", + }, + ]); + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item` + ).length, + 3, + "should have 3 chat items" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have Partner 1" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have Partner 1" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] `).length, - 1, - "should have Partner 2" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have Partner 2" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId3}"] `).length, - 1, - "should have Partner 3" - ); - const chat1 = document.querySelector(` + 1, + "should have Partner 3" + ); + const chat1 = document.querySelector(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] `); - const chat2 = document.querySelector(` + const chat2 = document.querySelector(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] `); - const chat3 = document.querySelector(` + const chat3 = document.querySelector(` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId3}"] `); - assert.strictEqual( - chat1.querySelectorAll(`:scope .o_ThreadIconView_offline`).length, - 1, - "chat1 should have offline icon" - ); - assert.strictEqual( - chat2.querySelectorAll(`:scope .o_ThreadIconView_online`).length, - 1, - "chat2 should have online icon" - ); - assert.strictEqual( - chat3.querySelectorAll(`:scope .o_ThreadIconView_away`).length, - 1, - "chat3 should have away icon" - ); -}); + assert.strictEqual( + chat1.querySelectorAll(`:scope .o_ThreadIconView_offline`).length, + 1, + "chat1 should have offline icon" + ); + assert.strictEqual( + chat2.querySelectorAll(`:scope .o_ThreadIconView_online`).length, + 1, + "chat2 should have online icon" + ); + assert.strictEqual( + chat3.querySelectorAll(`:scope .o_ThreadIconView_away`).length, + 1, + "chat3 should have away icon" + ); + }); -QUnit.test('sidebar: chat custom name', async function (assert) { - assert.expect(1); + QUnit.test("sidebar: chat custom name", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Marc Demo" }); - pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - custom_channel_name: "Marc", - partner_id: pyEnv.currentPartnerId, - }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { openDiscuss } = await start(); - await openDiscuss(); - const chat = document.querySelector(`.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item`); - assert.strictEqual( - chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, - "Marc", - "chat should have custom name as name" - ); -}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Marc Demo" }); + pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + custom_channel_name: "Marc", + partner_id: pyEnv.currentPartnerId, + }, + ], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { openDiscuss } = await start(); + await openDiscuss(); + const chat = document.querySelector( + `.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item` + ); + assert.strictEqual( + chat.querySelector(`:scope .o_DiscussSidebarCategoryItem_name`).textContent, + "Marc", + "chat should have custom name as name" + ); + }); -QUnit.test('default thread rendering', async function (assert) { - assert.expect(16); + QUnit.test("default thread rendering", async function (assert) { + assert.expect(16); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `).length, - 1, - "should have inbox mailbox in the sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + 1, + "should have inbox mailbox in the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] `).length, - 1, - "should have starred mailbox in the sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + 1, + "should have starred mailbox in the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `).length, - 1, - "should have history mailbox in the sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have history mailbox in the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have channel 1 in the sidebar" - ); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox mailbox should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have channel 1 in the sidebar" + ); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty `).length, - 1, - "should have empty thread in inbox" - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have empty thread in inbox" + ); + assert.strictEqual( + document + .querySelector( + ` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty - `).textContent.trim(), - "Congratulations, your inbox is empty New messages appear here." - ); + ` + ) + .textContent.trim(), + "Congratulations, your inbox is empty New messages appear here." + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] `); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] - `).classList.contains('o-active'), - "starred mailbox should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(` + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] + ` + ) + .classList.contains("o-active"), + "starred mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty `).length, - 1, - "should have empty thread in starred" - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have empty thread in starred" + ); + assert.strictEqual( + document + .querySelector( + ` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty - `).textContent.trim(), - "No starred messages You can mark any message as 'starred', and it shows up in this mailbox." - ); + ` + ) + .textContent.trim(), + "No starred messages You can mark any message as 'starred', and it shows up in this mailbox." + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] - `).classList.contains('o-active'), - "history mailbox should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(` + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] + ` + ) + .classList.contains("o-active"), + "history mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty `).length, - 1, - "should have empty thread in starred" - ); - assert.strictEqual( - document.querySelector(`.o_DiscussView_thread .o_MessageListView_empty`).textContent.trim(), - "No history messages Messages marked as read will appear in the history." - ); + 1, + "should have empty thread in starred" + ); + assert.strictEqual( + document + .querySelector(`.o_DiscussView_thread .o_MessageListView_empty`) + .textContent.trim(), + "No history messages Messages marked as read will appear in the history." + ); - await click(` + await click(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `); - assert.ok( - document.querySelector(` + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - `).classList.contains('o-active'), - "channel 20 should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(` + ` + ) + .classList.contains("o-active"), + "channel 20 should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty `).length, - 1, - "should have empty thread in starred" - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have empty thread in starred" + ); + assert.strictEqual( + document + .querySelector( + ` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_empty - `).textContent.trim(), - "There are no messages in this conversation." - ); -}); + ` + ) + .textContent.trim(), + "There are no messages in this conversation." + ); + }); -QUnit.test('initially load messages from inbox', async function (assert) { - assert.expect(3); + QUnit.test("initially load messages from inbox", async function (assert) { + assert.expect(3); - const { openDiscuss } = await start({ - async mockRPC(route, args) { - if (route === '/mail/inbox/messages') { - assert.step('/mail/channel/messages'); - assert.strictEqual( - args.limit, - 30, - "should fetch up to 30 messages" - ); - } - }, - }); - await openDiscuss(); - assert.verifySteps(['/mail/channel/messages']); -}); + const { openDiscuss } = await start({ + async mockRPC(route, args) { + if (route === "/mail/inbox/messages") { + assert.step("/mail/channel/messages"); + assert.strictEqual(args.limit, 30, "should fetch up to 30 messages"); + } + }, + }); + await openDiscuss(); + assert.verifySteps(["/mail/channel/messages"]); + }); -QUnit.test('default select thread in discuss params', async function (assert) { - assert.expect(1); + QUnit.test("default select thread in discuss params", async function (assert) { + assert.expect(1); - const { messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: 'mail.box_starred', - }, - } - }); - await openDiscuss(); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] - `).classList.contains('o-active'), - "starred mailbox should become active" - ); -}); + const { messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: "mail.box_starred", + }, + }, + }); + await openDiscuss(); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] + ` + ) + .classList.contains("o-active"), + "starred mailbox should become active" + ); + }); -QUnit.test('auto-select thread in discuss context', async function (assert) { - assert.expect(1); + QUnit.test("auto-select thread in discuss context", async function (assert) { + assert.expect(1); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { - active_id: 'mail.box_starred', - }, - }, - }); - await openDiscuss(); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] - `).classList.contains('o-active'), - "starred mailbox should become active" - ); -}); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { + active_id: "mail.box_starred", + }, + }, + }); + await openDiscuss(); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] + ` + ) + .classList.contains("o-active"), + "starred mailbox should become active" + ); + }); -QUnit.test('load single message from channel initially', async function (assert) { - assert.expect(6); + QUnit.test("load single message from channel initially", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - date: "2019-04-20 10:00:00", - model: 'mail.channel', - res_id: mailChannelId1 - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - assert.strictEqual( - args.limit, - 30, - "should fetch up to 30 messages" - ); - } - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_ThreadView_messageList`).length, - 1, - "should have list of messages" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_separatorDate`).length, - 1, - "should have a single date separator" // to check: may be client timezone dependent - ); - assert.strictEqual( - document.querySelector(`.o_DiscussView_thread .o_MessageListView_separatorLabelDate`).textContent, - "April 20, 2019", - "should display date day of messages" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`).length, - 1, - "should have a single message" - ); - assert.strictEqual( - document.querySelectorAll(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + date: "2019-04-20 10:00:00", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + assert.strictEqual(args.limit, 30, "should fetch up to 30 messages"); + } + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_ThreadView_messageList`).length, + 1, + "should have list of messages" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_separatorDate`) + .length, + 1, + "should have a single date separator" // to check: may be client timezone dependent + ); + assert.strictEqual( + document.querySelector( + `.o_DiscussView_thread .o_MessageListView_separatorLabelDate` + ).textContent, + "April 20, 2019", + "should display date day of messages" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`) + .length, + 1, + "should have a single message" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_MessageListView_message[data-message-id="${mailMessageId1}"] `).length, - 1, - "should have message with Id 100" - ); -}); + 1, + "should have message with Id 100" + ); + }); -QUnit.test('open channel from active_id as channel id', async function (assert) { - assert.expect(1); + QUnit.test("open channel from active_id as channel id", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - } - }); - await openDiscuss(); - assert.containsOnce( - document.body, - ` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ` .o_DiscussView_thread[data-thread-id="${mailChannelId1}"][data-thread-model="mail.channel"] `, - "should have channel 1 open in Discuss when providing its active_id" - ); -}); - -QUnit.test('basic rendering of message', async function (assert) { - // AKU TODO: should be in message-only tests - assert.expect(15); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: "

body

", - date: "2019-04-20 10:00:00", - model: 'mail.channel', - res_id: mailChannelId1 - }); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - const message = document.querySelector(` - .o_DiscussView_thread - .o_ThreadView_messageList - .o_MessageListView_message[data-message-id="${mailMessageId1}"] - `); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_sidebar`).length, - 1, - "should have message sidebar of message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_authorAvatar`).length, - 1, - "should have author avatar in sidebar of message" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_authorAvatar`).dataset.src, - `/mail/channel/${mailChannelId1}/partner/${resPartnerId1}/avatar_128`, - "should have url of message in author avatar sidebar" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_core`).length, - 1, - "should have core part of message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_header`).length, - 1, - "should have header in core part of message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_authorName`).length, - 1, - "should have author name in header of message" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_authorName`).textContent, - "Demo", - "should have textually author name in header of message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_header .o_MessageView_date`).length, - 1, - "should have date in header of message" - ); - - await click('.o_MessageView'); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionList`).length, - 1, - "should action list in message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView`).length, - 3, - "should have 3 actions in action list of message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, - 1, - "should have action to star message" - ); - assert.containsOnce( - document.body, - '.o_MessageActionView_actionReaction', - "should have action to add a reaction" - ); - assert.containsOnce( - message, - '.o_MessageActionView_actionReplyTo', - "should have action to reply to message" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_content`).length, - 1, - "should have content in core part of message" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_content`).textContent.trim(), - "body", - "should have body of message in content part of message" - ); -}); - -QUnit.test('should not be able to reply to temporary/transient messages', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - // these user interactions is to forge a transient message response from channel command "/who" - await insertText('.o_ComposerTextInputView_textarea', "/who"); - await click('.o_ComposerView_buttonSend'); - // click on message to show actions on the transient message resulting from the "/who" command - await click('.o_MessageView'); - assert.containsNone( - document.body, - '.o_MessageActionView_actionReplyTo', - "should not have action to reply to temporary/transient messages" - ); -}); - -QUnit.test('basic rendering of squashed message', async function (assert) { - // messages are squashed when "close", e.g. less than 1 minute has elapsed - // from messages of same author and same thread. Note that this should - // be working in non-mailboxes - // AKU TODO: should be message and/or message list-only tests - assert.expect(12); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - author_id: resPartnerId1, // must be same author as other message - body: "

body1

", // random body, set for consistency - date: "2019-04-20 10:00:00", // date must be within 1 min from other message - message_type: 'comment', // must be a squash-able type- - model: 'mail.channel', // to link message to channel - res_id: mailChannelId1, // id of related channel - }, - { - author_id: resPartnerId1, // must be same author as other message - body: "

body2

", // random body, will be asserted in the test - date: "2019-04-20 10:00:30", // date must be within 1 min from other message - message_type: 'comment', // must be a squash-able type - model: 'mail.channel', // to link message to channel - res_id: mailChannelId1, // id of related channel - } - ]); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message - `).length, - 2, - "should have 2 messages" - ); - const message1 = document.querySelector(` - .o_DiscussView_thread - .o_ThreadView_messageList - .o_MessageListView_message[data-message-id="${mailMessageId1}"] - `); - const message2 = document.querySelector(` - .o_DiscussView_thread - .o_ThreadView_messageList - .o_MessageListView_message[data-message-id="${mailMessageId2}"] - `); - assert.notOk( - message1.classList.contains('o-squashed'), - "message 1 should not be squashed" - ); - assert.notOk( - message1.querySelector(`:scope .o_MessageView_sidebar`).classList.contains('o-message-squashed'), - "message 1 should not have squashed sidebar" - ); - assert.ok( - message2.classList.contains('o-squashed'), - "message 2 should be squashed" - ); - assert.ok( - message2.querySelector(`:scope .o_MessageView_sidebar`).classList.contains('o-message-squashed'), - "message 2 should have squashed sidebar" - ); - - await click('.o_MessageView.o-squashed'); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageView_sidebar .o_MessageView_date`).length, - 1, - "message 2 should have date in sidebar" - ); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageActionList`).length, - 1, - "message 2 should have some actions" - ); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, - 1, - "message 2 should have star action in action list" - ); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageView_core`).length, - 1, - "message 2 should have core part" - ); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageView_header`).length, - 0, - "message 2 should have a header in core part" - ); - assert.strictEqual( - message2.querySelectorAll(`:scope .o_MessageView_content`).length, - 1, - "message 2 should have some content in core part" - ); - assert.strictEqual( - message2.querySelector(`:scope .o_MessageView_content`).textContent.trim(), - "body2", - "message 2 should have body in content part" - ); -}); - -QUnit.test('inbox messages are never squashed', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - author_id: resPartnerId1, // must be same author as other message - body: "

body1

", // random body, set for consistency - date: "2019-04-20 10:00:00", // date must be within 1 min from other message - message_type: 'comment', // must be a squash-able type- - model: 'mail.channel', // to link message to channel - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency - res_id: mailChannelId1, // id of related channel - }, - { - author_id: resPartnerId1, // must be same author as other message - body: "

body2

", // random body, will be asserted in the test - date: "2019-04-20 10:00:30", // date must be within 1 min from other message - message_type: 'comment', // must be a squash-able type - model: 'mail.channel', // to link message to channel - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency - res_id: mailChannelId1, // id of related channel - } - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, { - mail_message_id: mailMessageId2, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - ]); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + "should have channel 1 open in Discuss when providing its active_id" ); - }, - }); + }); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message - `).length, - 2, - "should have 2 messages" - ); - const message1 = document.querySelector(` + QUnit.test("basic rendering of message", async function (assert) { + // AKU TODO: should be in message-only tests + assert.expect(15); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailMessageId1 = pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "

body

", + date: "2019-04-20 10:00:00", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + const message = document.querySelector(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message[data-message-id="${mailMessageId1}"] `); - const message2 = document.querySelector(` - .o_DiscussView_thread - .o_ThreadView_messageList - .o_MessageListView_message[data-message-id="${mailMessageId2}"] - `); - assert.notOk( - message1.classList.contains('o-squashed'), - "message 1 should not be squashed" - ); - assert.notOk( - message2.classList.contains('o-squashed'), - "message 2 should not be squashed" - ); -}); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_sidebar`).length, + 1, + "should have message sidebar of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_authorAvatar`).length, + 1, + "should have author avatar in sidebar of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_authorAvatar`).dataset.src, + `/mail/channel/${mailChannelId1}/partner/${resPartnerId1}/avatar_128`, + "should have url of message in author avatar sidebar" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_core`).length, + 1, + "should have core part of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_header`).length, + 1, + "should have header in core part of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_authorName`).length, + 1, + "should have author name in header of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_authorName`).textContent, + "Demo", + "should have textually author name in header of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_header .o_MessageView_date`).length, + 1, + "should have date in header of message" + ); -QUnit.test('load all messages from channel initially, less than fetch limit (29 < 30)', async function (assert) { - // AKU TODO: thread specific test - assert.expect(5); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['res.partner'].create({}); - for (let i = 28; i >= 0; i--) { - pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: "not empty", - date: "2019-04-20 10:00:00", - model: 'mail.channel', - res_id: mailChannelId1, + await click(".o_MessageView"); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionList`).length, + 1, + "should action list in message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView`).length, + 3, + "should have 3 actions in action list of message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, + 1, + "should have action to star message" + ); + assert.containsOnce( + document.body, + ".o_MessageActionView_actionReaction", + "should have action to add a reaction" + ); + assert.containsOnce( + message, + ".o_MessageActionView_actionReplyTo", + "should have action to reply to message" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_content`).length, + 1, + "should have content in core part of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_content`).textContent.trim(), + "body", + "should have body of message in content part of message" + ); }); - } - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - assert.strictEqual(args.limit, 30, "should fetch up to 30 messages"); + + QUnit.test( + "should not be able to reply to temporary/transient messages", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + // these user interactions is to forge a transient message response from channel command "/who" + await insertText(".o_ComposerTextInputView_textarea", "/who"); + await click(".o_ComposerView_buttonSend"); + // click on message to show actions on the transient message resulting from the "/who" command + await click(".o_MessageView"); + assert.containsNone( + document.body, + ".o_MessageActionView_actionReplyTo", + "should not have action to reply to temporary/transient messages" + ); } - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` + ); + + QUnit.test("basic rendering of squashed message", async function (assert) { + // messages are squashed when "close", e.g. less than 1 minute has elapsed + // from messages of same author and same thread. Note that this should + // be working in non-mailboxes + // AKU TODO: should be message and/or message list-only tests + assert.expect(12); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + author_id: resPartnerId1, // must be same author as other message + body: "

body1

", // random body, set for consistency + date: "2019-04-20 10:00:00", // date must be within 1 min from other message + message_type: "comment", // must be a squash-able type- + model: "mail.channel", // to link message to channel + res_id: mailChannelId1, // id of related channel + }, + { + author_id: resPartnerId1, // must be same author as other message + body: "

body2

", // random body, will be asserted in the test + date: "2019-04-20 10:00:30", // date must be within 1 min from other message + message_type: "comment", // must be a squash-able type + model: "mail.channel", // to link message to channel + res_id: mailChannelId1, // id of related channel + }, + ]); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message + `).length, + 2, + "should have 2 messages" + ); + const message1 = document.querySelector(` + .o_DiscussView_thread + .o_ThreadView_messageList + .o_MessageListView_message[data-message-id="${mailMessageId1}"] + `); + const message2 = document.querySelector(` + .o_DiscussView_thread + .o_ThreadView_messageList + .o_MessageListView_message[data-message-id="${mailMessageId2}"] + `); + assert.notOk( + message1.classList.contains("o-squashed"), + "message 1 should not be squashed" + ); + assert.notOk( + message1 + .querySelector(`:scope .o_MessageView_sidebar`) + .classList.contains("o-message-squashed"), + "message 1 should not have squashed sidebar" + ); + assert.ok(message2.classList.contains("o-squashed"), "message 2 should be squashed"); + assert.ok( + message2 + .querySelector(`:scope .o_MessageView_sidebar`) + .classList.contains("o-message-squashed"), + "message 2 should have squashed sidebar" + ); + + await click(".o_MessageView.o-squashed"); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageView_sidebar .o_MessageView_date`) + .length, + 1, + "message 2 should have date in sidebar" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageActionList`).length, + 1, + "message 2 should have some actions" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, + 1, + "message 2 should have star action in action list" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageView_core`).length, + 1, + "message 2 should have core part" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageView_header`).length, + 0, + "message 2 should have a header in core part" + ); + assert.strictEqual( + message2.querySelectorAll(`:scope .o_MessageView_content`).length, + 1, + "message 2 should have some content in core part" + ); + assert.strictEqual( + message2.querySelector(`:scope .o_MessageView_content`).textContent.trim(), + "body2", + "message 2 should have body in content part" + ); + }); + + QUnit.test("inbox messages are never squashed", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + author_id: resPartnerId1, // must be same author as other message + body: "

body1

", // random body, set for consistency + date: "2019-04-20 10:00:00", // date must be within 1 min from other message + message_type: "comment", // must be a squash-able type- + model: "mail.channel", // to link message to channel + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency + res_id: mailChannelId1, // id of related channel + }, + { + author_id: resPartnerId1, // must be same author as other message + body: "

body2

", // random body, will be asserted in the test + date: "2019-04-20 10:00:30", // date must be within 1 min from other message + message_type: "comment", // must be a squash-able type + model: "mail.channel", // to link message to channel + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency + res_id: mailChannelId1, // id of related channel + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + { + mail_message_id: mailMessageId2, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + ]); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message + `).length, + 2, + "should have 2 messages" + ); + const message1 = document.querySelector(` + .o_DiscussView_thread + .o_ThreadView_messageList + .o_MessageListView_message[data-message-id="${mailMessageId1}"] + `); + const message2 = document.querySelector(` + .o_DiscussView_thread + .o_ThreadView_messageList + .o_MessageListView_message[data-message-id="${mailMessageId2}"] + `); + assert.notOk( + message1.classList.contains("o-squashed"), + "message 1 should not be squashed" + ); + assert.notOk( + message2.classList.contains("o-squashed"), + "message 2 should not be squashed" + ); + }); + + QUnit.test( + "load all messages from channel initially, less than fetch limit (29 < 30)", + async function (assert) { + // AKU TODO: thread specific test + assert.expect(5); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["res.partner"].create({}); + for (let i = 28; i >= 0; i--) { + pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "not empty", + date: "2019-04-20 10:00:00", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + assert.strictEqual(args.limit, 30, "should fetch up to 30 messages"); + } + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_separatorDate `).length, - 1, - "should have a single date separator" // to check: may be client timezone dependent - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have a single date separator" // to check: may be client timezone dependent + ); + assert.strictEqual( + document.querySelector(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_separatorLabelDate `).textContent, - "April 20, 2019", - "should display date day of messages" - ); - assert.strictEqual( - document.querySelectorAll(` + "April 20, 2019", + "should display date day of messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 29, - "should have 29 messages" - ); - assert.strictEqual( - document.querySelectorAll(` + 29, + "should have 29 messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_loadMore `).length, - 0, - "should not have load more link" - ); -}); + 0, + "should not have load more link" + ); + } + ); -QUnit.test('load more messages from channel', async function (assert) { - // AKU: thread specific test - assert.expect(6); + QUnit.test("load more messages from channel", async function (assert) { + // AKU: thread specific test + assert.expect(6); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['res.partner'].create({}); - for (let i = 0; i < 40; i++) { - pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: "not empty", - date: "2019-04-20 10:00:00", - model: 'mail.channel', - res_id: mailChannelId1, - }); - } - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["res.partner"].create({}); + for (let i = 0; i < 40; i++) { + pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "not empty", + date: "2019-04-20 10:00:00", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_separatorDate `).length, - 1, - "should have a single date separator" // to check: may be client timezone dependent - ); - assert.strictEqual( - document.querySelector(` + 1, + "should have a single date separator" // to check: may be client timezone dependent + ); + assert.strictEqual( + document.querySelector(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_separatorLabelDate `).textContent, - "April 20, 2019", - "should display date day of messages" - ); - assert.strictEqual( - document.querySelectorAll(` + "April 20, 2019", + "should display date day of messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 30, - "should have 30 messages" - ); - assert.strictEqual( - document.querySelectorAll(` + 30, + "should have 30 messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_loadMore `).length, - 1, - "should have load more link" - ); + 1, + "should have load more link" + ); - await click(`.o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_loadMore`); - assert.strictEqual( - document.querySelectorAll(` + await click( + `.o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_loadMore` + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 40, - "should have 40 messages" - ); - assert.strictEqual( - document.querySelectorAll(` + 40, + "should have 40 messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_loadMore `).length, - 0, - "should not longer have load more link (all messages loaded)" - ); -}); - -QUnit.test('auto-scroll to bottom of thread', async function (assert) { - // AKU TODO: thread specific test - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 1; i <= 25; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, + 0, + "should not longer have load more link (all messages loaded)" + ); }); - } - const { afterEvent, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: openDiscuss, - message: "should wait until channel scrolled to its last message initially", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) - ); - }, - }); - assert.strictEqual( - document.querySelectorAll(` + + QUnit.test("auto-scroll to bottom of thread", async function (assert) { + // AKU TODO: thread specific test + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 1; i <= 25; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: openDiscuss, + message: "should wait until channel scrolled to its last message initially", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 25, - "should have 25 messages" - ); - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - assert.ok( - isScrolledToBottom(messageList), - "should have scrolled to bottom of thread" - ); -}); - -QUnit.test('load more messages from channel (auto-load on scroll)', async function (assert) { - // AKU TODO: thread specific test - assert.expect(3); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i < 40; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, + 25, + "should have 25 messages" + ); + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + assert.ok(isScrolledToBottom(messageList), "should have scrolled to bottom of thread"); }); - } - const { afterEvent, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: openDiscuss, - message: "should wait until channel scrolled to its last message initially", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) - ); - }, - }); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message - `).length, - 30, - "should have 30 messages" - ); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => document.querySelector('.o_ThreadView_messageList').scrollTop = 0, - message: "should wait until channel loaded more messages after scrolling to top", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'more-messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 - ); - }, - }); - assert.strictEqual( - document.querySelectorAll(` + QUnit.test( + "load more messages from channel (auto-load on scroll)", + async function (assert) { + // AKU TODO: thread specific test + assert.expect(3); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i < 40; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: openDiscuss, + message: "should wait until channel scrolled to its last message initially", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 40, - "should have 40 messages" - ); - assert.strictEqual( - document.querySelectorAll(` + 30, + "should have 30 messages" + ); + + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => (document.querySelector(".o_ThreadView_messageList").scrollTop = 0), + message: + "should wait until channel loaded more messages after scrolling to top", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "more-messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message + `).length, + 40, + "should have 40 messages" + ); + assert.strictEqual( + document.querySelectorAll(` .o_Dsiscuss_thread .o_ThreadView_messageList .o_MessageListView_loadMore `).length, - 0, - "should not longer have load more link (all messages loaded)" - ); -}); + 0, + "should not longer have load more link (all messages loaded)" + ); + } + ); -QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) { - // this test requires several messages so that the last message is not - // visible. This is necessary in order to display 'new messages' and not - // remove from DOM right away from seeing last message. - // AKU TODO: thread specific test - assert.expect(6); + QUnit.test("new messages separator [REQUIRE FOCUS]", async function (assert) { + // this test requires several messages so that the last message is not + // visible. This is necessary in order to display 'new messages' and not + // remove from DOM right away from seeing last message. + // AKU TODO: thread specific test + assert.expect(6); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Foreigner partner" }); - const resUsersId1 = pyEnv['res.users'].create({ - name: "Foreigner user", - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ uuid: 'randomuuid' }); - let lastMessageId; - for (let i = 1; i <= 25; i++) { - lastMessageId = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - } - const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: lastMessageId }); - const { afterEvent, messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: openDiscuss, - message: "should wait until channel scrolled to its last message initially", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Foreigner partner" }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ uuid: "randomuuid" }); + let lastMessageId; + for (let i = 1; i <= 25; i++) { + lastMessageId = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const [mailChannelMemberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId], { + seen_message_id: lastMessageId, + }); + const { afterEvent, messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: openDiscuss, + message: "should wait until channel scrolled to its last message initially", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + assert.containsN( + document.body, + ".o_MessageListView_message", + 25, + "should have 25 messages" ); - }, - }); - assert.containsN( - document.body, - '.o_MessageListView_message', - 25, - "should have 25 messages" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should not display 'new messages' separator" - ); - // scroll to top - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop = 0; - }, - message: "should wait until channel scrolled to top", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - scrollTop === 0 + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should not display 'new messages' separator" ); - }, - }); - // composer is focused by default, we remove that focus - document.querySelector('.o_ComposerTextInputView_textarea').blur(); - // simulate receiving a message - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: 'randomuuid', - }, - })); - - assert.containsN( - document.body, - '.o_MessageListView_message', - 26, - "should have 26 messages" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "should display 'new messages' separator" - ); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight; - }, - message: "should wait until channel scrolled to bottom", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - return ( - thread && - thread.model === 'mail.channel' && - thread.id === mailChannelId1 && - isScrolledToBottom(messageList) + // scroll to top + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ).scrollTop = 0; + }, + message: "should wait until channel scrolled to top", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + scrollTop === 0 + ); + }, + }); + // composer is focused by default, we remove that focus + document.querySelector(".o_ComposerTextInputView_textarea").blur(); + // simulate receiving a message + await afterNextRender(async () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: "randomuuid", + }, + }) ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "should still display 'new messages' separator as composer is not focused" - ); - await afterNextRender(() => - document.querySelector('.o_ComposerTextInputView_textarea').focus() - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should no longer display 'new messages' separator (message seen)" - ); -}); + assert.containsN( + document.body, + ".o_MessageListView_message", + 26, + "should have 26 messages" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "should display 'new messages' separator" + ); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + messageList.scrollTop = messageList.scrollHeight - messageList.clientHeight; + }, + message: "should wait until channel scrolled to bottom", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + return ( + thread && + thread.model === "mail.channel" && + thread.id === mailChannelId1 && + isScrolledToBottom(messageList) + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "should still display 'new messages' separator as composer is not focused" + ); -QUnit.test('restore thread scroll position', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: 'Channel1' }, { name: 'Channel2' }]); - for (let i = 1; i <= 25; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, + await afterNextRender(() => + document.querySelector(".o_ComposerTextInputView_textarea").focus() + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should no longer display 'new messages' separator (message seen)" + ); }); - } - for (let i = 1; i <= 24; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId2, - }); - } - const { afterEvent, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: openDiscuss, - message: "should wait until channel 1 scrolled to its last message", - predicate: ({ thread }) => { - return thread && thread.channel && thread.channel.id === mailChannelId1; - }, - }); - assert.strictEqual( - document.querySelectorAll(` + + QUnit.test("restore thread scroll position", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "Channel1" }, + { name: "Channel2" }, + ]); + for (let i = 1; i <= 25; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + for (let i = 1; i <= 24; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId2, + }); + } + const { afterEvent, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: openDiscuss, + message: "should wait until channel 1 scrolled to its last message", + predicate: ({ thread }) => { + return thread && thread.channel && thread.channel.id === mailChannelId1; + }, + }); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 25, - "should have 25 messages in channel 1" - ); - const initialMessageList = document.querySelector(` + 25, + "should have 25 messages in channel 1" + ); + const initialMessageList = document.querySelector(` .o_DiscussView_thread .o_ThreadView_messageList `); - assert.ok( - isScrolledToBottom(initialMessageList), - "should have scrolled to bottom of channel 1 initially" - ); + assert.ok( + isScrolledToBottom(initialMessageList), + "should have scrolled to bottom of channel 1 initially" + ); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop = 0, - message: "should wait until channel 1 changed its scroll position to top", - predicate: ({ thread }) => { - return thread && thread.channel && thread.channel.id === mailChannelId1; - }, - }); - assert.strictEqual( - document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop, - 0, - "should have scrolled to top of channel 1", - ); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => + (document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ).scrollTop = 0), + message: "should wait until channel 1 changed its scroll position to top", + predicate: ({ thread }) => { + return thread && thread.channel && thread.channel.id === mailChannelId1; + }, + }); + assert.strictEqual( + document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop, + 0, + "should have scrolled to top of channel 1" + ); - // Ensure scrollIntoView of channel 2 has enough time to complete before - // going back to channel 1. Await is needed to prevent the scrollIntoView - // initially planned for channel 2 to actually apply on channel 1. - // task-2333535 - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - // select channel 2 - document.querySelector(` + // Ensure scrollIntoView of channel 2 has enough time to complete before + // going back to channel 1. Await is needed to prevent the scrollIntoView + // initially planned for channel 2 to actually apply on channel 1. + // task-2333535 + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + // select channel 2 + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] - `).click(); - }, - message: "should wait until channel 2 scrolled to its last message", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector('.o_ThreadView_messageList'); - return ( - thread && - thread.channel && - thread.channel.id === mailChannelId2 && - isScrolledToBottom(messageList) - ); - }, - }); - assert.strictEqual( - document.querySelectorAll(` + ` + ) + .click(); + }, + message: "should wait until channel 2 scrolled to its last message", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(".o_ThreadView_messageList"); + return ( + thread && + thread.channel && + thread.channel.id === mailChannelId2 && + isScrolledToBottom(messageList) + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussView_thread .o_ThreadView_messageList .o_MessageListView_message `).length, - 24, - "should have 24 messages in channel 2" - ); + 24, + "should have 24 messages in channel 2" + ); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - // select channel 1 - document.querySelector(` + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + // select channel 1 + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] - `).click(); - }, - message: "should wait until channel 1 restored its scroll position", - predicate: ({ scrollTop, thread }) => { - return ( - thread && - thread.channel && - thread.channel.id === mailChannelId1 && - scrollTop === 0 + ` + ) + .click(); + }, + message: "should wait until channel 1 restored its scroll position", + predicate: ({ scrollTop, thread }) => { + return ( + thread && + thread.channel && + thread.channel.id === mailChannelId1 && + scrollTop === 0 + ); + }, + }); + assert.strictEqual( + document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop, + 0, + "should have recovered scroll position of channel 1 (scroll to top)" ); - }, - }); - assert.strictEqual( - document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`).scrollTop, - 0, - "should have recovered scroll position of channel 1 (scroll to top)" - ); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - // select channel 2 - document.querySelector(` + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + // select channel 2 + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] - `).click(); - }, - message: "should wait until channel 2 recovered its scroll position (to bottom)", - predicate: ({ scrollTop, thread }) => { - const messageList = document.querySelector('.o_ThreadView_messageList'); - return ( - thread && - thread.channel && - thread.channel.id === mailChannelId2 && - isScrolledToBottom(messageList) + ` + ) + .click(); + }, + message: "should wait until channel 2 recovered its scroll position (to bottom)", + predicate: ({ scrollTop, thread }) => { + const messageList = document.querySelector(".o_ThreadView_messageList"); + return ( + thread && + thread.channel && + thread.channel.id === mailChannelId2 && + isScrolledToBottom(messageList) + ); + }, + }); + const messageList = document.querySelector(".o_ThreadView_messageList"); + assert.ok( + isScrolledToBottom(messageList), + "should have recovered scroll position of channel 2 (scroll to bottom)" ); - }, - }); - const messageList = document.querySelector('.o_ThreadView_messageList'); - assert.ok( - isScrolledToBottom(messageList), - "should have recovered scroll position of channel 2 (scroll to bottom)" - ); -}); + }); -QUnit.test('redirect to author (open chat)', async function (assert) { - assert.expect(7); + QUnit.test("redirect to author (open chat)", async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { name: "General" }, - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - } - ]); - const mailMessageId1 = pyEnv['mail.message'].create( - { - author_id: resPartnerId1, - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - } - ); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.ok( - document.querySelector(` + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "General" }, + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }, + ]); + const mailMessageId1 = pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] - `).classList.contains('o-active'), - "channel 'General' should be active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "channel 'General' should be active" + ); + assert.notOk( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] - `).classList.contains('o-active'), - "Chat 'Demo' should not be active" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, - 1, - "should have 1 message" - ); - const msg1 = document.querySelector(` + ` + ) + .classList.contains("o-active"), + "Chat 'Demo' should not be active" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, + 1, + "should have 1 message" + ); + const msg1 = document.querySelector(` .o_DiscussView_thread .o_MessageView[data-message-id="${mailMessageId1}"] `); - assert.strictEqual( - msg1.querySelectorAll(`:scope .o_MessageView_authorAvatar`).length, - 1, - "message1 should have author image" - ); - assert.ok( - msg1.querySelector(`:scope .o_MessageView_authorAvatar`).classList.contains('o_redirect'), - "message1 should have redirect to author" - ); + assert.strictEqual( + msg1.querySelectorAll(`:scope .o_MessageView_authorAvatar`).length, + 1, + "message1 should have author image" + ); + assert.ok( + msg1 + .querySelector(`:scope .o_MessageView_authorAvatar`) + .classList.contains("o_redirect"), + "message1 should have redirect to author" + ); - await afterNextRender(() => - msg1.querySelector(`:scope .o_MessageView_authorAvatar`).click() - ); - assert.notOk( - document.querySelector(` + await afterNextRender(() => + msg1.querySelector(`:scope .o_MessageView_authorAvatar`).click() + ); + assert.notOk( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId1}"] - `).classList.contains('o-active'), - "channel 'General' should become inactive after author redirection" - ); - assert.ok( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "channel 'General' should become inactive after author redirection" + ); + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_item[data-channel-id="${mailChannelId2}"] - `).classList.contains('o-active'), - "chat 'Demo' should become active after author redirection" - ); -}); + ` + ) + .classList.contains("o-active"), + "chat 'Demo' should become active after author redirection" + ); + }); -QUnit.test('sidebar quick search', async function (assert) { - // feature enables at 20 or more channels - assert.expect(6); + QUnit.test("sidebar quick search", async function (assert) { + // feature enables at 20 or more channels + assert.expect(6); - const pyEnv = await startServer(); - for (let id = 1; id <= 20; id++) { - pyEnv['mail.channel'].create({ name: `channel${id}` }); - } - const { openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 20, - "should have 20 channel items" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_sidebar input.o_DiscussSidebarView_quickSearch`).length, - 1, - "should have quick search in sidebar" - ); + const pyEnv = await startServer(); + for (let id = 1; id <= 20; id++) { + pyEnv["mail.channel"].create({ name: `channel${id}` }); + } + const { openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 20, + "should have 20 channel items" + ); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussView_sidebar input.o_DiscussSidebarView_quickSearch` + ).length, + 1, + "should have quick search in sidebar" + ); - const quickSearch = document.querySelector(` + const quickSearch = document.querySelector(` .o_DiscussView_sidebar input.o_DiscussSidebarView_quickSearch `); - await afterNextRender(() => { - quickSearch.value = "1"; - const kevt1 = new window.KeyboardEvent('input'); - quickSearch.dispatchEvent(kevt1); - }); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 11, - "should have filtered to 11 channel items" - ); + await afterNextRender(() => { + quickSearch.value = "1"; + const kevt1 = new window.KeyboardEvent("input"); + quickSearch.dispatchEvent(kevt1); + }); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 11, + "should have filtered to 11 channel items" + ); - await afterNextRender(() => { - quickSearch.value = "12"; - const kevt2 = new window.KeyboardEvent('input'); - quickSearch.dispatchEvent(kevt2); - }); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 1, - "should have filtered to a single channel item" - ); - assert.strictEqual( - Number(document.querySelector(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).dataset.channelId), - pyEnv['mail.channel'].search([['name', '=', 'channel12']])[0], - "should have filtered to a single channel (channel12)" - ); + await afterNextRender(() => { + quickSearch.value = "12"; + const kevt2 = new window.KeyboardEvent("input"); + quickSearch.dispatchEvent(kevt2); + }); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 1, + "should have filtered to a single channel item" + ); + assert.strictEqual( + Number( + document.querySelector( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).dataset.channelId + ), + pyEnv["mail.channel"].search([["name", "=", "channel12"]])[0], + "should have filtered to a single channel (channel12)" + ); - await afterNextRender(() => { - quickSearch.value = "123"; - const kevt3 = new window.KeyboardEvent('input'); - quickSearch.dispatchEvent(kevt3); - }); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item`).length, - 0, - "should have filtered to no channel item" - ); -}); + await afterNextRender(() => { + quickSearch.value = "123"; + const kevt3 = new window.KeyboardEvent("input"); + quickSearch.dispatchEvent(kevt3); + }); + assert.strictEqual( + document.querySelectorAll( + `.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item` + ).length, + 0, + "should have filtered to no channel item" + ); + }); -QUnit.test('basic top bar rendering', async function (assert) { - assert.expect(8); + QUnit.test("basic top bar rendering", async function (assert) { + assert.expect(8); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" }); - const { click, messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelector(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "General" }); + const { click, messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelector(` .o_ThreadViewTopbar_threadName `).textContent, - "Inbox", - "display inbox in the top bar" - ); - const markAllReadButton = document.querySelector(`.o_ThreadViewTopbar_markAllReadButton`); - assert.isVisible( - markAllReadButton, - "should have visible button 'Mark all read' in the top bar of inbox" - ); - assert.ok( - markAllReadButton.disabled, - "should have disabled button 'Mark all read' in the top bar of inbox (no messages)" - ); + "Inbox", + "display inbox in the top bar" + ); + const markAllReadButton = document.querySelector( + `.o_ThreadViewTopbar_markAllReadButton` + ); + assert.isVisible( + markAllReadButton, + "should have visible button 'Mark all read' in the top bar of inbox" + ); + assert.ok( + markAllReadButton.disabled, + "should have disabled button 'Mark all read' in the top bar of inbox (no messages)" + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] `); - assert.strictEqual( - document.querySelector(` + assert.strictEqual( + document.querySelector(` .o_ThreadViewTopbar_threadName `).textContent, - "Starred", - "display starred in the breadcrumb" - ); - const unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); - assert.isVisible( - unstarAllButton, - "should have visible button 'Unstar all' in the top bar of starred" - ); - assert.ok( - unstarAllButton.disabled, - "should have disabled button 'Unstar all' in the top bar of starred (no messages)" - ); + "Starred", + "display starred in the breadcrumb" + ); + const unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); + assert.isVisible( + unstarAllButton, + "should have visible button 'Unstar all' in the top bar of starred" + ); + assert.ok( + unstarAllButton.disabled, + "should have disabled button 'Unstar all' in the top bar of starred (no messages)" + ); - await click(` + await click(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `); - assert.strictEqual( - document.querySelector(` + assert.strictEqual( + document.querySelector(` .o_ThreadViewTopbar_threadName `).textContent, - "General", - "display general in the breadcrumb" - ); - const inviteButton = document.querySelector(`.o_ThreadViewTopbar_inviteButton`); - assert.isVisible( - inviteButton, - "should have button 'Invite' in the top bar of channel" - ); -}); + "General", + "display general in the breadcrumb" + ); + const inviteButton = document.querySelector(`.o_ThreadViewTopbar_inviteButton`); + assert.isVisible(inviteButton, "should have button 'Invite' in the top bar of channel"); + }); -QUnit.test('inbox: mark all messages as read', async function (assert) { - assert.expect(8); + QUnit.test("inbox: mark all messages as read", async function (assert) { + assert.expect(8); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - body: "not empty", - model: 'mail.channel', - needaction: true, - res_id: mailChannelId1, - }, - { - body: "not empty", - model: 'mail.channel', - needaction: true, - res_id: mailChannelId1, - } - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }, - { - mail_message_id: mailMessageId2, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - } - ]); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + body: "not empty", + model: "mail.channel", + needaction: true, + res_id: mailChannelId1, + }, + { + body: "not empty", + model: "mail.channel", + needaction: true, + res_id: mailChannelId1, + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }, + { + mail_message_id: mailMessageId2, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }, + ]); + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - "2", - "inbox should have counter of 2" - ); - assert.strictEqual( - document.querySelector(` + "2", + "inbox should have counter of 2" + ); + assert.strictEqual( + document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] .o_DiscussSidebarCategoryItem_counter `).textContent, - "2", - "channel should have counter of 2" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 2, - "should have 2 messages in inbox" - ); - let markAllReadButton = document.querySelector(`.o_ThreadViewTopbar_markAllReadButton`); - assert.notOk( - markAllReadButton.disabled, - "should have enabled button 'Mark all read' in the top bar of inbox (has messages)" - ); + "2", + "channel should have counter of 2" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 2, + "should have 2 messages in inbox" + ); + let markAllReadButton = document.querySelector(`.o_ThreadViewTopbar_markAllReadButton`); + assert.notOk( + markAllReadButton.disabled, + "should have enabled button 'Mark all read' in the top bar of inbox (has messages)" + ); - await afterNextRender(() => markAllReadButton.click()); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + await afterNextRender(() => markAllReadButton.click()); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 0, - "inbox should display no counter (= 0)" - ); - assert.strictEqual( - document.querySelectorAll(` + 0, + "inbox should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] .o_DiscussSidebarCategoryItem_counter `).length, - 0, - "channel should display no counter (= 0)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 0, - "should have no message in inbox" - ); - markAllReadButton = document.querySelector(`.o_ThreadViewTopbar_markAllReadButton`); - assert.ok( - markAllReadButton.disabled, - "should have disabled button 'Mark all read' in the top bar of inbox (no messages)" - ); -}); + 0, + "channel should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 0, + "should have no message in inbox" + ); + markAllReadButton = document.querySelector(`.o_ThreadViewTopbar_markAllReadButton`); + assert.ok( + markAllReadButton.disabled, + "should have disabled button 'Mark all read' in the top bar of inbox (no messages)" + ); + }); -QUnit.test('starred: unstar all', async function (assert) { - assert.expect(6); + QUnit.test("starred: unstar all", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - pyEnv['mail.message'].create([ - { body: "not empty", starred_partner_ids: [pyEnv.currentPartnerId] }, - { body: "not empty", starred_partner_ids: [pyEnv.currentPartnerId] } - ]); - const { messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: 'mail.box_starred', - }, - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + const pyEnv = await startServer(); + pyEnv["mail.message"].create([ + { body: "not empty", starred_partner_ids: [pyEnv.currentPartnerId] }, + { body: "not empty", starred_partner_ids: [pyEnv.currentPartnerId] }, + ]); + const { messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: "mail.box_starred", + }, + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - "2", - "starred should have counter of 2" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 2, - "should have 2 messages in starred" - ); - let unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); - assert.notOk( - unstarAllButton.disabled, - "should have enabled button 'Unstar all' in the top bar starred (has messages)" - ); + "2", + "starred should have counter of 2" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 2, + "should have 2 messages in starred" + ); + let unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); + assert.notOk( + unstarAllButton.disabled, + "should have enabled button 'Unstar all' in the top bar starred (has messages)" + ); - await afterNextRender(() => unstarAllButton.click()); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await afterNextRender(() => unstarAllButton.click()); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 0, - "starred should display no counter (= 0)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 0, - "should have no message in starred" - ); - unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); - assert.ok( - unstarAllButton.disabled, - "should have disabled button 'Unstar all' in the top bar of starred (no messages)" - ); -}); + 0, + "starred should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 0, + "should have no message in starred" + ); + unstarAllButton = document.querySelector(`.o_ThreadViewTopbar_unstarAllButton`); + assert.ok( + unstarAllButton.disabled, + "should have disabled button 'Unstar all' in the top bar of starred (no messages)" + ); + }); -QUnit.test('toggle_star message', async function (assert) { - assert.expect(16); + QUnit.test("toggle_star message", async function (assert) { + assert.expect(16); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - const { messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (args.method === 'toggle_message_starred') { - assert.step('rpc:toggle_message_starred'); - assert.strictEqual( - args.args[0][0], - mailMessageId1, - "should have message Id in args" - ); - } - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (args.method === "toggle_message_starred") { + assert.step("rpc:toggle_message_starred"); + assert.strictEqual( + args.args[0][0], + mailMessageId1, + "should have message Id in args" + ); + } + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 0, - "starred should display no counter (= 0)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 1, - "should have 1 message in channel" - ); - let message = document.querySelector(`.o_DiscussView .o_MessageView`); - assert.notOk( - message.classList.contains('o-starred'), - "message should not be starred" - ); - await afterNextRender(() => message.click()); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, - 1, - "message should have star action" - ); + 0, + "starred should display no counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 1, + "should have 1 message in channel" + ); + let message = document.querySelector(`.o_DiscussView .o_MessageView`); + assert.notOk(message.classList.contains("o-starred"), "message should not be starred"); + await afterNextRender(() => message.click()); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, + 1, + "message should have star action" + ); - await afterNextRender(() => message.querySelector(`:scope .o_MessageActionView_actionToggleStar`).click()); - assert.verifySteps(['rpc:toggle_message_starred']); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await afterNextRender(() => + message.querySelector(`:scope .o_MessageActionView_actionToggleStar`).click() + ); + assert.verifySteps(["rpc:toggle_message_starred"]); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - "1", - "starred should display a counter of 1" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 1, - "should have kept 1 message in channel" - ); - message = document.querySelector(`.o_DiscussView .o_MessageView`); - assert.ok( - message.classList.contains('o-starred'), - "message should be starred" - ); + "1", + "starred should display a counter of 1" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 1, + "should have kept 1 message in channel" + ); + message = document.querySelector(`.o_DiscussView .o_MessageView`); + assert.ok(message.classList.contains("o-starred"), "message should be starred"); - await afterNextRender(() => message.querySelector(`:scope .o_MessageActionView_actionToggleStar`).click()); - assert.verifySteps(['rpc:toggle_message_starred']); - assert.strictEqual( - document.querySelectorAll(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.starred.localId - }"] + await afterNextRender(() => + message.querySelector(`:scope .o_MessageActionView_actionToggleStar`).click() + ); + assert.verifySteps(["rpc:toggle_message_starred"]); + assert.strictEqual( + document.querySelectorAll(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.starred.localId}"] .o_DiscussSidebarMailboxView_counter `).length, - 0, - "starred should no longer display a counter (= 0)" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, - 1, - "should still have 1 message in channel" - ); - message = document.querySelector(`.o_DiscussView .o_MessageView`); - assert.notOk( - message.classList.contains('o-starred'), - "message should no longer be starred" - ); -}); + 0, + "starred should no longer display a counter (= 0)" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView .o_MessageView`).length, + 1, + "should still have 1 message in channel" + ); + message = document.querySelector(`.o_DiscussView .o_MessageView`); + assert.notOk( + message.classList.contains("o-starred"), + "message should no longer be starred" + ); + }); -QUnit.test('composer state: text save and restore', async function (assert) { - assert.expect(2); + QUnit.test("composer state: text save and restore", async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const [mailChannelId1] = pyEnv['mail.channel'].create([ - { name: "General" }, - { name: "Special" }, - ]); - const { click, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - // Write text in composer for #general - await insertText('.o_ComposerTextInputView_textarea', "A message"); - await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Special"]`); - await insertText('.o_ComposerTextInputView_textarea', "An other message"); - // Switch back to #general - await click(`.o_DiscussSidebarCategoryItem[data-channel-name="General"]`); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "A message", - "should restore the input text" - ); + const pyEnv = await startServer(); + const [mailChannelId1] = pyEnv["mail.channel"].create([ + { name: "General" }, + { name: "Special" }, + ]); + const { click, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + // Write text in composer for #general + await insertText(".o_ComposerTextInputView_textarea", "A message"); + await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Special"]`); + await insertText(".o_ComposerTextInputView_textarea", "An other message"); + // Switch back to #general + await click(`.o_DiscussSidebarCategoryItem[data-channel-name="General"]`); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "A message", + "should restore the input text" + ); - await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Special"]`); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "An other message", - "should restore the input text" - ); -}); + await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Special"]`); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "An other message", + "should restore the input text" + ); + }); -QUnit.test('composer state: attachments save and restore', async function (assert) { - assert.expect(6); + QUnit.test("composer state: attachments save and restore", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - const [mailChannelId1] = pyEnv['mail.channel'].create([ - { name: "General" }, - { name: "Special" }, - ]); - const { messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - const channels = document.querySelectorAll(` + const pyEnv = await startServer(); + const [mailChannelId1] = pyEnv["mail.channel"].create([ + { name: "General" }, + { name: "Special" }, + ]); + const { messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + const channels = document.querySelectorAll(` .o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_item `); - // Add attachment in a message for #general - await afterNextRender(async () => { - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', + // Add attachment in a message for #general + await afterNextRender(async () => { + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [ + file, + ]); + }); + // Switch to #special + await afterNextRender(() => channels[1].click()); + // Add attachments in a message for #special + const files = [ + await createFile({ + content: "hello2, world", + contentType: "text/plain", + name: "text2.txt", + }), + await createFile({ + content: "hello3, world", + contentType: "text/plain", + name: "text3.txt", + }), + await createFile({ + content: "hello4, world", + contentType: "text/plain", + name: "text4.txt", + }), + ]; + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, files) + ); + // Switch back to #general + await afterNextRender(() => channels[0].click()); + // Check attachment is reloaded + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 1, + "should have 1 attachment in the composer" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerView .o_AttachmentCard`).dataset.id, + messaging.models["Attachment"].findFromIdentifyingData({ id: 1 }).localId, + "should have correct 1st attachment in the composer" + ); + + // Switch back to #special + await afterNextRender(() => channels[1].click()); + // Check attachments are reloaded + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, + 3, + "should have 3 attachments in the composer" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[0].dataset.id, + messaging.models["Attachment"].findFromIdentifyingData({ id: 2 }).localId, + "should have attachment with id 2 as 1st attachment" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[1].dataset.id, + messaging.models["Attachment"].findFromIdentifyingData({ id: 3 }).localId, + "should have attachment with id 3 as 2nd attachment" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[2].dataset.id, + messaging.models["Attachment"].findFromIdentifyingData({ id: 4 }).localId, + "should have attachment with id 4 as 3rd attachment" + ); }); - inputFiles( - messaging.discuss.threadView.composerView.fileUploader.fileInput, - [file] - ); - }); - // Switch to #special - await afterNextRender(() => channels[1].click()); - // Add attachments in a message for #special - const files = [ - await createFile({ - content: 'hello2, world', - contentType: 'text/plain', - name: 'text2.txt', - }), - await createFile({ - content: 'hello3, world', - contentType: 'text/plain', - name: 'text3.txt', - }), - await createFile({ - content: 'hello4, world', - contentType: 'text/plain', - name: 'text4.txt', - }), - ]; - await afterNextRender(() => - inputFiles( - messaging.discuss.threadView.composerView.fileUploader.fileInput, - files - ) - ); - // Switch back to #general - await afterNextRender(() => channels[0].click()); - // Check attachment is reloaded - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 1, - "should have 1 attachment in the composer" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerView .o_AttachmentCard`).dataset.id, - messaging.models['Attachment'].findFromIdentifyingData({ id: 1 }).localId, - "should have correct 1st attachment in the composer" - ); - // Switch back to #special - await afterNextRender(() => channels[1].click()); - // Check attachments are reloaded - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`).length, - 3, - "should have 3 attachments in the composer" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[0].dataset.id, - messaging.models['Attachment'].findFromIdentifyingData({ id: 2 }).localId, - "should have attachment with id 2 as 1st attachment" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[1].dataset.id, - messaging.models['Attachment'].findFromIdentifyingData({ id: 3 }).localId, - "should have attachment with id 3 as 2nd attachment" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView .o_AttachmentCard`)[2].dataset.id, - messaging.models['Attachment'].findFromIdentifyingData({ id: 4 }).localId, - "should have attachment with id 4 as 3rd attachment" - ); -}); + QUnit.test("post a simple message", async function (assert) { + assert.expect(16); -QUnit.test('post a simple message', async function (assert) { - assert.expect(16); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("message_post"); + assert.strictEqual( + args.thread_model, + "mail.channel", + "should post message to channel" + ); + assert.strictEqual( + args.thread_id, + mailChannelId1, + "should post message to channel 1" + ); + assert.strictEqual( + args.post_data.body, + "Test", + "should post with provided content in composer input" + ); + assert.strictEqual( + args.post_data.message_type, + "comment", + "should set message type as 'comment'" + ); + assert.strictEqual( + args.post_data.subtype_xmlid, + "mail.mt_comment", + "should set subtype_xmlid as 'comment'" + ); + } + }, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(`.o_MessageListView_empty`).length, + 1, + "should display thread with no message initially" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 0, + "should display no message initially" + ); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "should have empty content initially" + ); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('message_post'); - assert.strictEqual( - args.thread_model, - 'mail.channel', - "should post message to channel" + // insert some HTML in editable + await insertText(".o_ComposerTextInputView_textarea", "Test"); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "Test", + "should have inserted text in editable" + ); + + await click(".o_ComposerView_buttonSend"); + assert.verifySteps(["message_post"]); + assert.strictEqual( + document.querySelector(`.o_ComposerTextInputView_textarea`).value, + "", + "should have no content in composer input after posting message" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 1, + "should display a message after posting message" + ); + const [postedMessageId] = pyEnv["mail.message"].search([], { order: "id DESC" }); + const message = document.querySelector(`.o_MessageView`); + assert.strictEqual( + parseInt(message.dataset.messageId), + postedMessageId, + "new message in thread should be linked to newly created message from message post" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_authorName`).textContent, + "Mitchell Admin", + "new message in thread should be from current partner name" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_content`).textContent, + "Test", + "new message in thread should have content typed from composer text input" + ); + }); + + QUnit.test( + 'post message on channel with "Enter" keyboard shortcut', + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.containsNone( + document.body, + ".o_MessageView", + "should not have any message initially in channel" ); - assert.strictEqual( - args.thread_id, - mailChannelId1, - "should post message to channel 1" - ); - assert.strictEqual( - args.post_data.body, - "Test", - "should post with provided content in composer input" - ); - assert.strictEqual( - args.post_data.message_type, - "comment", - "should set message type as 'comment'" - ); - assert.strictEqual( - args.post_data.subtype_xmlid, - "mail.mt_comment", - "should set subtype_xmlid as 'comment'" + + // insert some HTML in editable + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await afterNextRender(() => { + const kevt = new window.KeyboardEvent("keydown", { key: "Enter" }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should now have single message in channel after posting message from pressing 'Enter' in text input of composer" ); } - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_MessageListView_empty`).length, - 1, - "should display thread with no message initially" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 0, - "should display no message initially" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "should have empty content initially" - ); + ); - // insert some HTML in editable - await insertText('.o_ComposerTextInputView_textarea', "Test"); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "Test", - "should have inserted text in editable" - ); + QUnit.test( + 'do not post message on channel with "SHIFT-Enter" keyboard shortcut', + async function (assert) { + // Note that test doesn't assert SHIFT-Enter makes a newline, because this + // default browser cannot be simulated with just dispatching + // programmatically crafted events... + assert.expect(2); - await click('.o_ComposerView_buttonSend'); - assert.verifySteps(['message_post']); - assert.strictEqual( - document.querySelector(`.o_ComposerTextInputView_textarea`).value, - "", - "should have no content in composer input after posting message" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 1, - "should display a message after posting message" - ); - const [postedMessageId] = pyEnv['mail.message'].search([], { order: 'id DESC' }); - const message = document.querySelector(`.o_MessageView`); - assert.strictEqual( - parseInt(message.dataset.messageId), - postedMessageId, - "new message in thread should be linked to newly created message from message post" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_authorName`).textContent, - "Mitchell Admin", - "new message in thread should be from current partner name" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_content`).textContent, - "Test", - "new message in thread should have content typed from composer text input" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.containsNone( + document.body, + ".o_MessageView", + "should not have any message initially in channel" + ); -QUnit.test('post message on channel with "Enter" keyboard shortcut', async function (assert) { - assert.expect(2); + // insert some HTML in editable + await insertText(".o_ComposerTextInputView_textarea", "Test"); + const kevt = new window.KeyboardEvent("keydown", { key: "Enter", shiftKey: true }); + document.querySelector(".o_ComposerTextInputView_textarea").dispatchEvent(kevt); + await nextAnimationFrame(); + assert.containsNone( + document.body, + ".o_MessageView", + "should still not have any message in channel after pressing 'Shift-Enter' in text input of composer" + ); + } + ); + QUnit.test("rendering of inbox message", async function (assert) { + // AKU TODO: kinda message specific test + assert.expect(8); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.containsNone( - document.body, - '.o_MessageView', - "should not have any message initially in channel" - ); - - // insert some HTML in editable - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await afterNextRender(() => { - const kevt = new window.KeyboardEvent('keydown', { key: "Enter" }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should now have single message in channel after posting message from pressing 'Enter' in text input of composer" - ); -}); - -QUnit.test('do not post message on channel with "SHIFT-Enter" keyboard shortcut', async function (assert) { - // Note that test doesn't assert SHIFT-Enter makes a newline, because this - // default browser cannot be simulated with just dispatching - // programmatically crafted events... - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.containsNone( - document.body, - '.o_MessageView', - "should not have any message initially in channel" - ); - - // insert some HTML in editable - await insertText('.o_ComposerTextInputView_textarea', "Test"); - const kevt = new window.KeyboardEvent('keydown', { key: "Enter", shiftKey: true }); - document.querySelector('.o_ComposerTextInputView_textarea').dispatchEvent(kevt); - await nextAnimationFrame(); - assert.containsNone( - document.body, - '.o_MessageView', - "should still not have any message in channel after pressing 'Shift-Enter' in text input of composer" - ); -}); -QUnit.test('rendering of inbox message', async function (assert) { - // AKU TODO: kinda message specific test - assert.expect(8); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Refactoring" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until inbox displayed its messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread === messaging.inbox.thread + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Refactoring" }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], // for consistency + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until inbox displayed its messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread === messaging.inbox.thread + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(".o_MessageView").length, + 1, + "should display a message" ); - }, - }); - assert.strictEqual( - document.querySelectorAll('.o_MessageView').length, - 1, - "should display a message" - ); - const message = document.querySelector('.o_MessageView'); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageView_originThread`).length, - 1, - "should display origin thread of message" - ); - assert.strictEqual( - message.querySelector(`:scope .o_MessageView_originThread`).textContent, - " on Refactoring", - "should display origin thread name" - ); - await afterNextRender(() => message.click()); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView`).length, - 4, - "should display 4 actions" - ); - assert.containsOnce( - document.body, - '.o_MessageActionView_actionReaction', - "should have action to add a reaction" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, - 1, - "should display star action" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView_actionReplyTo`).length, - 1, - "should display reply action" - ); - assert.strictEqual( - message.querySelectorAll(`:scope .o_MessageActionView_actionMarkAsRead`).length, - 1, - "should display mark as read action" - ); -}); + const message = document.querySelector(".o_MessageView"); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageView_originThread`).length, + 1, + "should display origin thread of message" + ); + assert.strictEqual( + message.querySelector(`:scope .o_MessageView_originThread`).textContent, + " on Refactoring", + "should display origin thread name" + ); + await afterNextRender(() => message.click()); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView`).length, + 4, + "should display 4 actions" + ); + assert.containsOnce( + document.body, + ".o_MessageActionView_actionReaction", + "should have action to add a reaction" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView_actionToggleStar`).length, + 1, + "should display star action" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView_actionReplyTo`).length, + 1, + "should display reply action" + ); + assert.strictEqual( + message.querySelectorAll(`:scope .o_MessageActionView_actionMarkAsRead`).length, + 1, + "should display mark as read action" + ); + }); -QUnit.test('mark channel as seen on last message visible [REQUIRE FOCUS]', async function (assert) { - assert.expect(3); + QUnit.test( + "mark channel as seen on last message visible [REQUIRE FOCUS]", + async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 1, - partner_id: pyEnv.currentPartnerId, - }], - ], - }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - const { afterEvent, openDiscuss } = await start(); - await openDiscuss(); - assert.containsOnce( - document.body, - `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, - "should have discuss sidebar item with the channel" - ); - assert.hasClass( - document.querySelector(` + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 1, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { afterEvent, openDiscuss } = await start(); + await openDiscuss(); + assert.containsOnce( + document.body, + `.o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"]`, + "should have discuss sidebar item with the channel" + ); + assert.hasClass( + document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `), - 'o-unread', - "sidebar item of channel 1 should be unread" - ); + "o-unread", + "sidebar item of channel 1 should be unread" + ); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => { - document.querySelector(` + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => { + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - `).click(); - }, - message: "should wait until last seen by current partner message id changed", - predicate: ({ thread }) => { - return ( - thread.channel && - thread.channel.id === mailChannelId1 && - thread.lastSeenByCurrentPartnerMessageId === mailMessageId1 - ); - }, - })); - assert.doesNotHaveClass( - document.querySelector(` + ` + ) + .click(); + }, + message: + "should wait until last seen by current partner message id changed", + predicate: ({ thread }) => { + return ( + thread.channel && + thread.channel.id === mailChannelId1 && + thread.lastSeenByCurrentPartnerMessageId === mailMessageId1 + ); + }, + }) + ); + assert.doesNotHaveClass( + document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `), - 'o-unread', - "sidebar item of channel 1 should not longer be unread" - ); -}); + "o-unread", + "sidebar item of channel 1 should not longer be unread" + ); + } + ); -QUnit.test('receive new needaction messages', async function (assert) { - assert.expect(12); + QUnit.test("receive new needaction messages", async function (assert) { + assert.expect(12); - const { messaging, openDiscuss, pyEnv } = await start(); - await openDiscuss(); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + const { messaging, openDiscuss, pyEnv } = await start(); + await openDiscuss(); + assert.ok( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `), - "should have inbox in sidebar" - ); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox should be current discuss thread" - ); - assert.notOk( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + "should have inbox in sidebar" + ); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox should be current discuss thread" + ); + assert.notOk( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `), - "inbox item in sidebar should not have any counter" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, - 0, - "should have no messages in inbox initially" - ); + "inbox item in sidebar should not have any counter" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, + 0, + "should have no messages in inbox initially" + ); - // simulate receiving a new needaction message - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.message/inbox', { - 'body': "not empty", - 'id': 100, - 'needaction_partner_ids': [pyEnv.currentPartnerId], - 'model': 'res.partner', - 'res_id': 20, - }); - }); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + // simulate receiving a new needaction message + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.message/inbox", { + body: "not empty", + id: 100, + needaction_partner_ids: [pyEnv.currentPartnerId], + model: "res.partner", + res_id: 20, + }); + }); + assert.ok( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `), - "inbox item in sidebar should now have counter" - ); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + "inbox item in sidebar should now have counter" + ); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - '1', - "inbox item in sidebar should have counter of '1'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, - 1, - "should have one message in inbox" - ); - assert.strictEqual( - parseInt(document.querySelector(`.o_DiscussView_thread .o_MessageView`).dataset.messageId), - 100, - "should display newly received needaction message" - ); + "1", + "inbox item in sidebar should have counter of '1'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, + 1, + "should have one message in inbox" + ); + assert.strictEqual( + parseInt( + document.querySelector(`.o_DiscussView_thread .o_MessageView`).dataset.messageId + ), + 100, + "should display newly received needaction message" + ); - // simulate receiving another new needaction message - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(pyEnv.currentPartner, 'mail.message/inbox', { - 'body': "not empty", - 'id': 101, - 'needaction_partner_ids': [pyEnv.currentPartnerId], - 'model': 'res.partner', - 'res_id': 20, - }); - }); - assert.strictEqual( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + // simulate receiving another new needaction message + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.message/inbox", { + body: "not empty", + id: 101, + needaction_partner_ids: [pyEnv.currentPartnerId], + model: "res.partner", + res_id: 20, + }); + }); + assert.strictEqual( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] .o_DiscussSidebarMailboxView_counter `).textContent, - '2', - "inbox item in sidebar should have counter of '2'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, - 2, - "should have 2 messages in inbox" - ); - assert.ok( - document.querySelector(` + "2", + "inbox item in sidebar should have counter of '2'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageView`).length, + 2, + "should have 2 messages in inbox" + ); + assert.ok( + document.querySelector(` .o_DiscussView_thread .o_MessageView[data-message-id="100"] `), - "should still display 1st needaction message" - ); - assert.ok( - document.querySelector(` + "should still display 1st needaction message" + ); + assert.ok( + document.querySelector(` .o_DiscussView_thread .o_MessageView[data-message-id="101"] `), - "should display 2nd needaction message" - ); -}); + "should display 2nd needaction message" + ); + }); -QUnit.test('reply to message from inbox (message linked to document)', async function (assert) { - assert.expect(19); + QUnit.test( + "reply to message from inbox (message linked to document)", + async function (assert) { + assert.expect(19); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Refactoring" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "

Test

", - date: "2019-04-20 11:00:00", - message_type: 'comment', - needaction: true, - model: 'res.partner', - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }); - const { click, insertText, openDiscuss } = await start({ - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('message_post'); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Refactoring" }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "

Test

", + date: "2019-04-20 11:00:00", + message_type: "comment", + needaction: true, + model: "res.partner", + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }); + const { click, insertText, openDiscuss } = await start({ + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("message_post"); + assert.strictEqual( + args.thread_model, + "res.partner", + "should post message to record with model 'res.partner'" + ); + assert.strictEqual( + args.thread_id, + resPartnerId1, + "should post message to record with Id 20" + ); + assert.strictEqual( + args.post_data.body, + "Test", + "should post with provided content in composer input" + ); + assert.strictEqual( + args.post_data.message_type, + "comment", + "should set message type as 'comment'" + ); + } + }, + services: { + notification: makeFakeNotificationService((notification) => { + assert.ok(true, "should display a notification after posting reply"); + assert.strictEqual( + notification, + 'Message posted on "Refactoring"', + "notification should tell that message has been posted to the record 'Refactoring'" + ); + }), + }, + }); + await openDiscuss(); assert.strictEqual( - args.thread_model, - 'res.partner', - "should post message to record with model 'res.partner'" + document.querySelectorAll(".o_MessageView").length, + 1, + "should display a single message" ); assert.strictEqual( - args.thread_id, - resPartnerId1, - "should post message to record with Id 20" + parseInt(document.querySelector(".o_MessageView").dataset.messageId), + mailMessageId1, + "should display message with ID 100" ); assert.strictEqual( - args.post_data.body, - "Test", - "should post with provided content in composer input" + document.querySelector(".o_MessageView_originThread").textContent, + " on Refactoring", + "should display message originates from record 'Refactoring'" + ); + + await click(".o_MessageView"); + await click(".o_MessageActionView_actionReplyTo"); + assert.ok( + document.querySelector(".o_MessageView").classList.contains("o-selected"), + "message should be selected after clicking on reply icon" + ); + assert.ok( + document.querySelector(".o_ComposerView"), + "should have composer after clicking on reply to message" ); assert.strictEqual( - args.post_data.message_type, - "comment", - "should set message type as 'comment'" + document.querySelector(`.o_ComposerView_threadName`).textContent, + " on: Refactoring", + "composer should display origin thread name of message" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ComposerTextInputView_textarea`), + "composer text input should be auto-focus" + ); + + await insertText(".o_ComposerTextInputView_textarea", "Test"); + await click(".o_ComposerView_buttonSend"); + assert.verifySteps(["message_post"]); + assert.notOk( + document.querySelector(".o_ComposerView"), + "should no longer have composer after posting reply to message" + ); + assert.strictEqual( + document.querySelectorAll(".o_MessageView").length, + 1, + "should still display a single message after posting reply" + ); + assert.strictEqual( + parseInt(document.querySelector(".o_MessageView").dataset.messageId), + mailMessageId1, + "should still display message with ID 100 after posting reply" + ); + assert.notOk( + document.querySelector(".o_MessageView").classList.contains("o-selected"), + "message should not longer be selected after posting reply" ); } - }, - services: { - notification: makeFakeNotificationService(notification => { - assert.ok( - true, - "should display a notification after posting reply" - ); - assert.strictEqual( - notification, - "Message posted on \"Refactoring\"", - "notification should tell that message has been posted to the record 'Refactoring'" - ); - }), - }, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll('.o_MessageView').length, - 1, - "should display a single message" - ); - assert.strictEqual( - parseInt(document.querySelector('.o_MessageView').dataset.messageId), - mailMessageId1, - "should display message with ID 100" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_originThread').textContent, - " on Refactoring", - "should display message originates from record 'Refactoring'" - ); + ); - await click('.o_MessageView'); - await click('.o_MessageActionView_actionReplyTo'); - assert.ok( - document.querySelector('.o_MessageView').classList.contains('o-selected'), - "message should be selected after clicking on reply icon" - ); - assert.ok( - document.querySelector('.o_ComposerView'), - "should have composer after clicking on reply to message" - ); - assert.strictEqual( - document.querySelector(`.o_ComposerView_threadName`).textContent, - " on: Refactoring", - "composer should display origin thread name of message" - ); - assert.strictEqual( - document.activeElement, - document.querySelector(`.o_ComposerTextInputView_textarea`), - "composer text input should be auto-focus" - ); + QUnit.test('messages marked as read move to "History" mailbox', async function (assert) { + assert.expect(10); - await insertText('.o_ComposerTextInputView_textarea', "Test"); - await click('.o_ComposerView_buttonSend'); - assert.verifySteps(['message_post']); - assert.notOk( - document.querySelector('.o_ComposerView'), - "should no longer have composer after posting reply to message" - ); - assert.strictEqual( - document.querySelectorAll('.o_MessageView').length, - 1, - "should still display a single message after posting reply" - ); - assert.strictEqual( - parseInt(document.querySelector('.o_MessageView').dataset.messageId), - mailMessageId1, - "should still display message with ID 100 after posting reply" - ); - assert.notOk( - document.querySelector('.o_MessageView').classList.contains('o-selected'), - "message should not longer be selected after posting reply" - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + body: "not empty", + model: "mail.channel", // value to link message to channel + needaction: true, + res_id: mailChannelId1, // id of related channel + }, + { + body: "not empty", + model: "mail.channel", // value to link message to channel + needaction: true, + res_id: mailChannelId1, // id of related channel + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }, + { + mail_message_id: mailMessageId2, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }, + ]); + const { click, messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: "mail.box_history", + }, + }, + }); + await openDiscuss(); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] + ` + ) + .classList.contains("o-active"), + "history mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, + 1, + "should have empty thread in history" + ); -QUnit.test('messages marked as read move to "History" mailbox', async function (assert) { - assert.expect(10); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - body: "not empty", - model: 'mail.channel', // value to link message to channel - needaction: true, - res_id: mailChannelId1, // id of related channel - }, - { - body: "not empty", - model: 'mail.channel', // value to link message to channel - needaction: true, - res_id: mailChannelId1, // id of related channel - } - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }, - { - mail_message_id: mailMessageId2, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - } - ]); - const { click, messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: 'mail.box_history', - }, - }, - }); - await openDiscuss(); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] - `).classList.contains('o-active'), - "history mailbox should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, - 1, - "should have empty thread in history" - ); - - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox mailbox should be active thread" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, - 0, - "inbox mailbox should not be empty" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`).length, - 2, - "inbox mailbox should have 2 messages" - ); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox mailbox should be active thread" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, + 0, + "inbox mailbox should not be empty" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`) + .length, + 2, + "inbox mailbox should have 2 messages" + ); - await click('.o_ThreadViewTopbar_markAllReadButton'); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] - `).classList.contains('o-active'), - "inbox mailbox should still be active after mark as read" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, - 1, - "inbox mailbox should now be empty after mark as read" - ); + await click(".o_ThreadViewTopbar_markAllReadButton"); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] + ` + ) + .classList.contains("o-active"), + "inbox mailbox should still be active after mark as read" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, + 1, + "inbox mailbox should now be empty after mark as read" + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `); - assert.ok( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] - `).classList.contains('o-active'), - "history mailbox should be active" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, - 0, - "history mailbox should not be empty after mark as read" - ); - assert.strictEqual( - document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`).length, - 2, - "history mailbox should have 2 messages" - ); -}); + assert.ok( + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] + ` + ) + .classList.contains("o-active"), + "history mailbox should be active" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_empty`).length, + 0, + "history mailbox should not be empty after mark as read" + ); + assert.strictEqual( + document.querySelectorAll(`.o_DiscussView_thread .o_MessageListView_message`) + .length, + 2, + "history mailbox should have 2 messages" + ); + }); -QUnit.test('mark a single message as read should only move this message to "History" mailbox', async function (assert) { - assert.expect(9); + QUnit.test( + 'mark a single message as read should only move this message to "History" mailbox', + async function (assert) { + assert.expect(9); - const pyEnv = await startServer(); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - { - body: "not empty", - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - }, - { - body: "not empty", - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - } - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }, - { - mail_message_id: mailMessageId2, - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - } - ]); - const { click, messaging, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: 'mail.box_history', - }, - }, - }); - await openDiscuss(); - assert.hasClass( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + const pyEnv = await startServer(); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + { + body: "not empty", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + }, + { + body: "not empty", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + { + mail_message_id: mailMessageId2, + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }, + ]); + const { click, messaging, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: "mail.box_history", + }, + }, + }); + await openDiscuss(); + assert.hasClass( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `), - 'o-active', - "history mailbox should initially be the active thread" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_empty', - "history mailbox should initially be empty" - ); + "o-active", + "history mailbox should initially be the active thread" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_empty", + "history mailbox should initially be empty" + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `); - assert.hasClass( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.inbox.localId - }"] + assert.hasClass( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.inbox.localId}"] `), - 'o-active', - "inbox mailbox should be active thread after clicking on it" - ); - assert.containsN( - document.body, - '.o_MessageView', - 2, - "inbox mailbox should have 2 messages" - ); + "o-active", + "inbox mailbox should be active thread after clicking on it" + ); + assert.containsN( + document.body, + ".o_MessageView", + 2, + "inbox mailbox should have 2 messages" + ); - await click(` + await click(` .o_MessageView[data-message-id="${mailMessageId1}"] `); - await click(` + await click(` .o_MessageView[data-message-id="${mailMessageId1}"] .o_MessageActionView_actionMarkAsRead `); - assert.containsOnce( - document.body, - '.o_MessageView', - "inbox mailbox should have one less message after clicking mark as read" - ); - assert.containsOnce( - document.body, - `.o_MessageView[data-message-id="${mailMessageId2}"]`, - "message still in inbox should be the one not marked as read" - ); + assert.containsOnce( + document.body, + ".o_MessageView", + "inbox mailbox should have one less message after clicking mark as read" + ); + assert.containsOnce( + document.body, + `.o_MessageView[data-message-id="${mailMessageId2}"]`, + "message still in inbox should be the one not marked as read" + ); - await click(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + await click(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `); - assert.hasClass( - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] + assert.hasClass( + document.querySelector(` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] `), - 'o-active', - "history mailbox should be active after clicking on it" - ); - assert.containsOnce( - document.body, - '.o_MessageView', - "history mailbox should have only 1 message after mark as read" - ); - assert.containsOnce( - document.body, - `.o_MessageView[data-message-id="${mailMessageId1}"]`, - "message moved in history should be the one marked as read" - ); -}); + "o-active", + "history mailbox should be active after clicking on it" + ); + assert.containsOnce( + document.body, + ".o_MessageView", + "history mailbox should have only 1 message after mark as read" + ); + assert.containsOnce( + document.body, + `.o_MessageView[data-message-id="${mailMessageId1}"]`, + "message moved in history should be the one marked as read" + ); + } + ); -QUnit.test('all messages in "Inbox" in "History" after marked all as read', async function (assert) { - assert.expect(2); + QUnit.test( + 'all messages in "Inbox" in "History" after marked all as read', + async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - for (let i = 0; i < 40; i++) { - const currentMailMessageId = pyEnv['mail.message'].create({ - body: "not empty", - needaction: true, - }); - pyEnv['mail.notification'].create({ - mail_message_id: currentMailMessageId, // id of related message - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, // must be for current partner - }); + const pyEnv = await startServer(); + for (let i = 0; i < 40; i++) { + const currentMailMessageId = pyEnv["mail.message"].create({ + body: "not empty", + needaction: true, + }); + pyEnv["mail.notification"].create({ + mail_message_id: currentMailMessageId, // id of related message + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, // must be for current partner + }); + } + const { afterEvent, click, messaging, openDiscuss } = await start(); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: openDiscuss, + message: "should wait until inbox scrolled to its last message initially", + predicate: ({ orderedMessages, scrollTop, thread }) => { + const messageList = document.querySelector( + `.o_DiscussView_thread .o_ThreadView_messageList` + ); + return ( + thread === messaging.inbox.thread && + orderedMessages.length === 30 && + isScrolledToBottom(messageList) + ); + }, + }); - } - const { afterEvent, click, messaging, openDiscuss } = await start(); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: openDiscuss, - message: "should wait until inbox scrolled to its last message initially", - predicate: ({ orderedMessages, scrollTop, thread }) => { - const messageList = document.querySelector(`.o_DiscussView_thread .o_ThreadView_messageList`); - return ( - thread === messaging.inbox.thread && - orderedMessages.length === 30 && - isScrolledToBottom(messageList) - ); - }, - }); + await click(".o_ThreadViewTopbar_markAllReadButton"); + assert.containsNone( + document.body, + ".o_MessageView", + "there should no message in Inbox anymore" + ); - await click('.o_ThreadViewTopbar_markAllReadButton'); - assert.containsNone( - document.body, - '.o_MessageView', - "there should no message in Inbox anymore" - ); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => { + document + .querySelector( + ` + .o_DiscussSidebarMailboxView[data-mailbox-local-id="${messaging.history.localId}"] + ` + ) + .click(); + }, + message: + "should wait until history scrolled to its last message after opening it from the discuss sidebar", + predicate: ({ orderedMessages, scrollTop, thread }) => { + const messageList = document.querySelector(".o_MessageListView"); + return ( + thread && + thread.mailbox && + thread.mailbox === messaging.history && + orderedMessages.length === 30 && + isScrolledToBottom(messageList) + ); + }, + }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => { - document.querySelector(` - .o_DiscussSidebarMailboxView[data-mailbox-local-id="${ - messaging.history.localId - }"] - `).click(); - }, - message: "should wait until history scrolled to its last message after opening it from the discuss sidebar", - predicate: ({ orderedMessages, scrollTop, thread }) => { - const messageList = document.querySelector('.o_MessageListView'); - return ( - thread && - thread.mailbox && - thread.mailbox === messaging.history && - orderedMessages.length === 30 && - isScrolledToBottom(messageList) - ); - }, - }); + // simulate a scroll to top to load more messages + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => (document.querySelector(".o_MessageListView").scrollTop = 0), + message: + "should wait until mailbox history loaded more messages after scrolling to top", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "more-messages-loaded" && + threadViewer.thread.mailbox && + threadViewer.thread.mailbox === messaging.history + ); + }, + }); + assert.containsN( + document.body, + ".o_MessageView", + 40, + "there should be 40 messages in History" + ); + } + ); - // simulate a scroll to top to load more messages - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => document.querySelector('.o_MessageListView').scrollTop = 0, - message: "should wait until mailbox history loaded more messages after scrolling to top", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'more-messages-loaded' && - threadViewer.thread.mailbox && - threadViewer.thread.mailbox === messaging.history - ); - }, - }); - assert.containsN( - document.body, - '.o_MessageView', - 40, - "there should be 40 messages in History" - ); -}); + QUnit.test( + "receive new chat message: out of odoo focus (notification, channel)", + async function (assert) { + assert.expect(4); -QUnit.test('receive new chat message: out of odoo focus (notification, channel)', async function (assert) { - assert.expect(4); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ channel_type: "chat" }); + const { env, openDiscuss } = await start({ + services: { + presence: makeFakePresenceService({ isOdooFocused: () => false }), + }, + }); + await openDiscuss(); + env.bus.addEventListener("set_title_part", ({ detail: payload }) => { + assert.step("set_title_part"); + assert.strictEqual(payload.part, "_chat"); + assert.strictEqual(payload.title, "1 Message"); + }); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ channel_type: 'chat' }); - const { env, openDiscuss } = await start({ - services: { - presence: makeFakePresenceService({ isOdooFocused: () => false }), - }, - }); - await openDiscuss(); - env.bus.addEventListener('set_title_part', ({ detail: payload }) => { - assert.step('set_title_part'); - assert.strictEqual(payload.part, '_chat'); - assert.strictEqual(payload.title, "1 Message"); - }); + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId1], + ])[0]; + // simulate receiving a new message with odoo focused + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel/new_message", { + id: mailChannelId1, + message: { + id: 126, + model: "mail.channel", + res_id: mailChannelId1, + }, + }); + }); + assert.verifySteps(["set_title_part"]); + } + ); - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - // simulate receiving a new message with odoo focused - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel/new_message', { - 'id': mailChannelId1, - 'message': { - id: 126, - model: 'mail.channel', - res_id: mailChannelId1, - }, - }); - }); - assert.verifySteps(['set_title_part']); -}); + QUnit.test( + "receive new chat message: out of odoo focus (notification, chat)", + async function (assert) { + assert.expect(4); -QUnit.test('receive new chat message: out of odoo focus (notification, chat)', async function (assert) { - assert.expect(4); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ channel_type: "chat" }); + const { env, openDiscuss } = await start({ + services: { + presence: makeFakePresenceService({ isOdooFocused: () => false }), + }, + }); + await openDiscuss(); + env.bus.addEventListener("set_title_part", ({ detail: payload }) => { + assert.step("set_title_part"); + assert.strictEqual(payload.part, "_chat"); + assert.strictEqual(payload.title, "1 Message"); + }); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ channel_type: "chat" }); - const { env, openDiscuss } = await start({ - services: { - presence: makeFakePresenceService({ isOdooFocused: () => false }), - }, - }); - await openDiscuss(); - env.bus.addEventListener('set_title_part', ({ detail: payload }) => { - assert.step('set_title_part'); - assert.strictEqual(payload.part, '_chat'); - assert.strictEqual(payload.title, "1 Message"); - }); + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId1], + ])[0]; + // simulate receiving a new message with odoo focused + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel/new_message", { + id: mailChannelId1, + message: { + id: 126, + model: "mail.channel", + res_id: mailChannelId1, + }, + }); + }); + assert.verifySteps(["set_title_part"]); + } + ); - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - // simulate receiving a new message with odoo focused - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel/new_message', { - 'id': mailChannelId1, - 'message': { - id: 126, - model: 'mail.channel', - res_id: mailChannelId1, - }, - }); - }); - assert.verifySteps(['set_title_part']); -}); + QUnit.test( + "receive new chat messages: out of odoo focus (tab title)", + async function (assert) { + assert.expect(12); -QUnit.test('receive new chat messages: out of odoo focus (tab title)', async function (assert) { - assert.expect(12); + let step = 0; + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { channel_type: "chat" }, + { channel_type: "chat" }, + ]); + const { env, openDiscuss } = await start({ + services: { + presence: makeFakePresenceService({ isOdooFocused: () => false }), + }, + }); + await openDiscuss(); + env.bus.addEventListener("set_title_part", ({ detail: payload }) => { + step++; + assert.step("set_title_part"); + assert.strictEqual(payload.part, "_chat"); + if (step === 1) { + assert.strictEqual(payload.title, "1 Message"); + } + if (step === 2) { + assert.strictEqual(payload.title, "2 Messages"); + } + if (step === 3) { + assert.strictEqual(payload.title, "3 Messages"); + } + }); - let step = 0; - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { channel_type: 'chat' }, - { channel_type: 'chat' }, - ]); - const { env, openDiscuss } = await start({ - services: { - presence: makeFakePresenceService({ isOdooFocused: () => false }), - }, - }); - await openDiscuss(); - env.bus.addEventListener('set_title_part', ({ detail: payload }) => { - step++; - assert.step('set_title_part'); - assert.strictEqual(payload.part, '_chat'); - if (step === 1) { - assert.strictEqual(payload.title, "1 Message"); - } - if (step === 2) { - assert.strictEqual(payload.title, "2 Messages"); - } - if (step === 3) { - assert.strictEqual(payload.title, "3 Messages"); - } - }); + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId1], + ])[0]; + // simulate receiving a new message in chat 1 with odoo focused + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel/new_message", { + id: mailChannelId1, + message: { + id: 126, + model: "mail.channel", + res_id: mailChannelId1, + }, + }); + }); + assert.verifySteps(["set_title_part"]); - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - // simulate receiving a new message in chat 1 with odoo focused - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel/new_message', { - 'id': mailChannelId1, - 'message': { - id: 126, - model: 'mail.channel', - res_id: mailChannelId1, - }, - }); - }); - assert.verifySteps(['set_title_part']); + const mailChannel2 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId2], + ])[0]; + // simulate receiving a new message in chat 2 with odoo focused + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel2, "mail.channel/new_message", { + id: mailChannelId2, + message: { + id: 127, + model: "mail.channel", + res_id: mailChannelId2, + }, + }); + }); + assert.verifySteps(["set_title_part"]); - const mailChannel2 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId2]])[0]; - // simulate receiving a new message in chat 2 with odoo focused - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel2, 'mail.channel/new_message', { - 'id': mailChannelId2, - 'message': { - id: 127, - model: 'mail.channel', - res_id: mailChannelId2, - }, - }); - }); - assert.verifySteps(['set_title_part']); + // simulate receiving another new message in chat 2 with odoo focused + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel2, "mail.channel/new_message", { + id: mailChannelId2, + message: { + id: 128, + model: "mail.channel", + res_id: mailChannelId2, + }, + }); + }); + assert.verifySteps(["set_title_part"]); + } + ); - // simulate receiving another new message in chat 2 with odoo focused - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel2, 'mail.channel/new_message', { - 'id': mailChannelId2, - 'message': { - id: 128, - model: 'mail.channel', - res_id: mailChannelId2, - }, - }); - }); - assert.verifySteps(['set_title_part']); -}); + QUnit.test("auto-focus composer on opening thread", async function (assert) { + assert.expect(14); -QUnit.test('auto-focus composer on opening thread', async function (assert) { - assert.expect(14); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - pyEnv['mail.channel'].create([ - { name: "General" }, - { - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - } - ]); - const { click, openDiscuss } = await start(); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(` + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + pyEnv["mail.channel"].create([ + { name: "General" }, + { + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }, + ]); + const { click, openDiscuss } = await start(); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarMailboxView[data-mailbox-name="Inbox"] `).length, - 1, - "should have mailbox 'Inbox' in the sidebar" - ); - assert.ok( - document.querySelector(` + 1, + "should have mailbox 'Inbox' in the sidebar" + ); + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarMailboxView[data-mailbox-name="Inbox"] - `).classList.contains('o-active'), - "mailbox 'Inbox' should be active initially" - ); - assert.strictEqual( - document.querySelectorAll(` + ` + ) + .classList.contains("o-active"), + "mailbox 'Inbox' should be active initially" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarCategoryItem[data-channel-name="General"] `).length, - 1, - "should have channel 'General' in the sidebar" - ); - assert.notOk( - document.querySelector(` + 1, + "should have channel 'General' in the sidebar" + ); + assert.notOk( + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-name="General"] - `).classList.contains('o-active'), - "channel 'General' should not be active initially" - ); - assert.strictEqual( - document.querySelectorAll(` + ` + ) + .classList.contains("o-active"), + "channel 'General' should not be active initially" + ); + assert.strictEqual( + document.querySelectorAll(` .o_DiscussSidebarCategoryItem[data-channel-name="Demo User"] `).length, - 1, - "should have chat 'Demo User' in the sidebar" - ); - assert.notOk( - document.querySelector(` + 1, + "should have chat 'Demo User' in the sidebar" + ); + assert.notOk( + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-name="Demo User"] - `).classList.contains('o-active'), - "chat 'Demo User' should not be active initially" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView`).length, - 0, - "there should be no composer when active thread of discuss is mailbox 'Inbox'" - ); + ` + ) + .classList.contains("o-active"), + "chat 'Demo User' should not be active initially" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView`).length, + 0, + "there should be no composer when active thread of discuss is mailbox 'Inbox'" + ); - await click(`.o_DiscussSidebarCategoryItem[data-channel-name="General"]`); - assert.ok( - document.querySelector(` + await click(`.o_DiscussSidebarCategoryItem[data-channel-name="General"]`); + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-name="General"] - `).classList.contains('o-active'), - "channel 'General' should become active after selecting it from the sidebar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView`).length, - 1, - "there should be a composer when active thread of discuss is channel 'General'" - ); - assert.strictEqual( - document.activeElement, - document.querySelector(`.o_ComposerTextInputView_textarea`), - "composer of channel 'General' should be automatically focused on opening" - ); + ` + ) + .classList.contains("o-active"), + "channel 'General' should become active after selecting it from the sidebar" + ); + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView`).length, + 1, + "there should be a composer when active thread of discuss is channel 'General'" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ComposerTextInputView_textarea`), + "composer of channel 'General' should be automatically focused on opening" + ); - document.querySelector(`.o_ComposerTextInputView_textarea`).blur(); - assert.notOk( - document.activeElement === document.querySelector(`.o_ComposerTextInputView_textarea`), - "composer of channel 'General' should no longer focused on click away" - ); + document.querySelector(`.o_ComposerTextInputView_textarea`).blur(); + assert.notOk( + document.activeElement === + document.querySelector(`.o_ComposerTextInputView_textarea`), + "composer of channel 'General' should no longer focused on click away" + ); - await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Demo User"]`); - assert.ok( - document.querySelector(` + await click(`.o_DiscussSidebarCategoryItem[data-channel-name="Demo User"]`); + assert.ok( + document + .querySelector( + ` .o_DiscussSidebarCategory_item[data-channel-name="Demo User"] - `).classList.contains('o-active'), - "chat 'Demo User' should become active after selecting it from the sidebar" - ); - assert.strictEqual( - document.querySelectorAll(`.o_ComposerView`).length, - 1, - "there should be a composer when active thread of discuss is chat 'Demo User'" - ); - assert.strictEqual( - document.activeElement, - document.querySelector(`.o_ComposerTextInputView_textarea`), - "composer of chat 'Demo User' should be automatically focused on opening" - ); -}); - -QUnit.test('mark channel as seen if last message is visible when switching channels when the previous channel had a more recent last message than the current channel [REQUIRE FOCUS]', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { - channel_member_ids: [ - [0, 0, { - message_unread_counter: 1, - partner_id: pyEnv.currentPartnerId, - }], - ], - name: 'Bla', - }, - { - channel_member_ids: [ - [0, 0, { - message_unread_counter: 1, - partner_id: pyEnv.currentPartnerId, - }], - ], - name: 'Blu', - }, - ]); - const [mailMessageId1] = pyEnv['mail.message'].create([{ - body: 'oldest message', - model: "mail.channel", - res_id: mailChannelId1, - }, { - body: 'newest message', - model: "mail.channel", - res_id: mailChannelId2, - }]); - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId2}`, - }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until channel 2 loaded its messages initially", - predicate: ({ hint, threadViewer }) => { - return ( - threadViewer.thread.channel && - threadViewer.thread.channel.id === mailChannelId2 && - hint.type === 'messages-loaded' + ` + ) + .classList.contains("o-active"), + "chat 'Demo User' should become active after selecting it from the sidebar" ); - }, - }); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => { - document.querySelector(` + assert.strictEqual( + document.querySelectorAll(`.o_ComposerView`).length, + 1, + "there should be a composer when active thread of discuss is chat 'Demo User'" + ); + assert.strictEqual( + document.activeElement, + document.querySelector(`.o_ComposerTextInputView_textarea`), + "composer of chat 'Demo User' should be automatically focused on opening" + ); + }); + + QUnit.test( + "mark channel as seen if last message is visible when switching channels when the previous channel had a more recent last message than the current channel [REQUIRE FOCUS]", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 1, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + name: "Bla", + }, + { + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 1, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + name: "Blu", + }, + ]); + const [mailMessageId1] = pyEnv["mail.message"].create([ + { + body: "oldest message", + model: "mail.channel", + res_id: mailChannelId1, + }, + { + body: "newest message", + model: "mail.channel", + res_id: mailChannelId2, + }, + ]); + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId2}`, + }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until channel 2 loaded its messages initially", + predicate: ({ hint, threadViewer }) => { + return ( + threadViewer.thread.channel && + threadViewer.thread.channel.id === mailChannelId2 && + hint.type === "messages-loaded" + ); + }, + }); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => { + document + .querySelector( + ` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] - `).click(); - }, - message: "should wait until last seen by current partner message id changed", - predicate: ({ thread }) => { - return ( - thread.channel && - thread.channel.id === mailChannelId1 && - thread.lastSeenByCurrentPartnerMessageId === mailMessageId1 - ); - }, - })); - assert.doesNotHaveClass( - document.querySelector(` + ` + ) + .click(); + }, + message: + "should wait until last seen by current partner message id changed", + predicate: ({ thread }) => { + return ( + thread.channel && + thread.channel.id === mailChannelId1 && + thread.lastSeenByCurrentPartnerMessageId === mailMessageId1 + ); + }, + }) + ); + assert.doesNotHaveClass( + document.querySelector(` .o_DiscussSidebarCategoryItem[data-channel-id="${mailChannelId1}"] `), - 'o-unread', - "sidebar item of channel 1 should no longer be unread" - ); -}); - -QUnit.test('warning on send with shortcut when attempting to post message with still-uploading attachments', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route) { - if (route === '/mail/attachment/upload') { - // simulates attachment is never finished uploading - await new Promise(() => {}); - } - }, - services: { - notification: makeFakeNotificationService((message, options) => { - assert.strictEqual( - message, - "Please wait while the file is uploading.", - "notification content should be about the uploading file" + "o-unread", + "sidebar item of channel 1 should no longer be unread" ); - assert.strictEqual( - options.type, - 'warning', - "notification should be a warning" - ); - assert.step('notification'); - }), - }, - }); - await openDiscuss(); - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - await afterNextRender(() => - inputFiles( - messaging.discuss.threadView.composerView.fileUploader.fileInput, - [file] - ) - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard', - "should have only one attachment" - ); - assert.containsOnce( - document.body, - '.o_AttachmentCard.o-isUploading', - "attachment displayed is being uploaded" - ); - assert.containsOnce( - document.body, - '.o_ComposerView_buttonSend', - "composer send button should be displayed" - ); - - // Try to send message - document - .querySelector(`.o_ComposerTextInputView_textarea`) - .dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter' })); - assert.verifySteps( - ['notification'], - "should have triggered a notification for inability to post message at the moment (some attachments are still being uploaded)" - ); -}); - -QUnit.test('send message only once when enter is pressed twice quickly', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - async mockRPC(route, args) { - if (route === '/mail/message/post') { - assert.step('message_post'); } - }, - }); - await openDiscuss(); - // Type message - await insertText('.o_ComposerTextInputView_textarea', "test message"); - await afterNextRender(() => { - const enterEvent = new window.KeyboardEvent('keydown', { key: 'Enter' }); - document.querySelector(`.o_ComposerTextInputView_textarea`) - .dispatchEvent(enterEvent); - document.querySelector(`.o_ComposerTextInputView_textarea`) - .dispatchEvent(enterEvent); - }); - assert.verifySteps( - ['message_post'], - "The message has been posted only once" - ); -}); + ); -QUnit.test('message being a replied to another message should show message being replied in the message view', async function (assert) { - assert.expect(1); + QUnit.test( + "warning on send with shortcut when attempting to post message with still-uploading attachments", + async function (assert) { + assert.expect(7); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "1st message", - model: 'mail.channel', - res_id: mailChannelId1, - }); - const mailMessageId2 = pyEnv['mail.message'].create({ - body: "2nd message", - model: 'mail.channel', - parent_id: mailMessageId1, - res_id: mailChannelId1, - }); - const { openDiscuss } = await start({ - discuss: { - context: { - active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.querySelector(`.o_MessageView[data-message-id="${mailMessageId2}"]`), - '.o_MessageInReplyToView', - "message being a replied to another message should show message being replied in the message view", - ); -}); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route) { + if (route === "/mail/attachment/upload") { + // simulates attachment is never finished uploading + await new Promise(() => {}); + } + }, + services: { + notification: makeFakeNotificationService((message, options) => { + assert.strictEqual( + message, + "Please wait while the file is uploading.", + "notification content should be about the uploading file" + ); + assert.strictEqual( + options.type, + "warning", + "notification should be a warning" + ); + assert.step("notification"); + }), + }, + }); + await openDiscuss(); + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + await afterNextRender(() => + inputFiles(messaging.discuss.threadView.composerView.fileUploader.fileInput, [ + file, + ]) + ); + assert.containsOnce( + document.body, + ".o_AttachmentCard", + "should have only one attachment" + ); + assert.containsOnce( + document.body, + ".o_AttachmentCard.o-isUploading", + "attachment displayed is being uploaded" + ); + assert.containsOnce( + document.body, + ".o_ComposerView_buttonSend", + "composer send button should be displayed" + ); -}); + // Try to send message + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .dispatchEvent(new window.KeyboardEvent("keydown", { key: "Enter" })); + assert.verifySteps( + ["notification"], + "should have triggered a notification for inability to post message at the moment (some attachments are still being uploaded)" + ); + } + ); + + QUnit.test( + "send message only once when enter is pressed twice quickly", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + async mockRPC(route, args) { + if (route === "/mail/message/post") { + assert.step("message_post"); + } + }, + }); + await openDiscuss(); + // Type message + await insertText(".o_ComposerTextInputView_textarea", "test message"); + await afterNextRender(() => { + const enterEvent = new window.KeyboardEvent("keydown", { key: "Enter" }); + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .dispatchEvent(enterEvent); + document + .querySelector(`.o_ComposerTextInputView_textarea`) + .dispatchEvent(enterEvent); + }); + assert.verifySteps(["message_post"], "The message has been posted only once"); + } + ); + + QUnit.test( + "message being a replied to another message should show message being replied in the message view", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "1st message", + model: "mail.channel", + res_id: mailChannelId1, + }); + const mailMessageId2 = pyEnv["mail.message"].create({ + body: "2nd message", + model: "mail.channel", + parent_id: mailMessageId1, + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + context: { + active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.querySelector(`.o_MessageView[data-message-id="${mailMessageId2}"]`), + ".o_MessageInReplyToView", + "message being a replied to another message should show message being replied in the message view" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/emojis_text_field_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/emojis_text_field_tests.js index ed9f43f7a63..815b41ec8bd 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/emojis_text_field_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/emojis_text_field_tests.js @@ -13,11 +13,11 @@ QUnit.module("Field text emojis", (hooks) => { models: { partner: { fields: { - foo: { type: "char" } + foo: { type: "char" }, }, - records: [{ id: 1 }] - } - } + records: [{ id: 1 }], + }, + }, }; setupViewRegistries(); @@ -29,7 +29,7 @@ QUnit.module("Field text emojis", (hooks) => { resId: 1, resModel: "partner", arch: `
`, - serverData + serverData, }); assert.containsOnce(target, ".o_field_text_emojis"); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js index af096065f28..6ccec694a59 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follow_button_tests.js @@ -1,183 +1,177 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('follow_button_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("follow_button_tests.js"); -QUnit.test('base rendering not editable', async function (assert) { - assert.expect(2); + QUnit.test("base rendering not editable", async function (assert) { + assert.expect(2); - const { openView, pyEnv } = await start(); - await openView({ - res_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - views: [[false, 'form']], + const { openView, pyEnv } = await start(); + await openView({ + res_id: pyEnv.currentPartnerId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_FollowButtonView", + "should have follow button component" + ); + assert.containsOnce( + document.body, + ".o_FollowButtonView_follow", + "should have 'Follow' button" + ); + }); + + QUnit.test("hover following button", async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + const followerId = pyEnv["mail.followers"].create({ + is_active: true, + partner_id: pyEnv.currentPartnerId, + res_id: threadId, + res_model: "res.partner", + }); + pyEnv["res.partner"].write([pyEnv.currentPartnerId], { + message_follower_ids: [followerId], + }); + const { openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_FollowButtonView", + "should have follow button component" + ); + assert.containsOnce( + document.body, + ".o_FollowButtonView_unfollow", + "should have 'Unfollow' button" + ); + assert.strictEqual( + document.querySelector(".o_FollowButtonView_text").textContent.trim(), + "Following", + "'unfollow' button should display 'Following' as text when not hovered" + ); + assert.containsNone( + document.querySelector(".o_FollowButtonView_unfollow"), + ".fa-times", + "'unfollow' button should not contain a cross icon when not hovered" + ); + assert.containsOnce( + document.querySelector(".o_FollowButtonView_unfollow"), + ".fa-check", + "'unfollow' button should contain a check icon when not hovered" + ); + + await afterNextRender(() => { + document + .querySelector(".o_FollowButtonView_unfollow") + .dispatchEvent(new window.MouseEvent("mouseenter")); + }); + assert.strictEqual( + document.querySelector(".o_FollowButtonView_text").textContent.trim(), + "Unfollow", + "'unfollow' button should display 'Unfollow' as text when hovered" + ); + assert.containsOnce( + document.querySelector(".o_FollowButtonView_unfollow"), + ".fa-times", + "'unfollow' button should contain a cross icon when hovered" + ); + assert.containsNone( + document.querySelector(".o_FollowButtonView_unfollow"), + ".fa-check", + "'unfollow' button should not contain a check icon when hovered" + ); + }); + + QUnit.test('click on "follow" button', async function (assert) { + assert.expect(4); + + const { click, openView, pyEnv } = await start(); + await openView({ + res_id: pyEnv.currentPartnerId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_FollowButtonView", + "should have follow button component" + ); + assert.containsOnce( + document.body, + ".o_FollowButtonView_follow", + "should have button follow" + ); + + await click(".o_FollowButtonView_follow"); + assert.containsNone( + document.body, + ".o_FollowButtonView_follow", + "should not have follow button after clicked on follow" + ); + assert.containsOnce( + document.body, + ".o_FollowButtonView_unfollow", + "should have unfollow button after clicked on follow" + ); + }); + + QUnit.test('click on "unfollow" button', async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + pyEnv["mail.followers"].create({ + is_active: true, + partner_id: pyEnv.currentPartnerId, + res_id: threadId, + res_model: "res.partner", + }); + const { click, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_FollowButtonView", + "should have follow button component" + ); + assert.containsNone( + document.body, + ".o_FollowButtonView_follow", + "should not have button follow" + ); + assert.containsOnce( + document.body, + ".o_FollowButtonView_unfollow", + "should have button unfollow" + ); + + await click(".o_FollowButtonView_unfollow"); + assert.containsOnce( + document.body, + ".o_FollowButtonView_follow", + "should have follow button after clicked on unfollow" + ); + assert.containsNone( + document.body, + ".o_FollowButtonView_unfollow", + "should not have unfollow button after clicked on unfollow" + ); + }); }); - assert.containsOnce( - document.body, - '.o_FollowButtonView', - "should have follow button component" - ); - assert.containsOnce( - document.body, - '.o_FollowButtonView_follow', - "should have 'Follow' button" - ); -}); - -QUnit.test('hover following button', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - const followerId = pyEnv['mail.followers'].create({ - is_active: true, - partner_id: pyEnv.currentPartnerId, - res_id: threadId, - res_model: 'res.partner', - }); - pyEnv['res.partner'].write([pyEnv.currentPartnerId], { - message_follower_ids: [followerId], - }); - const { openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_FollowButtonView', - "should have follow button component" - ); - assert.containsOnce( - document.body, - '.o_FollowButtonView_unfollow', - "should have 'Unfollow' button" - ); - assert.strictEqual( - document.querySelector('.o_FollowButtonView_text').textContent.trim(), - 'Following', - "'unfollow' button should display 'Following' as text when not hovered" - ); - assert.containsNone( - document.querySelector('.o_FollowButtonView_unfollow'), - '.fa-times', - "'unfollow' button should not contain a cross icon when not hovered" - ); - assert.containsOnce( - document.querySelector('.o_FollowButtonView_unfollow'), - '.fa-check', - "'unfollow' button should contain a check icon when not hovered" - ); - - await afterNextRender(() => { - document - .querySelector('.o_FollowButtonView_unfollow') - .dispatchEvent(new window.MouseEvent('mouseenter')); - } - ); - assert.strictEqual( - document.querySelector('.o_FollowButtonView_text').textContent.trim(), - 'Unfollow', - "'unfollow' button should display 'Unfollow' as text when hovered" - ); - assert.containsOnce( - document.querySelector('.o_FollowButtonView_unfollow'), - '.fa-times', - "'unfollow' button should contain a cross icon when hovered" - ); - assert.containsNone( - document.querySelector('.o_FollowButtonView_unfollow'), - '.fa-check', - "'unfollow' button should not contain a check icon when hovered" - ); -}); - -QUnit.test('click on "follow" button', async function (assert) { - assert.expect(4); - - const { click, openView, pyEnv } = await start(); - await openView({ - res_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_FollowButtonView', - "should have follow button component" - ); - assert.containsOnce( - document.body, - '.o_FollowButtonView_follow', - "should have button follow" - ); - - await click('.o_FollowButtonView_follow'); - assert.containsNone( - document.body, - '.o_FollowButtonView_follow', - "should not have follow button after clicked on follow" - ); - assert.containsOnce( - document.body, - '.o_FollowButtonView_unfollow', - "should have unfollow button after clicked on follow" - ); -}); - -QUnit.test('click on "unfollow" button', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - pyEnv['mail.followers'].create({ - is_active: true, - partner_id: pyEnv.currentPartnerId, - res_id: threadId, - res_model: 'res.partner', - }); - const { click, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_FollowButtonView', - "should have follow button component" - ); - assert.containsNone( - document.body, - '.o_FollowButtonView_follow', - "should not have button follow" - ); - assert.containsOnce( - document.body, - '.o_FollowButtonView_unfollow', - "should have button unfollow" - ); - - await click('.o_FollowButtonView_unfollow'); - assert.containsOnce( - document.body, - '.o_FollowButtonView_follow', - "should have follow button after clicked on unfollow" - ); - assert.containsNone( - document.body, - '.o_FollowButtonView_unfollow', - "should not have unfollow button after clicked on unfollow" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js index f9a954cd8fc..1f6d23026bd 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follower_list_menu_tests.js @@ -1,457 +1,462 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -import { nextTick } from 'web.test_utils'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { nextTick } from "web.test_utils"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('follower_list_menu_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("follower_list_menu_tests.js"); -QUnit.test('base rendering not editable', async function (assert) { - assert.expect(5); + QUnit.test("base rendering not editable", async function (assert) { + assert.expect(5); - const { openView } = await start(); - await openView( - { - res_model: 'res.partner', - views: [[false, 'form']], - }, - { mode: 'edit' }, - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "should have followers menu component" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_buttonFollowers', - "should have followers button" - ); - assert.ok( - document.querySelector('.o_FollowerListMenuView_buttonFollowers').disabled, - "followers button should be disabled" - ); - assert.containsNone( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should not be opened" - ); + const { openView } = await start(); + await openView( + { + res_model: "res.partner", + views: [[false, "form"]], + }, + { mode: "edit" } + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "should have followers menu component" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_buttonFollowers", + "should have followers button" + ); + assert.ok( + document.querySelector(".o_FollowerListMenuView_buttonFollowers").disabled, + "followers button should be disabled" + ); + assert.containsNone( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should not be opened" + ); - document.querySelector('.o_FollowerListMenuView_buttonFollowers').click(); - await nextTick(); - assert.containsNone( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should still be closed as button is disabled" - ); -}); + document.querySelector(".o_FollowerListMenuView_buttonFollowers").click(); + await nextTick(); + assert.containsNone( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should still be closed as button is disabled" + ); + }); -QUnit.test('base rendering editable', async function (assert) { - assert.expect(5); + QUnit.test("base rendering editable", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); - const { click, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(route, args); - res['hasWriteAccess'] = true; - return res; - } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); + const { click, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(route, args); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "should have followers menu component" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_buttonFollowers', - "should have followers button" - ); - assert.notOk( - document.querySelector('.o_FollowerListMenuView_buttonFollowers').disabled, - "followers button should not be disabled" - ); - assert.containsNone( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should not be opened" - ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "should have followers menu component" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_buttonFollowers", + "should have followers button" + ); + assert.notOk( + document.querySelector(".o_FollowerListMenuView_buttonFollowers").disabled, + "followers button should not be disabled" + ); + assert.containsNone( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should not be opened" + ); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should be opened" - ); -}); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should be opened" + ); + }); -QUnit.test('click on "add followers" button', async function (assert) { - assert.expect(15); + QUnit.test('click on "add followers" button', async function (assert) { + assert.expect(15); - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([ - { name: 'resPartner1' }, - { name: 'resPartner2' }, - { name: 'resPartner3' }, - ]); - pyEnv['mail.followers'].create({ - partner_id: resPartnerId2, - email: "bla@bla.bla", - is_active: true, - name: "François Perusse", - res_id: resPartnerId1, - res_model: 'res.partner', - }); + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv["res.partner"].create([ + { name: "resPartner1" }, + { name: "resPartner2" }, + { name: "resPartner3" }, + ]); + pyEnv["mail.followers"].create({ + partner_id: resPartnerId2, + email: "bla@bla.bla", + is_active: true, + name: "François Perusse", + res_id: resPartnerId1, + res_model: "res.partner", + }); - const { click, env, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(route, args); - res['hasWriteAccess'] = true; - return res; - } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action, options) { - assert.step('action:open_view'); - assert.strictEqual( - action.context.default_res_model, - 'res.partner', - "'The 'add followers' action should contain thread model in context'" + const { click, env, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(route, args); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action, options) { + assert.step("action:open_view"); + assert.strictEqual( + action.context.default_res_model, + "res.partner", + "'The 'add followers' action should contain thread model in context'" + ); + assert.strictEqual( + action.context.default_res_id, + resPartnerId1, + "The 'add followers' action should contain thread id in context" + ); + assert.strictEqual( + action.res_model, + "mail.wizard.invite", + "The 'add followers' action should be a wizard invite of mail module" + ); + assert.strictEqual( + action.type, + "ir.actions.act_window", + "The 'add followers' action should be of type 'ir.actions.act_window'" + ); + pyEnv["mail.followers"].create({ + partner_id: resPartnerId3, + email: "bla@bla.bla", + is_active: true, + name: "Wololo", + res_id: resPartnerId1, + res_model: "res.partner", + }); + options.onClose(); + }, + }); + + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "should have followers menu component" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_buttonFollowers", + "should have followers button" ); assert.strictEqual( - action.context.default_res_id, - resPartnerId1, - "The 'add followers' action should contain thread id in context" + document.querySelector(".o_FollowerListMenuView_buttonFollowersCount").textContent, + "1", + "Followers counter should be equal to 1" + ); + + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should be opened" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_addFollowersButton", + "followers dropdown should contain a 'Add followers' button" + ); + + await click(".o_FollowerListMenuView_addFollowersButton"); + assert.containsNone( + document.body, + ".o_FollowerListMenuView_dropdown", + "followers dropdown should be closed after click on 'Add followers'" + ); + assert.verifySteps(["action:open_view"]); + assert.strictEqual( + document.querySelector(".o_FollowerListMenuView_buttonFollowersCount").textContent, + "2", + "Followers counter should now be equal to 2" + ); + + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsN( + document.body, + ".o_FollowerMenu_follower", + 2, + "Follower list should be refreshed and contain 2 followers" ); assert.strictEqual( - action.res_model, - 'mail.wizard.invite', - "The 'add followers' action should be a wizard invite of mail module" + document.querySelector(".o_FollowerView_name").textContent, + "François Perusse", + "Follower added in follower list should be the one added" ); - assert.strictEqual( - action.type, - "ir.actions.act_window", - "The 'add followers' action should be of type 'ir.actions.act_window'" - ); - pyEnv['mail.followers'].create({ - partner_id: resPartnerId3, + }); + + QUnit.test("click on remove follower", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { name: "resPartner1" }, + { name: "resPartner2" }, + ]); + pyEnv["mail.followers"].create({ + partner_id: resPartnerId2, email: "bla@bla.bla", is_active: true, name: "Wololo", res_id: resPartnerId1, - res_model: 'res.partner', + res_model: "res.partner", + }); + const { click, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(route, args); + res["hasWriteAccess"] = true; + return res; + } + if (route.includes("message_unsubscribe")) { + assert.step("message_unsubscribe"); + assert.deepEqual( + args.args, + [[resPartnerId1], [resPartnerId2]], + "message_unsubscribe should be called with right argument" + ); + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], }); - options.onClose(); - }, - }); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "should have followers menu component" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_buttonFollowers', - "should have followers button" - ); - assert.strictEqual( - document.querySelector('.o_FollowerListMenuView_buttonFollowersCount').textContent, - "1", - "Followers counter should be equal to 1" - ); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_removeButton", + "should display a remove button" + ); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should be opened" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_addFollowersButton', - "followers dropdown should contain a 'Add followers' button" - ); + await click(".o_FollowerView_removeButton"); + assert.verifySteps( + ["message_unsubscribe"], + "clicking on remove button should call 'message_unsubscribe' route" + ); + assert.containsNone( + document.body, + ".o_FollowerView", + "should no longer have follower component" + ); + }); - await click('.o_FollowerListMenuView_addFollowersButton'); - assert.containsNone( - document.body, - '.o_FollowerListMenuView_dropdown', - "followers dropdown should be closed after click on 'Add followers'" - ); - assert.verifySteps([ - 'action:open_view', - ]); - assert.strictEqual( - document.querySelector('.o_FollowerListMenuView_buttonFollowersCount').textContent, - "2", - "Followers counter should now be equal to 2" - ); + QUnit.test( + 'Hide "Add follower" and subtypes edition/removal buttons except own user on read only record', + async function (assert) { + assert.expect(5); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsN( - document.body, - '.o_FollowerMenu_follower', - 2, - "Follower list should be refreshed and contain 2 followers" - ); - assert.strictEqual( - document.querySelector('.o_FollowerView_name').textContent, - "François Perusse", - "Follower added in follower list should be the one added" - ); -}); + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { name: "resPartner1" }, + { name: "resPartner2" }, + ]); + pyEnv["mail.followers"].create([ + { + name: "Jean Michang", + is_active: true, + partner_id: pyEnv.currentPartnerId, + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + name: "Eden Hazard", + is_active: true, + partner_id: resPartnerId2, + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const { click, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with no write access + const res = await performRPC(route, args); + res["hasWriteAccess"] = false; + return res; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); -QUnit.test('click on remove follower', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([ - { name: 'resPartner1' }, - { name: 'resPartner2' }, - ]); - pyEnv['mail.followers'].create({ - partner_id: resPartnerId2, - email: "bla@bla.bla", - is_active: true, - name: "Wololo", - res_id: resPartnerId1, - res_model: 'res.partner', - }); - const { click, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(route, args); - res['hasWriteAccess'] = true; - return res; - } - if (route.includes('message_unsubscribe')) { - assert.step('message_unsubscribe'); - assert.deepEqual( - args.args, - [[resPartnerId1], [resPartnerId2]], - "message_unsubscribe should be called with right argument" + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsNone( + document.body, + ".o_FollowerListMenuView_addFollowersButton", + "'Add followers' button should not be displayed for a readonly record" + ); + const followersList = document.querySelectorAll(".o_FollowerView"); + assert.containsOnce( + followersList[0], + ".o_FollowerView_editButton", + "should display edit button for a follower related to current user" + ); + assert.containsOnce( + followersList[0], + ".o_FollowerView_removeButton", + "should display remove button for a follower related to current user" + ); + assert.containsNone( + followersList[1], + ".o_FollowerView_editButton", + "should not display edit button for other followers on a readonly record" + ); + assert.containsNone( + followersList[1], + ".o_FollowerView_removeButton", + "should not display remove button for others on a readonly record" ); } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); + ); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_removeButton', - "should display a remove button" - ); + QUnit.test( + 'Show "Add follower" and subtypes edition/removal buttons on all followers if user has write access', + async function (assert) { + assert.expect(5); - await click('.o_FollowerView_removeButton'); - assert.verifySteps( - ['message_unsubscribe'], - "clicking on remove button should call 'message_unsubscribe' route" - ); - assert.containsNone( - document.body, - '.o_FollowerView', - "should no longer have follower component" - ); -}); + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { name: "resPartner1" }, + { name: "resPartner2" }, + ]); + pyEnv["mail.followers"].create([ + { + name: "Jean Michang", + is_active: true, + partner_id: pyEnv.currentPartnerId, + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + name: "Eden Hazard", + is_active: true, + partner_id: resPartnerId2, + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const { click, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(...arguments); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); -QUnit.test('Hide "Add follower" and subtypes edition/removal buttons except own user on read only record', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{ name: "resPartner1" }, { name: "resPartner2" }]); - pyEnv['mail.followers'].create([ - { - name: "Jean Michang", - is_active: true, - partner_id: pyEnv.currentPartnerId, - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - name: "Eden Hazard", - is_active: true, - partner_id: resPartnerId2, - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const { click, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with no write access - const res = await performRPC(route, args); - res['hasWriteAccess'] = false; - return res; + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_addFollowersButton", + "'Add followers' button should be displayed for the writable record" + ); + const followersList = document.querySelectorAll(".o_FollowerView"); + assert.containsOnce( + followersList[0], + ".o_FollowerView_editButton", + "should display edit button for a follower related to current user" + ); + assert.containsOnce( + followersList[0], + ".o_FollowerView_removeButton", + "should display remove button for a follower related to current user" + ); + assert.containsOnce( + followersList[1], + ".o_FollowerView_editButton", + "should display edit button for other followers also on the writable record" + ); + assert.containsOnce( + followersList[1], + ".o_FollowerView_removeButton", + "should display remove button for other followers also on the writable record" + ); } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); + ); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsNone( - document.body, - '.o_FollowerListMenuView_addFollowersButton', - "'Add followers' button should not be displayed for a readonly record", - ); - const followersList = document.querySelectorAll('.o_FollowerView'); - assert.containsOnce( - followersList[0], - '.o_FollowerView_editButton', - "should display edit button for a follower related to current user", - ); - assert.containsOnce( - followersList[0], - '.o_FollowerView_removeButton', - "should display remove button for a follower related to current user", - ); - assert.containsNone( - followersList[1], - '.o_FollowerView_editButton', - "should not display edit button for other followers on a readonly record", - ); - assert.containsNone( - followersList[1], - '.o_FollowerView_removeButton', - "should not display remove button for others on a readonly record", - ); -}); + QUnit.test( + 'Show "No Followers" dropdown-item if there are no followers and user dose not have write access', + async function (assert) { + assert.expect(1); -QUnit.test('Show "Add follower" and subtypes edition/removal buttons on all followers if user has write access', async function (assert) { - assert.expect(5); + const pyEnv = await startServer(); - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([{ name: "resPartner1" }, { name: "resPartner2" }]); - pyEnv['mail.followers'].create([ - { - name: "Jean Michang", - is_active: true, - partner_id: pyEnv.currentPartnerId, - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - name: "Eden Hazard", - is_active: true, - partner_id: resPartnerId2, - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const { click, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(...arguments); - res['hasWriteAccess'] = true; - return res; + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { click, openView } = await start({ + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user without write access + const res = await performRPC(route, args); + res["hasWriteAccess"] = false; + return res; + } + }, + }); + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView_noFollowers.disabled", + "should display 'No Followers' dropdown-item" + ); } - }, + ); }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_addFollowersButton', - "'Add followers' button should be displayed for the writable record", - ); - const followersList = document.querySelectorAll('.o_FollowerView'); - assert.containsOnce( - followersList[0], - '.o_FollowerView_editButton', - "should display edit button for a follower related to current user", - ); - assert.containsOnce( - followersList[0], - '.o_FollowerView_removeButton', - "should display remove button for a follower related to current user", - ); - assert.containsOnce( - followersList[1], - '.o_FollowerView_editButton', - "should display edit button for other followers also on the writable record", - ); - assert.containsOnce( - followersList[1], - '.o_FollowerView_removeButton', - "should display remove button for other followers also on the writable record", - ); -}); - -QUnit.test('Show "No Followers" dropdown-item if there are no followers and user dose not have write access', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { click, openView } = await start({ - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user without write access - const res = await performRPC(route, args); - res['hasWriteAccess'] = false; - return res; - } - }, - }); - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView_noFollowers.disabled', - "should display 'No Followers' dropdown-item", - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js index 2ad25d8946e..8aff4716323 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follower_subtype_tests.js @@ -1,169 +1,178 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('follower_subtype_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("follower_subtype_tests.js"); -QUnit.test('simplest layout of a followed subtype', async function (assert) { - assert.expect(5); + QUnit.test("simplest layout of a followed subtype", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const subtypeId = pyEnv['mail.message.subtype'].create({ - default: true, - name: 'TestSubtype', + const pyEnv = await startServer(); + const subtypeId = pyEnv["mail.message.subtype"].create({ + default: true, + name: "TestSubtype", + }); + const followerId = pyEnv["mail.followers"].create({ + display_name: "François Perusse", + partner_id: pyEnv.currentPartnerId, + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + subtype_ids: [subtypeId], + }); + pyEnv["res.partner"].write([pyEnv.currentPartnerId], { + message_follower_ids: [followerId], + }); + const { click, openView } = await start({ + // FIXME: should adapt mock server code to provide `hasWriteAccess` + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(...arguments); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + await click(".o_FollowerView_editButton"); + assert.containsOnce( + document.body, + ".o_FollowerSubtypeView:contains(TestSubtype)", + "should have a follower subtype for 'TestSubtype'" + ); + assert.containsOnce( + document.querySelector(".o_FollowerSubtypeView"), + ".o_FollowerSubtypeView_label", + "should have a label" + ); + assert.containsOnce( + $(".o_FollowerSubtypeView:contains(TestSubtype)"), + ".o_FollowerSubtypeView_checkbox", + "should have a checkbox" + ); + assert.strictEqual( + $(".o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_label")[0] + .textContent, + "TestSubtype", + "should have the name of the subtype as label" + ); + assert.ok( + $(".o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_checkbox")[0] + .checked, + "checkbox should be checked as follower subtype is followed" + ); + }); + + QUnit.test("simplest layout of a not followed subtype", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + pyEnv["mail.message.subtype"].create({ + default: true, + name: "TestSubtype", + }); + const followerId = pyEnv["mail.followers"].create({ + display_name: "François Perusse", + partner_id: pyEnv.currentPartnerId, + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + }); + pyEnv["res.partner"].write([pyEnv.currentPartnerId], { + message_follower_ids: [followerId], + }); + const { click, openView } = await start({ + // FIXME: should adapt mock server code to provide `hasWriteAccess` + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(...arguments); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + await click(".o_FollowerView_editButton"); + assert.notOk( + $(".o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_checkbox")[0] + .checked, + "checkbox should not be checked as follower subtype is not followed" + ); + }); + + QUnit.test("toggle follower subtype checkbox", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const followerSubtypeId = pyEnv["mail.message.subtype"].create({ + default: true, + name: "TestSubtype", + }); + const followerId = pyEnv["mail.followers"].create({ + display_name: "François Perusse", + partner_id: pyEnv.currentPartnerId, + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + }); + pyEnv["res.partner"].write([pyEnv.currentPartnerId], { + message_follower_ids: [followerId], + }); + const { click, openView } = await start({ + // FIXME: should adapt mock server code to provide `hasWriteAccess` + async mockRPC(route, args, performRPC) { + if (route === "/mail/thread/data") { + // mimic user with write access + const res = await performRPC(...arguments); + res["hasWriteAccess"] = true; + return res; + } + }, + }); + await openView({ + res_model: "res.partner", + res_id: pyEnv.currentPartnerId, + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + await click(".o_FollowerView_editButton"); + assert.notOk( + document.querySelector( + `.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox` + ).checked, + "checkbox should not be checked as follower subtype is not followed" + ); + + await click( + `.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox` + ); + assert.ok( + document.querySelector( + `.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox` + ).checked, + "checkbox should now be checked" + ); + + await click( + `.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox` + ); + assert.notOk( + document.querySelector( + `.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox` + ).checked, + "checkbox should be no more checked" + ); + }); }); - const followerId = pyEnv['mail.followers'].create({ - display_name: "François Perusse", - partner_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - subtype_ids: [subtypeId], - }); - pyEnv['res.partner'].write([pyEnv.currentPartnerId], { - message_follower_ids: [followerId], - }); - const { click, openView } = await start({ - // FIXME: should adapt mock server code to provide `hasWriteAccess` - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(...arguments); - res['hasWriteAccess'] = true; - return res; - } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - await click('.o_FollowerView_editButton'); - assert.containsOnce( - document.body, - '.o_FollowerSubtypeView:contains(TestSubtype)', - "should have a follower subtype for 'TestSubtype'" - ); - assert.containsOnce( - document.querySelector('.o_FollowerSubtypeView'), - '.o_FollowerSubtypeView_label', - "should have a label" - ); - assert.containsOnce( - $('.o_FollowerSubtypeView:contains(TestSubtype)'), - '.o_FollowerSubtypeView_checkbox', - "should have a checkbox" - ); - assert.strictEqual( - $('.o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_label')[0].textContent, - "TestSubtype", - "should have the name of the subtype as label" - ); - assert.ok( - $('.o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_checkbox')[0].checked, - "checkbox should be checked as follower subtype is followed" - ); -}); - -QUnit.test('simplest layout of a not followed subtype', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - pyEnv['mail.message.subtype'].create({ - default: true, - name: 'TestSubtype', - }); - const followerId = pyEnv['mail.followers'].create({ - display_name: "François Perusse", - partner_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - }); - pyEnv['res.partner'].write([pyEnv.currentPartnerId], { - message_follower_ids: [followerId], - }); - const { click, openView } = await start({ - // FIXME: should adapt mock server code to provide `hasWriteAccess` - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(...arguments); - res['hasWriteAccess'] = true; - return res; - } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - await click('.o_FollowerView_editButton'); - assert.notOk( - $('.o_FollowerSubtypeView:contains(TestSubtype) .o_FollowerSubtypeView_checkbox')[0].checked, - "checkbox should not be checked as follower subtype is not followed" - ); -}); - -QUnit.test('toggle follower subtype checkbox', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const followerSubtypeId = pyEnv['mail.message.subtype'].create({ - default: true, - name: 'TestSubtype', - }); - const followerId = pyEnv['mail.followers'].create({ - display_name: "François Perusse", - partner_id: pyEnv.currentPartnerId, - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - }); - pyEnv['res.partner'].write([pyEnv.currentPartnerId], { - message_follower_ids: [followerId], - }); - const { click, openView } = await start({ - // FIXME: should adapt mock server code to provide `hasWriteAccess` - async mockRPC(route, args, performRPC) { - if (route === '/mail/thread/data') { - // mimic user with write access - const res = await performRPC(...arguments); - res['hasWriteAccess'] = true; - return res; - } - }, - }); - await openView({ - res_model: 'res.partner', - res_id: pyEnv.currentPartnerId, - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - await click('.o_FollowerView_editButton'); - assert.notOk( - document.querySelector(`.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox`).checked, - "checkbox should not be checked as follower subtype is not followed" - ); - - await click(`.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox`); - assert.ok( - document.querySelector(`.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox`).checked, - "checkbox should now be checked" - ); - - await click(`.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox`); - assert.notOk( - document.querySelector(`.o_FollowerSubtypeView[data-follower-subtype-id="${followerSubtypeId}"] .o_FollowerSubtypeView_checkbox`).checked, - "checkbox should be no more checked" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js index c898c16d7b3..46e1ede0458 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/follower_tests.js @@ -1,295 +1,276 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { makeDeferred } from "@mail/utils/deferred"; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('follower_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("follower_tests.js"); -QUnit.test('base rendering not editable', async function (assert) { - assert.expect(5); + QUnit.test("base rendering not editable", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]); - pyEnv['mail.followers'].create({ - is_active: true, - partner_id: partnerId, - res_id: threadId, - res_model: 'res.partner', - }); - const { click, openView } = await start({ - async mockRPC(route, args, performRpc) { - if (route === '/mail/thread/data') { - // mimic user without write access - const res = await performRpc(...arguments); - res['hasWriteAccess'] = false; - return res; - } - }, - }); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_details', - "should display a details part" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_avatar', - "should display the avatar of the follower" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_name', - "should display the name of the follower" - ); - assert.containsNone( - document.body, - '.o_FollowerView_button', - "should have no button as follower is not editable" - ); -}); - -QUnit.test('base rendering editable', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]); - pyEnv['mail.followers'].create({ - is_active: true, - partner_id: partnerId, - res_id: threadId, - res_model: 'res.partner', - }); - const { click, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_details', - "should display a details part" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_avatar', - "should display the avatar of the follower" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_name', - "should display the name of the follower" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_editButton', - "should have an edit button" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_removeButton', - "should have a remove button" - ); -}); - -QUnit.test('click on partner follower details', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]); - pyEnv['mail.followers'].create({ - is_active: true, - partner_id: partnerId, - res_id: threadId, - res_model: 'res.partner', - }); - const openFormDef = makeDeferred(); - const { click, env, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.step('do_action'); - assert.strictEqual( - action.res_id, - partnerId, - "The redirect action should redirect to the right res id (partnerId)" + const pyEnv = await startServer(); + const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]); + pyEnv["mail.followers"].create({ + is_active: true, + partner_id: partnerId, + res_id: threadId, + res_model: "res.partner", + }); + const { click, openView } = await start({ + async mockRPC(route, args, performRpc) { + if (route === "/mail/thread/data") { + // mimic user without write access + const res = await performRpc(...arguments); + res["hasWriteAccess"] = false; + return res; + } + }, + }); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_details", + "should display a details part" ); - assert.strictEqual( - action.res_model, - 'res.partner', - "The redirect action should redirect to the right res model (res.partner)" + assert.containsOnce( + document.body, + ".o_FollowerView_avatar", + "should display the avatar of the follower" ); - assert.strictEqual( - action.type, - "ir.actions.act_window", - "The redirect action should be of type 'ir.actions.act_window'" + assert.containsOnce( + document.body, + ".o_FollowerView_name", + "should display the name of the follower" ); - openFormDef.resolve(); - }, - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_details', - "should display a details part" - ); + assert.containsNone( + document.body, + ".o_FollowerView_button", + "should have no button as follower is not editable" + ); + }); - document.querySelector('.o_FollowerView_details').click(); - await openFormDef; - assert.verifySteps( - ['do_action'], - "clicking on follower should redirect to partner form view" - ); -}); - -QUnit.test('click on edit follower', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]); - pyEnv['mail.followers'].create({ - is_active: true, - partner_id: partnerId, - res_id: threadId, - res_model: 'res.partner', - }); - const { click, messaging, openView } = await start({ - async mockRPC(route, args) { - if (route.includes('/mail/read_subscription_data')) { - assert.step('fetch_subtypes'); - } - }, - }); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - const thread = messaging.models['Thread'].insert({ - id: threadId, - model: 'res.partner', - }); - await thread.fetchData(['followers']); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_editButton', - "should display an edit button" - ); - - await click('.o_FollowerView_editButton'); - assert.verifySteps( - ['fetch_subtypes'], - "clicking on edit follower should fetch subtypes" - ); - assert.containsOnce( - document.body, - '.o_FollowerSubtypeList', - "A dialog allowing to edit follower subtypes should have been created" - ); -}); - -QUnit.test('edit follower and close subtype dialog', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]); - pyEnv['mail.followers'].create({ - is_active: true, - partner_id: partnerId, - res_id: threadId, - res_model: 'res.partner', - }); - const { click, openView } = await start({ - async mockRPC(route, args) { - if (route.includes('/mail/read_subscription_data')) { - assert.step('fetch_subtypes'); - return [{ - default: true, - followed: true, - internal: false, - id: 1, - name: "Dummy test", - res_model: 'res.partner' - }]; - } - }, - }); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - await click('.o_FollowerListMenuView_buttonFollowers'); - assert.containsOnce( - document.body, - '.o_FollowerView', - "should have follower component" - ); - assert.containsOnce( - document.body, - '.o_FollowerView_editButton', - "should display an edit button" - ); - - await click('.o_FollowerView_editButton'); - assert.verifySteps( - ['fetch_subtypes'], - "clicking on edit follower should fetch subtypes" - ); - assert.containsOnce( - document.body, - '.o_FollowerSubtypeList', - "dialog allowing to edit follower subtypes should have been created" - ); - - await click('.o_FollowerSubtypeList_closeButton'); - assert.containsNone( - document.body, - '.o_DialogManager_dialog', - "follower subtype dialog should be closed after clicking on close button" - ); -}); - -}); + QUnit.test("base rendering editable", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]); + pyEnv["mail.followers"].create({ + is_active: true, + partner_id: partnerId, + res_id: threadId, + res_model: "res.partner", + }); + const { click, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_details", + "should display a details part" + ); + assert.containsOnce( + document.body, + ".o_FollowerView_avatar", + "should display the avatar of the follower" + ); + assert.containsOnce( + document.body, + ".o_FollowerView_name", + "should display the name of the follower" + ); + assert.containsOnce( + document.body, + ".o_FollowerView_editButton", + "should have an edit button" + ); + assert.containsOnce( + document.body, + ".o_FollowerView_removeButton", + "should have a remove button" + ); + }); + + QUnit.test("click on partner follower details", async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]); + pyEnv["mail.followers"].create({ + is_active: true, + partner_id: partnerId, + res_id: threadId, + res_model: "res.partner", + }); + const openFormDef = makeDeferred(); + const { click, env, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.step("do_action"); + assert.strictEqual( + action.res_id, + partnerId, + "The redirect action should redirect to the right res id (partnerId)" + ); + assert.strictEqual( + action.res_model, + "res.partner", + "The redirect action should redirect to the right res model (res.partner)" + ); + assert.strictEqual( + action.type, + "ir.actions.act_window", + "The redirect action should be of type 'ir.actions.act_window'" + ); + openFormDef.resolve(); + }, + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_details", + "should display a details part" + ); + + document.querySelector(".o_FollowerView_details").click(); + await openFormDef; + assert.verifySteps( + ["do_action"], + "clicking on follower should redirect to partner form view" + ); + }); + + QUnit.test("click on edit follower", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]); + pyEnv["mail.followers"].create({ + is_active: true, + partner_id: partnerId, + res_id: threadId, + res_model: "res.partner", + }); + const { click, messaging, openView } = await start({ + async mockRPC(route, args) { + if (route.includes("/mail/read_subscription_data")) { + assert.step("fetch_subtypes"); + } + }, + }); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + const thread = messaging.models["Thread"].insert({ + id: threadId, + model: "res.partner", + }); + await thread.fetchData(["followers"]); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_editButton", + "should display an edit button" + ); + + await click(".o_FollowerView_editButton"); + assert.verifySteps( + ["fetch_subtypes"], + "clicking on edit follower should fetch subtypes" + ); + assert.containsOnce( + document.body, + ".o_FollowerSubtypeList", + "A dialog allowing to edit follower subtypes should have been created" + ); + }); + + QUnit.test("edit follower and close subtype dialog", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]); + pyEnv["mail.followers"].create({ + is_active: true, + partner_id: partnerId, + res_id: threadId, + res_model: "res.partner", + }); + const { click, openView } = await start({ + async mockRPC(route, args) { + if (route.includes("/mail/read_subscription_data")) { + assert.step("fetch_subtypes"); + return [ + { + default: true, + followed: true, + internal: false, + id: 1, + name: "Dummy test", + res_model: "res.partner", + }, + ]; + } + }, + }); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + await click(".o_FollowerListMenuView_buttonFollowers"); + assert.containsOnce(document.body, ".o_FollowerView", "should have follower component"); + assert.containsOnce( + document.body, + ".o_FollowerView_editButton", + "should display an edit button" + ); + + await click(".o_FollowerView_editButton"); + assert.verifySteps( + ["fetch_subtypes"], + "clicking on edit follower should fetch subtypes" + ); + assert.containsOnce( + document.body, + ".o_FollowerSubtypeList", + "dialog allowing to edit follower subtypes should have been created" + ); + + await click(".o_FollowerSubtypeList_closeButton"); + assert.containsNone( + document.body, + ".o_DialogManager_dialog", + "follower subtype dialog should be closed after clicking on close button" + ); + }); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/link_preview_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/link_preview_tests.js index aa3e65e7f1c..395d5a91910 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/link_preview_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/link_preview_tests.js @@ -1,384 +1,381 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('link_preview_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("link_preview_tests.js"); -const linkPreviewGifPayload = { - og_description: 'test description', - og_image: 'https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif', - og_mimetype: 'image/gif', - og_title: 'Yay Minions GIF - Yay Minions Happiness - Discover & Share GIFs', - og_type: 'video.other', - source_url: 'https://tenor.com/view/yay-minions-happiness-happy-excited-gif-15324023', -}; -const linkPreviewCardPayload = { - og_description: 'Description', - og_title: 'Article title', - og_type: 'article', - source_url: 'https://www.odoo.com', -}; -const linkPreviewCardImagePayload = { - og_description: 'Description', - og_image: 'https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif', - og_title: 'Article title', - og_type: 'article', - source_url: 'https://www.odoo.com', -}; -const linkPreviewVideoPayload = { - og_description: 'Description', - og_image: 'https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif', - og_title: 'video title', - og_type: 'video.other', - source_url: 'https://www.odoo.com', -}; -const linkPreviewImagePayload = { - image_mimetype: 'image/jpg', - source_url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/290px-Siberischer_tiger_de_edit02.jpg', -}; + const linkPreviewGifPayload = { + og_description: "test description", + og_image: "https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif", + og_mimetype: "image/gif", + og_title: "Yay Minions GIF - Yay Minions Happiness - Discover & Share GIFs", + og_type: "video.other", + source_url: "https://tenor.com/view/yay-minions-happiness-happy-excited-gif-15324023", + }; + const linkPreviewCardPayload = { + og_description: "Description", + og_title: "Article title", + og_type: "article", + source_url: "https://www.odoo.com", + }; + const linkPreviewCardImagePayload = { + og_description: "Description", + og_image: "https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif", + og_title: "Article title", + og_type: "article", + source_url: "https://www.odoo.com", + }; + const linkPreviewVideoPayload = { + og_description: "Description", + og_image: "https://c.tenor.com/B_zYdea4l-4AAAAC/yay-minions.gif", + og_title: "video title", + og_type: "video.other", + source_url: "https://www.odoo.com", + }; + const linkPreviewImagePayload = { + image_mimetype: "image/jpg", + source_url: + "https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/290px-Siberischer_tiger_de_edit02.jpg", + }; -QUnit.test('auto layout with link preview list', async function (assert) { - assert.expect(1); + QUnit.test("auto layout with link preview list", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewGifPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView .o_LinkPreviewListView", + "Should have a link preview list in the DOM" + ); + }); + + QUnit.test("auto layout with link preview as gif", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewGifPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_LinkPreviewImageView", + "Should have a link preview gif in the DOM" + ); + }); + + QUnit.test("simplest card layout", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewCardPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView", + "should have link preview in DOM" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView_title", + "Should display the link preview title" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView_description", + "Link preview should show the link description" + ); + }); + + QUnit.test("simplest card layout with image", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewCardImagePayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView", + "should have link preview in DOM" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView_title", + "Should display the link preview title" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView_description", + "Link preview should show the link description" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewCardView_image", + "Should display an image inside the link preview card" + ); + }); + + QUnit.test("Link preview video layout", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewVideoPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_LinkPreviewVideoView", + "should have link preview video in DOM" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewVideoView_title", + "Should display the link preview title" + ); + assert.containsOnce( + document.body, + ".o_LinkPreviewVideoView_description", + "Link preview should show the link description" + ); + assert.containsOnce( + document.body, + ".o_linkPreviewVideo_overlay", + "Should display overlay inside the link preview video image" + ); + }); + + QUnit.test("Link preview image layout", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewImagePayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_LinkPreviewImageView", + "should have link preview image" + ); + }); + + QUnit.test("Remove link preview Gif", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewGifPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss, click, mouseenter } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + await mouseenter(".o_LinkPreviewImageView"); + await click(".o_LinkPreviewAside"); + assert.containsOnce( + document.body, + ".o_LinkPreviewDeleteConfirmView", + "Should have a link preview confirmation dialog" + ); + }); + + QUnit.test("Remove link preview card", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewCardPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss, click, mouseenter } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + await mouseenter(".o_LinkPreviewCardView"); + await click(".o_LinkPreviewAside"); + assert.containsOnce( + document.body, + ".o_LinkPreviewDeleteConfirmView", + "Should have a link preview confirmation dialog" + ); + }); + + QUnit.test("Remove link preview video", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewVideoPayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss, click, mouseenter } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + await mouseenter(".o_LinkPreviewVideoView"); + await click(".o_LinkPreviewAside"); + assert.containsOnce( + document.body, + ".o_LinkPreviewDeleteConfirmView", + "Should have a link preview confirmation dialog" + ); + }); + + QUnit.test("Remove link preview image", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const linkPreviewId = pyEnv["mail.link.preview"].create(linkPreviewImagePayload); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + link_preview_ids: [linkPreviewId], + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss, click, mouseenter } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + await mouseenter(".o_LinkPreviewImageView"); + await click(".o_LinkPreviewAside"); + assert.containsOnce( + document.body, + ".o_LinkPreviewDeleteConfirmView", + "Should have a link preview confirmation dialog" + ); + }); }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView .o_LinkPreviewListView', - "Should have a link preview list in the DOM" - ); -}); - -QUnit.test('auto layout with link preview as gif', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_LinkPreviewImageView', - "Should have a link preview gif in the DOM" - ); -}); - -QUnit.test('simplest card layout', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView', - "should have link preview in DOM" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView_title', - "Should display the link preview title" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView_description', - "Link preview should show the link description" - ); -}); - -QUnit.test('simplest card layout with image', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardImagePayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView', - "should have link preview in DOM" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView_title', - "Should display the link preview title" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView_description', - "Link preview should show the link description" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewCardView_image', - "Should display an image inside the link preview card" - ); -}); - -QUnit.test('Link preview video layout', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewVideoPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_LinkPreviewVideoView', - "should have link preview video in DOM" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewVideoView_title', - "Should display the link preview title" - ); - assert.containsOnce( - document.body, - '.o_LinkPreviewVideoView_description', - "Link preview should show the link description" - ); - assert.containsOnce( - document.body, - '.o_linkPreviewVideo_overlay', - "Should display overlay inside the link preview video image" - ); -}); - -QUnit.test('Link preview image layout', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewImagePayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_LinkPreviewImageView', - "should have link preview image" - ); -}); - -QUnit.test('Remove link preview Gif', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewGifPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss, click, mouseenter } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - await mouseenter('.o_LinkPreviewImageView'); - await click('.o_LinkPreviewAside'); - assert.containsOnce( - document.body, - '.o_LinkPreviewDeleteConfirmView', - 'Should have a link preview confirmation dialog' - ); -}); - -QUnit.test('Remove link preview card', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewCardPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss, click, mouseenter } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - await mouseenter('.o_LinkPreviewCardView'); - await click('.o_LinkPreviewAside'); - assert.containsOnce( - document.body, - '.o_LinkPreviewDeleteConfirmView', - 'Should have a link preview confirmation dialog' - ); -}); - -QUnit.test('Remove link preview video', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewVideoPayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss, click, mouseenter } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - await mouseenter('.o_LinkPreviewVideoView'); - await click('.o_LinkPreviewAside'); - assert.containsOnce( - document.body, - '.o_LinkPreviewDeleteConfirmView', - 'Should have a link preview confirmation dialog' - ); -}); - -QUnit.test('Remove link preview image', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const linkPreviewId = pyEnv['mail.link.preview'].create(linkPreviewImagePayload); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - link_preview_ids: [linkPreviewId], - message_type: 'comment', - model: "mail.channel", - res_id: mailChannelId, - }); - const { openDiscuss, click, mouseenter } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - await mouseenter('.o_LinkPreviewImageView'); - await click('.o_LinkPreviewAside'); - assert.containsOnce( - document.body, - '.o_LinkPreviewDeleteConfirmView', - 'Should have a link preview confirmation dialog' - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/message_in_reply_to_view_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/message_in_reply_to_view_tests.js index a644812cd44..d278f29fea9 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/message_in_reply_to_view_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/message_in_reply_to_view_tests.js @@ -1,43 +1,48 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function() { -QUnit.module('message_in_reply_to_view_tests'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("message_in_reply_to_view_tests"); -QUnit.test('click on message in reply to highlights the parent message', async function (assert) { - assert.expect(1); + QUnit.test( + "click on message in reply to highlights the parent message", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "Hey lol", - message_type: 'comment', - model: 'mail.channel', - res_id: mailChannelId1, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "Hey lol", + message_type: "comment", + model: "mail.channel", + res_id: mailChannelId1, + }); + const mailMessageId2 = pyEnv["mail.message"].create({ + body: "Response to Hey lol", + message_type: "comment", + model: "mail.channel", + parent_id: mailMessageId1, + res_id: mailChannelId1, + }); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId1}`, + }, + }, + }); + await openDiscuss(); + await click( + `.o_MessageView[data-id="${mailMessageId2}"] .o_MessageInReplyToView_body` + ); + assert.containsOnce( + document.body, + `.o-highlighted[data-id="${mailMessageId1}"]`, + "click on message in reply to should highlight the parent message" + ); + } + ); }); - const mailMessageId2 = pyEnv['mail.message'].create({ - body: "Response to Hey lol", - message_type: 'comment', - model: 'mail.channel', - parent_id: mailMessageId1, - res_id: mailChannelId1, - }); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId1}`, - }, - }, - }); - await openDiscuss(); - await click(`.o_MessageView[data-id="${mailMessageId2}"] .o_MessageInReplyToView_body`); - assert.containsOnce( - document.body, - `.o-highlighted[data-id="${mailMessageId1}"]`, - "click on message in reply to should highlight the parent message" - ); -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/message_seen_indicator_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/message_seen_indicator_tests.js index 506a8fe8332..06521587fe8 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/message_seen_indicator_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/message_seen_indicator_tests.js @@ -1,271 +1,285 @@ /** @odoo-module **/ -import { - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('message_seen_indicator_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("message_seen_indicator_tests.js"); -QUnit.test('rendering when just one has received the message', async function (assert) { - assert.expect(3); + QUnit.test("rendering when just one has received the message", async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', // only chat channel have seen notification + const pyEnv = await startServer(); + + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + const resPartnerId2 = pyEnv["res.partner"].create({ name: "Other User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", // only chat channel have seen notification + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const [mailChannelMemberId1] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ["partner_id", "=", resPartnerId1], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId1], { + fetched_message_id: mailMessageId, + seen_message_id: false, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView", + "should display a message seen indicator component" + ); + assert.doesNotHaveClass( + document.querySelector(".o_MessageSeenIndicatorView"), + "o-all-seen", + "indicator component should not be considered as all seen" + ); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView_icon", + "should display only one seen indicator icon" + ); + }); + + QUnit.test("rendering when everyone have received the message", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + const resPartnerId2 = pyEnv["res.partner"].create({ name: "Other User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const mailChannelMemberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(mailChannelMemberIds, { + fetched_message_id: mailMessageId, + seen_message_id: false, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView", + "should display a message seen indicator component" + ); + assert.doesNotHaveClass( + document.querySelector(".o_MessageSeenIndicatorView"), + "o-all-seen", + "indicator component should not be considered as all seen" + ); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView_icon", + "should display only one seen indicator icon" + ); + }); + + QUnit.test("rendering when just one has seen the message", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + const resPartnerId2 = pyEnv["res.partner"].create({ name: "Other User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const mailChannelMemberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(mailChannelMemberIds, { + fetched_message_id: mailMessageId, + seen_message_id: false, + }); + const [mailChannelMemberId1] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ["partner_id", "=", resPartnerId1], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId1], { + seen_message_id: mailMessageId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView", + "should display a message seen indicator component" + ); + assert.doesNotHaveClass( + document.querySelector(".o_MessageSeenIndicatorView"), + "o-all-seen", + "indicator component should not be considered as all seen" + ); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 2, + "should display two seen indicator icon" + ); + }); + + QUnit.test( + "rendering when just one has seen & received the message", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + const resPartnerId2 = pyEnv["res.partner"].create({ name: "Other User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const [mailChannelMemberId1] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ["partner_id", "=", resPartnerId1], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId1], { + seen_message_id: mailMessageId, + fetched_message_id: mailMessageId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView", + "should display a message seen indicator component" + ); + assert.doesNotHaveClass( + document.querySelector(".o_MessageSeenIndicatorView"), + "o-all-seen", + "indicator component should not be considered as all seen" + ); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 2, + "should display two seen indicator icon" + ); + } + ); + + QUnit.test("rendering when just everyone has seen the message", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo User" }); + const resPartnerId2 = pyEnv["res.partner"].create({ name: "Other User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + channel_type: "chat", + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const mailChannelMemberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(mailChannelMemberIds, { + fetched_message_id: mailMessageId, + seen_message_id: mailMessageId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView", + "should display a message seen indicator component" + ); + assert.hasClass( + document.querySelector(".o_MessageSeenIndicatorView"), + "o-all-seen", + "indicator component should not considered as all seen" + ); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 2, + "should display two seen indicator icon" + ); + }); }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const [mailChannelMemberId1] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId], ['partner_id', '=', resPartnerId1]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId1], { - fetched_message_id: mailMessageId, - seen_message_id: false, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView', - "should display a message seen indicator component" - ); - assert.doesNotHaveClass( - document.querySelector('.o_MessageSeenIndicatorView'), - 'o-all-seen', - "indicator component should not be considered as all seen" - ); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView_icon', - "should display only one seen indicator icon" - ); -}); - -QUnit.test('rendering when everyone have received the message', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const mailChannelMemberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(mailChannelMemberIds, { - fetched_message_id: mailMessageId, - seen_message_id: false, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView', - "should display a message seen indicator component" - ); - assert.doesNotHaveClass( - document.querySelector('.o_MessageSeenIndicatorView'), - 'o-all-seen', - "indicator component should not be considered as all seen" - ); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView_icon', - "should display only one seen indicator icon" - ); -}); - -QUnit.test('rendering when just one has seen the message', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const mailChannelMemberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(mailChannelMemberIds, { - fetched_message_id: mailMessageId, - seen_message_id: false, - }); - const [mailChannelMemberId1] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId], ['partner_id', '=', resPartnerId1]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId1], { - seen_message_id: mailMessageId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView', - "should display a message seen indicator component" - ); - assert.doesNotHaveClass( - document.querySelector('.o_MessageSeenIndicatorView'), - 'o-all-seen', - "indicator component should not be considered as all seen" - ); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 2, - "should display two seen indicator icon" - ); -}); - -QUnit.test('rendering when just one has seen & received the message', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const [mailChannelMemberId1] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId], ['partner_id', '=', resPartnerId1]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId1], { - seen_message_id: mailMessageId, - fetched_message_id: mailMessageId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView', - "should display a message seen indicator component" - ); - assert.doesNotHaveClass( - document.querySelector('.o_MessageSeenIndicatorView'), - 'o-all-seen', - "indicator component should not be considered as all seen" - ); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 2, - "should display two seen indicator icon" - ); -}); - -QUnit.test('rendering when just everyone has seen the message', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo User" }); - const resPartnerId2 = pyEnv['res.partner'].create({ name: "Other User" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - channel_type: 'chat', - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const mailChannelMemberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(mailChannelMemberIds, { - fetched_message_id: mailMessageId, - seen_message_id: mailMessageId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView', - "should display a message seen indicator component" - ); - assert.hasClass( - document.querySelector('.o_MessageSeenIndicatorView'), - 'o-all-seen', - "indicator component should not considered as all seen" - ); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 2, - "should display two seen indicator icon" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js index 07b785fed7c..d96dcab2fc9 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/message_tests.js @@ -1,1095 +1,1184 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; +import { makeDeferred } from "@mail/utils/deferred"; import { afterNextRender, nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('message_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("message_tests.js"); -QUnit.test('basic rendering', async function (assert) { - assert.expect(12); + QUnit.test("basic rendering", async function (assert) { + assert.expect(12); - const pyEnv = await startServer(); - const [threadId, otherPartnerId] = pyEnv['res.partner'].create([{}, { name: 'Demo User' }]); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: otherPartnerId, - body: '

Test

', - model: 'res.partner', - res_id: threadId, - }); - const { openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll('.o_MessageView').length, - 1, - "should display a message component" - ); - const messageEl = document.querySelector('.o_MessageView'); - assert.strictEqual( - messageEl.dataset.id, - mailMessageId.toString(), - "message component should be linked to message store model" - ); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageView_sidebar`).length, - 1, - "message should have a sidebar" - ); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageView_sidebar .o_MessageView_authorAvatar`).length, - 1, - "message should have author avatar in the sidebar" - ); - assert.strictEqual( - messageEl.querySelector(`:scope .o_MessageView_authorAvatar`).tagName, - 'IMG', - "message author avatar should be an image" - ); - assert.strictEqual( - messageEl.querySelector(`:scope .o_MessageView_authorAvatar`).dataset.src, - `/web/image/res.partner/${otherPartnerId}/avatar_128`, - "message author avatar should GET image of the related partner" - ); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageView_authorName`).length, - 1, - "message should display author name" - ); - assert.strictEqual( - messageEl.querySelector(`:scope .o_MessageView_authorName`).textContent, - "Demo User", - "message should display correct author name" - ); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageView_date`).length, - 1, - "message should display date" - ); - await afterNextRender(() => messageEl.click()); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageActionList`).length, - 1, - "message should display list of actions" - ); - assert.strictEqual( - messageEl.querySelectorAll(`:scope .o_MessageView_content`).length, - 1, - "message should display the content" - ); - assert.strictEqual( - messageEl.querySelector(`:scope .o_MessageView_prettyBody`).innerHTML, - "

Test

", - "message should display the correct content" - ); -}); - -QUnit.test('Notification Sent', async function (assert) { - assert.expect(9); - - const pyEnv = await startServer(); - const [threadId, resPartnerId] = pyEnv['res.partner'].create([{}, { name: 'Someone', partner_share: true }]); - const mailMessageId = pyEnv['mail.message'].create({ - body: 'not empty', - message_type: 'email', - model: 'res.partner', - res_id: threadId, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId, - notification_status: 'sent', - notification_type: 'email', - res_partner_id: resPartnerId, - }); - const { click, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsOnce( - document.body, - '.o_MessageView_notificationIconClickable', - "should display the notification icon container" - ); - assert.containsOnce( - document.body, - '.o_MessageView_notificationIcon', - "should display the notification icon" - ); - assert.hasClass( - document.querySelector('.o_MessageView_notificationIcon'), - 'fa-envelope-o', - "icon should represent email success" - ); - - await click('.o_MessageView_notificationIconClickable'); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView', - "notification popover should be open" - ); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView_notificationIcon', - "popover should have one icon" - ); - assert.hasClass( - document.querySelector('.o_MessageNotificationPopoverContentView_notificationIcon'), - 'fa-check', - "popover should have the sent icon" - ); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView_notificationPartnerName', - "popover should have the partner name" - ); - assert.strictEqual( - document.querySelector('.o_MessageNotificationPopoverContentView_notificationPartnerName').textContent.trim(), - "Someone", - "partner name should be correct" - ); -}); - -QUnit.test('Notification Error', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const [threadId, resPartnerId] = pyEnv['res.partner'].create([{}, { name: "Someone", partner_share: true }]); - const mailMessageId = pyEnv['mail.message'].create({ - body: 'not empty', - message_type: 'email', - model: 'res.partner', - res_id: threadId, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId, - notification_status: 'exception', - notification_type: 'email', - res_partner_id: resPartnerId, - }); - const openResendActionDef = makeDeferred(); - const { env, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action, options) { - assert.step('do_action'); + const pyEnv = await startServer(); + const [threadId, otherPartnerId] = pyEnv["res.partner"].create([ + {}, + { name: "Demo User" }, + ]); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: otherPartnerId, + body: "

Test

", + model: "res.partner", + res_id: threadId, + }); + const { openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); assert.strictEqual( - action, - 'mail.mail_resend_message_action', - "action should be the one to resend email" + document.querySelectorAll(".o_MessageView").length, + 1, + "should display a message component" + ); + const messageEl = document.querySelector(".o_MessageView"); + assert.strictEqual( + messageEl.dataset.id, + mailMessageId.toString(), + "message component should be linked to message store model" ); assert.strictEqual( - options.additionalContext.mail_message_to_resend, - mailMessageId, - "action should have correct message id" + messageEl.querySelectorAll(`:scope .o_MessageView_sidebar`).length, + 1, + "message should have a sidebar" + ); + assert.strictEqual( + messageEl.querySelectorAll( + `:scope .o_MessageView_sidebar .o_MessageView_authorAvatar` + ).length, + 1, + "message should have author avatar in the sidebar" + ); + assert.strictEqual( + messageEl.querySelector(`:scope .o_MessageView_authorAvatar`).tagName, + "IMG", + "message author avatar should be an image" + ); + assert.strictEqual( + messageEl.querySelector(`:scope .o_MessageView_authorAvatar`).dataset.src, + `/web/image/res.partner/${otherPartnerId}/avatar_128`, + "message author avatar should GET image of the related partner" + ); + assert.strictEqual( + messageEl.querySelectorAll(`:scope .o_MessageView_authorName`).length, + 1, + "message should display author name" + ); + assert.strictEqual( + messageEl.querySelector(`:scope .o_MessageView_authorName`).textContent, + "Demo User", + "message should display correct author name" + ); + assert.strictEqual( + messageEl.querySelectorAll(`:scope .o_MessageView_date`).length, + 1, + "message should display date" + ); + await afterNextRender(() => messageEl.click()); + assert.strictEqual( + messageEl.querySelectorAll(`:scope .o_MessageActionList`).length, + 1, + "message should display list of actions" + ); + assert.strictEqual( + messageEl.querySelectorAll(`:scope .o_MessageView_content`).length, + 1, + "message should display the content" + ); + assert.strictEqual( + messageEl.querySelector(`:scope .o_MessageView_prettyBody`).innerHTML, + "

Test

", + "message should display the correct content" ); - openResendActionDef.resolve(); - }, - }); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsOnce( - document.body, - '.o_MessageView_notificationIconClickable', - "should display the notification icon container" - ); - assert.containsOnce( - document.body, - '.o_MessageView_notificationIcon', - "should display the notification icon" - ); - assert.hasClass( - document.querySelector('.o_MessageView_notificationIcon'), - 'fa-envelope', - "icon should represent email error" - ); - document.querySelector('.o_MessageView_notificationIconClickable').click(); - await openResendActionDef; - assert.verifySteps( - ['do_action'], - "should do an action to display the resend email dialog" - ); -}); - -QUnit.test("'channel_fetch' notification received is correctly handled", async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({}); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - channel_type: 'chat', - }); - pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsNone( - document.body, - '.o_MessageSeenIndicatorView_icon', - "message component should not have any check (V) as message is not yet received" - ); - - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId]])[0]; - // Simulate received channel fetched notification - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel.member/fetched', { - 'channel_id': mailChannelId, - 'last_message_id': 100, - 'partner_id': resPartnerId, }); - }); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView_icon', - "message seen indicator component should only contain one check (V) as message is just received" - ); -}); + QUnit.test("Notification Sent", async function (assert) { + assert.expect(9); -QUnit.test("'channel_seen' notification received is correctly handled", async function (assert) { - assert.expect(3); + const pyEnv = await startServer(); + const [threadId, resPartnerId] = pyEnv["res.partner"].create([ + {}, + { name: "Someone", partner_share: true }, + ]); + const mailMessageId = pyEnv["mail.message"].create({ + body: "not empty", + message_type: "email", + model: "res.partner", + res_id: threadId, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId, + notification_status: "sent", + notification_type: "email", + res_partner_id: resPartnerId, + }); + const { click, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsOnce( + document.body, + ".o_MessageView_notificationIconClickable", + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + ".o_MessageView_notificationIcon", + "should display the notification icon" + ); + assert.hasClass( + document.querySelector(".o_MessageView_notificationIcon"), + "fa-envelope-o", + "icon should represent email success" + ); - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({}); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - channel_type: 'chat', - }); - pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsNone( - document.body, - '.o_MessageSeenIndicatorView_icon', - "message component should not have any check (V) as message is not yet received" - ); - - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId]])[0]; - // Simulate received channel seen notification - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel.member/seen', { - 'channel_id': mailChannelId, - 'last_message_id': 100, - 'partner_id': resPartnerId, - }); - }); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 2, - "message seen indicator component should contain two checks (V) as message is seen" - ); -}); - -QUnit.test("'channel_fetch' notification then 'channel_seen' received are correctly handled", async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ display_name: "Recipient" }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - channel_type: 'chat', - }); - pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsNone( - document.body, - '.o_MessageSeenIndicatorView_icon', - "message component should not have any check (V) as message is not yet received" - ); - - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId]])[0]; - // Simulate received channel fetched notification - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel.member/fetched', { - 'channel_id': mailChannelId, - 'last_message_id': 100, - 'partner_id': resPartnerId, - }); - }); - assert.containsOnce( - document.body, - '.o_MessageSeenIndicatorView_icon', - "message seen indicator component should only contain one check (V) as message is just received" - ); - - // Simulate received channel seen notification - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel.member/seen', { - 'channel_id': mailChannelId, - 'last_message_id': 100, - 'partner_id': resPartnerId, - }); - }); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 2, - "message seen indicator component should contain two checks (V) as message is now seen" - ); -}); - -QUnit.test('do not show message seen indicator on the last message seen by everyone when the current user is not author of the message', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const otherPartnerId = pyEnv['res.partner'].create({ name: 'Demo User' }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_type: 'chat', - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: otherPartnerId }], - ], - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: otherPartnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const memberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(memberIds, { seen_message_id: mailMessageId }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsNone( - document.body, - '.o_MessageView_seenIndicator', - "message component should not have any message seen indicator because the current user is not author" - ); -}); - -QUnit.test('do not show message seen indicator on all the messages of the current user that are older than the last message seen by everyone', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const otherPartnerId = pyEnv['res.partner'].create({ name: 'Demo User' }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_type: 'chat', - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: otherPartnerId }], - ], - }); - const [beforeLastMailMessageId, lastMailMessageId] = pyEnv['mail.message'].create([ - { - author_id: pyEnv.currentPartnerId, - body: "

Message before last seen

", - model: 'mail.channel', - res_id: mailChannelId, - }, - { - author_id: pyEnv.currentPartnerId, - body: "

Last seen by everyone

", - model: 'mail.channel', - res_id: mailChannelId, - }, - ]); - const memberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(memberIds, { seen_message_id: lastMailMessageId }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - `.o_MessageView[data-id=${beforeLastMailMessageId}]`, - "should display a message component" - ); - assert.containsOnce( - document.body, - `.o_MessageView[data-id=${beforeLastMailMessageId}] .o_MessageView_seenIndicator`, - "message component should have a message seen indicator because the current user is author" - ); - assert.containsNone( - document.body, - `.o_MessageView[data-id=${beforeLastMailMessageId}] .o_MessageSeenIndicatorView_icon`, - "message component should not have any check (V) because it is older than the last message seen by everyone" - ); -}); - -QUnit.test('only show messaging seen indicator if authored by me, after last seen by all message', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const otherPartnerId = pyEnv['res.partner'].create({ name: 'Demo User' }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_type: 'chat', - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: otherPartnerId }], - ], - }); - const mailMessageId = pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "

Test

", - res_id: mailChannelId, - model: 'mail.channel', - }); - const memberIds = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId]]); - pyEnv['mail.channel.member'].write(memberIds, { - fetched_message_id: mailMessageId, - seen_message_id: mailMessageId - 1, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a message component" - ); - assert.containsOnce( - document.body, - '.o_MessageView_seenIndicator', - "message component should have a message seen indicator" - ); - assert.containsN( - document.body, - '.o_MessageSeenIndicatorView_icon', - 1, - "message component should have one check (V) because the message was fetched by everyone but no other member than author has seen the message" - ); -}); - -QUnit.test('allow attachment delete on authored message', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - attachment_ids: [[0, 0, { - mimetype: 'image/jpeg', - name: "BLAH", - res_id: mailChannelId, - res_model: 'mail.channel', - }]], - author_id: pyEnv.currentPartnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const { click, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_AttachmentImage', - "should have an attachment", - ); - assert.containsOnce( - document.body, - '.o_AttachmentImage_actionUnlink', - "should have delete attachment button" - ); - - await click('.o_AttachmentImage_actionUnlink'); - assert.containsOnce( - document.body, - '.o_AttachmentDeleteConfirmView', - "An attachment delete confirmation dialog should have been opened" - ); - assert.strictEqual( - document.querySelector('.o_AttachmentDeleteConfirmView_mainText').textContent, - `Do you really want to delete "BLAH"?`, - "Confirmation dialog should contain the attachment delete confirmation text" - ); - - await click('.o_AttachmentDeleteConfirmView_confirmButton'); - assert.containsNone( - document.body, - '.o_AttachmentCard', - "should no longer have an attachment", - ); -}); - -QUnit.test('prevent attachment delete on non-authored message in channels', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create({}); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - attachment_ids: [[0, 0, { - mimetype: 'image/jpeg', - name: "BLAH", - res_id: mailChannelId, - res_model: 'mail.channel', - }]], - author_id: partnerId, - body: "

Test

", - model: 'mail.channel', - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_AttachmentImage', - "should have an attachment", - ); - assert.containsNone( - document.body, - '.o_AttachmentImage_actionUnlink', - "delete attachment button should not be printed" - ); -}); - -QUnit.test('allow attachment image download on message', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const irAttachmentId1 = pyEnv['ir.attachment'].create({ - name: "Blah.jpg", - mimetype: 'image/jpeg', - }); - pyEnv['mail.message'].create({ - attachment_ids: [irAttachmentId1], - body: '

Test

', - model: 'mail.channel', - res_id: mailChannelId1, - }); - const { openDiscuss } = await start({ - discuss: { - context: { - active_id: mailChannelId1, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_AttachmentImage_actionDownload', - "should have download attachment button" - ); -}); - -QUnit.test('subtype description should be displayed if it is different than body', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - const subtypeId = pyEnv['mail.message.subtype'].create({ description: "Bonjour" }); - pyEnv['mail.message'].create({ - body: "

Hello

", - model: 'res.partner', - res_id: threadId, - subtype_id: subtypeId, - }); - const { openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_MessageView_content', - "message should have content" - ); - assert.strictEqual( - document.querySelector(`.o_MessageView_content`).textContent, - "HelloBonjour", - "message content should display both body and subtype description when they are different" - ); -}); - -QUnit.test('subtype description should not be displayed if it is similar to body', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - const subtypeId = pyEnv['mail.message.subtype'].create({ description: "hello" }); - pyEnv['mail.message'].create({ - body: "

Hello

", - model: 'res.partner', - res_id: threadId, - subtype_id: subtypeId, - }); - const { openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_MessageView_content', - "message should have content" - ); - assert.strictEqual( - document.querySelector(`.o_MessageView_content`).textContent, - "Hello", - "message content should display only body when subtype description is similar" - ); -}); - -QUnit.test('data-oe-id & data-oe-model link redirection on click', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - pyEnv['mail.message'].create({ - body: `

some.model_250

`, - model: 'res.partner', - res_id: threadId, - }); - const { env, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.strictEqual( - action.type, - 'ir.actions.act_window', - "action should open view" + await click(".o_MessageView_notificationIconClickable"); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView", + "notification popover should be open" + ); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView_notificationIcon", + "popover should have one icon" + ); + assert.hasClass( + document.querySelector(".o_MessageNotificationPopoverContentView_notificationIcon"), + "fa-check", + "popover should have the sent icon" + ); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView_notificationPartnerName", + "popover should have the partner name" ); assert.strictEqual( - action.res_model, - 'some.model', - "action should open view on 'some.model' model" + document + .querySelector( + ".o_MessageNotificationPopoverContentView_notificationPartnerName" + ) + .textContent.trim(), + "Someone", + "partner name should be correct" + ); + }); + + QUnit.test("Notification Error", async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const [threadId, resPartnerId] = pyEnv["res.partner"].create([ + {}, + { name: "Someone", partner_share: true }, + ]); + const mailMessageId = pyEnv["mail.message"].create({ + body: "not empty", + message_type: "email", + model: "res.partner", + res_id: threadId, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId, + notification_status: "exception", + notification_type: "email", + res_partner_id: resPartnerId, + }); + const openResendActionDef = makeDeferred(); + const { env, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action, options) { + assert.step("do_action"); + assert.strictEqual( + action, + "mail.mail_resend_message_action", + "action should be the one to resend email" + ); + assert.strictEqual( + options.additionalContext.mail_message_to_resend, + mailMessageId, + "action should have correct message id" + ); + openResendActionDef.resolve(); + }, + }); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsOnce( + document.body, + ".o_MessageView_notificationIconClickable", + "should display the notification icon container" + ); + assert.containsOnce( + document.body, + ".o_MessageView_notificationIcon", + "should display the notification icon" + ); + assert.hasClass( + document.querySelector(".o_MessageView_notificationIcon"), + "fa-envelope", + "icon should represent email error" + ); + document.querySelector(".o_MessageView_notificationIconClickable").click(); + await openResendActionDef; + assert.verifySteps( + ["do_action"], + "should do an action to display the resend email dialog" + ); + }); + + QUnit.test( + "'channel_fetch' notification received is correctly handled", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({}); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + channel_type: "chat", + }); + pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsNone( + document.body, + ".o_MessageSeenIndicatorView_icon", + "message component should not have any check (V) as message is not yet received" + ); + + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId], + ])[0]; + // Simulate received channel fetched notification + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel.member/fetched", { + channel_id: mailChannelId, + last_message_id: 100, + partner_id: resPartnerId, + }); + }); + + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView_icon", + "message seen indicator component should only contain one check (V) as message is just received" + ); + } + ); + + QUnit.test( + "'channel_seen' notification received is correctly handled", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({}); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + channel_type: "chat", + }); + pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsNone( + document.body, + ".o_MessageSeenIndicatorView_icon", + "message component should not have any check (V) as message is not yet received" + ); + + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId], + ])[0]; + // Simulate received channel seen notification + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel.member/seen", { + channel_id: mailChannelId, + last_message_id: 100, + partner_id: resPartnerId, + }); + }); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 2, + "message seen indicator component should contain two checks (V) as message is seen" + ); + } + ); + + QUnit.test( + "'channel_fetch' notification then 'channel_seen' received are correctly handled", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ display_name: "Recipient" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + channel_type: "chat", + }); + pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsNone( + document.body, + ".o_MessageSeenIndicatorView_icon", + "message component should not have any check (V) as message is not yet received" + ); + + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId], + ])[0]; + // Simulate received channel fetched notification + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel.member/fetched", { + channel_id: mailChannelId, + last_message_id: 100, + partner_id: resPartnerId, + }); + }); + assert.containsOnce( + document.body, + ".o_MessageSeenIndicatorView_icon", + "message seen indicator component should only contain one check (V) as message is just received" + ); + + // Simulate received channel seen notification + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel.member/seen", { + channel_id: mailChannelId, + last_message_id: 100, + partner_id: resPartnerId, + }); + }); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 2, + "message seen indicator component should contain two checks (V) as message is now seen" + ); + } + ); + + QUnit.test( + "do not show message seen indicator on the last message seen by everyone when the current user is not author of the message", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const otherPartnerId = pyEnv["res.partner"].create({ name: "Demo User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_type: "chat", + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: otherPartnerId }], + ], + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: otherPartnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const memberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(memberIds, { seen_message_id: mailMessageId }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsNone( + document.body, + ".o_MessageView_seenIndicator", + "message component should not have any message seen indicator because the current user is not author" + ); + } + ); + + QUnit.test( + "do not show message seen indicator on all the messages of the current user that are older than the last message seen by everyone", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const otherPartnerId = pyEnv["res.partner"].create({ name: "Demo User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_type: "chat", + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: otherPartnerId }], + ], + }); + const [beforeLastMailMessageId, lastMailMessageId] = pyEnv["mail.message"].create([ + { + author_id: pyEnv.currentPartnerId, + body: "

Message before last seen

", + model: "mail.channel", + res_id: mailChannelId, + }, + { + author_id: pyEnv.currentPartnerId, + body: "

Last seen by everyone

", + model: "mail.channel", + res_id: mailChannelId, + }, + ]); + const memberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(memberIds, { + seen_message_id: lastMailMessageId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + `.o_MessageView[data-id=${beforeLastMailMessageId}]`, + "should display a message component" + ); + assert.containsOnce( + document.body, + `.o_MessageView[data-id=${beforeLastMailMessageId}] .o_MessageView_seenIndicator`, + "message component should have a message seen indicator because the current user is author" + ); + assert.containsNone( + document.body, + `.o_MessageView[data-id=${beforeLastMailMessageId}] .o_MessageSeenIndicatorView_icon`, + "message component should not have any check (V) because it is older than the last message seen by everyone" + ); + } + ); + + QUnit.test( + "only show messaging seen indicator if authored by me, after last seen by all message", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const otherPartnerId = pyEnv["res.partner"].create({ name: "Demo User" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_type: "chat", + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: otherPartnerId }], + ], + }); + const mailMessageId = pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "

Test

", + res_id: mailChannelId, + model: "mail.channel", + }); + const memberIds = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId], + ]); + pyEnv["mail.channel.member"].write(memberIds, { + fetched_message_id: mailMessageId, + seen_message_id: mailMessageId - 1, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a message component" + ); + assert.containsOnce( + document.body, + ".o_MessageView_seenIndicator", + "message component should have a message seen indicator" + ); + assert.containsN( + document.body, + ".o_MessageSeenIndicatorView_icon", + 1, + "message component should have one check (V) because the message was fetched by everyone but no other member than author has seen the message" + ); + } + ); + + QUnit.test("allow attachment delete on authored message", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + attachment_ids: [ + [ + 0, + 0, + { + mimetype: "image/jpeg", + name: "BLAH", + res_id: mailChannelId, + res_model: "mail.channel", + }, + ], + ], + author_id: pyEnv.currentPartnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const { click, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce(document.body, ".o_AttachmentImage", "should have an attachment"); + assert.containsOnce( + document.body, + ".o_AttachmentImage_actionUnlink", + "should have delete attachment button" + ); + + await click(".o_AttachmentImage_actionUnlink"); + assert.containsOnce( + document.body, + ".o_AttachmentDeleteConfirmView", + "An attachment delete confirmation dialog should have been opened" ); assert.strictEqual( - action.res_id, - 250, - "action should open view on 250" + document.querySelector(".o_AttachmentDeleteConfirmView_mainText").textContent, + `Do you really want to delete "BLAH"?`, + "Confirmation dialog should contain the attachment delete confirmation text" ); - assert.step('do-action:openFormView_some.model_250'); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView_content', - "message should have content" - ); - assert.containsOnce( - document.querySelector('.o_MessageView_content'), - 'a', - "message content should have a link" - ); - document.querySelector(`.o_MessageView_content a`).click(); - assert.verifySteps( - ['do-action:openFormView_some.model_250'], - "should have open form view on related record after click on link" - ); -}); - -QUnit.test('chat with author should be opened after clicking on their avatar', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const [threadId, resPartnerId] = pyEnv['res.partner'].create([{}, {}]); - pyEnv['res.users'].create({ partner_id: resPartnerId }); - pyEnv['mail.message'].create({ - author_id: resPartnerId, - body: 'not empty', - model: 'res.partner', - res_id: threadId, - }); - const { click, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_MessageView_authorAvatar', - "message should have the author avatar" - ); - assert.hasClass( - document.querySelector('.o_MessageView_authorAvatar'), - 'o_redirect', - "author avatar should have the redirect style" - ); - - await click('.o_MessageView_authorAvatar'); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window with thread should be opened after clicking on author avatar" - ); - assert.strictEqual( - document.querySelector('.o_ChatWindow_thread').dataset.correspondentId, - resPartnerId.toString(), - "chat with author should be opened after clicking on their avatar" - ); -}); - -QUnit.test('chat with author should be opened after clicking on their name', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({}); - pyEnv['res.users'].create({ partner_id: resPartnerId }); - pyEnv['mail.message'].create({ - author_id: resPartnerId, - body: 'not empty', - model: 'res.partner', - res_id: resPartnerId, - }); - const { click, openFormView } = await start(); - await openFormView({ - res_model: 'res.partner', - res_id: resPartnerId, - }); - assert.containsOnce( - document.body, - '.o_MessageView_authorName', - "message should have the author name" - ); - assert.hasClass( - document.querySelector('.o_MessageView_authorName'), - 'o_redirect', - "author name should have the redirect style" - ); - - await click('.o_MessageView_authorName'); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window with thread should be opened after clicking on author name" - ); - assert.strictEqual( - document.querySelector('.o_ChatWindow_thread').dataset.correspondentId, - resPartnerId.toString(), - "chat with author should be opened after clicking on their name" - ); -}); - -QUnit.test('chat with author should be opened after clicking on their im status icon', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const [threadId, resPartnerId] = pyEnv['res.partner'].create([{}, { im_status: 'online' }]); - pyEnv['res.users'].create({ - im_status: 'online', - partner_id: resPartnerId, - }); - pyEnv['mail.message'].create({ - author_id: resPartnerId, - body: 'not empty', - model: 'res.partner', - res_id: threadId, - }); - const { advanceTime, click, openView } = await start({ - hasTimeControl: true, - }); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - await afterNextRender(() => advanceTime(50 * 1000)); // next fetch of im_status - assert.containsOnce( - document.body, - '.o_MessageView_personaImStatusIcon', - "message should have the author im status icon" - ); - assert.hasClass( - document.querySelector('.o_MessageView_personaImStatusIcon'), - 'o-has-open-chat', - "author im status icon should have the open chat style" - ); - - await click('.o_MessageView_personaImStatusIcon'); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window with thread should be opened after clicking on author im status icon" - ); - assert.strictEqual( - document.querySelector('.o_ChatWindow_thread').dataset.correspondentId, - resPartnerId.toString(), - "chat with author should be opened after clicking on their im status icon" - ); -}); - -QUnit.test('open chat with author on avatar click should be disabled when currently chatting with the author', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({}); - pyEnv['res.users'].create({ partner_id: resPartnerId }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId }], - ], - channel_type: 'chat', - }); - pyEnv['mail.message'].create({ - author_id: resPartnerId, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, - }); - const { openDiscuss } = await start({ - discuss: { - params: { - default_active_id: `mail.channel_${mailChannelId}`, - }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageView_authorAvatar', - "message should have the author avatar" - ); - assert.doesNotHaveClass( - document.querySelector('.o_MessageView_authorAvatar'), - 'o_redirect', - "author avatar should not have the redirect style" - ); - - document.querySelector('.o_MessageView_authorAvatar').click(); - await nextAnimationFrame(); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should have no thread opened after clicking on author avatar when currently chatting with the author" - ); -}); - -QUnit.test('Chat with partner should be opened after clicking on their mention', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({ name: 'Test Partner', email: 'testpartner@odoo.com' }); - pyEnv['res.users'].create({ partner_id: resPartnerId }); - const { click, insertText, openFormView } = await start(); - await openFormView({ - res_model: 'res.partner', - res_id: resPartnerId, - }); - - await click('.o_ChatterTopbar_buttonSendMessage'); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - await click('.o_mail_redirect'); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window with thread should be opened after clicking on partner mention" - ); - assert.strictEqual( - document.querySelector('.o_ChatWindow_thread').dataset.correspondentId, - resPartnerId.toString(), - "chat with partner should be opened after clicking on their mention" - ); -}); - -QUnit.test('Channel should be opened after clicking on its mention', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId = pyEnv['res.partner'].create({}); - pyEnv['mail.channel'].create({ name: 'my-channel' }); - const { click, insertText, openFormView } = await start(); - await openFormView({ - res_model: 'res.partner', - res_id: resPartnerId, - }); - - await click('.o_ChatterTopbar_buttonSendMessage'); - await insertText('.o_ComposerTextInputView_textarea', "#my-channel"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - await click('.o_channel_redirect'); - assert.containsOnce( - document.body, - '.o_ChatWindow_thread', - "chat window with thread should be opened after clicking on channel mention" - ); -}); - -QUnit.test('message should not be considered as "clicked" after clicking on its author avatar', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [threadId, partnerId] = pyEnv['res.partner'].create([{}, {}]); - pyEnv['mail.message'].create({ - author_id: partnerId, - body: "

Test

", - model: 'res.partner', - res_id: threadId, - }); - const { openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - document.querySelector(`.o_MessageView_authorAvatar`).click(); - await nextAnimationFrame(); - assert.doesNotHaveClass( - document.querySelector(`.o_MessageView`), - 'o-clicked', - "message should not be considered as 'clicked' after clicking on its author avatar" - ); -}); - -QUnit.test('message should not be considered as "clicked" after clicking on notification failure icon', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const threadId = pyEnv['res.partner'].create({}); - const mailMessageId = pyEnv['mail.message'].create({ - body: 'not empty', - model: 'res.partner', - res_id: threadId, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId, - notification_status: 'exception', - notification_type: 'email', - }); - const { env, openView } = await start(); - await openView({ - res_id: threadId, - res_model: 'res.partner', - views: [[false, 'form']], - }); - patchWithCleanup(env.services.action, { - // intercept the action: this action is not relevant in the context of this test. - doAction() {}, - }); - document.querySelector('.o_MessageView_notificationIconClickable.o-error').click(); - await nextAnimationFrame(); - assert.doesNotHaveClass( - document.querySelector(`.o_MessageView`), - 'o-clicked', - "message should not be considered as 'clicked' after clicking on notification failure icon" - ); -}); - -}); + await click(".o_AttachmentDeleteConfirmView_confirmButton"); + assert.containsNone( + document.body, + ".o_AttachmentCard", + "should no longer have an attachment" + ); + }); + + QUnit.test( + "prevent attachment delete on non-authored message in channels", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({}); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + attachment_ids: [ + [ + 0, + 0, + { + mimetype: "image/jpeg", + name: "BLAH", + res_id: mailChannelId, + res_model: "mail.channel", + }, + ], + ], + author_id: partnerId, + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_AttachmentImage", + "should have an attachment" + ); + assert.containsNone( + document.body, + ".o_AttachmentImage_actionUnlink", + "delete attachment button should not be printed" + ); + } + ); + + QUnit.test("allow attachment image download on message", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const irAttachmentId1 = pyEnv["ir.attachment"].create({ + name: "Blah.jpg", + mimetype: "image/jpeg", + }); + pyEnv["mail.message"].create({ + attachment_ids: [irAttachmentId1], + body: "

Test

", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + context: { + active_id: mailChannelId1, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_AttachmentImage_actionDownload", + "should have download attachment button" + ); + }); + + QUnit.test( + "subtype description should be displayed if it is different than body", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + const subtypeId = pyEnv["mail.message.subtype"].create({ description: "Bonjour" }); + pyEnv["mail.message"].create({ + body: "

Hello

", + model: "res.partner", + res_id: threadId, + subtype_id: subtypeId, + }); + const { openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_MessageView_content", + "message should have content" + ); + assert.strictEqual( + document.querySelector(`.o_MessageView_content`).textContent, + "HelloBonjour", + "message content should display both body and subtype description when they are different" + ); + } + ); + + QUnit.test( + "subtype description should not be displayed if it is similar to body", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + const subtypeId = pyEnv["mail.message.subtype"].create({ description: "hello" }); + pyEnv["mail.message"].create({ + body: "

Hello

", + model: "res.partner", + res_id: threadId, + subtype_id: subtypeId, + }); + const { openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_MessageView_content", + "message should have content" + ); + assert.strictEqual( + document.querySelector(`.o_MessageView_content`).textContent, + "Hello", + "message content should display only body when subtype description is similar" + ); + } + ); + + QUnit.test("data-oe-id & data-oe-model link redirection on click", async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + pyEnv["mail.message"].create({ + body: `

some.model_250

`, + model: "res.partner", + res_id: threadId, + }); + const { env, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.strictEqual( + action.type, + "ir.actions.act_window", + "action should open view" + ); + assert.strictEqual( + action.res_model, + "some.model", + "action should open view on 'some.model' model" + ); + assert.strictEqual(action.res_id, 250, "action should open view on 250"); + assert.step("do-action:openFormView_some.model_250"); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView_content", + "message should have content" + ); + assert.containsOnce( + document.querySelector(".o_MessageView_content"), + "a", + "message content should have a link" + ); + + document.querySelector(`.o_MessageView_content a`).click(); + assert.verifySteps( + ["do-action:openFormView_some.model_250"], + "should have open form view on related record after click on link" + ); + }); + + QUnit.test( + "chat with author should be opened after clicking on their avatar", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [threadId, resPartnerId] = pyEnv["res.partner"].create([{}, {}]); + pyEnv["res.users"].create({ partner_id: resPartnerId }); + pyEnv["mail.message"].create({ + author_id: resPartnerId, + body: "not empty", + model: "res.partner", + res_id: threadId, + }); + const { click, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_MessageView_authorAvatar", + "message should have the author avatar" + ); + assert.hasClass( + document.querySelector(".o_MessageView_authorAvatar"), + "o_redirect", + "author avatar should have the redirect style" + ); + + await click(".o_MessageView_authorAvatar"); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window with thread should be opened after clicking on author avatar" + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow_thread").dataset.correspondentId, + resPartnerId.toString(), + "chat with author should be opened after clicking on their avatar" + ); + } + ); + + QUnit.test( + "chat with author should be opened after clicking on their name", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({}); + pyEnv["res.users"].create({ partner_id: resPartnerId }); + pyEnv["mail.message"].create({ + author_id: resPartnerId, + body: "not empty", + model: "res.partner", + res_id: resPartnerId, + }); + const { click, openFormView } = await start(); + await openFormView({ + res_model: "res.partner", + res_id: resPartnerId, + }); + assert.containsOnce( + document.body, + ".o_MessageView_authorName", + "message should have the author name" + ); + assert.hasClass( + document.querySelector(".o_MessageView_authorName"), + "o_redirect", + "author name should have the redirect style" + ); + + await click(".o_MessageView_authorName"); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window with thread should be opened after clicking on author name" + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow_thread").dataset.correspondentId, + resPartnerId.toString(), + "chat with author should be opened after clicking on their name" + ); + } + ); + + QUnit.test( + "chat with author should be opened after clicking on their im status icon", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [threadId, resPartnerId] = pyEnv["res.partner"].create([ + {}, + { im_status: "online" }, + ]); + pyEnv["res.users"].create({ + im_status: "online", + partner_id: resPartnerId, + }); + pyEnv["mail.message"].create({ + author_id: resPartnerId, + body: "not empty", + model: "res.partner", + res_id: threadId, + }); + const { advanceTime, click, openView } = await start({ + hasTimeControl: true, + }); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + await afterNextRender(() => advanceTime(50 * 1000)); // next fetch of im_status + assert.containsOnce( + document.body, + ".o_MessageView_personaImStatusIcon", + "message should have the author im status icon" + ); + assert.hasClass( + document.querySelector(".o_MessageView_personaImStatusIcon"), + "o-has-open-chat", + "author im status icon should have the open chat style" + ); + + await click(".o_MessageView_personaImStatusIcon"); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window with thread should be opened after clicking on author im status icon" + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow_thread").dataset.correspondentId, + resPartnerId.toString(), + "chat with author should be opened after clicking on their im status icon" + ); + } + ); + + QUnit.test( + "open chat with author on avatar click should be disabled when currently chatting with the author", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({}); + pyEnv["res.users"].create({ partner_id: resPartnerId }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId }], + ], + channel_type: "chat", + }); + pyEnv["mail.message"].create({ + author_id: resPartnerId, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { openDiscuss } = await start({ + discuss: { + params: { + default_active_id: `mail.channel_${mailChannelId}`, + }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageView_authorAvatar", + "message should have the author avatar" + ); + assert.doesNotHaveClass( + document.querySelector(".o_MessageView_authorAvatar"), + "o_redirect", + "author avatar should not have the redirect style" + ); + + document.querySelector(".o_MessageView_authorAvatar").click(); + await nextAnimationFrame(); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should have no thread opened after clicking on author avatar when currently chatting with the author" + ); + } + ); + + QUnit.test( + "Chat with partner should be opened after clicking on their mention", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({ + name: "Test Partner", + email: "testpartner@odoo.com", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId }); + const { click, insertText, openFormView } = await start(); + await openFormView({ + res_model: "res.partner", + res_id: resPartnerId, + }); + + await click(".o_ChatterTopbar_buttonSendMessage"); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + await click(".o_mail_redirect"); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window with thread should be opened after clicking on partner mention" + ); + assert.strictEqual( + document.querySelector(".o_ChatWindow_thread").dataset.correspondentId, + resPartnerId.toString(), + "chat with partner should be opened after clicking on their mention" + ); + } + ); + + QUnit.test( + "Channel should be opened after clicking on its mention", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId = pyEnv["res.partner"].create({}); + pyEnv["mail.channel"].create({ name: "my-channel" }); + const { click, insertText, openFormView } = await start(); + await openFormView({ + res_model: "res.partner", + res_id: resPartnerId, + }); + + await click(".o_ChatterTopbar_buttonSendMessage"); + await insertText(".o_ComposerTextInputView_textarea", "#my-channel"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + await click(".o_channel_redirect"); + assert.containsOnce( + document.body, + ".o_ChatWindow_thread", + "chat window with thread should be opened after clicking on channel mention" + ); + } + ); + + QUnit.test( + 'message should not be considered as "clicked" after clicking on its author avatar', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [threadId, partnerId] = pyEnv["res.partner"].create([{}, {}]); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "

Test

", + model: "res.partner", + res_id: threadId, + }); + const { openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + document.querySelector(`.o_MessageView_authorAvatar`).click(); + await nextAnimationFrame(); + assert.doesNotHaveClass( + document.querySelector(`.o_MessageView`), + "o-clicked", + "message should not be considered as 'clicked' after clicking on its author avatar" + ); + } + ); + + QUnit.test( + 'message should not be considered as "clicked" after clicking on notification failure icon', + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const threadId = pyEnv["res.partner"].create({}); + const mailMessageId = pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + res_id: threadId, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId, + notification_status: "exception", + notification_type: "email", + }); + const { env, openView } = await start(); + await openView({ + res_id: threadId, + res_model: "res.partner", + views: [[false, "form"]], + }); + patchWithCleanup(env.services.action, { + // intercept the action: this action is not relevant in the context of this test. + doAction() {}, + }); + document.querySelector(".o_MessageView_notificationIconClickable.o-error").click(); + await nextAnimationFrame(); + assert.doesNotHaveClass( + document.querySelector(`.o_MessageView`), + "o-clicked", + "message should not be considered as 'clicked' after clicking on notification failure icon" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js index 01b6f1e53fd..e773886738e 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/messaging_menu_tests.js @@ -1,914 +1,998 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -import { browser } from '@web/core/browser/browser'; +import { browser } from "@web/core/browser/browser"; import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; import { patchWithCleanup } from "@web/../tests/helpers/utils"; -import { makeTestPromise } from 'web.test_utils'; +import { makeTestPromise } from "web.test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('messaging_menu_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("messaging_menu_tests.js"); -QUnit.test('[technical] messaging not created then becomes created', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - */ - assert.expect(2); + QUnit.test( + "[technical] messaging not created then becomes created", + async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(2); - const messagingBeforeCreationDeferred = makeTestPromise(); - await start({ - messagingBeforeCreationDeferred, - waitUntilMessagingCondition: 'none', - }); - assert.containsOnce( - document.body, - '.o_MessagingMenuContainer_spinner', - "messaging menu container should have spinner when messaging is not yet created" - ); + const messagingBeforeCreationDeferred = makeTestPromise(); + await start({ + messagingBeforeCreationDeferred, + waitUntilMessagingCondition: "none", + }); + assert.containsOnce( + document.body, + ".o_MessagingMenuContainer_spinner", + "messaging menu container should have spinner when messaging is not yet created" + ); - // simulate messaging becoming created - await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); - assert.containsOnce( - document.body, - '.o_MessagingMenu', - "messaging menu container should contain messaging menu after messaging has been created" - ); -}); - -QUnit.test('messaging not initialized', async function (assert) { - assert.expect(2); - - const messaginginitializedDeferred = makeTestPromise(); - const { click } = await start({ - async mockRPC(route) { - if (route === '/mail/init_messaging') { - await messaginginitializedDeferred; // simulate messaging never initialized + // simulate messaging becoming created + await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); + assert.containsOnce( + document.body, + ".o_MessagingMenu", + "messaging menu container should contain messaging menu after messaging has been created" + ); } - }, - waitUntilMessagingCondition: 'created', - }); - assert.strictEqual( - document.querySelectorAll('.o_MessagingMenu_loading').length, - 1, - "should display loading icon on messaging menu when messaging not yet initialized" - ); + ); - await click(`.o_MessagingMenu_toggler`); - assert.strictEqual( - document.querySelector('.o_MessagingMenu_dropdownMenu').textContent, - "Please wait...", - "should prompt loading when opening messaging menu" - ); - messaginginitializedDeferred.resolve(); // ensure proper teardown -}); + QUnit.test("messaging not initialized", async function (assert) { + assert.expect(2); -QUnit.test('messaging becomes initialized', async function (assert) { - assert.expect(2); + const messaginginitializedDeferred = makeTestPromise(); + const { click } = await start({ + async mockRPC(route) { + if (route === "/mail/init_messaging") { + await messaginginitializedDeferred; // simulate messaging never initialized + } + }, + waitUntilMessagingCondition: "created", + }); + assert.strictEqual( + document.querySelectorAll(".o_MessagingMenu_loading").length, + 1, + "should display loading icon on messaging menu when messaging not yet initialized" + ); - const messagingInitializedProm = makeTestPromise(); + await click(`.o_MessagingMenu_toggler`); + assert.strictEqual( + document.querySelector(".o_MessagingMenu_dropdownMenu").textContent, + "Please wait...", + "should prompt loading when opening messaging menu" + ); + messaginginitializedDeferred.resolve(); // ensure proper teardown + }); - const { click } = await start({ - async mockRPC(route) { - if (route === '/mail/init_messaging') { - await messagingInitializedProm; - } - }, - waitUntilMessagingCondition: 'created', - }); - await click(`.o_MessagingMenu_toggler`); + QUnit.test("messaging becomes initialized", async function (assert) { + assert.expect(2); - // simulate messaging becomes initialized - await afterNextRender(() => messagingInitializedProm.resolve()); - assert.strictEqual( - document.querySelectorAll('.o_MessagingMenu_loading').length, - 0, - "should no longer display loading icon on messaging menu when messaging becomes initialized" - ); - assert.notOk( - document.querySelector('.o_MessagingMenu_dropdownMenu').textContent.includes("Please wait..."), - "should no longer prompt loading when opening messaging menu when messaging becomes initialized" - ); -}); + const messagingInitializedProm = makeTestPromise(); -QUnit.test('basic rendering', async function (assert) { - assert.expect(21); + const { click } = await start({ + async mockRPC(route) { + if (route === "/mail/init_messaging") { + await messagingInitializedProm; + } + }, + waitUntilMessagingCondition: "created", + }); + await click(`.o_MessagingMenu_toggler`); - patchWithCleanup(browser, { - Notification: { - ...browser.Notification, - permission: 'denied', - }, - }); - const { click } = await start(); - assert.strictEqual( - document.querySelectorAll('.o_MessagingMenu').length, - 1, - "should have messaging menu" - ); - assert.notOk( - document.querySelector('.o_MessagingMenu').classList.contains('show'), - "should not mark messaging menu item as shown by default" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_toggler`).length, - 1, - "should have clickable element on messaging menu" - ); - assert.notOk( - document.querySelector(`.o_MessagingMenu_toggler`).classList.contains('show'), - "should not mark messaging menu clickable item as shown by default" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_icon`).length, - 1, - "should have icon on clickable element in messaging menu" - ); - assert.ok( - document.querySelector(`.o_MessagingMenu_icon`).classList.contains('fa-comments'), - "should have 'comments' icon on clickable element in messaging menu" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, - 0, - "should not display any messaging menu dropdown by default" - ); + // simulate messaging becomes initialized + await afterNextRender(() => messagingInitializedProm.resolve()); + assert.strictEqual( + document.querySelectorAll(".o_MessagingMenu_loading").length, + 0, + "should no longer display loading icon on messaging menu when messaging becomes initialized" + ); + assert.notOk( + document + .querySelector(".o_MessagingMenu_dropdownMenu") + .textContent.includes("Please wait..."), + "should no longer prompt loading when opening messaging menu when messaging becomes initialized" + ); + }); - await click(`.o_MessagingMenu_toggler`); - assert.hasClass( - document.querySelector('.o_MessagingMenu'), - "show", - "should mark messaging menu as opened" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, - 1, - "should display messaging menu dropdown after click" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenuHeader`).length, - 1, - "should have dropdown menu header" - ); - assert.strictEqual( - document.querySelectorAll(` + QUnit.test("basic rendering", async function (assert) { + assert.expect(21); + + patchWithCleanup(browser, { + Notification: { + ...browser.Notification, + permission: "denied", + }, + }); + const { click } = await start(); + assert.strictEqual( + document.querySelectorAll(".o_MessagingMenu").length, + 1, + "should have messaging menu" + ); + assert.notOk( + document.querySelector(".o_MessagingMenu").classList.contains("show"), + "should not mark messaging menu item as shown by default" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_toggler`).length, + 1, + "should have clickable element on messaging menu" + ); + assert.notOk( + document.querySelector(`.o_MessagingMenu_toggler`).classList.contains("show"), + "should not mark messaging menu clickable item as shown by default" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_icon`).length, + 1, + "should have icon on clickable element in messaging menu" + ); + assert.ok( + document.querySelector(`.o_MessagingMenu_icon`).classList.contains("fa-comments"), + "should have 'comments' icon on clickable element in messaging menu" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 0, + "should not display any messaging menu dropdown by default" + ); + + await click(`.o_MessagingMenu_toggler`); + assert.hasClass( + document.querySelector(".o_MessagingMenu"), + "show", + "should mark messaging menu as opened" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu`).length, + 1, + "should display messaging menu dropdown after click" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenuHeader`).length, + 1, + "should have dropdown menu header" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenuHeader .o_MessagingMenuTabView `).length, - 3, - "should have 3 tab buttons to filter items in the header" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="all"]`).length, - 1, - "1 tab button should be 'All'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="chat"]`).length, - 1, - "1 tab button should be 'Chat'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="channel"]`).length, - 1, - "1 tab button should be 'Channels'" - ); - assert.ok( - document.querySelector(` + 3, + "should have 3 tab buttons to filter items in the header" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="all"]`).length, + 1, + "1 tab button should be 'All'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="chat"]`).length, + 1, + "1 tab button should be 'Chat'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="channel"]`).length, + 1, + "1 tab button should be 'Channels'" + ); + assert.ok( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="all"] - `).classList.contains('o-active'), - "'all' tab button should be active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'all' tab button should be active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="chat"] - `).classList.contains('o-active'), - "'chat' tab button should not be active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'chat' tab button should not be active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="channel"] - `).classList.contains('o-active'), - "'channel' tab button should not be active" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, - 1, - "should have button to make a new message" - ); - assert.strictEqual( - document.querySelectorAll(` + ` + ) + .classList.contains("o-active"), + "'channel' tab button should not be active" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, + 1, + "should have button to make a new message" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_NotificationListView `).length, - 1, - "should display thread preview list" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should display thread preview list" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_NotificationListView_noConversation `).length, - 1, - "should display no conversation in thread preview list" - ); + 1, + "should display no conversation in thread preview list" + ); - await click(`.o_MessagingMenu_toggler`); - assert.doesNotHaveClass( - document.querySelector('.o_MessagingMenu'), - "show", - "should mark messaging menu as closed" - ); -}); + await click(`.o_MessagingMenu_toggler`); + assert.doesNotHaveClass( + document.querySelector(".o_MessagingMenu"), + "show", + "should mark messaging menu as closed" + ); + }); -QUnit.test('counter is taking into account failure notification', async function (assert) { - assert.expect(2); + QUnit.test("counter is taking into account failure notification", async function (assert) { + assert.expect(2); - patchWithCleanup(browser, { - Notification: { - ...browser.Notification, - permission: 'denied', - }, - }); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - model: 'mail.channel', - res_id: mailChannelId1, - }); - const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: mailMessageId1 }); - // failure that is expected to be used in the test - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, // id of the related message - notification_status: 'exception', // necessary value to have a failure - notification_type: 'email', - }); - await start(); + patchWithCleanup(browser, { + Notification: { + ...browser.Notification, + permission: "denied", + }, + }); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + model: "mail.channel", + res_id: mailChannelId1, + }); + const [mailChannelMemberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId], { + seen_message_id: mailMessageId1, + }); + // failure that is expected to be used in the test + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, // id of the related message + notification_status: "exception", // necessary value to have a failure + notification_type: "email", + }); + await start(); - assert.containsOnce( - document.body, - '.o_MessagingMenu_counter', - "should display a notification counter next to the messaging menu for one notification" - ); - assert.strictEqual( - document.querySelector('.o_MessagingMenu_counter').textContent, - "1", - "should display a counter of '1' next to the messaging menu" - ); -}); + assert.containsOnce( + document.body, + ".o_MessagingMenu_counter", + "should display a notification counter next to the messaging menu for one notification" + ); + assert.strictEqual( + document.querySelector(".o_MessagingMenu_counter").textContent, + "1", + "should display a counter of '1' next to the messaging menu" + ); + }); -QUnit.test('switch tab', async function (assert) { - assert.expect(15); + QUnit.test("switch tab", async function (assert) { + assert.expect(15); - const { click } = await start(); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="all"]`).length, - 1, - "1 tab button should be 'All'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="chat"]`).length, - 1, - "1 tab button should be 'Chat'" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="channel"]`).length, - 1, - "1 tab button should be 'Channels'" - ); - assert.ok( - document.querySelector(` + await click(`.o_MessagingMenu_toggler`); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="all"]`).length, + 1, + "1 tab button should be 'All'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="chat"]`).length, + 1, + "1 tab button should be 'Chat'" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenuTabView[data-tab-id="channel"]`).length, + 1, + "1 tab button should be 'Channels'" + ); + assert.ok( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="all"] - `).classList.contains('o-active'), - "'all' tab button should be active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'all' tab button should be active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="chat"] - `).classList.contains('o-active'), - "'chat' tab button should not be active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'chat' tab button should not be active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="channel"] - `).classList.contains('o-active'), - "'channel' tab button should not be active" - ); + ` + ) + .classList.contains("o-active"), + "'channel' tab button should not be active" + ); - await click(`.o_MessagingMenuTabView[data-tab-id="chat"]`); - assert.notOk( - document.querySelector(` + await click(`.o_MessagingMenuTabView[data-tab-id="chat"]`); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="all"] - `).classList.contains('o-active'), - "'all' tab button should become inactive" - ); - assert.ok( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'all' tab button should become inactive" + ); + assert.ok( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="chat"] - `).classList.contains('o-active'), - "'chat' tab button should not become active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'chat' tab button should not become active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="channel"] - `).classList.contains('o-active'), - "'channel' tab button should stay inactive" - ); + ` + ) + .classList.contains("o-active"), + "'channel' tab button should stay inactive" + ); - await click(`.o_MessagingMenuTabView[data-tab-id="channel"]`); - assert.notOk( - document.querySelector(` + await click(`.o_MessagingMenuTabView[data-tab-id="channel"]`); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="all"] - `).classList.contains('o-active'), - "'all' tab button should stay active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'all' tab button should stay active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="chat"] - `).classList.contains('o-active'), - "'chat' tab button should become inactive" - ); - assert.ok( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'chat' tab button should become inactive" + ); + assert.ok( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="channel"] - `).classList.contains('o-active'), - "'channel' tab button should become active" - ); + ` + ) + .classList.contains("o-active"), + "'channel' tab button should become active" + ); - await click(`.o_MessagingMenuTabView[data-tab-id="all"]`); - assert.ok( - document.querySelector(` + await click(`.o_MessagingMenuTabView[data-tab-id="all"]`); + assert.ok( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="all"] - `).classList.contains('o-active'), - "'all' tab button should become active" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'all' tab button should become active" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="chat"] - `).classList.contains('o-active'), - "'chat' tab button should stay inactive" - ); - assert.notOk( - document.querySelector(` + ` + ) + .classList.contains("o-active"), + "'chat' tab button should stay inactive" + ); + assert.notOk( + document + .querySelector( + ` .o_MessagingMenuTabView[data-tab-id="channel"] - `).classList.contains('o-active'), - "'channel' tab button should become inactive" - ); -}); + ` + ) + .classList.contains("o-active"), + "'channel' tab button should become inactive" + ); + }); -QUnit.test('new message', async function (assert) { - assert.expect(3); + QUnit.test("new message", async function (assert) { + assert.expect(3); - const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_newMessageButton`); + const { click } = await start(); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_newMessageButton`); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open a chat window" - ); - assert.ok( - document.querySelector(`.o_ChatWindow`).classList.contains('o-new-message'), - "chat window should be for new message" - ); - assert.ok( - document.querySelector(`.o_ChatWindow`).classList.contains('o-focused'), - "chat window should be focused" - ); -}); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + assert.ok( + document.querySelector(`.o_ChatWindow`).classList.contains("o-new-message"), + "chat window should be for new message" + ); + assert.ok( + document.querySelector(`.o_ChatWindow`).classList.contains("o-focused"), + "chat window should be focused" + ); + }); -QUnit.test('no new message when discuss is open', async function (assert) { - assert.expect(3); + QUnit.test("no new message when discuss is open", async function (assert) { + assert.expect(3); - const { click, openDiscuss, openView } = await start(); - await openDiscuss({ waitUntilMessagesLoaded: false }); + const { click, openDiscuss, openView } = await start(); + await openDiscuss({ waitUntilMessagesLoaded: false }); - await click(`.o_MessagingMenu_toggler`); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, - 0, - "should not have 'new message' when discuss is open" - ); + await click(`.o_MessagingMenu_toggler`); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, + 0, + "should not have 'new message' when discuss is open" + ); - await openView({ - res_model: 'res.partner', - views: [[false, 'form']], - }); + await openView({ + res_model: "res.partner", + views: [[false, "form"]], + }); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, - 1, - "should have 'new message' when discuss is closed" - ); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, + 1, + "should have 'new message' when discuss is closed" + ); - await openDiscuss({ waitUntilMessagesLoaded: false }); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, - 0, - "should not have 'new message' when discuss is open again" - ); -}); + await openDiscuss({ waitUntilMessagesLoaded: false }); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_newMessageButton`).length, + 0, + "should not have 'new message' when discuss is open again" + ); + }); -QUnit.test('channel preview: basic rendering', async function (assert) { - assert.expect(9); + QUnit.test("channel preview: basic rendering", async function (assert) { + assert.expect(9); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Demo" }); - const mailChannelId1 = pyEnv['mail.channel'].create({ name: "General" }); - pyEnv['mail.message'].create({ - author_id: resPartnerId1, // not current partner, will be asserted in the test - body: "

test

", // random body, will be asserted in the test - model: 'mail.channel', // necessary to link message to channel - res_id: mailChannelId1, // id of related channel - }); - const { click } = await start(); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ name: "General" }); + pyEnv["mail.message"].create({ + author_id: resPartnerId1, // not current partner, will be asserted in the test + body: "

test

", // random body, will be asserted in the test + model: "mail.channel", // necessary to link message to channel + res_id: mailChannelId1, // id of related channel + }); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - assert.strictEqual( - document.querySelectorAll(` + await click(`.o_MessagingMenu_toggler`); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView `).length, - 1, - "should have one preview" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have one preview" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_sidebar `).length, - 1, - "preview should have a sidebar" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "preview should have a sidebar" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_content `).length, - 1, - "preview should have some content" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "preview should have some content" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_header `).length, - 1, - "preview should have header in content" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "preview should have header in content" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_header .o_ChannelPreviewView_name `).length, - 1, - "preview should have name in header of content" - ); - assert.strictEqual( - document.querySelector(` + 1, + "preview should have name in header of content" + ); + assert.strictEqual( + document.querySelector(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_name `).textContent, - "General", "preview should have name of channel" - ); - assert.strictEqual( - document.querySelectorAll(` + "General", + "preview should have name of channel" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_content .o_ChannelPreviewView_core `).length, - 1, - "preview should have core in content" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "preview should have core in content" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_core .o_ChannelPreviewView_inlineText `).length, - 1, - "preview should have inline text in core of content" - ); - assert.strictEqual( - document.querySelector(` + 1, + "preview should have inline text in core of content" + ); + assert.strictEqual( + document + .querySelector( + ` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView_core .o_ChannelPreviewView_inlineText - `).textContent.trim(), - "Demo: test", - "preview should have message content as inline text of core content" - ); -}); + ` + ) + .textContent.trim(), + "Demo: test", + "preview should have message content as inline text of core content" + ); + }); -QUnit.test('filtered previews', async function (assert) { - assert.expect(12); + QUnit.test("filtered previews", async function (assert) { + assert.expect(12); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { channel_type: "chat" }, - { name: "mailChannel1" }, - ]); - pyEnv['mail.message'].create([ - { - model: 'mail.channel', // to link message to channel - res_id: mailChannelId1, // id of related channel - }, - { - model: 'mail.channel', // to link message to channel - res_id: mailChannelId2, // id of related channel - }, - ]); - const { click } = await start(); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { channel_type: "chat" }, + { name: "mailChannel1" }, + ]); + pyEnv["mail.message"].create([ + { + model: "mail.channel", // to link message to channel + res_id: mailChannelId1, // id of related channel + }, + { + model: "mail.channel", // to link message to channel + res_id: mailChannelId2, // id of related channel + }, + ]); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`).length, - 2, - "should have 2 previews" - ); - assert.strictEqual( - document.querySelectorAll(` + await click(`.o_MessagingMenu_toggler`); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`) + .length, + 2, + "should have 2 previews" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have preview of chat" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have preview of chat" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `).length, - 1, - "should have preview of channel" - ); + 1, + "should have preview of channel" + ); - await click('.o_MessagingMenuTabView[data-tab-id="chat"]'); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`).length, - 1, - "should have one preview" - ); - assert.strictEqual( - document.querySelectorAll(` + await click('.o_MessagingMenuTabView[data-tab-id="chat"]'); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`) + .length, + 1, + "should have one preview" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have preview of chat" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have preview of chat" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `).length, - 0, - "should not have preview of channel" - ); + 0, + "should not have preview of channel" + ); - await click('.o_MessagingMenuTabView[data-tab-id="channel"]'); - assert.strictEqual( - document.querySelectorAll(` + await click('.o_MessagingMenuTabView[data-tab-id="channel"]'); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView `).length, - 1, - "should have one preview" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have one preview" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `).length, - 0, - "should not have preview of chat" - ); - assert.strictEqual( - document.querySelectorAll(` + 0, + "should not have preview of chat" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `).length, - 1, - "should have preview of channel" - ); + 1, + "should have preview of channel" + ); - await click('.o_MessagingMenuTabView[data-tab-id="all"]'); - assert.strictEqual( - document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`).length, - 2, - "should have 2 previews" - ); - assert.strictEqual( - document.querySelectorAll(` + await click('.o_MessagingMenuTabView[data-tab-id="all"]'); + assert.strictEqual( + document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`) + .length, + 2, + "should have 2 previews" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have preview of chat" - ); - assert.strictEqual( - document.querySelectorAll(` + 1, + "should have preview of chat" + ); + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId2}"] `).length, - 1, - "should have preview of channel" - ); -}); + 1, + "should have preview of channel" + ); + }); -QUnit.test('open chat window from preview', async function (assert) { - assert.expect(1); + QUnit.test("open chat window from preview", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({}); - const { click } = await start(); + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({}); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - await click(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`); - assert.strictEqual( - document.querySelectorAll(`.o_ChatWindow`).length, - 1, - "should have open a chat window" - ); -}); + await click(`.o_MessagingMenu_toggler`); + await click(`.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView`); + assert.strictEqual( + document.querySelectorAll(`.o_ChatWindow`).length, + 1, + "should have open a chat window" + ); + }); -QUnit.test('no code injection in message body preview', async function (assert) { - assert.expect(5); + QUnit.test("no code injection in message body preview", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: "

&shoulnotberaised

", - model: "mail.channel", - res_id: mailChannelId1, - }); - const { click } = await start(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: + "

&shoulnotberaised

", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - assert.containsOnce( - document.body, - '.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView', - "should display a preview", - ); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_core', - "preview should have core in content", - ); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_inlineText', - "preview should have inline text in core of content", - ); - assert.strictEqual( - document.querySelector('.o_ChannelPreviewView_inlineText') - .textContent.replace(/\s/g, ""), - "You:&shoulnotberaisedthrownewError('CodeInjectionError');", - "should display correct uninjected last message inline content" - ); - assert.containsNone( - document.querySelector('.o_ChannelPreviewView_inlineText'), - 'script', - "last message inline content should not have any code injection" - ); -}); + await click(`.o_MessagingMenu_toggler`); + assert.containsOnce( + document.body, + ".o_MessagingMenu_dropdownMenu .o_ChannelPreviewView", + "should display a preview" + ); + assert.containsOnce( + document.body, + ".o_ChannelPreviewView_core", + "preview should have core in content" + ); + assert.containsOnce( + document.body, + ".o_ChannelPreviewView_inlineText", + "preview should have inline text in core of content" + ); + assert.strictEqual( + document + .querySelector(".o_ChannelPreviewView_inlineText") + .textContent.replace(/\s/g, ""), + "You:&shoulnotberaisedthrownewError('CodeInjectionError');", + "should display correct uninjected last message inline content" + ); + assert.containsNone( + document.querySelector(".o_ChannelPreviewView_inlineText"), + "script", + "last message inline content should not have any code injection" + ); + }); -QUnit.test('no code injection in message body preview from sanitized message', async function (assert) { - assert.expect(5); + QUnit.test( + "no code injection in message body preview from sanitized message", + async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: "

<em>&shoulnotberaised</em><script>throw new Error('CodeInjectionError');</script>

", - model: "mail.channel", - res_id: mailChannelId1, - }); - const { click } = await start(); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: + "

<em>&shoulnotberaised</em><script>throw new Error('CodeInjectionError');</script>

", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { click } = await start(); - await click(`.o_MessagingMenu_toggler`); - assert.containsOnce( - document.body, - '.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView', - "should display a preview", - ); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_core', - "preview should have core in content", - ); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_inlineText', - "preview should have inline text in core of content", - ); - assert.strictEqual( - document.querySelector('.o_ChannelPreviewView_inlineText') - .textContent.replace(/\s/g, ""), - "You:&shoulnotberaised", - "should display correct uninjected last message inline content" - ); - assert.containsNone( - document.querySelector('.o_ChannelPreviewView_inlineText'), - 'script', - "last message inline content should not have any code injection" - ); -}); - -QUnit.test('
tags in message body preview are transformed in spaces', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: "

a
b
c
d

", - model: "mail.channel", - res_id: mailChannelId1, - }); - const { click } = await start(); - - await click(`.o_MessagingMenu_toggler`); - assert.containsOnce( - document.body, - '.o_MessagingMenu_dropdownMenu .o_ChannelPreviewView', - "should display a preview", - ); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_core', - "preview should have core in content", - ); - assert.containsOnce( - document.body, - '.o_ChannelPreviewView_inlineText', - "preview should have inline text in core of content", - ); - assert.strictEqual( - document.querySelector('.o_ChannelPreviewView_inlineText').textContent, - "You: a b c d", - "should display correct last message inline content with brs replaced by spaces" - ); -}); - -QUnit.test('rendering with OdooBot has a request (default)', async function (assert) { - assert.expect(4); - - patchWithCleanup(browser, { - Notification: { - ...browser.Notification, - permission: 'default', - }, - }); - const { click } = await start(); - - assert.ok( - document.querySelector('.o_MessagingMenu_counter'), - "should display a notification counter next to the messaging menu for OdooBot request" - ); - assert.strictEqual( - document.querySelector('.o_MessagingMenu_counter').textContent, - "1", - "should display a counter of '1' next to the messaging menu" - ); - - await click('.o_MessagingMenu_toggler'); - assert.containsOnce( - document.body, - '.o_NotificationRequestView', - "should display a notification in the messaging menu" - ); - assert.strictEqual( - document.querySelector('.o_NotificationRequestView_name').textContent.trim(), - 'OdooBot has a request', - "notification should display that OdooBot has a request" - ); -}); - -QUnit.test('rendering without OdooBot has a request (denied)', async function (assert) { - assert.expect(2); - - patchWithCleanup(browser, { - Notification: { - permission: 'denied', - }, - }); - const { click } = await start(); - - assert.containsNone( - document.body, - '.o_MessagingMenu_counter', - "should not display a notification counter next to the messaging menu" - ); - - await click('.o_MessagingMenu_toggler'); - assert.containsNone( - document.body, - '.o_NotificationRequestView', - "should display no notification in the messaging menu" - ); -}); - -QUnit.test('rendering without OdooBot has a request (accepted)', async function (assert) { - assert.expect(2); - - patchWithCleanup(browser, { - Notification: { - permission: 'granted', - }, - }); - const { click } = await start(); - - assert.containsNone( - document.body, - '.o_MessagingMenu_counter', - "should not display a notification counter next to the messaging menu" - ); - - await click('.o_MessagingMenu_toggler'); - assert.containsNone( - document.body, - '.o_NotificationRequestView', - "should display no notification in the messaging menu" - ); -}); - -QUnit.test('respond to notification prompt (denied)', async function (assert) { - assert.expect(4); - - patchWithCleanup(browser, { - Notification: { - permission: 'default', - async requestPermission() { - this.permission = 'denied'; - return this.permission; - }, - }, - }); - const { click } = await start({ - services: { - notification: makeFakeNotificationService(() => { - assert.step( - "should display a toast notification with the deny confirmation" + await click(`.o_MessagingMenu_toggler`); + assert.containsOnce( + document.body, + ".o_MessagingMenu_dropdownMenu .o_ChannelPreviewView", + "should display a preview" ); - }), - }, - }); + assert.containsOnce( + document.body, + ".o_ChannelPreviewView_core", + "preview should have core in content" + ); + assert.containsOnce( + document.body, + ".o_ChannelPreviewView_inlineText", + "preview should have inline text in core of content" + ); + assert.strictEqual( + document + .querySelector(".o_ChannelPreviewView_inlineText") + .textContent.replace(/\s/g, ""), + "You:&shoulnotberaised", + "should display correct uninjected last message inline content" + ); + assert.containsNone( + document.querySelector(".o_ChannelPreviewView_inlineText"), + "script", + "last message inline content should not have any code injection" + ); + } + ); - await click('.o_MessagingMenu_toggler'); - await click('.o_NotificationRequestView'); - assert.verifySteps([ - "should display a toast notification with the deny confirmation", - ]); + QUnit.test( + "
tags in message body preview are transformed in spaces", + async function (assert) { + assert.expect(4); - assert.containsNone( - document.body, - '.o_MessagingMenu_counter', - "should not display a notification counter next to the messaging menu" - ); + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "

a
b
c
d

", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { click } = await start(); - await click('.o_MessagingMenu_toggler'); - assert.containsNone( - document.body, - '.o_NotificationRequestView', - "should display no notification in the messaging menu" - ); -}); + await click(`.o_MessagingMenu_toggler`); + assert.containsOnce( + document.body, + ".o_MessagingMenu_dropdownMenu .o_ChannelPreviewView", + "should display a preview" + ); + assert.containsOnce( + document.body, + ".o_ChannelPreviewView_core", + "preview should have core in content" + ); + assert.containsOnce( + document.body, + ".o_ChannelPreviewView_inlineText", + "preview should have inline text in core of content" + ); + assert.strictEqual( + document.querySelector(".o_ChannelPreviewView_inlineText").textContent, + "You: a b c d", + "should display correct last message inline content with brs replaced by spaces" + ); + } + ); -QUnit.test('Group chat should be displayed inside the chat section of the messaging menu', async function (assert) { - assert.expect(1); + QUnit.test("rendering with OdooBot has a request (default)", async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'group', - }); - const { click } = await start(); + patchWithCleanup(browser, { + Notification: { + ...browser.Notification, + permission: "default", + }, + }); + const { click } = await start(); - await click('.o_MessagingMenu_toggler'); - await click(`.o_MessagingMenuTabView[data-tab-id="chat"]`); + assert.ok( + document.querySelector(".o_MessagingMenu_counter"), + "should display a notification counter next to the messaging menu for OdooBot request" + ); + assert.strictEqual( + document.querySelector(".o_MessagingMenu_counter").textContent, + "1", + "should display a counter of '1' next to the messaging menu" + ); - assert.strictEqual( - document.querySelectorAll(` + await click(".o_MessagingMenu_toggler"); + assert.containsOnce( + document.body, + ".o_NotificationRequestView", + "should display a notification in the messaging menu" + ); + assert.strictEqual( + document.querySelector(".o_NotificationRequestView_name").textContent.trim(), + "OdooBot has a request", + "notification should display that OdooBot has a request" + ); + }); + + QUnit.test("rendering without OdooBot has a request (denied)", async function (assert) { + assert.expect(2); + + patchWithCleanup(browser, { + Notification: { + permission: "denied", + }, + }); + const { click } = await start(); + + assert.containsNone( + document.body, + ".o_MessagingMenu_counter", + "should not display a notification counter next to the messaging menu" + ); + + await click(".o_MessagingMenu_toggler"); + assert.containsNone( + document.body, + ".o_NotificationRequestView", + "should display no notification in the messaging menu" + ); + }); + + QUnit.test("rendering without OdooBot has a request (accepted)", async function (assert) { + assert.expect(2); + + patchWithCleanup(browser, { + Notification: { + permission: "granted", + }, + }); + const { click } = await start(); + + assert.containsNone( + document.body, + ".o_MessagingMenu_counter", + "should not display a notification counter next to the messaging menu" + ); + + await click(".o_MessagingMenu_toggler"); + assert.containsNone( + document.body, + ".o_NotificationRequestView", + "should display no notification in the messaging menu" + ); + }); + + QUnit.test("respond to notification prompt (denied)", async function (assert) { + assert.expect(4); + + patchWithCleanup(browser, { + Notification: { + permission: "default", + async requestPermission() { + this.permission = "denied"; + return this.permission; + }, + }, + }); + const { click } = await start({ + services: { + notification: makeFakeNotificationService(() => { + assert.step( + "should display a toast notification with the deny confirmation" + ); + }), + }, + }); + + await click(".o_MessagingMenu_toggler"); + await click(".o_NotificationRequestView"); + assert.verifySteps(["should display a toast notification with the deny confirmation"]); + + assert.containsNone( + document.body, + ".o_MessagingMenu_counter", + "should not display a notification counter next to the messaging menu" + ); + + await click(".o_MessagingMenu_toggler"); + assert.containsNone( + document.body, + ".o_NotificationRequestView", + "should display no notification in the messaging menu" + ); + }); + + QUnit.test( + "Group chat should be displayed inside the chat section of the messaging menu", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "group", + }); + const { click } = await start(); + + await click(".o_MessagingMenu_toggler"); + await click(`.o_MessagingMenuTabView[data-tab-id="chat"]`); + + assert.strictEqual( + document.querySelectorAll(` .o_MessagingMenu_dropdownMenu .o_ChannelPreviewView[data-channel-id="${mailChannelId1}"] `).length, - 1, - "should have one preview of group" - ); -}); - -}); + 1, + "should have one preview of group" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js index 96f0b51679c..ba4f2d5032f 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/notification_list_notification_group_tests.js @@ -1,496 +1,502 @@ /** @odoo-module **/ -import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('notification_list_notification_group_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("notification_list_notification_group_tests.js"); -QUnit.test('notification group basic layout', async function (assert) { - assert.expect(10); + QUnit.test("notification group basic layout", async function (assert) { + assert.expect(10); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - message_type: 'email', // message must be email (goal of the test) - model: 'mail.channel', // expected value to link message to channel - res_id: mailChannelId1, - res_model_name: "Channel", // random res model name, will be asserted in the test - }); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, - notification_status: 'exception', - notification_type: 'email', - }, - { - mail_message_id: mailMessageId1, - notification_status: 'exception', - notification_type: 'email', - }, - ]); - const { click } = await start(); - await click('.o_MessagingMenu_toggler'); - assert.containsOnce( - document.body, - '.o_NotificationGroupView', - "should have 1 notification group" - ); - assert.containsOnce( - document.body, - '.o_NotificationGroupView_name', - "should have 1 group name" - ); - assert.strictEqual( - document.querySelector('.o_NotificationGroupView_name').textContent, - "Channel", - "should have model name as group name" - ); - assert.containsOnce( - document.body, - '.o_NotificationGroupView_counter', - "should have 1 group counter" - ); - assert.strictEqual( - document.querySelector('.o_NotificationGroupView_counter').textContent.trim(), - "(2)", - "should have 2 notifications in the group" - ); - assert.containsOnce( - document.body, - '.o_NotificationGroupView_date', - "should have 1 group date" - ); - assert.strictEqual( - document.querySelector('.o_NotificationGroupView_date').textContent, - "a few seconds ago", - "should have the group date corresponding to now" - ); - assert.containsOnce( - document.body, - '.o_NotificationGroupView_inlineText', - "should have 1 group text" - ); - assert.strictEqual( - document.querySelector('.o_NotificationGroupView_inlineText').textContent.trim(), - "An error occurred when sending an email.", - "should have the group text corresponding to email" - ); - assert.containsOnce( - document.body, - '.o_NotificationGroupView_markAsRead', - "should have 1 mark as read button" - ); -}); - -QUnit.test('mark as read', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - message_type: 'email', // message must be email (goal of the test) - model: 'mail.channel', // expected value to link message to channel - res_id: mailChannelId1, - res_model_name: "Channel", // random res model name, will be asserted in the test - }); - // failure that is expected to be used in the test - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, // id of the related message - notification_status: 'exception', // necessary value to have a failure - notification_type: 'email', - }); - const { click } = await start(); - await click('.o_MessagingMenu_toggler'); - assert.containsOnce( - document.body, - '.o_NotificationGroupView_markAsRead', - "should have 1 mark as read button" - ); - - await afterNextRender(() => { - document.querySelector('.o_NotificationGroupView_markAsRead').click(); - }); - assert.containsNone( - document.body, - '.o_NotificationGroupView', - "should have no notification group" - ); -}); - -QUnit.test('grouped notifications by document', async function (assert) { - // If some failures linked to a document refers to a same document, a single - // notification should group all those failures. - assert.expect(5); - - const pyEnv = await startServer(); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - // first message that is expected to have a failure - { - message_type: 'email', // message must be email (goal of the test) - model: 'res.partner', // same model as second message (and not `mail.channel`) - res_id: 31, // same res_id as second message - res_model_name: "Partner", // random related model name - }, - // second message that is expected to have a failure - { - message_type: 'email', // message must be email (goal of the test) - model: 'res.partner', // same model as first message (and not `mail.channel`) - res_id: 31, // same res_id as first message - res_model_name: "Partner", // same related model name for consistency - }, - ]); - pyEnv['mail.notification'].create([ - // first failure that is expected to be used in the test - { - mail_message_id: mailMessageId1, // id of the related first message - notification_status: 'exception', // one possible value to have a failure - notification_type: 'email', // expected failure type for email message - }, - // second failure that is expected to be used in the test - { - mail_message_id: mailMessageId2, // id of the related second message - notification_status: 'bounce', // other possible value to have a failure - notification_type: 'email', // expected failure type for email message - } - ]); - const { click } = await start(); - await click('.o_MessagingMenu_toggler'); - - assert.containsOnce( - document.body, - '.o_NotificationGroupView', - "should have 1 notification group" - ); - assert.containsOnce( - document.body, - '.o_NotificationGroupView_counter', - "should have 1 group counter" - ); - assert.strictEqual( - document.querySelector('.o_NotificationGroupView_counter').textContent.trim(), - "(2)", - "should have 2 notifications in the group" - ); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should have no chat window initially" - ); - - await click('.o_NotificationGroupView'); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have opened the thread in a chat window after clicking on it" - ); -}); - -QUnit.test('grouped notifications by document model', async function (assert) { - // If all failures linked to a document model refers to different documents, - // a single notification should group all failures that are linked to this - // document model. - assert.expect(12); - - const pyEnv = await startServer(); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - // first message that is expected to have a failure - { - message_type: 'email', // message must be email (goal of the test) - model: 'res.partner', // same model as second message (and not `mail.channel`) - res_id: 31, // different res_id from second message - res_model_name: "Partner", // random related model name - }, - // second message that is expected to have a failure - { - message_type: 'email', // message must be email (goal of the test) - model: 'res.partner', // same model as first message (and not `mail.channel`) - res_id: 32, // different res_id from first message - res_model_name: "Partner", // same related model name for consistency - }, - ]); - pyEnv['mail.notification'].create([ - // first failure that is expected to be used in the test - { - mail_message_id: mailMessageId1, // id of the related first message - notification_status: 'exception', // one possible value to have a failure - notification_type: 'email', // expected failure type for email message - }, - // second failure that is expected to be used in the test - { - mail_message_id: mailMessageId2, // id of the related second message - notification_status: 'bounce', // other possible value to have a failure - notification_type: 'email', // expected failure type for email message - }, - ]); - - const { click, env } = await start(); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.step('do_action'); - assert.strictEqual( - action.name, - "Mail Failures", - "action should have 'Mail Failures' as name", + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + message_type: "email", // message must be email (goal of the test) + model: "mail.channel", // expected value to link message to channel + res_id: mailChannelId1, + res_model_name: "Channel", // random res model name, will be asserted in the test + }); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, + notification_status: "exception", + notification_type: "email", + }, + { + mail_message_id: mailMessageId1, + notification_status: "exception", + notification_type: "email", + }, + ]); + const { click } = await start(); + await click(".o_MessagingMenu_toggler"); + assert.containsOnce( + document.body, + ".o_NotificationGroupView", + "should have 1 notification group" + ); + assert.containsOnce( + document.body, + ".o_NotificationGroupView_name", + "should have 1 group name" ); assert.strictEqual( - action.type, - 'ir.actions.act_window', - "action should have the type act_window" + document.querySelector(".o_NotificationGroupView_name").textContent, + "Channel", + "should have model name as group name" + ); + assert.containsOnce( + document.body, + ".o_NotificationGroupView_counter", + "should have 1 group counter" ); assert.strictEqual( - action.view_mode, - 'kanban,list,form', - "action should have 'kanban,list,form' as view_mode" + document.querySelector(".o_NotificationGroupView_counter").textContent.trim(), + "(2)", + "should have 2 notifications in the group" + ); + assert.containsOnce( + document.body, + ".o_NotificationGroupView_date", + "should have 1 group date" ); assert.strictEqual( - JSON.stringify(action.views), - JSON.stringify([[false, 'kanban'], [false, 'list'], [false, 'form']]), - "action should have correct views" + document.querySelector(".o_NotificationGroupView_date").textContent, + "a few seconds ago", + "should have the group date corresponding to now" + ); + assert.containsOnce( + document.body, + ".o_NotificationGroupView_inlineText", + "should have 1 group text" ); assert.strictEqual( - action.target, - 'current', - "action should have 'current' as target" + document.querySelector(".o_NotificationGroupView_inlineText").textContent.trim(), + "An error occurred when sending an email.", + "should have the group text corresponding to email" + ); + assert.containsOnce( + document.body, + ".o_NotificationGroupView_markAsRead", + "should have 1 mark as read button" + ); + }); + + QUnit.test("mark as read", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + message_type: "email", // message must be email (goal of the test) + model: "mail.channel", // expected value to link message to channel + res_id: mailChannelId1, + res_model_name: "Channel", // random res model name, will be asserted in the test + }); + // failure that is expected to be used in the test + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, // id of the related message + notification_status: "exception", // necessary value to have a failure + notification_type: "email", + }); + const { click } = await start(); + await click(".o_MessagingMenu_toggler"); + assert.containsOnce( + document.body, + ".o_NotificationGroupView_markAsRead", + "should have 1 mark as read button" + ); + + await afterNextRender(() => { + document.querySelector(".o_NotificationGroupView_markAsRead").click(); + }); + assert.containsNone( + document.body, + ".o_NotificationGroupView", + "should have no notification group" + ); + }); + + QUnit.test("grouped notifications by document", async function (assert) { + // If some failures linked to a document refers to a same document, a single + // notification should group all those failures. + assert.expect(5); + + const pyEnv = await startServer(); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + // first message that is expected to have a failure + { + message_type: "email", // message must be email (goal of the test) + model: "res.partner", // same model as second message (and not `mail.channel`) + res_id: 31, // same res_id as second message + res_model_name: "Partner", // random related model name + }, + // second message that is expected to have a failure + { + message_type: "email", // message must be email (goal of the test) + model: "res.partner", // same model as first message (and not `mail.channel`) + res_id: 31, // same res_id as first message + res_model_name: "Partner", // same related model name for consistency + }, + ]); + pyEnv["mail.notification"].create([ + // first failure that is expected to be used in the test + { + mail_message_id: mailMessageId1, // id of the related first message + notification_status: "exception", // one possible value to have a failure + notification_type: "email", // expected failure type for email message + }, + // second failure that is expected to be used in the test + { + mail_message_id: mailMessageId2, // id of the related second message + notification_status: "bounce", // other possible value to have a failure + notification_type: "email", // expected failure type for email message + }, + ]); + const { click } = await start(); + await click(".o_MessagingMenu_toggler"); + + assert.containsOnce( + document.body, + ".o_NotificationGroupView", + "should have 1 notification group" + ); + assert.containsOnce( + document.body, + ".o_NotificationGroupView_counter", + "should have 1 group counter" ); assert.strictEqual( - action.res_model, - 'res.partner', - "action should have the group model as res_model" + document.querySelector(".o_NotificationGroupView_counter").textContent.trim(), + "(2)", + "should have 2 notifications in the group" + ); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should have no chat window initially" + ); + + await click(".o_NotificationGroupView"); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have opened the thread in a chat window after clicking on it" + ); + }); + + QUnit.test("grouped notifications by document model", async function (assert) { + // If all failures linked to a document model refers to different documents, + // a single notification should group all failures that are linked to this + // document model. + assert.expect(12); + + const pyEnv = await startServer(); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + // first message that is expected to have a failure + { + message_type: "email", // message must be email (goal of the test) + model: "res.partner", // same model as second message (and not `mail.channel`) + res_id: 31, // different res_id from second message + res_model_name: "Partner", // random related model name + }, + // second message that is expected to have a failure + { + message_type: "email", // message must be email (goal of the test) + model: "res.partner", // same model as first message (and not `mail.channel`) + res_id: 32, // different res_id from first message + res_model_name: "Partner", // same related model name for consistency + }, + ]); + pyEnv["mail.notification"].create([ + // first failure that is expected to be used in the test + { + mail_message_id: mailMessageId1, // id of the related first message + notification_status: "exception", // one possible value to have a failure + notification_type: "email", // expected failure type for email message + }, + // second failure that is expected to be used in the test + { + mail_message_id: mailMessageId2, // id of the related second message + notification_status: "bounce", // other possible value to have a failure + notification_type: "email", // expected failure type for email message + }, + ]); + + const { click, env } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.step("do_action"); + assert.strictEqual( + action.name, + "Mail Failures", + "action should have 'Mail Failures' as name" + ); + assert.strictEqual( + action.type, + "ir.actions.act_window", + "action should have the type act_window" + ); + assert.strictEqual( + action.view_mode, + "kanban,list,form", + "action should have 'kanban,list,form' as view_mode" + ); + assert.strictEqual( + JSON.stringify(action.views), + JSON.stringify([ + [false, "kanban"], + [false, "list"], + [false, "form"], + ]), + "action should have correct views" + ); + assert.strictEqual( + action.target, + "current", + "action should have 'current' as target" + ); + assert.strictEqual( + action.res_model, + "res.partner", + "action should have the group model as res_model" + ); + assert.strictEqual( + JSON.stringify(action.domain), + JSON.stringify([["message_has_error", "=", true]]), + "action should have 'message_has_error' as domain" + ); + }, + }); + await click(".o_MessagingMenu_toggler"); + + assert.containsOnce( + document.body, + ".o_NotificationGroupView", + "should have 1 notification group" + ); + assert.containsOnce( + document.body, + ".o_NotificationGroupView_counter", + "should have 1 group counter" ); assert.strictEqual( - JSON.stringify(action.domain), - JSON.stringify([['message_has_error', '=', true]]), - "action should have 'message_has_error' as domain" + document.querySelector(".o_NotificationGroupView_counter").textContent.trim(), + "(2)", + "should have 2 notifications in the group" ); - }, + + document.querySelector(".o_NotificationGroupView").click(); + assert.verifySteps(["do_action"], "should do an action to display the related records"); + }); + + QUnit.test("different mail.channel are not grouped", async function (assert) { + // `mail.channel` is a special case where notifications are not grouped when + // they are linked to different channels, even though the model is the same. + assert.expect(6); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "mailChannel1" }, + { name: "mailChannel2" }, + ]); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + // first message that is expected to have a failure + { + message_type: "email", // message must be email (goal of the test) + model: "mail.channel", // testing a channel is the goal of the test + res_id: mailChannelId1, // different res_id from second message + res_model_name: "Channel", // random related model name + }, + // second message that is expected to have a failure + { + message_type: "email", // message must be email (goal of the test) + model: "mail.channel", // testing a channel is the goal of the test + res_id: mailChannelId2, // different res_id from first message + res_model_name: "Channel", // same related model name for consistency + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, // id of the related first message + notification_status: "exception", // one possible value to have a failure + notification_type: "email", // expected failure type for email message + }, + { + mail_message_id: mailMessageId1, + notification_status: "exception", + notification_type: "email", + }, + { + mail_message_id: mailMessageId2, // id of the related second message + notification_status: "bounce", // other possible value to have a failure + notification_type: "email", // expected failure type for email message + }, + { + mail_message_id: mailMessageId2, + notification_status: "bounce", + notification_type: "email", + }, + ]); + const { click } = await start(); + await click(".o_MessagingMenu_toggler"); + assert.containsN( + document.body, + ".o_NotificationGroupView", + 2, + "should have 2 notifications group" + ); + const groups = document.querySelectorAll(".o_NotificationGroupView"); + assert.containsOnce( + groups[0], + ".o_NotificationGroupView_counter", + "should have 1 group counter in first group" + ); + assert.strictEqual( + groups[0].querySelector(".o_NotificationGroupView_counter").textContent.trim(), + "(2)", + "should have 2 notifications in first group" + ); + assert.containsOnce( + groups[1], + ".o_NotificationGroupView_counter", + "should have 1 group counter in second group" + ); + assert.strictEqual( + groups[1].querySelector(".o_NotificationGroupView_counter").textContent.trim(), + "(2)", + "should have 2 notifications in second group" + ); + + await afterNextRender(() => groups[0].click()); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have opened the channel related to the first group in a chat window" + ); + }); + + QUnit.test( + "multiple grouped notifications by document model, sorted by the most recent message of each group", + async function (assert) { + assert.expect(9); + + const pyEnv = await startServer(); + const [mailMessageId1, mailMessageId2] = pyEnv["mail.message"].create([ + // first message that is expected to have a failure + { + message_type: "email", // message must be email (goal of the test) + model: "res.partner", // different model from second message + res_id: 31, + res_model_name: "Partner", // random related model name + }, + // second message that is expected to have a failure + { + message_type: "email", // message must be email (goal of the test) + model: "res.company", // different model from first message + res_id: 32, + res_model_name: "Company", // random related model name + }, + ]); + pyEnv["mail.notification"].create([ + { + mail_message_id: mailMessageId1, // id of the related first message + notification_status: "exception", // one possible value to have a failure + notification_type: "email", // expected failure type for email message + }, + { + mail_message_id: mailMessageId1, + notification_status: "exception", + notification_type: "email", + }, + { + mail_message_id: mailMessageId2, // id of the related second message + notification_status: "bounce", // other possible value to have a failure + notification_type: "email", // expected failure type for email message + }, + { + mail_message_id: mailMessageId2, + notification_status: "bounce", + notification_type: "email", + }, + ]); + const { click } = await start(); + await click(".o_MessagingMenu_toggler"); + assert.containsN( + document.body, + ".o_NotificationGroupView", + 2, + "should have 2 notifications group" + ); + const groups = document.querySelectorAll(".o_NotificationGroupView"); + assert.containsOnce( + groups[0], + ".o_NotificationGroupView_name", + "should have 1 group name in first group" + ); + assert.strictEqual( + groups[0].querySelector(".o_NotificationGroupView_name").textContent, + "Company", + "should have first model name as group name" + ); + assert.containsOnce( + groups[0], + ".o_NotificationGroupView_counter", + "should have 1 group counter in first group" + ); + assert.strictEqual( + groups[0].querySelector(".o_NotificationGroupView_counter").textContent.trim(), + "(2)", + "should have 2 notifications in first group" + ); + assert.containsOnce( + groups[1], + ".o_NotificationGroupView_name", + "should have 1 group name in second group" + ); + assert.strictEqual( + groups[1].querySelector(".o_NotificationGroupView_name").textContent, + "Partner", + "should have second model name as group name" + ); + assert.containsOnce( + groups[1], + ".o_NotificationGroupView_counter", + "should have 1 group counter in second group" + ); + assert.strictEqual( + groups[1].querySelector(".o_NotificationGroupView_counter").textContent.trim(), + "(2)", + "should have 2 notifications in second group" + ); + } + ); + + QUnit.test("non-failure notifications are ignored", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + message_type: "email", // message must be email (goal of the test) + model: "res.partner", // random model + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, // id of the related first message + notification_status: "ready", // non-failure status + notification_type: "email", // expected notification type for email message + }); + const { click } = await start(); + await click(".o_MessagingMenu_toggler"); + assert.containsNone( + document.body, + ".o_NotificationGroupView", + "should have 0 notification group" + ); + }); }); - await click('.o_MessagingMenu_toggler'); - - assert.containsOnce( - document.body, - '.o_NotificationGroupView', - "should have 1 notification group" - ); - assert.containsOnce( - document.body, - '.o_NotificationGroupView_counter', - "should have 1 group counter" - ); - assert.strictEqual( - document.querySelector('.o_NotificationGroupView_counter').textContent.trim(), - "(2)", - "should have 2 notifications in the group" - ); - - document.querySelector('.o_NotificationGroupView').click(); - assert.verifySteps( - ['do_action'], - "should do an action to display the related records" - ); -}); - -QUnit.test('different mail.channel are not grouped', async function (assert) { - // `mail.channel` is a special case where notifications are not grouped when - // they are linked to different channels, even though the model is the same. - assert.expect(6); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([{ name: "mailChannel1" }, { name: "mailChannel2" }]); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - // first message that is expected to have a failure - { - message_type: 'email', // message must be email (goal of the test) - model: 'mail.channel', // testing a channel is the goal of the test - res_id: mailChannelId1, // different res_id from second message - res_model_name: "Channel", // random related model name - }, - // second message that is expected to have a failure - { - message_type: 'email', // message must be email (goal of the test) - model: 'mail.channel', // testing a channel is the goal of the test - res_id: mailChannelId2, // different res_id from first message - res_model_name: "Channel", // same related model name for consistency - }, - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, // id of the related first message - notification_status: 'exception', // one possible value to have a failure - notification_type: 'email', // expected failure type for email message - }, - { - mail_message_id: mailMessageId1, - notification_status: 'exception', - notification_type: 'email', - }, - { - mail_message_id: mailMessageId2, // id of the related second message - notification_status: 'bounce', // other possible value to have a failure - notification_type: 'email', // expected failure type for email message - }, - { - mail_message_id: mailMessageId2, - notification_status: 'bounce', - notification_type: 'email', - }, - ]); - const { click } = await start(); - await click('.o_MessagingMenu_toggler'); - assert.containsN( - document.body, - '.o_NotificationGroupView', - 2, - "should have 2 notifications group" - ); - const groups = document.querySelectorAll('.o_NotificationGroupView'); - assert.containsOnce( - groups[0], - '.o_NotificationGroupView_counter', - "should have 1 group counter in first group" - ); - assert.strictEqual( - groups[0].querySelector('.o_NotificationGroupView_counter').textContent.trim(), - "(2)", - "should have 2 notifications in first group" - ); - assert.containsOnce( - groups[1], - '.o_NotificationGroupView_counter', - "should have 1 group counter in second group" - ); - assert.strictEqual( - groups[1].querySelector('.o_NotificationGroupView_counter').textContent.trim(), - "(2)", - "should have 2 notifications in second group" - ); - - await afterNextRender(() => groups[0].click()); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have opened the channel related to the first group in a chat window" - ); -}); - -QUnit.test('multiple grouped notifications by document model, sorted by the most recent message of each group', async function (assert) { - assert.expect(9); - - const pyEnv = await startServer(); - const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([ - // first message that is expected to have a failure - { - message_type: 'email', // message must be email (goal of the test) - model: 'res.partner', // different model from second message - res_id: 31, - res_model_name: "Partner", // random related model name - }, - // second message that is expected to have a failure - { - message_type: 'email', // message must be email (goal of the test) - model: 'res.company', // different model from first message - res_id: 32, - res_model_name: "Company", // random related model name - }, - ]); - pyEnv['mail.notification'].create([ - { - mail_message_id: mailMessageId1, // id of the related first message - notification_status: 'exception', // one possible value to have a failure - notification_type: 'email', // expected failure type for email message - }, - { - mail_message_id: mailMessageId1, - notification_status: 'exception', - notification_type: 'email', - }, - { - mail_message_id: mailMessageId2, // id of the related second message - notification_status: 'bounce', // other possible value to have a failure - notification_type: 'email', // expected failure type for email message - }, - { - mail_message_id: mailMessageId2, - notification_status: 'bounce', - notification_type: 'email', - }, - ]); - const { click } = await start(); - await click('.o_MessagingMenu_toggler'); - assert.containsN( - document.body, - '.o_NotificationGroupView', - 2, - "should have 2 notifications group" - ); - const groups = document.querySelectorAll('.o_NotificationGroupView'); - assert.containsOnce( - groups[0], - '.o_NotificationGroupView_name', - "should have 1 group name in first group" - ); - assert.strictEqual( - groups[0].querySelector('.o_NotificationGroupView_name').textContent, - "Company", - "should have first model name as group name" - ); - assert.containsOnce( - groups[0], - '.o_NotificationGroupView_counter', - "should have 1 group counter in first group" - ); - assert.strictEqual( - groups[0].querySelector('.o_NotificationGroupView_counter').textContent.trim(), - "(2)", - "should have 2 notifications in first group" - ); - assert.containsOnce( - groups[1], - '.o_NotificationGroupView_name', - "should have 1 group name in second group" - ); - assert.strictEqual( - groups[1].querySelector('.o_NotificationGroupView_name').textContent, - "Partner", - "should have second model name as group name" - ); - assert.containsOnce( - groups[1], - '.o_NotificationGroupView_counter', - "should have 1 group counter in second group" - ); - assert.strictEqual( - groups[1].querySelector('.o_NotificationGroupView_counter').textContent.trim(), - "(2)", - "should have 2 notifications in second group" - ); -}); - -QUnit.test('non-failure notifications are ignored', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - message_type: 'email', // message must be email (goal of the test) - model: 'res.partner', // random model - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, // id of the related first message - notification_status: 'ready', // non-failure status - notification_type: 'email', // expected notification type for email message - }); - const { click } = await start(); - await click('.o_MessagingMenu_toggler'); - assert.containsNone( - document.body, - '.o_NotificationGroupView', - "should have 0 notification group" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/notification_list_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/notification_list_tests.js index de4abe58265..161203fdcc7 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/notification_list_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/notification_list_tests.js @@ -1,115 +1,126 @@ /** @odoo-module **/ -import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('notification_list_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("notification_list_tests.js"); -QUnit.test('marked as read thread notifications are ordered by last message date', async function (assert) { - assert.expect(3); + QUnit.test( + "marked as read thread notifications are ordered by last message date", + async function (assert) { + assert.expect(3); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { name: "Channel 2019" }, - { name: "Channel 2020" }, - ]); - pyEnv['mail.message'].create([ - { - date: "2019-01-01 00:00:00", - model: 'mail.channel', - res_id: mailChannelId1, - }, - { - date: "2020-01-01 00:00:00", - model: 'mail.channel', - res_id: mailChannelId2, - }, - ]); - const { click } = await start(); - await click('.o_MessagingMenu_toggler'); - assert.containsN( - document.body, - '.o_ChannelPreviewView', - 2, - "there should be two thread previews" - ); - const channelPreviewViewElList = document.querySelectorAll('.o_ChannelPreviewView'); - assert.strictEqual( - channelPreviewViewElList[0].querySelector(':scope .o_ChannelPreviewView_name').textContent, - 'Channel 2020', - "First channel in the list should be the channel of 2020 (more recent last message)" - ); - assert.strictEqual( - channelPreviewViewElList[1].querySelector(':scope .o_ChannelPreviewView_name').textContent, - 'Channel 2019', - "Second channel in the list should be the channel of 2019 (least recent last message)" - ); -}); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "Channel 2019" }, + { name: "Channel 2020" }, + ]); + pyEnv["mail.message"].create([ + { + date: "2019-01-01 00:00:00", + model: "mail.channel", + res_id: mailChannelId1, + }, + { + date: "2020-01-01 00:00:00", + model: "mail.channel", + res_id: mailChannelId2, + }, + ]); + const { click } = await start(); + await click(".o_MessagingMenu_toggler"); + assert.containsN( + document.body, + ".o_ChannelPreviewView", + 2, + "there should be two thread previews" + ); + const channelPreviewViewElList = document.querySelectorAll(".o_ChannelPreviewView"); + assert.strictEqual( + channelPreviewViewElList[0].querySelector(":scope .o_ChannelPreviewView_name") + .textContent, + "Channel 2020", + "First channel in the list should be the channel of 2020 (more recent last message)" + ); + assert.strictEqual( + channelPreviewViewElList[1].querySelector(":scope .o_ChannelPreviewView_name") + .textContent, + "Channel 2019", + "Second channel in the list should be the channel of 2019 (least recent last message)" + ); + } + ); -QUnit.test('thread notifications are re-ordered on receiving a new message', async function (assert) { - assert.expect(4); + QUnit.test( + "thread notifications are re-ordered on receiving a new message", + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { name: "Channel 2019" }, - { name: "Channel 2020" }, - ]); - pyEnv['mail.message'].create([ - { - date: "2019-01-01 00:00:00", - model: 'mail.channel', - res_id: mailChannelId1, - }, - { - date: "2020-01-01 00:00:00", - model: 'mail.channel', - res_id: mailChannelId2, - }, - ]); - const { click } = await start(); - await click('.o_MessagingMenu_toggler'); - assert.containsN( - document.body, - '.o_ChannelPreviewView', - 2, - "there should be two thread previews" - ); + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { name: "Channel 2019" }, + { name: "Channel 2020" }, + ]); + pyEnv["mail.message"].create([ + { + date: "2019-01-01 00:00:00", + model: "mail.channel", + res_id: mailChannelId1, + }, + { + date: "2020-01-01 00:00:00", + model: "mail.channel", + res_id: mailChannelId2, + }, + ]); + const { click } = await start(); + await click(".o_MessagingMenu_toggler"); + assert.containsN( + document.body, + ".o_ChannelPreviewView", + 2, + "there should be two thread previews" + ); - const mailChannel1 = pyEnv['mail.channel'].searchRead([['id', '=', mailChannelId1]])[0]; - await afterNextRender(() => { - pyEnv['bus.bus']._sendone(mailChannel1, 'mail.channel/new_message', { - 'id': mailChannelId1, - 'message': { - author_id: [7, "Demo User"], - body: "

New message !

", - date: "2020-03-23 10:00:00", - id: 44, - message_type: 'comment', - model: 'mail.channel', - record_name: 'Channel 2019', - res_id: mailChannelId1, - }, - }); + const mailChannel1 = pyEnv["mail.channel"].searchRead([ + ["id", "=", mailChannelId1], + ])[0]; + await afterNextRender(() => { + pyEnv["bus.bus"]._sendone(mailChannel1, "mail.channel/new_message", { + id: mailChannelId1, + message: { + author_id: [7, "Demo User"], + body: "

New message !

", + date: "2020-03-23 10:00:00", + id: 44, + message_type: "comment", + model: "mail.channel", + record_name: "Channel 2019", + res_id: mailChannelId1, + }, + }); + }); + assert.containsN( + document.body, + ".o_ChannelPreviewView", + 2, + "there should still be two thread previews" + ); + const channelPreviewViewElList = document.querySelectorAll(".o_ChannelPreviewView"); + assert.strictEqual( + channelPreviewViewElList[0].querySelector(":scope .o_ChannelPreviewView_name") + .textContent, + "Channel 2019", + "First channel in the list should now be 'Channel 2019'" + ); + assert.strictEqual( + channelPreviewViewElList[1].querySelector(":scope .o_ChannelPreviewView_name") + .textContent, + "Channel 2020", + "Second channel in the list should now be 'Channel 2020'" + ); + } + ); }); - assert.containsN( - document.body, - '.o_ChannelPreviewView', - 2, - "there should still be two thread previews" - ); - const channelPreviewViewElList = document.querySelectorAll('.o_ChannelPreviewView'); - assert.strictEqual( - channelPreviewViewElList[0].querySelector(':scope .o_ChannelPreviewView_name').textContent, - 'Channel 2019', - "First channel in the list should now be 'Channel 2019'" - ); - assert.strictEqual( - channelPreviewViewElList[1].querySelector(':scope .o_ChannelPreviewView_name').textContent, - 'Channel 2020', - "Second channel in the list should now be 'Channel 2020'" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/persona_im_status_icon_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/persona_im_status_icon_tests.js index 3aca4877a48..9e172ed4982 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/persona_im_status_icon_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/persona_im_status_icon_tests.js @@ -1,208 +1,210 @@ /** @odoo-module **/ -import { UPDATE_BUS_PRESENCE_DELAY } from '@bus/im_status_service'; +import { UPDATE_BUS_PRESENCE_DELAY } from "@bus/im_status_service"; -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('persona_im_status_icon_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("persona_im_status_icon_tests.js"); -QUnit.test('initially online', async function (assert) { - assert.expect(1); + QUnit.test("initially online", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create({ im_status: 'online' }); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - author_id: partnerId, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ im_status: "online" }); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { advanceTime, afterNextRender, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId, + }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, + 1, + "persona IM status icon should have online status rendering" + ); + }); + + QUnit.test("initially offline", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ im_status: "offline" }); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { advanceTime, afterNextRender, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId, + }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, + 1, + "persona IM status icon should have offline status rendering" + ); + }); + + QUnit.test("initially away", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ im_status: "away" }); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { advanceTime, afterNextRender, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId, + }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, + 1, + "persona IM status icon should have away status rendering" + ); + }); + + QUnit.test("change icon on change partner im_status", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const partnerId = pyEnv["res.partner"].create({ im_status: "online" }); + const mailChannelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + author_id: partnerId, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { advanceTime, afterNextRender, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId, + }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, + 1, + "persona IM status icon should have online status rendering" + ); + + pyEnv["res.partner"].write([partnerId], { im_status: "offline" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, + 1, + "persona IM status icon should have offline status rendering" + ); + + pyEnv["res.partner"].write([partnerId], { im_status: "away" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, + 1, + "persona IM status icon should have away status rendering" + ); + + pyEnv["res.partner"].write([partnerId], { im_status: "online" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, + 1, + "persona IM status icon should have online status rendering in the end" + ); + }); + + QUnit.test("change icon on change guest im_status", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const guestId = pyEnv["mail.guest"].create({ im_status: "online" }); + const mailChannelId = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { guest_id: guestId }], + ], + channel_type: "group", + }); + pyEnv["mail.message"].create({ + author_guest_id: guestId, + author_id: false, + body: "not empty", + model: "mail.channel", + res_id: mailChannelId, + }); + const { advanceTime, afterNextRender, openDiscuss } = await start({ + discuss: { + params: { + default_active_id: mailChannelId, + }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, + 1, + "persona IM status icon should have online status rendering" + ); + + pyEnv["mail.guest"].write([guestId], { im_status: "offline" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, + 1, + "persona IM status icon should have offline status rendering" + ); + + pyEnv["mail.guest"].write([guestId], { im_status: "away" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, + 1, + "persona IM status icon should have away status rendering" + ); + + pyEnv["mail.guest"].write([guestId], { im_status: "online" }); + await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); + assert.strictEqual( + document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, + 1, + "persona IM status icon should have online status rendering in the end" + ); + }); }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId, - }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, - 1, - "persona IM status icon should have online status rendering" - ); -}); - -QUnit.test('initially offline', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create({ im_status: 'offline' }); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - author_id: partnerId, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId, - }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, - 1, - "persona IM status icon should have offline status rendering" - ); -}); - -QUnit.test('initially away', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create({ im_status: 'away' }); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - author_id: partnerId, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId, - }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, - 1, - "persona IM status icon should have away status rendering" - ); -}); - -QUnit.test('change icon on change partner im_status', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const partnerId = pyEnv['res.partner'].create({ im_status: 'online' }); - const mailChannelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - author_id: partnerId, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId, - }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, - 1, - "persona IM status icon should have online status rendering" - ); - - pyEnv['res.partner'].write([partnerId], { im_status: 'offline' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, - 1, - "persona IM status icon should have offline status rendering" - ); - - pyEnv['res.partner'].write([partnerId], { im_status: 'away' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, - 1, - "persona IM status icon should have away status rendering" - ); - - pyEnv['res.partner'].write([partnerId], { im_status: 'online' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, - 1, - "persona IM status icon should have online status rendering in the end" - ); -}); - -QUnit.test('change icon on change guest im_status', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const guestId = pyEnv['mail.guest'].create({ im_status: 'online' }); - const mailChannelId = pyEnv['mail.channel'].create({ - channel_member_ids: [[0, 0, { partner_id: pyEnv.currentPartnerId }], [0, 0, { guest_id: guestId }]], - channel_type: 'group', - }); - pyEnv['mail.message'].create({ - author_guest_id: guestId, - author_id: false, - body: 'not empty', - model: 'mail.channel', - res_id: mailChannelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - discuss: { - params: { - default_active_id: mailChannelId, - }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, - 1, - "persona IM status icon should have online status rendering" - ); - - pyEnv['mail.guest'].write([guestId], { im_status: 'offline' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-offline`).length, - 1, - "persona IM status icon should have offline status rendering" - ); - - pyEnv['mail.guest'].write([guestId], { im_status: 'away' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-away`).length, - 1, - "persona IM status icon should have away status rendering" - ); - - pyEnv['mail.guest'].write([guestId], { im_status: 'online' }); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); - assert.strictEqual( - document.querySelectorAll(`.o_PersonaImStatusIconView.o-online`).length, - 1, - "persona IM status icon should have online status rendering in the end" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_icon_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_icon_tests.js index 79ea8701158..a2d338a2740 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_icon_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_icon_tests.js @@ -1,83 +1,82 @@ /** @odoo-module **/ -import { - afterNextRender, - start, - startServer, -} from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('thread_icon_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("thread_icon_tests.js"); -QUnit.test('chat: correspondent is typing', async function (assert) { - assert.expect(5); + QUnit.test("chat: correspondent is typing", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - im_status: 'online', - name: 'Demo', + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + im_status: "online", + name: "Demo", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { messaging, openDiscuss } = await start(); + await openDiscuss(); + + assert.containsOnce( + document.body.querySelector(".o_DiscussSidebarCategoryItem"), + ".o_ThreadIconView", + "should have thread icon in the sidebar" + ); + assert.containsOnce( + document.body, + ".o_ThreadIconView_online", + "should have thread icon with persona IM status icon 'online'" + ); + + // simulate receive typing notification from demo "is typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadIconView_typing", + "should have thread icon with partner currently typing" + ); + assert.strictEqual( + document.querySelector(".o_ThreadIconView_typing").title, + "Demo is typing...", + "title of icon should tell demo is currently typing" + ); + + // simulate receive typing notification from demo "no longer is typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: false, + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadIconView_online", + "should have thread icon with persona IM status icon 'online' (no longer typing)" + ); + }); }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { messaging, openDiscuss } = await start(); - await openDiscuss(); - - assert.containsOnce( - document.body.querySelector('.o_DiscussSidebarCategoryItem'), - '.o_ThreadIconView', - "should have thread icon in the sidebar" - ); - assert.containsOnce( - document.body, - '.o_ThreadIconView_online', - "should have thread icon with persona IM status icon 'online'" - ); - - // simulate receive typing notification from demo "is typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadIconView_typing', - "should have thread icon with partner currently typing" - ); - assert.strictEqual( - document.querySelector('.o_ThreadIconView_typing').title, - "Demo is typing...", - "title of icon should tell demo is currently typing" - ); - - // simulate receive typing notification from demo "no longer is typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': false, - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadIconView_online', - "should have thread icon with persona IM status icon 'online' (no longer typing)" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js index 20af52e0085..c11145ce8cd 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_needaction_preview_tests.js @@ -1,337 +1,361 @@ /** @odoo-module **/ -import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('thread_needaction_preview_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("thread_needaction_preview_tests.js"); -QUnit.test('mark as read', async function (assert) { - assert.expect(5); + QUnit.test("mark as read", async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging } = await start({ - async mockRPC(route, args) { - if (route.includes('mark_all_as_read')) { - assert.step('mark_all_as_read'); - assert.deepEqual( - args.kwargs.domain, - [ - ['model', '=', 'res.partner'], - ['res_id', '=', resPartnerId1], - ], - "should mark all as read the correct thread" + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging } = await start({ + async mockRPC(route, args) { + if (route.includes("mark_all_as_read")) { + assert.step("mark_all_as_read"); + assert.deepEqual( + args.kwargs.domain, + [ + ["model", "=", "res.partner"], + ["res_id", "=", resPartnerId1], + ], + "should mark all as read the correct thread" + ); + } + }, + }); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadNeedactionPreviewView_markAsRead", + "should have 1 mark as read button" + ); + + await click(".o_ThreadNeedactionPreviewView_markAsRead"); + assert.verifySteps(["mark_all_as_read"], "should have marked the thread as read"); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should not have opened the thread" + ); + }); + + QUnit.test( + "click on preview should mark as read and open the thread", + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging } = await start(); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadNeedactionPreviewView", + "should have a preview initially" + ); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should have no chat window initially" + ); + + await click(".o_ThreadNeedactionPreviewView"); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have opened the thread on clicking on the preview" + ); + await click(".o_MessagingMenu_toggler"); + assert.containsNone( + document.body, + ".o_ThreadNeedactionPreviewView", + "should have no preview because the message should be marked as read after opening its thread" ); } - }, - }); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadNeedactionPreviewView_markAsRead', - "should have 1 mark as read button" - ); + ); - await click('.o_ThreadNeedactionPreviewView_markAsRead'); - assert.verifySteps( - ['mark_all_as_read'], - "should have marked the thread as read" - ); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should not have opened the thread" - ); -}); + QUnit.test( + "click on expand from chat window should close the chat window and open the form view", + async function (assert) { + assert.expect(8); -QUnit.test('click on preview should mark as read and open the thread', async function (assert) { - assert.expect(4); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, env, messaging } = await start(); + patchWithCleanup(env.services.action, { + doAction(action) { + assert.step("do_action"); + assert.strictEqual( + action.res_id, + resPartnerId1, + "should redirect to the id of the thread" + ); + assert.strictEqual( + action.res_model, + "res.partner", + "should redirect to the model of the thread" + ); + }, + }); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadNeedactionPreviewView", + "should have a preview initially" + ); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging } = await start(); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadNeedactionPreviewView', - "should have a preview initially" - ); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should have no chat window initially" - ); + await click(".o_ThreadNeedactionPreviewView"); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have opened the thread on clicking on the preview" + ); + assert.containsOnce( + document.body, + ".o_ChatWindowHeaderView_commandExpand", + "should have an expand button" + ); - await click('.o_ThreadNeedactionPreviewView'); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have opened the thread on clicking on the preview" - ); - await click('.o_MessagingMenu_toggler'); - assert.containsNone( - document.body, - '.o_ThreadNeedactionPreviewView', - "should have no preview because the message should be marked as read after opening its thread" - ); -}); - -QUnit.test('click on expand from chat window should close the chat window and open the form view', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, env, messaging } = await start(); - patchWithCleanup(env.services.action, { - doAction(action) { - assert.step('do_action'); - assert.strictEqual( - action.res_id, - resPartnerId1, - "should redirect to the id of the thread" - ); - assert.strictEqual( - action.res_model, - 'res.partner', - "should redirect to the model of the thread" - ); - }, - }); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadNeedactionPreviewView', - "should have a preview initially" - ); - - await click('.o_ThreadNeedactionPreviewView'); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have opened the thread on clicking on the preview" - ); - assert.containsOnce( - document.body, - '.o_ChatWindowHeaderView_commandExpand', - "should have an expand button" - ); - - await click('.o_ChatWindowHeaderView_commandExpand'); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should have closed the chat window on clicking expand" - ); - assert.verifySteps( - ['do_action'], - "should have done an action to open the form view" - ); -}); - -QUnit.test('[technical] opening a non-channel chat window should not call channel_fold', async function (assert) { - // channel_fold should not be called when opening non-channels in chat - // window, because there is no server sync of fold state for them. - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging } = await start({ - async mockRPC(route, args) { - if (route.includes('channel_fold')) { - const message = "should not call channel_fold when opening a non-channel chat window"; - assert.step(message); - console.error(message); - throw Error(message); + await click(".o_ChatWindowHeaderView_commandExpand"); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should have closed the chat window on clicking expand" + ); + assert.verifySteps( + ["do_action"], + "should have done an action to open the form view" + ); } - }, - }); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadNeedactionPreviewView', - "should have a preview initially" - ); - assert.containsNone( - document.body, - '.o_ChatWindow', - "should have no chat window initially" - ); + ); - await click('.o_ThreadNeedactionPreviewView'); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have opened the chat window on clicking on the preview" - ); -}); - -QUnit.test('preview should display last needaction message preview even if there is a more recent message that is not needaction in the thread', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Stranger", - }); - const mailMessageId1 = pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: "I am the oldest but needaction", - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "I am more recent", - model: 'res.partner', - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, messaging } = await start(); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - assert.containsOnce( - document.body, - '.o_ThreadNeedactionPreviewView_inlineText', - "should have a preview from the last message" - ); - assert.strictEqual( - document.querySelector('.o_ThreadNeedactionPreviewView_inlineText').textContent, - 'Stranger: I am the oldest but needaction', - "the displayed message should be the one that needs action even if there is a more recent message that is not needaction on the thread" - ); -}); - -QUnit.test('chat window header should not have unread counter for non-channel thread', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: 'not empty', - model: 'res.partner', - needaction: true, - needaction_partner_ids: [pyEnv.currentPartnerId], - res_id: resPartnerId1, - }); - pyEnv['mail.notification'].create({ - mail_message_id: mailMessageId1, - notification_status: 'sent', - notification_type: 'inbox', - res_partner_id: pyEnv.currentPartnerId, - }); - const { afterEvent, click, messaging } = await start(); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-cache-loaded-messages', - func: () => document.querySelector('.o_MessagingMenu_toggler').click(), - message: "should wait until inbox loaded initial needaction messages", - predicate: ({ threadCache }) => { - return threadCache.thread === messaging.inbox.thread; - }, - })); - await click('.o_ThreadNeedactionPreviewView'); - assert.containsOnce( - document.body, - '.o_ChatWindow', - "should have opened the chat window on clicking on the preview" - ); - assert.containsNone( - document.body, - '.o_ChatWindowHeaderView_counter', - "chat window header should not have unread counter for non-channel thread" - ); -}); - -}); + QUnit.test( + "[technical] opening a non-channel chat window should not call channel_fold", + async function (assert) { + // channel_fold should not be called when opening non-channels in chat + // window, because there is no server sync of fold state for them. + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging } = await start({ + async mockRPC(route, args) { + if (route.includes("channel_fold")) { + const message = + "should not call channel_fold when opening a non-channel chat window"; + assert.step(message); + console.error(message); + throw Error(message); + } + }, + }); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadNeedactionPreviewView", + "should have a preview initially" + ); + assert.containsNone( + document.body, + ".o_ChatWindow", + "should have no chat window initially" + ); + + await click(".o_ThreadNeedactionPreviewView"); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have opened the chat window on clicking on the preview" + ); + } + ); + + QUnit.test( + "preview should display last needaction message preview even if there is a more recent message that is not needaction in the thread", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Stranger", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "I am the oldest but needaction", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "I am more recent", + model: "res.partner", + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, messaging } = await start(); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + assert.containsOnce( + document.body, + ".o_ThreadNeedactionPreviewView_inlineText", + "should have a preview from the last message" + ); + assert.strictEqual( + document.querySelector(".o_ThreadNeedactionPreviewView_inlineText").textContent, + "Stranger: I am the oldest but needaction", + "the displayed message should be the one that needs action even if there is a more recent message that is not needaction on the thread" + ); + } + ); + + QUnit.test( + "chat window header should not have unread counter for non-channel thread", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "not empty", + model: "res.partner", + needaction: true, + needaction_partner_ids: [pyEnv.currentPartnerId], + res_id: resPartnerId1, + }); + pyEnv["mail.notification"].create({ + mail_message_id: mailMessageId1, + notification_status: "sent", + notification_type: "inbox", + res_partner_id: pyEnv.currentPartnerId, + }); + const { afterEvent, click, messaging } = await start(); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-cache-loaded-messages", + func: () => document.querySelector(".o_MessagingMenu_toggler").click(), + message: "should wait until inbox loaded initial needaction messages", + predicate: ({ threadCache }) => { + return threadCache.thread === messaging.inbox.thread; + }, + }) + ); + await click(".o_ThreadNeedactionPreviewView"); + assert.containsOnce( + document.body, + ".o_ChatWindow", + "should have opened the chat window on clicking on the preview" + ); + assert.containsNone( + document.body, + ".o_ChatWindowHeaderView_counter", + "chat window header should not have unread counter for non-channel thread" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js index 553f43b65f0..876013abdc2 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_textual_typing_status_tests.js @@ -5,348 +5,379 @@ import { nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('thread_textual_typing_status_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("thread_textual_typing_status_tests.js"); -QUnit.test('receive other member typing status "is typing"', async function (assert) { - assert.expect(2); + QUnit.test('receive other member typing status "is typing"', async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from demo + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Demo is typing...", + "Should display that demo user is typing" + ); + }); + + QUnit.test( + 'receive other member typing status "is typing" then "no longer is typing"', + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from demo "is typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Demo is typing...", + "Should display that demo user is typing" + ); + + // simulate receive typing notification from demo "is no longer typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: false, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should no longer display that demo user is typing" + ); + } + ); + + QUnit.test( + 'assume other member typing status becomes "no longer is typing" after 60 seconds without any updated typing status', + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { advanceTime, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from demo "is typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Demo is typing...", + "Should display that demo user is typing" + ); + + await afterNextRender(() => advanceTime(60 * 1000)); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should no longer display that demo user is typing" + ); + } + ); + + QUnit.test( + 'other member typing status "is typing" refreshes 60 seconds timer of assuming no longer typing', + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Demo" }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { advanceTime, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + hasTimeControl: true, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from demo "is typing" + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Demo is typing...", + "Should display that demo user is typing" + ); + + // simulate receive typing notification from demo "is typing" again after 50s. + await advanceTime(50 * 1000); + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }); + await advanceTime(50 * 1000); + await nextAnimationFrame(); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Demo is typing...", + "Should still display that demo user is typing after 100 seconds (refreshed is typing status at 50s => (100 - 50) = 50s < 60s after assuming no-longer typing)" + ); + + await afterNextRender(() => advanceTime(11 * 1000)); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should no longer display that demo user is typing after 111 seconds (refreshed is typing status at 50s => (111 - 50) = 61s > 60s after assuming no-longer typing)" + ); + } + ); + + QUnit.test( + 'receive several other members typing status "is typing"', + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv["res.partner"].create([ + { name: "Other 10" }, + { name: "Other 11" }, + { name: "Other 12" }, + ]); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + [0, 0, { partner_id: resPartnerId3 }], + ], + }); + const { messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "", + "Should display no one is currently typing" + ); + + // simulate receive typing notification from other 10 (is typing) + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Other 10 is typing...", + "Should display that 'Other 10' member is typing" + ); + + // simulate receive typing notification from other 11 (is typing) + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId2, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Other 10 and Other 11 are typing...", + "Should display that members 'Other 10' and 'Other 11' are typing (order: longer typer named first)" + ); + + // simulate receive typing notification from other 12 (is typing) + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId3, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Other 10, Other 11 and more are typing...", + "Should display that members 'Other 10', 'Other 11' and more (at least 1 extra member) are typing (order: longer typer named first)" + ); + + // simulate receive typing notification from other 10 (no longer is typing) + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: false, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Other 11 and Other 12 are typing...", + "Should display that members 'Other 11' and 'Other 12' are typing ('Other 10' stopped typing)" + ); + + // simulate receive typing notification from other 10 (is typing again) + await afterNextRender(() => + messaging.rpc({ + route: "/mail/channel/notify_typing", + params: { + channel_id: mailChannelId1, + context: { + mockedPartnerId: resPartnerId1, + }, + is_typing: true, + }, + }) + ); + assert.strictEqual( + document.querySelector(".o_ThreadTextualTypingStatusView").textContent, + "Other 11, Other 12 and more are typing...", + "Should display that members 'Other 11' and 'Other 12' and more (at least 1 extra member) are typing (order by longer typer, 'Other 10' just recently restarted typing)" + ); + } + ); }); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should display no one is currently typing" - ); - - // simulate receive typing notification from demo - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Demo is typing...", - "Should display that demo user is typing" - ); -}); - -QUnit.test('receive other member typing status "is typing" then "no longer is typing"', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - }); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should display no one is currently typing" - ); - - // simulate receive typing notification from demo "is typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Demo is typing...", - "Should display that demo user is typing" - ); - - // simulate receive typing notification from demo "is no longer typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': false, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should no longer display that demo user is typing" - ); -}); - -QUnit.test('assume other member typing status becomes "no longer is typing" after 60 seconds without any updated typing status', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - }); - const { advanceTime, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should display no one is currently typing" - ); - - // simulate receive typing notification from demo "is typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Demo is typing...", - "Should display that demo user is typing" - ); - - await afterNextRender(() => advanceTime(60 * 1000)); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should no longer display that demo user is typing" - ); -}); - -QUnit.test ('other member typing status "is typing" refreshes 60 seconds timer of assuming no longer typing', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ name: 'Demo' }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - }); - const { advanceTime, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - hasTimeControl: true, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should display no one is currently typing" - ); - - // simulate receive typing notification from demo "is typing" - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Demo is typing...", - "Should display that demo user is typing" - ); - - // simulate receive typing notification from demo "is typing" again after 50s. - await advanceTime(50 * 1000); - messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - }); - await advanceTime(50 * 1000); - await nextAnimationFrame(); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Demo is typing...", - "Should still display that demo user is typing after 100 seconds (refreshed is typing status at 50s => (100 - 50) = 50s < 60s after assuming no-longer typing)" - ); - - await afterNextRender(() => advanceTime(11 * 1000)); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should no longer display that demo user is typing after 111 seconds (refreshed is typing status at 50s => (111 - 50) = 61s > 60s after assuming no-longer typing)" - ); -}); - -QUnit.test('receive several other members typing status "is typing"', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2, resPartnerId3] = pyEnv['res.partner'].create([ - { name: 'Other 10' }, - { name: 'Other 11' }, - { name: 'Other 12' }, - ]); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - [0, 0, { partner_id: resPartnerId3 }], - ], - }); - const { messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "", - "Should display no one is currently typing" - ); - - // simulate receive typing notification from other 10 (is typing) - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Other 10 is typing...", - "Should display that 'Other 10' member is typing" - ); - - // simulate receive typing notification from other 11 (is typing) - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId2, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Other 10 and Other 11 are typing...", - "Should display that members 'Other 10' and 'Other 11' are typing (order: longer typer named first)" - ); - - // simulate receive typing notification from other 12 (is typing) - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId3, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Other 10, Other 11 and more are typing...", - "Should display that members 'Other 10', 'Other 11' and more (at least 1 extra member) are typing (order: longer typer named first)" - ); - - // simulate receive typing notification from other 10 (no longer is typing) - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': false, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Other 11 and Other 12 are typing...", - "Should display that members 'Other 11' and 'Other 12' are typing ('Other 10' stopped typing)" - ); - - // simulate receive typing notification from other 10 (is typing again) - await afterNextRender(() => messaging.rpc({ - route: '/mail/channel/notify_typing', - params: { - 'channel_id': mailChannelId1, - 'context': { - 'mockedPartnerId': resPartnerId1, - }, - 'is_typing': true, - }, - })); - assert.strictEqual( - document.querySelector('.o_ThreadTextualTypingStatusView').textContent, - "Other 11, Other 12 and more are typing...", - "Should display that members 'Other 11' and 'Other 12' and more (at least 1 extra member) are typing (order by longer typer, 'Other 10' just recently restarted typing)" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js b/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js index 2bd044e8b4c..9b92c9317bc 100644 --- a/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/components/thread_view_tests.js @@ -6,1636 +6,1792 @@ import { isScrolledToBottom, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('thread_view_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("thread_view_tests.js"); -QUnit.test('dragover files on thread with composer', async function (assert) { - assert.expect(1); + QUnit.test("dragover files on thread with composer", async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - group_public_id: false, - name: "General", - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await afterNextRender(() => - dragenterFiles(document.querySelector('.o_ThreadView')) - ); - assert.ok( - document.querySelector('.o_ComposerView_dropZone'), - "should have dropzone when dragging file over the thread" - ); -}); - -QUnit.test('message list asc order', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - group_public_id: false, - name: "General", - }); - for (let i = 0; i <= 60; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - }); - } - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until channel 1 loaded initial messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 - ); - }, - }); - const messageItems = document.querySelectorAll(`.o_MessageListView_item`); - assert.notOk( - messageItems[messageItems.length - 1].classList.contains("o_MessageListView_loadMore"), - "load more link should be before messages" - ); - assert.ok( - messageItems[0].classList.contains("o_MessageListView_loadMore"), - "load more link should NOT be after messages" - ); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 30, - "should have 30 messages at the beginning" - ); - - // scroll to top - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => document.querySelector(`.o_ThreadView_messageList`).scrollTop = 0, - message: "should wait until channel 1 loaded more messages after scrolling to top", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'more-messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 - ); - }, - }); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 60, - "should have 60 messages after scrolled to top" - ); - - // scroll to bottom - await afterNextRender(() => { - document.querySelector(`.o_ThreadView_messageList`).scrollTop = - document.querySelector(`.o_ThreadView_messageList`).scrollHeight; - }); - assert.strictEqual( - document.querySelectorAll(`.o_MessageView`).length, - 60, - "scrolling to bottom should not trigger any message fetching" - ); -}); - -QUnit.test('mark channel as fetched when a new message is loaded and as seen when focusing composer [REQUIRE FOCUS]', async function (assert) { - assert.expect(7); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "fred@example.com", - name: "Fred", - }); - const resUsersId1 = pyEnv['res.users'].create({ - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: 'chat', - }); - const { afterEvent, click, messaging } = await start({ - mockRPC(route, args) { - if (args.method === 'channel_fetched') { - assert.strictEqual( - args.args[0][0], - mailChannelId1, - 'channel_fetched is called on the right channel id' - ); - assert.strictEqual( - args.model, - 'mail.channel', - 'channel_fetched is called on the right channel model' - ); - assert.step('rpc:channel_fetch'); - } else if (route === '/mail/channel/set_last_seen_message') { - assert.strictEqual( - args.channel_id, - mailChannelId1, - 'set_last_seen_message is called on the right channel id' - ); - assert.step('rpc:set_last_seen_message'); - } - } - }); - await click(`.o_MessagingMenu_toggler`); - const thread = messaging.models['Thread'].findFromIdentifyingData({ - id: mailChannelId1, - model: 'mail.channel', - }); - await afterNextRender(async () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "new message", - uuid: thread.uuid, - }, - })); - assert.verifySteps( - ['rpc:channel_fetch'], - "Channel should have been fetched but not seen yet" - ); - - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => document.querySelector('.o_ComposerTextInputView_textarea').focus(), - message: "should wait until last seen by current partner message id changed after focusing the thread", - predicate: ({ thread }) => { - return ( - thread.id === mailChannelId1 && - thread.model === 'mail.channel' - ); - }, - })); - assert.verifySteps( - ['rpc:set_last_seen_message'], - "Channel should have been marked as seen after threadView got the focus" - ); -}); - -QUnit.test('mark channel as fetched and seen when a new message is loaded if composer is focused [REQUIRE FOCUS]', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const resUsersId1 = pyEnv['res.users'].create({ - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { afterEvent, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - mockRPC(route, args) { - if (args.method === 'channel_fetched' && args.args[0] === mailChannelId1) { - throw new Error("'channel_fetched' RPC must not be called for created channel as message is directly seen"); - } else if (route === '/mail/channel/set_last_seen_message') { - assert.strictEqual( - args.channel_id, - mailChannelId1, - 'set_last_seen_message is called on the right channel id' - ); - assert.step('rpc:set_last_seen_message'); - } - } - }); - await openDiscuss(); - document.querySelector('.o_ComposerTextInputView_textarea').focus(); - // simulate receiving a message - await afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + group_public_id: false, + name: "General", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, }, - message_content: "

fdsfsd

", - uuid: messaging.models['Thread'].findFromIdentifyingData({ - model: 'mail.channel', - id: mailChannelId1, - }).uuid, - }, - }), - message: "should wait until last seen by current partner message id changed after receiving a message while thread is focused", - predicate: ({ thread }) => { - return ( - thread.id === mailChannelId1 && - thread.model === 'mail.channel' + }); + await openDiscuss(); + await afterNextRender(() => dragenterFiles(document.querySelector(".o_ThreadView"))); + assert.ok( + document.querySelector(".o_ComposerView_dropZone"), + "should have dropzone when dragging file over the thread" ); - }, - }); - assert.verifySteps( - ['rpc:set_last_seen_message'], - "Channel should have been mark as seen directly" - ); -}); - -QUnit.test('show message subject when subject is not the same as the thread name', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - group_public_id: false, - name: "General", - }); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - subject: "Salutations, voyageur", - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display a single message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_subject', - "should display subject of the message" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_subject').textContent, - "Subject: Salutations, voyageur", - "Subject of the message should be 'Salutations, voyageur'" - ); -}); - -QUnit.test('do not show message subject when subject is the same as the thread name', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - group_public_id: false, - name: "Salutations, voyageur", - }); - pyEnv['mail.message'].create({ - body: "not empty", - model: 'mail.channel', - res_id: mailChannelId1, - subject: "Salutations, voyageur", - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_MessageView_subject', - "should not display subject of the message" - ); -}); - -QUnit.test('[technical] new messages separator on posting message', async function (assert) { - // technical as we need to remove focus from text input to avoid `set_last_seen_message` call - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 0, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'channel', - name: "General", - }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "first message", - model: "mail.channel", - res_id: mailChannelId1, - }); - const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: mailMessageId1 }); - const { insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageView', - "should display one message in thread initially" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should not display 'new messages' separator" - ); - - await insertText('.o_ComposerTextInputView_textarea', "hey !"); - await afterNextRender(() => { - // need to remove focus from text area to avoid set_last_seen_message - document.querySelector('.o_ComposerView_buttonSend').focus(); - document.querySelector('.o_ComposerView_buttonSend').click(); - - }); - assert.containsN( - document.body, - '.o_MessageView', - 2, - "should display 2 messages (initial & newly posted), after posting a message" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "still no separator shown when current partner posted a message" - ); -}); - -QUnit.test('new messages separator on receiving new message [REQUIRE FOCUS]', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Foreigner partner", - }); - const resUsersId1 = pyEnv['res.users'].create({ - name: "Foreigner user", - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 0, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'channel', - name: "General", - uuid: 'randomuuid', - }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "blah", - model: "mail.channel", - res_id: mailChannelId1, - }); - const [mailChannelMemberId] = pyEnv['mail.channel.member'].search([['channel_id', '=', mailChannelId1], ['partner_id', '=', pyEnv.currentPartnerId]]); - pyEnv['mail.channel.member'].write([mailChannelMemberId], { seen_message_id: mailMessageId1 }); - const { afterEvent, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.containsOnce( - document.body, - '.o_MessageListView_message', - "should have an initial message" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should not display 'new messages' separator" - ); - - document.querySelector('.o_ComposerTextInputView_textarea').blur(); - // simulate receiving a message - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "hu", - uuid: messaging.models['Thread'].findFromIdentifyingData({ - model: 'mail.channel', - id: mailChannelId1, - }).uuid, - }, - }), - message: "should wait until new message is received", - predicate: ({ hint, threadViewer }) => { - return ( - threadViewer.thread.id === mailChannelId1 && - threadViewer.thread.model === 'mail.channel' && - hint.type === 'message-received' - ); - }, - }); - assert.containsN( - document.body, - '.o_MessageView', - 2, - "should now have 2 messages after receiving a new message" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "'new messages' separator should be shown" - ); - - assert.containsOnce( - document.body, - `.o_MessageListView_separatorNewMessages ~ .o_MessageView[data-message-id="${mailMessageId1 + 1}"]`, - "'new messages' separator should be shown above new message received" - ); - - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-last-seen-by-current-partner-message-id-changed', - func: () => document.querySelector('.o_ComposerTextInputView_textarea').focus(), - message: "should wait until last seen by current partner message id changed after focusing the thread", - predicate: ({ thread }) => { - return ( - thread.id === mailChannelId1 && - thread.model === 'mail.channel' - ); - }, - })); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "'new messages' separator should no longer be shown as last message has been seen" - ); -}); - -QUnit.test('new messages separator on posting message', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { - message_unread_counter: 0, - partner_id: pyEnv.currentPartnerId, - }], - ], - channel_type: 'channel', - name: "General", - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - assert.containsNone( - document.body, - '.o_MessageListView_message', - "should have no messages" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "should not display 'new messages' separator" - ); - - await insertText('.o_ComposerTextInputView_textarea', "hey !"); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.body, - '.o_MessageView', - "should have the message current partner just posted" - ); - assert.containsNone( - document.body, - '.o_MessageListView_separatorNewMessages', - "still no separator shown when current partner posted a message" - ); -}); - -QUnit.test('basic rendering of canceled notification', async function (assert) { - assert.expect(8); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const resPartnerId1 = pyEnv['res.partner'].create({ name: "Someone" }); - const mailMessageId1 = pyEnv['mail.message'].create({ - body: "not empty", - message_type: 'email', - model: 'mail.channel', - res_id: mailChannelId1, - }); - pyEnv['mail.notification'].create({ - failure_type: 'SMTP', - mail_message_id: mailMessageId1, - notification_status: 'canceled', - notification_type: 'email', - res_partner_id: resPartnerId1, - }); - const { afterEvent, click, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "thread become loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 - ); - }, - }); - - assert.containsOnce( - document.body, - '.o_MessageView_notificationIconClickable', - "should display the notification icon container on the message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_notificationIcon', - "should display the notification icon on the message" - ); - assert.hasClass( - document.querySelector('.o_MessageView_notificationIcon'), - 'fa-envelope-o', - "notification icon shown on the message should represent email" - ); - - await click('.o_MessageView_notificationIconClickable'); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView', - "notification popover should be opened after notification has been clicked" - ); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView_notificationIcon', - "an icon should be shown in notification popover" - ); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView_notificationIcon.fa.fa-trash-o', - "the icon shown in notification popover should be the canceled icon" - ); - assert.containsOnce( - document.body, - '.o_MessageNotificationPopoverContentView_notificationPartnerName', - "partner name should be shown in notification popover" - ); - assert.strictEqual( - document.querySelector('.o_MessageNotificationPopoverContentView_notificationPartnerName').textContent.trim(), - "Someone", - "partner name shown in notification popover should be the one concerned by the notification" - ); -}); - -QUnit.test('should scroll to bottom on receiving new message if the list is initially scrolled to bottom (asc order)', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - // Needed partner & user to allow simulation of message reception - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Foreigner partner", - }); - const resUsersId1 = pyEnv['res.users'].create({ - name: "Foreigner user", - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i <= 10; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, }); - } - const { afterEvent, click, messaging } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await click(`.o_MessagingMenu_toggler`); - const thread = messaging.models['Thread'].findFromIdentifyingData({ model: 'mail.channel', id: mailChannelId1 }); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - async func() { - await click(`.o_NotificationListView_preview`); - }, - message: "should wait until channel scrolled initially", - predicate: data => thread === data.threadViewer.thread, - }); - const initialMessageList = document.querySelector('.o_ThreadView_messageList'); - assert.ok( - isScrolledToBottom(initialMessageList), - "should have scrolled to bottom of channel 20 initially" - ); - // simulate receiving a message - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => - messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, + QUnit.test("message list asc order", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + group_public_id: false, + name: "General", + }); + for (let i = 0; i <= 60; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until channel 1 loaded initial messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + const messageItems = document.querySelectorAll(`.o_MessageListView_item`); + assert.notOk( + messageItems[messageItems.length - 1].classList.contains( + "o_MessageListView_loadMore" + ), + "load more link should be before messages" + ); + assert.ok( + messageItems[0].classList.contains("o_MessageListView_loadMore"), + "load more link should NOT be after messages" + ); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 30, + "should have 30 messages at the beginning" + ); + + // scroll to top + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => (document.querySelector(`.o_ThreadView_messageList`).scrollTop = 0), + message: "should wait until channel 1 loaded more messages after scrolling to top", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "more-messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 60, + "should have 60 messages after scrolled to top" + ); + + // scroll to bottom + await afterNextRender(() => { + document.querySelector( + `.o_ThreadView_messageList` + ).scrollTop = document.querySelector(`.o_ThreadView_messageList`).scrollHeight; + }); + assert.strictEqual( + document.querySelectorAll(`.o_MessageView`).length, + 60, + "scrolling to bottom should not trigger any message fetching" + ); + }); + + QUnit.test( + "mark channel as fetched when a new message is loaded and as seen when focusing composer [REQUIRE FOCUS]", + async function (assert) { + assert.expect(7); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "fred@example.com", + name: "Fred", + }); + const resUsersId1 = pyEnv["res.users"].create({ + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { afterEvent, click, messaging } = await start({ + mockRPC(route, args) { + if (args.method === "channel_fetched") { + assert.strictEqual( + args.args[0][0], + mailChannelId1, + "channel_fetched is called on the right channel id" + ); + assert.strictEqual( + args.model, + "mail.channel", + "channel_fetched is called on the right channel model" + ); + assert.step("rpc:channel_fetch"); + } else if (route === "/mail/channel/set_last_seen_message") { + assert.strictEqual( + args.channel_id, + mailChannelId1, + "set_last_seen_message is called on the right channel id" + ); + assert.step("rpc:set_last_seen_message"); + } }, - message_content: "hello", - uuid: thread.uuid, - }, - }), - message: "should wait until channel scrolled after receiving a message", - predicate: data => thread === data.threadViewer.thread, - }); - const messageList = document.querySelector('.o_ThreadView_messageList'); - assert.ok( - isScrolledToBottom(messageList), - "should scroll to bottom on receiving new message because the list is initially scrolled to bottom" - ); -}); + }); + await click(`.o_MessagingMenu_toggler`); + const thread = messaging.models["Thread"].findFromIdentifyingData({ + id: mailChannelId1, + model: "mail.channel", + }); + await afterNextRender(async () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "new message", + uuid: thread.uuid, + }, + }) + ); + assert.verifySteps( + ["rpc:channel_fetch"], + "Channel should have been fetched but not seen yet" + ); -QUnit.test('should not scroll on receiving new message if the list is initially scrolled anywhere else than bottom (asc order)', async function (assert) { - assert.expect(3); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => + document.querySelector(".o_ComposerTextInputView_textarea").focus(), + message: + "should wait until last seen by current partner message id changed after focusing the thread", + predicate: ({ thread }) => { + return thread.id === mailChannelId1 && thread.model === "mail.channel"; + }, + }) + ); + assert.verifySteps( + ["rpc:set_last_seen_message"], + "Channel should have been marked as seen after threadView got the focus" + ); + } + ); - const pyEnv = await startServer(); - // Needed partner & user to allow simulation of message reception - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Foreigner partner", - }); - const resUsersId1 = pyEnv['res.users'].create({ - name: "Foreigner user", - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i <= 10; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, - }); - } - const { afterEvent, click, messaging } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); + QUnit.test( + "mark channel as fetched and seen when a new message is loaded if composer is focused [REQUIRE FOCUS]", + async function (assert) { + assert.expect(3); - const thread = messaging.models['Thread'].findFromIdentifyingData({ model: 'mail.channel', id: mailChannelId1 }); - await click(`.o_MessagingMenu_toggler`); - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - async func() { - await click(`.o_NotificationListView_preview`); - }, - message: "should wait until channel scrolled initially", - predicate: data => thread === data.threadViewer.thread, - }); - const initialMessageList = document.querySelector('.o_ThreadView_messageList'); - assert.ok( - isScrolledToBottom(initialMessageList), - "should have scrolled to bottom of channel 1 initially" - ); - - await afterEvent({ - eventName: 'o-component-message-list-scrolled', - func: () => initialMessageList.scrollTop = 0, - message: "should wait until channel 1 processed manual scroll", - predicate: data => thread === data.threadViewer.thread, - }); - assert.strictEqual( - initialMessageList.scrollTop, - 0, - "should have scrolled to the top of channel 1 manually" - ); - - // simulate receiving a message - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => - messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const resUsersId1 = pyEnv["res.users"].create({ + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { afterEvent, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, }, - message_content: "hello", - uuid: thread.uuid, + mockRPC(route, args) { + if (args.method === "channel_fetched" && args.args[0] === mailChannelId1) { + throw new Error( + "'channel_fetched' RPC must not be called for created channel as message is directly seen" + ); + } else if (route === "/mail/channel/set_last_seen_message") { + assert.strictEqual( + args.channel_id, + mailChannelId1, + "set_last_seen_message is called on the right channel id" + ); + assert.step("rpc:set_last_seen_message"); + } + }, + }); + await openDiscuss(); + document.querySelector(".o_ComposerTextInputView_textarea").focus(); + // simulate receiving a message + await afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "

fdsfsd

", + uuid: messaging.models["Thread"].findFromIdentifyingData({ + model: "mail.channel", + id: mailChannelId1, + }).uuid, + }, + }), + message: + "should wait until last seen by current partner message id changed after receiving a message while thread is focused", + predicate: ({ thread }) => { + return thread.id === mailChannelId1 && thread.model === "mail.channel"; + }, + }); + assert.verifySteps( + ["rpc:set_last_seen_message"], + "Channel should have been mark as seen directly" + ); + } + ); + + QUnit.test( + "show message subject when subject is not the same as the thread name", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + group_public_id: false, + name: "General", + }); + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + subject: "Salutations, voyageur", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display a single message" + ); + assert.containsOnce( + document.body, + ".o_MessageView_subject", + "should display subject of the message" + ); + assert.strictEqual( + document.querySelector(".o_MessageView_subject").textContent, + "Subject: Salutations, voyageur", + "Subject of the message should be 'Salutations, voyageur'" + ); + } + ); + + QUnit.test( + "do not show message subject when subject is the same as the thread name", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + group_public_id: false, + name: "Salutations, voyageur", + }); + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + subject: "Salutations, voyageur", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsNone( + document.body, + ".o_MessageView_subject", + "should not display subject of the message" + ); + } + ); + + QUnit.test( + "[technical] new messages separator on posting message", + async function (assert) { + // technical as we need to remove focus from text input to avoid `set_last_seen_message` call + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 0, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "channel", + name: "General", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "first message", + model: "mail.channel", + res_id: mailChannelId1, + }); + const [mailChannelMemberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId], { + seen_message_id: mailMessageId1, + }); + const { insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageView", + "should display one message in thread initially" + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should not display 'new messages' separator" + ); + + await insertText(".o_ComposerTextInputView_textarea", "hey !"); + await afterNextRender(() => { + // need to remove focus from text area to avoid set_last_seen_message + document.querySelector(".o_ComposerView_buttonSend").focus(); + document.querySelector(".o_ComposerView_buttonSend").click(); + }); + assert.containsN( + document.body, + ".o_MessageView", + 2, + "should display 2 messages (initial & newly posted), after posting a message" + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "still no separator shown when current partner posted a message" + ); + } + ); + + QUnit.test( + "new messages separator on receiving new message [REQUIRE FOCUS]", + async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Foreigner partner", + }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 0, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "channel", + name: "General", + uuid: "randomuuid", + }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "blah", + model: "mail.channel", + res_id: mailChannelId1, + }); + const [mailChannelMemberId] = pyEnv["mail.channel.member"].search([ + ["channel_id", "=", mailChannelId1], + ["partner_id", "=", pyEnv.currentPartnerId], + ]); + pyEnv["mail.channel.member"].write([mailChannelMemberId], { + seen_message_id: mailMessageId1, + }); + const { afterEvent, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + assert.containsOnce( + document.body, + ".o_MessageListView_message", + "should have an initial message" + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should not display 'new messages' separator" + ); + + document.querySelector(".o_ComposerTextInputView_textarea").blur(); + // simulate receiving a message + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hu", + uuid: messaging.models["Thread"].findFromIdentifyingData({ + model: "mail.channel", + id: mailChannelId1, + }).uuid, + }, + }), + message: "should wait until new message is received", + predicate: ({ hint, threadViewer }) => { + return ( + threadViewer.thread.id === mailChannelId1 && + threadViewer.thread.model === "mail.channel" && + hint.type === "message-received" + ); + }, + }); + assert.containsN( + document.body, + ".o_MessageView", + 2, + "should now have 2 messages after receiving a new message" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "'new messages' separator should be shown" + ); + + assert.containsOnce( + document.body, + `.o_MessageListView_separatorNewMessages ~ .o_MessageView[data-message-id="${ + mailMessageId1 + 1 + }"]`, + "'new messages' separator should be shown above new message received" + ); + + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-last-seen-by-current-partner-message-id-changed", + func: () => + document.querySelector(".o_ComposerTextInputView_textarea").focus(), + message: + "should wait until last seen by current partner message id changed after focusing the thread", + predicate: ({ thread }) => { + return thread.id === mailChannelId1 && thread.model === "mail.channel"; + }, + }) + ); + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "'new messages' separator should no longer be shown as last message has been seen" + ); + } + ); + + QUnit.test("new messages separator on posting message", async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [ + 0, + 0, + { + message_unread_counter: 0, + partner_id: pyEnv.currentPartnerId, + }, + ], + ], + channel_type: "channel", + name: "General", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, }, - }), - message: "should wait until channel processed new message hint", - predicate: data => thread === data.threadViewer.thread && data.hint.type === 'message-received', - }); - assert.strictEqual( - document.querySelector('.o_ThreadView_messageList').scrollTop, - 0, - "should not scroll on receiving new message because the list is initially scrolled anywhere else than bottom" - ); -}); + }); + await openDiscuss(); -QUnit.test("delete all attachments of message without content should no longer display the message", async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const irAttachmentId1 = pyEnv['ir.attachment'].create({ - mimetype: 'text/plain', - name: "Blah.txt", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create( - { - attachment_ids: [irAttachmentId1], - model: "mail.channel", - res_id: mailChannelId1, - } - ); - const { afterEvent, click, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - // wait for messages of the thread to be loaded - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "thread become loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + assert.containsNone( + document.body, + ".o_MessageListView_message", + "should have no messages" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "there should be 1 message displayed initially" - ); - - await click(`.o_AttachmentCard[data-id="${ - messaging.models['Attachment'].findFromIdentifyingData({ id: irAttachmentId1 }).localId - }"] .o_AttachmentCard_asideItemUnlink`); - await click('.o_AttachmentDeleteConfirmView_confirmButton'); - assert.containsNone( - document.body, - '.o_MessageView', - "message should no longer be displayed after removing all its attachments (empty content)" - ); -}); - -QUnit.test('delete all attachments of a message with some text content should still keep it displayed', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const irAttachmentId1 = pyEnv['ir.attachment'].create({ - mimetype: 'text/plain', - name: "Blah.txt", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - attachment_ids: [irAttachmentId1], - body: "Some content", - model: "mail.channel", - res_id: mailChannelId1, - }); - const { afterEvent, click, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - // wait for messages of the thread to be loaded - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "thread become loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "should not display 'new messages' separator" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "there should be 1 message displayed initially" - ); - await click(`.o_AttachmentCard[data-id="${ - messaging.models['Attachment'].findFromIdentifyingData({ id: irAttachmentId1 }).localId - }"] .o_AttachmentCard_asideItemUnlink`); - await click('.o_AttachmentDeleteConfirmView_confirmButton'); - assert.containsOnce( - document.body, - '.o_MessageView', - "message should still be displayed after removing its attachments (non-empty content)" - ); -}); - -QUnit.test('Post a message containing an email address followed by a mention on another line', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "testpartner@odoo.com", - name: "TestPartner", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "email@odoo.com\n"); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@TestPartner")`, - "Conversation should have a message that has been posted, which contains partner mention" - ); -}); - -QUnit.test(`Mention a partner with special character (e.g. apostrophe ')`, async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - email: "usatyi@example.com", - name: "Pynya's spokesman", - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "@Pyn"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@Pynya's spokesman")`, - "Conversation should have a message that has been posted, which contains partner mention" - ); -}); - -QUnit.test('mention 2 different partners that have the same name', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([ - { - email: "partner1@example.com", - name: "TestPartner", - }, - { - email: "partner2@example.com", - name: "TestPartner", - }, - ]); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - [0, 0, { partner_id: resPartnerId2 }], - ], - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[0].click()); - await insertText('.o_ComposerTextInputView_textarea', "@Te"); - await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[1].click()); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce(document.body, '.o_MessageView_content', 'should have one message after posting it'); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@TestPartner")`, - "message should contain the first partner mention" - ); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_mail_redirect[data-oe-id="${resPartnerId2}"][data-oe-model="res.partner"]:contains("@TestPartner")`, - "message should also contain the second partner mention" - ); -}); - -QUnit.test('mention a channel with space in the name', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - name: "General good boy", - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "#"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector('.o_MessageView_content'), - '.o_channel_redirect', - "message must contain a link to the mentioned channel" - ); - assert.strictEqual( - document.querySelector('.o_channel_redirect').textContent, - '#General good boy', - "link to the channel must contains # + the channel name" - ); -}); - -QUnit.test('mention a channel with "&" in the name', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - name: "General & good", - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - await insertText('.o_ComposerTextInputView_textarea', "#"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector('.o_MessageView_content'), - '.o_channel_redirect', - "message should contain a link to the mentioned channel" - ); - assert.strictEqual( - document.querySelector('.o_channel_redirect').textContent, - '#General & good', - "link to the channel must contains # + the channel name" - ); -}); - -QUnit.test('mention a channel on a second line when the first line contains #', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - name: "General good", - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "#blabla\n"); - await insertText('.o_ComposerTextInputView_textarea', "#"); - await click('.o_ComposerSuggestionView'); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector('.o_MessageView_content'), - '.o_channel_redirect', - "message should contain a link to the mentioned channel" - ); - assert.strictEqual( - document.querySelector('.o_channel_redirect').textContent, - '#General good', - "link to the channel must contains # + the channel name" - ); -}); - -QUnit.test('mention a channel when replacing the space after the mention by another char', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - name: "General good", - }); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - - await insertText('.o_ComposerTextInputView_textarea', "#"); - await click('.o_ComposerSuggestionView'); - const text = document.querySelector(`.o_ComposerTextInputView_textarea`).value; - document.querySelector(`.o_ComposerTextInputView_textarea`).value = text.slice(0, -1); - await insertText('.o_ComposerTextInputView_textarea', ", test"); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce( - document.querySelector('.o_MessageView_content'), - '.o_channel_redirect', - "message should contain a link to the mentioned channel" - ); - assert.strictEqual( - document.querySelector('.o_channel_redirect').textContent, - '#General good', - "link to the channel must contains # + the channel name" - ); -}); - -QUnit.test('mention 2 different channels that have the same name', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const [mailChannelId1, mailChannelId2] = pyEnv['mail.channel'].create([ - { - channel_type: 'channel', - group_public_id: false, - name: "my channel", - }, - { - channel_type: 'channel', - name: "my channel", - }, - ]); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - document.querySelector('.o_ComposerTextInputView_textarea').focus(); - await insertText('.o_ComposerTextInputView_textarea', "#my"); - await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[0].click()); - await insertText('.o_ComposerTextInputView_textarea', "#my"); - await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestionView')[1].click()); - await click('.o_ComposerView_buttonSend'); - assert.containsOnce(document.body, '.o_MessageView_content', 'should have one message after posting it'); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_channel_redirect[data-oe-id="${mailChannelId1}"][data-oe-model="mail.channel"]:contains("#my channel")`, - "message should contain the first channel mention" - ); - assert.containsOnce( - document.querySelector(`.o_MessageView_content`), - `.o_channel_redirect[data-oe-id="${mailChannelId2}"][data-oe-model="mail.channel"]:contains("#my channel")`, - "message should also contain the second channel mention" - ); -}); - -QUnit.test('show empty placeholder when thread contains no message', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until thread becomes loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + await insertText(".o_ComposerTextInputView_textarea", "hey !"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.body, + ".o_MessageView", + "should have the message current partner just posted" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageListView_empty', - "message list empty placeholder should be shown as thread does not contain any messages" - ); - assert.containsNone( - document.body, - '.o_MessageView', - "no message should be shown as thread does not contain any" - ); -}); - -QUnit.test('show empty placeholder when thread contains only empty messages', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - model: "mail.channel", - res_id: mailChannelId1, - }); - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "thread become loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + assert.containsNone( + document.body, + ".o_MessageListView_separatorNewMessages", + "still no separator shown when current partner posted a message" ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageListView_empty', - "message list empty placeholder should be shown as thread contain only empty messages" - ); - assert.containsNone( - document.body, - '.o_MessageView', - "no message should be shown as thread contains only empty ones" - ); -}); - -QUnit.test('message with subtype should be displayed (and not considered as empty)', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const mailMessageSubtypeId1 = pyEnv['mail.message.subtype'].create({ - description: "Task created", - }); - pyEnv['mail.message'].create({ - model: "mail.channel", - res_id: mailChannelId1, - subtype_id: mailMessageSubtypeId1, - }); - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until thread becomes loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 - ); - }, - }); - assert.containsOnce( - document.body, - '.o_MessageView', - "should display 1 message (message with subtype description 'task created')" - ); - assert.strictEqual( - document.body.querySelector('.o_MessageView_content').textContent, - "Task created", - "message should have 'Task created' (from its subtype description)" - ); -}); - -QUnit.test('[technical] message list with a full page of empty messages should show load more if there are other messages', async function (assert) { - // Technical assumptions : - // - /mail/channel/messages fetching exactly 30 messages, - // - empty messages not being displayed - // - auto-load more being triggered on scroll, not automatically when the 30 first messages are empty - assert.expect(2); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - for (let i = 0; i <= 30; i++) { - pyEnv['mail.message'].create({ - body: "not empty", - model: "mail.channel", - res_id: mailChannelId1, }); - } - for (let i = 0; i <= 30; i++) { - pyEnv['mail.message'].create({ - model: "mail.channel", - res_id: mailChannelId1, + + QUnit.test("basic rendering of canceled notification", async function (assert) { + assert.expect(8); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const resPartnerId1 = pyEnv["res.partner"].create({ name: "Someone" }); + const mailMessageId1 = pyEnv["mail.message"].create({ + body: "not empty", + message_type: "email", + model: "mail.channel", + res_id: mailChannelId1, + }); + pyEnv["mail.notification"].create({ + failure_type: "SMTP", + mail_message_id: mailMessageId1, + notification_status: "canceled", + notification_type: "email", + res_partner_id: resPartnerId1, + }); + const { afterEvent, click, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + + assert.containsOnce( + document.body, + ".o_MessageView_notificationIconClickable", + "should display the notification icon container on the message" + ); + assert.containsOnce( + document.body, + ".o_MessageView_notificationIcon", + "should display the notification icon on the message" + ); + assert.hasClass( + document.querySelector(".o_MessageView_notificationIcon"), + "fa-envelope-o", + "notification icon shown on the message should represent email" + ); + + await click(".o_MessageView_notificationIconClickable"); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView", + "notification popover should be opened after notification has been clicked" + ); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView_notificationIcon", + "an icon should be shown in notification popover" + ); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView_notificationIcon.fa.fa-trash-o", + "the icon shown in notification popover should be the canceled icon" + ); + assert.containsOnce( + document.body, + ".o_MessageNotificationPopoverContentView_notificationPartnerName", + "partner name should be shown in notification popover" + ); + assert.strictEqual( + document + .querySelector( + ".o_MessageNotificationPopoverContentView_notificationPartnerName" + ) + .textContent.trim(), + "Someone", + "partner name shown in notification popover should be the one concerned by the notification" + ); }); - } - const { afterEvent, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: openDiscuss, - message: "should wait until thread becomes loaded with messages", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + + QUnit.test( + "should scroll to bottom on receiving new message if the list is initially scrolled to bottom (asc order)", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + // Needed partner & user to allow simulation of message reception + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Foreigner partner", + }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i <= 10; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, click, messaging } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await click(`.o_MessagingMenu_toggler`); + const thread = messaging.models["Thread"].findFromIdentifyingData({ + model: "mail.channel", + id: mailChannelId1, + }); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + async func() { + await click(`.o_NotificationListView_preview`); + }, + message: "should wait until channel scrolled initially", + predicate: (data) => thread === data.threadViewer.thread, + }); + const initialMessageList = document.querySelector(".o_ThreadView_messageList"); + assert.ok( + isScrolledToBottom(initialMessageList), + "should have scrolled to bottom of channel 20 initially" + ); + + // simulate receiving a message + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hello", + uuid: thread.uuid, + }, + }), + message: "should wait until channel scrolled after receiving a message", + predicate: (data) => thread === data.threadViewer.thread, + }); + const messageList = document.querySelector(".o_ThreadView_messageList"); + assert.ok( + isScrolledToBottom(messageList), + "should scroll to bottom on receiving new message because the list is initially scrolled to bottom" + ); + } + ); + + QUnit.test( + "should not scroll on receiving new message if the list is initially scrolled anywhere else than bottom (asc order)", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + // Needed partner & user to allow simulation of message reception + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Foreigner partner", + }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i <= 10; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, click, messaging } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + + const thread = messaging.models["Thread"].findFromIdentifyingData({ + model: "mail.channel", + id: mailChannelId1, + }); + await click(`.o_MessagingMenu_toggler`); + await afterEvent({ + eventName: "o-component-message-list-scrolled", + async func() { + await click(`.o_NotificationListView_preview`); + }, + message: "should wait until channel scrolled initially", + predicate: (data) => thread === data.threadViewer.thread, + }); + const initialMessageList = document.querySelector(".o_ThreadView_messageList"); + assert.ok( + isScrolledToBottom(initialMessageList), + "should have scrolled to bottom of channel 1 initially" + ); + + await afterEvent({ + eventName: "o-component-message-list-scrolled", + func: () => (initialMessageList.scrollTop = 0), + message: "should wait until channel 1 processed manual scroll", + predicate: (data) => thread === data.threadViewer.thread, + }); + assert.strictEqual( + initialMessageList.scrollTop, + 0, + "should have scrolled to the top of channel 1 manually" + ); + + // simulate receiving a message + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "hello", + uuid: thread.uuid, + }, + }), + message: "should wait until channel processed new message hint", + predicate: (data) => + thread === data.threadViewer.thread && + data.hint.type === "message-received", + }); + assert.strictEqual( + document.querySelector(".o_ThreadView_messageList").scrollTop, + 0, + "should not scroll on receiving new message because the list is initially scrolled anywhere else than bottom" + ); + } + ); + + QUnit.test( + "delete all attachments of message without content should no longer display the message", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const irAttachmentId1 = pyEnv["ir.attachment"].create({ + mimetype: "text/plain", + name: "Blah.txt", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + attachment_ids: [irAttachmentId1], + model: "mail.channel", + res_id: mailChannelId1, + }); + const { afterEvent, click, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + // wait for messages of the thread to be loaded + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "there should be 1 message displayed initially" + ); + + await click( + `.o_AttachmentCard[data-id="${ + messaging.models["Attachment"].findFromIdentifyingData({ + id: irAttachmentId1, + }).localId + }"] .o_AttachmentCard_asideItemUnlink` + ); + await click(".o_AttachmentDeleteConfirmView_confirmButton"); + assert.containsNone( + document.body, + ".o_MessageView", + "message should no longer be displayed after removing all its attachments (empty content)" + ); + } + ); + + QUnit.test( + "delete all attachments of a message with some text content should still keep it displayed", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const irAttachmentId1 = pyEnv["ir.attachment"].create({ + mimetype: "text/plain", + name: "Blah.txt", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + attachment_ids: [irAttachmentId1], + body: "Some content", + model: "mail.channel", + res_id: mailChannelId1, + }); + const { afterEvent, click, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + // wait for messages of the thread to be loaded + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "there should be 1 message displayed initially" + ); + + await click( + `.o_AttachmentCard[data-id="${ + messaging.models["Attachment"].findFromIdentifyingData({ + id: irAttachmentId1, + }).localId + }"] .o_AttachmentCard_asideItemUnlink` + ); + await click(".o_AttachmentDeleteConfirmView_confirmButton"); + assert.containsOnce( + document.body, + ".o_MessageView", + "message should still be displayed after removing its attachments (non-empty content)" + ); + } + ); + + QUnit.test( + "Post a message containing an email address followed by a mention on another line", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "testpartner@odoo.com", + name: "TestPartner", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "email@odoo.com\n"); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@TestPartner")`, + "Conversation should have a message that has been posted, which contains partner mention" + ); + } + ); + + QUnit.test( + `Mention a partner with special character (e.g. apostrophe ')`, + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + email: "usatyi@example.com", + name: "Pynya's spokesman", + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "@Pyn"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@Pynya's spokesman")`, + "Conversation should have a message that has been posted, which contains partner mention" + ); + } + ); + + QUnit.test("mention 2 different partners that have the same name", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { + email: "partner1@example.com", + name: "TestPartner", + }, + { + email: "partner2@example.com", + name: "TestPartner", + }, + ]); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + [0, 0, { partner_id: resPartnerId2 }], + ], + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + await afterNextRender(() => + document.querySelectorAll(".o_ComposerSuggestionView")[0].click() ); - }, - }); - assert.containsNone( - document.body, - '.o_MessageView', - "No message should be shown as all 30 first messages are empty" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_loadMore', - "Load more button should be shown as there are more messages to show" - ); -}); - -QUnit.test('first unseen message should be directly preceded by the new message separator if there is a transient message just before it while composer is not focused [REQUIRE FOCUS]', async function (assert) { - // The goal of removing the focus is to ensure the thread is not marked as seen automatically. - // Indeed that would trigger set_last_seen_message no matter what, which is already covered by other tests. - // The goal of this test is to cover the conditions specific to transient messages, - // and the conditions from focus would otherwise shadow them. - assert.expect(3); - - const pyEnv = await startServer(); - // Needed partner & user to allow simulation of message reception - const resPartnerId1 = pyEnv['res.partner'].create({ - name: "Foreigner partner", - }); - const resUsersId1 = pyEnv['res.users'].create({ - name: "Foreigner user", - partner_id: resPartnerId1, - }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - uuid: 'channel20uuid', - }); - const { click, insertText, messaging, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - // send a command that leads to receiving a transient message - await insertText('.o_ComposerTextInputView_textarea', "/who"); - await click('.o_ComposerView_buttonSend'); - const transientMessage = messaging.discuss.threadViewer.threadView.messageListView.messageListViewItems[0].message; - - // composer is focused by default, we remove that focus - document.querySelector('.o_ComposerTextInputView_textarea').blur(); - // simulate receiving a message - await afterNextRender(() => messaging.rpc({ - route: '/mail/chat_post', - params: { - context: { - mockedUserId: resUsersId1, - }, - message_content: "test", - uuid: 'channel20uuid', - }, - })); - assert.containsN( - document.body, - '.o_MessageView', - 2, - "should display 2 messages (the transient & the received message), after posting a command" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_separatorNewMessages', - "separator should be shown as a message has been received" - ); - assert.containsOnce( - document.body, - `.o_MessageView[data-message-id="${ - transientMessage.id - }"] + .o_MessageListView_separatorNewMessages`, - "separator should be shown just after transient message" - ); -}); - -QUnit.test('composer should be focused automatically after clicking on the send button [REQUIRE FOCUS]', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({}); - const { click, insertText, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - await insertText('.o_ComposerTextInputView_textarea', "Dummy Message"); - await click('.o_ComposerView_buttonSend'); - assert.hasClass( - document.querySelector('.o_ComposerView'), - 'o-focused', - "composer should be focused automatically after clicking on the send button" - ); -}); - -QUnit.test('failure on loading messages should display error', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - return Promise.reject(); - } - }, - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - - assert.containsOnce( - document.body, - '.o_ThreadView_loadingFailed', - "should show loading error message" - ); -}); - -QUnit.test('failure on loading messages should prompt retry button', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - return Promise.reject(); - } - }, - }); - await openDiscuss({ waitUntilMessagesLoaded: false }); - - assert.containsOnce( - document.body, - '.o_ThreadView_loadingFailedRetryButton', - "should show a button to allow user to retry loading" - ); -}); - -QUnit.test('failure on loading more messages should not alter message list display', async function (assert) { - assert.expect(1); - - // first call needs to be successful as it is the initial loading of messages - // second call comes from load more and needs to fail in order to show the error alert - // any later call should work so that retry button and load more clicks would now work - let messageFetchShouldFail = false; - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - pyEnv['mail.message'].create([...Array(60).keys()].map(() => { - return { - body: 'coucou', - model: "mail.channel", - res_id: mailChannelId1, - }; - })); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - if (messageFetchShouldFail) { - throw new Error(); - } - } - }, - }); - await openDiscuss(); - - messageFetchShouldFail = true; - await click('.o_MessageListView_loadMore'); - assert.containsN( - document.body, - '.o_MessageView', - 30, - "should still show 30 messages as load more has failed" - ); -}); - -QUnit.test('failure on loading more messages should display error and prompt retry button', async function (assert) { - assert.expect(3); - - // first call needs to be successful as it is the initial loading of messages - // second call comes from load more and needs to fail in order to show the error alert - // any later call should work so that retry button and load more clicks would now work - let messageFetchShouldFail = false; - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - pyEnv['mail.message'].create([...Array(60).keys()].map(() => { - return { - body: 'coucou', - model: "mail.channel", - res_id: mailChannelId1, - }; - })); - const { click, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - if (messageFetchShouldFail) { - throw new Error(); - } - } - }, - }); - await openDiscuss(); - - messageFetchShouldFail = true; - await click('.o_MessageListView_loadMore'); - assert.containsOnce( - document.body, - '.o_MessageListView_alertLoadingFailed', - "should show loading error message" - ); - assert.containsOnce( - document.body, - '.o_MessageListView_alertLoadingFailedRetryButton', - "should show loading error message button" - ); - assert.containsNone( - document.body, - '.o_MessageListView_loadMore', - "should not show load more buttton" - ); -}); - -QUnit.test('Retry loading more messages on failed load more messages should load more messages', async function (assert) { - assert.expect(0); - - // first call needs to be successful as it is the initial loading of messages - // second call comes from load more and needs to fail in order to show the error alert - // any later call should work so that retry button and load more clicks would now work - let messageFetchShouldFail = false; - const pyEnv = await startServer(); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - pyEnv['mail.message'].create([...Array(90).keys()].map(() => { - return { - body: 'coucou', - model: "mail.channel", - res_id: mailChannelId1, - }; - })); - const { afterEvent, click, openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - async mockRPC(route, args) { - if (route === '/mail/channel/messages') { - if (messageFetchShouldFail) { - return Promise.reject(); - } - } - } - }); - await openDiscuss(); - messageFetchShouldFail = true; - await click('.o_MessageListView_loadMore'); - - messageFetchShouldFail = false; - await afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => document.querySelector('.o_MessageListView_alertLoadingFailedRetryButton').click(), - message: "should wait until channel loaded more messages after clicked on load more", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'more-messages-loaded' && - threadViewer.thread.model === 'mail.channel' && - threadViewer.thread.id === mailChannelId1 + await insertText(".o_ComposerTextInputView_textarea", "@Te"); + await afterNextRender(() => + document.querySelectorAll(".o_ComposerSuggestionView")[1].click() ); - }, + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.body, + ".o_MessageView_content", + "should have one message after posting it" + ); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_mail_redirect[data-oe-id="${resPartnerId1}"][data-oe-model="res.partner"]:contains("@TestPartner")`, + "message should contain the first partner mention" + ); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_mail_redirect[data-oe-id="${resPartnerId2}"][data-oe-model="res.partner"]:contains("@TestPartner")`, + "message should also contain the second partner mention" + ); + }); + + QUnit.test("mention a channel with space in the name", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "General good boy", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "#"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(".o_MessageView_content"), + ".o_channel_redirect", + "message must contain a link to the mentioned channel" + ); + assert.strictEqual( + document.querySelector(".o_channel_redirect").textContent, + "#General good boy", + "link to the channel must contains # + the channel name" + ); + }); + + QUnit.test('mention a channel with "&" in the name', async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "General & good", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + await insertText(".o_ComposerTextInputView_textarea", "#"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(".o_MessageView_content"), + ".o_channel_redirect", + "message should contain a link to the mentioned channel" + ); + assert.strictEqual( + document.querySelector(".o_channel_redirect").textContent, + "#General & good", + "link to the channel must contains # + the channel name" + ); + }); + + QUnit.test( + "mention a channel on a second line when the first line contains #", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "General good", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "#blabla\n"); + await insertText(".o_ComposerTextInputView_textarea", "#"); + await click(".o_ComposerSuggestionView"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(".o_MessageView_content"), + ".o_channel_redirect", + "message should contain a link to the mentioned channel" + ); + assert.strictEqual( + document.querySelector(".o_channel_redirect").textContent, + "#General good", + "link to the channel must contains # + the channel name" + ); + } + ); + + QUnit.test( + "mention a channel when replacing the space after the mention by another char", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + name: "General good", + }); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + + await insertText(".o_ComposerTextInputView_textarea", "#"); + await click(".o_ComposerSuggestionView"); + const text = document.querySelector(`.o_ComposerTextInputView_textarea`).value; + document.querySelector(`.o_ComposerTextInputView_textarea`).value = text.slice( + 0, + -1 + ); + await insertText(".o_ComposerTextInputView_textarea", ", test"); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.querySelector(".o_MessageView_content"), + ".o_channel_redirect", + "message should contain a link to the mentioned channel" + ); + assert.strictEqual( + document.querySelector(".o_channel_redirect").textContent, + "#General good", + "link to the channel must contains # + the channel name" + ); + } + ); + + QUnit.test("mention 2 different channels that have the same name", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const [mailChannelId1, mailChannelId2] = pyEnv["mail.channel"].create([ + { + channel_type: "channel", + group_public_id: false, + name: "my channel", + }, + { + channel_type: "channel", + name: "my channel", + }, + ]); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + document.querySelector(".o_ComposerTextInputView_textarea").focus(); + await insertText(".o_ComposerTextInputView_textarea", "#my"); + await afterNextRender(() => + document.querySelectorAll(".o_ComposerSuggestionView")[0].click() + ); + await insertText(".o_ComposerTextInputView_textarea", "#my"); + await afterNextRender(() => + document.querySelectorAll(".o_ComposerSuggestionView")[1].click() + ); + await click(".o_ComposerView_buttonSend"); + assert.containsOnce( + document.body, + ".o_MessageView_content", + "should have one message after posting it" + ); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_channel_redirect[data-oe-id="${mailChannelId1}"][data-oe-model="mail.channel"]:contains("#my channel")`, + "message should contain the first channel mention" + ); + assert.containsOnce( + document.querySelector(`.o_MessageView_content`), + `.o_channel_redirect[data-oe-id="${mailChannelId2}"][data-oe-model="mail.channel"]:contains("#my channel")`, + "message should also contain the second channel mention" + ); + }); + + QUnit.test( + "show empty placeholder when thread contains no message", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until thread becomes loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageListView_empty", + "message list empty placeholder should be shown as thread does not contain any messages" + ); + assert.containsNone( + document.body, + ".o_MessageView", + "no message should be shown as thread does not contain any" + ); + } + ); + + QUnit.test( + "show empty placeholder when thread contains only empty messages", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + model: "mail.channel", + res_id: mailChannelId1, + }); + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageListView_empty", + "message list empty placeholder should be shown as thread contain only empty messages" + ); + assert.containsNone( + document.body, + ".o_MessageView", + "no message should be shown as thread contains only empty ones" + ); + } + ); + + QUnit.test( + "message with subtype should be displayed (and not considered as empty)", + async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const mailMessageSubtypeId1 = pyEnv["mail.message.subtype"].create({ + description: "Task created", + }); + pyEnv["mail.message"].create({ + model: "mail.channel", + res_id: mailChannelId1, + subtype_id: mailMessageSubtypeId1, + }); + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until thread becomes loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsOnce( + document.body, + ".o_MessageView", + "should display 1 message (message with subtype description 'task created')" + ); + assert.strictEqual( + document.body.querySelector(".o_MessageView_content").textContent, + "Task created", + "message should have 'Task created' (from its subtype description)" + ); + } + ); + + QUnit.test( + "[technical] message list with a full page of empty messages should show load more if there are other messages", + async function (assert) { + // Technical assumptions : + // - /mail/channel/messages fetching exactly 30 messages, + // - empty messages not being displayed + // - auto-load more being triggered on scroll, not automatically when the 30 first messages are empty + assert.expect(2); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + for (let i = 0; i <= 30; i++) { + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: mailChannelId1, + }); + } + for (let i = 0; i <= 30; i++) { + pyEnv["mail.message"].create({ + model: "mail.channel", + res_id: mailChannelId1, + }); + } + const { afterEvent, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: openDiscuss, + message: "should wait until thread becomes loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + assert.containsNone( + document.body, + ".o_MessageView", + "No message should be shown as all 30 first messages are empty" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_loadMore", + "Load more button should be shown as there are more messages to show" + ); + } + ); + + QUnit.test( + "first unseen message should be directly preceded by the new message separator if there is a transient message just before it while composer is not focused [REQUIRE FOCUS]", + async function (assert) { + // The goal of removing the focus is to ensure the thread is not marked as seen automatically. + // Indeed that would trigger set_last_seen_message no matter what, which is already covered by other tests. + // The goal of this test is to cover the conditions specific to transient messages, + // and the conditions from focus would otherwise shadow them. + assert.expect(3); + + const pyEnv = await startServer(); + // Needed partner & user to allow simulation of message reception + const resPartnerId1 = pyEnv["res.partner"].create({ + name: "Foreigner partner", + }); + const resUsersId1 = pyEnv["res.users"].create({ + name: "Foreigner user", + partner_id: resPartnerId1, + }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + uuid: "channel20uuid", + }); + const { click, insertText, messaging, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + // send a command that leads to receiving a transient message + await insertText(".o_ComposerTextInputView_textarea", "/who"); + await click(".o_ComposerView_buttonSend"); + const transientMessage = + messaging.discuss.threadViewer.threadView.messageListView + .messageListViewItems[0].message; + + // composer is focused by default, we remove that focus + document.querySelector(".o_ComposerTextInputView_textarea").blur(); + // simulate receiving a message + await afterNextRender(() => + messaging.rpc({ + route: "/mail/chat_post", + params: { + context: { + mockedUserId: resUsersId1, + }, + message_content: "test", + uuid: "channel20uuid", + }, + }) + ); + assert.containsN( + document.body, + ".o_MessageView", + 2, + "should display 2 messages (the transient & the received message), after posting a command" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_separatorNewMessages", + "separator should be shown as a message has been received" + ); + assert.containsOnce( + document.body, + `.o_MessageView[data-message-id="${transientMessage.id}"] + .o_MessageListView_separatorNewMessages`, + "separator should be shown just after transient message" + ); + } + ); + + QUnit.test( + "composer should be focused automatically after clicking on the send button [REQUIRE FOCUS]", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({}); + const { click, insertText, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + await insertText(".o_ComposerTextInputView_textarea", "Dummy Message"); + await click(".o_ComposerView_buttonSend"); + assert.hasClass( + document.querySelector(".o_ComposerView"), + "o-focused", + "composer should be focused automatically after clicking on the send button" + ); + } + ); + + QUnit.test("failure on loading messages should display error", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + return Promise.reject(); + } + }, + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + + assert.containsOnce( + document.body, + ".o_ThreadView_loadingFailed", + "should show loading error message" + ); + }); + + QUnit.test( + "failure on loading messages should prompt retry button", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + return Promise.reject(); + } + }, + }); + await openDiscuss({ waitUntilMessagesLoaded: false }); + + assert.containsOnce( + document.body, + ".o_ThreadView_loadingFailedRetryButton", + "should show a button to allow user to retry loading" + ); + } + ); + + QUnit.test( + "failure on loading more messages should not alter message list display", + async function (assert) { + assert.expect(1); + + // first call needs to be successful as it is the initial loading of messages + // second call comes from load more and needs to fail in order to show the error alert + // any later call should work so that retry button and load more clicks would now work + let messageFetchShouldFail = false; + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + pyEnv["mail.message"].create( + [...Array(60).keys()].map(() => { + return { + body: "coucou", + model: "mail.channel", + res_id: mailChannelId1, + }; + }) + ); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + if (messageFetchShouldFail) { + throw new Error(); + } + } + }, + }); + await openDiscuss(); + + messageFetchShouldFail = true; + await click(".o_MessageListView_loadMore"); + assert.containsN( + document.body, + ".o_MessageView", + 30, + "should still show 30 messages as load more has failed" + ); + } + ); + + QUnit.test( + "failure on loading more messages should display error and prompt retry button", + async function (assert) { + assert.expect(3); + + // first call needs to be successful as it is the initial loading of messages + // second call comes from load more and needs to fail in order to show the error alert + // any later call should work so that retry button and load more clicks would now work + let messageFetchShouldFail = false; + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + pyEnv["mail.message"].create( + [...Array(60).keys()].map(() => { + return { + body: "coucou", + model: "mail.channel", + res_id: mailChannelId1, + }; + }) + ); + const { click, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + if (messageFetchShouldFail) { + throw new Error(); + } + } + }, + }); + await openDiscuss(); + + messageFetchShouldFail = true; + await click(".o_MessageListView_loadMore"); + assert.containsOnce( + document.body, + ".o_MessageListView_alertLoadingFailed", + "should show loading error message" + ); + assert.containsOnce( + document.body, + ".o_MessageListView_alertLoadingFailedRetryButton", + "should show loading error message button" + ); + assert.containsNone( + document.body, + ".o_MessageListView_loadMore", + "should not show load more buttton" + ); + } + ); + + QUnit.test( + "Retry loading more messages on failed load more messages should load more messages", + async function (assert) { + assert.expect(0); + + // first call needs to be successful as it is the initial loading of messages + // second call comes from load more and needs to fail in order to show the error alert + // any later call should work so that retry button and load more clicks would now work + let messageFetchShouldFail = false; + const pyEnv = await startServer(); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + pyEnv["mail.message"].create( + [...Array(90).keys()].map(() => { + return { + body: "coucou", + model: "mail.channel", + res_id: mailChannelId1, + }; + }) + ); + const { afterEvent, click, openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + async mockRPC(route, args) { + if (route === "/mail/channel/messages") { + if (messageFetchShouldFail) { + return Promise.reject(); + } + } + }, + }); + await openDiscuss(); + messageFetchShouldFail = true; + await click(".o_MessageListView_loadMore"); + + messageFetchShouldFail = false; + await afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => + document + .querySelector(".o_MessageListView_alertLoadingFailedRetryButton") + .click(), + message: + "should wait until channel loaded more messages after clicked on load more", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "more-messages-loaded" && + threadViewer.thread.model === "mail.channel" && + threadViewer.thread.id === mailChannelId1 + ); + }, + }); + } + ); + + QUnit.test( + "highlight the message mentioning the current user inside the channel", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "Test Partner", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + pyEnv["mail.message"].create({ + author_id: resPartnerId1, + body: "hello @Admin", + model: "mail.channel", + partner_ids: [pyEnv.currentPartnerId], + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.hasClass( + document.querySelector(`.o_MessageListView .o_MessageView`), + "o-highlighted", + "message should be highlighted" + ); + } + ); + + QUnit.test( + "not highlighting the message if not mentioning the current user inside the channel", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "testPartner", + email: "testPartner@odoo.com", + }); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_type: "channel", + name: "General", + }); + pyEnv["mail.message"].create({ + author_id: pyEnv.currentPartnerId, + body: "hello @testPartner", + model: "mail.channel", + partner_ids: [resPartnerId1], + res_id: mailChannelId1, + }); + const { openDiscuss } = await start({ + discuss: { + context: { active_id: mailChannelId1 }, + }, + }); + await openDiscuss(); + assert.doesNotHaveClass( + document.querySelector(`.o_MessageListView .o_MessageView`), + "o-highlighted", + "message should not be highlighted" + ); + } + ); }); }); - -QUnit.test("highlight the message mentioning the current user inside the channel", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "Test Partner", - }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - pyEnv['mail.message'].create({ - author_id: resPartnerId1, - body: "hello @Admin", - model: 'mail.channel', - partner_ids: [pyEnv.currentPartnerId], - res_id: mailChannelId1, - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.hasClass( - document.querySelector(`.o_MessageListView .o_MessageView`), - 'o-highlighted', - "message should be highlighted" - ); -}); - -QUnit.test("not highlighting the message if not mentioning the current user inside the channel", async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "testPartner", - email: "testPartner@odoo.com", - }); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_type: 'channel', - name: "General", - }); - pyEnv['mail.message'].create({ - author_id: pyEnv.currentPartnerId, - body: "hello @testPartner", - model: 'mail.channel', - partner_ids: [resPartnerId1], - res_id: mailChannelId1, - }); - const { openDiscuss } = await start({ - discuss: { - context: { active_id: mailChannelId1 }, - }, - }); - await openDiscuss(); - assert.doesNotHaveClass( - document.querySelector(`.o_MessageListView .o_MessageView`), - 'o-highlighted', - "message should not be highlighted" - ); -}); - -}); -}); diff --git a/addons/mail/static/tests/qunit_suite_tests/debug_manager_tests.js b/addons/mail/static/tests/qunit_suite_tests/debug_manager_tests.js index 40b3f2db3d5..70cac40866a 100644 --- a/addons/mail/static/tests/qunit_suite_tests/debug_manager_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/debug_manager_tests.js @@ -1,8 +1,17 @@ /** @odoo-module **/ import { manageMessages } from "@mail/js/tools/debug_manager"; -import { click, getFixture, legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils"; -import { createWebClient, doAction, getActionManagerServerData } from "@web/../tests/webclient/helpers"; +import { + click, + getFixture, + legacyExtraNextTick, + patchWithCleanup, +} from "@web/../tests/helpers/utils"; +import { + createWebClient, + doAction, + getActionManagerServerData, +} from "@web/../tests/webclient/helpers"; import { registry } from "@web/core/registry"; QUnit.module("DebugMenu"); @@ -44,9 +53,7 @@ QUnit.test("Manage Messages", async function (assert) { await legacyExtraNextTick(); await click(target, ".o_debug_manager .dropdown-toggle"); - const dropdownItems = target.querySelectorAll( - ".o_debug_manager .dropdown-menu .dropdown-item" - ); + const dropdownItems = target.querySelectorAll(".o_debug_manager .dropdown-menu .dropdown-item"); assert.strictEqual(dropdownItems.length, 1); assert.strictEqual( dropdownItems[0].innerText.trim(), diff --git a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js index 995bccd7b8b..4688029d7a4 100644 --- a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_legacy_tests.js @@ -1,253 +1,311 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; -import { click, getFixture, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils"; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; +import { + click, + getFixture, + legacyExtraNextTick, + patchWithCleanup, + triggerHotkey, +} from "@web/../tests/helpers/utils"; import { registry } from "@web/core/registry"; import { makeLegacyCommandService } from "@web/legacy/utils"; -import core from 'web.core'; -import session from 'web.session'; +import core from "web.core"; +import session from "web.session"; import makeTestEnvironment from "web.test_env"; -import { dom, nextTick } from 'web.test_utils'; +import { dom, nextTick } from "web.test_utils"; let target; -QUnit.module('mail', {}, function () { - QUnit.module('M2XAvatarUserLegacy', { +QUnit.module("mail", {}, function () { + QUnit.module("M2XAvatarUserLegacy", { beforeEach() { target = getFixture(); }, }); - QUnit.test('many2many_avatar_user widget in form view', async function (assert) { + QUnit.test("many2many_avatar_user widget in form view", async function (assert) { assert.expect(2); const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 }); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] }); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "Partner 1" }); + const resUsersId1 = pyEnv["res.users"].create({ name: "Mario", partner_id: resPartnerId1 }); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_ids: [resUsersId1] }); const views = { - 'm2x.avatar.user,false,form': '
', + "m2x.avatar.user,false,form": + '
', }; const { openView } = await start({ serverData: { views }, }); await openView({ - res_model: 'm2x.avatar.user', + res_model: "m2x.avatar.user", res_id: m2xAvatarUserId1, - views: [[false, 'form']], + views: [[false, "form"]], }); - await dom.click(document.querySelector('.o_field_many2manytags.avatar .badge .o_m2m_avatar')); - assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened'); + await dom.click( + document.querySelector(".o_field_many2manytags.avatar .badge .o_m2m_avatar") + ); + assert.containsOnce(document.body, ".o_ChatWindow", "Chat window should be opened"); assert.strictEqual( - document.querySelector('.o_ChatWindowHeaderView_name').textContent, - 'Partner 1', - 'First chat window should be related to partner 1' + document.querySelector(".o_ChatWindowHeaderView_name").textContent, + "Partner 1", + "First chat window should be related to partner 1" ); }); - QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { - assert.expect(4); + QUnit.test( + 'many2one_avatar_user widget edited by the smart action "Assign to..."', + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const [resUsersId1] = pyEnv['res.users'].create( - [{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }], - ); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + const pyEnv = await startServer(); + const [resUsersId1] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Luigi" }, + { name: "Yoshi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_id: resUsersId1 }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); - const views = { - 'm2x.avatar.user,false,form': '
', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario") + const views = { + "m2x.avatar.user,false,form": + '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario"); - triggerHotkey("control+k") - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I") - assert.ok(idx >= 0); + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign to ...ALT + I"); + assert.ok(idx >= 0); - await click([...target.querySelectorAll(".o_command")][idx]) - await nextTick(); - assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [ - "Your Company, Mitchell Admin", - "Public user", - "Mario", - "Luigi", - "Yoshi", - ]) - await click(target, "#o_command_3") - await legacyExtraNextTick(); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi") - }); + await click([...target.querySelectorAll(".o_command")][idx]); + await nextTick(); + assert.deepEqual( + [...target.querySelectorAll(".o_command")].map((el) => el.textContent), + ["Your Company, Mitchell Admin", "Public user", "Mario", "Luigi", "Yoshi"] + ); + await click(target, "#o_command_3"); + await legacyExtraNextTick(); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi"); + } + ); - QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { - assert.expect(4); + QUnit.test( + 'many2one_avatar_user widget edited by the smart action "Assign to me"', + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{ name: "Mario" }, { name: "Luigi" }]); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - patchWithCleanup(session, { user_id: [resUsersId2] }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Luigi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_id: resUsersId1 }); + patchWithCleanup(session, { user_id: [resUsersId2] }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); - const views = { - 'm2x.avatar.user,false,form': '
', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario") - triggerHotkey("control+k") - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I") - assert.ok(idx >= 0); + const views = { + "m2x.avatar.user,false,form": + '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario"); + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign/unassign to meALT + SHIFT + I"); + assert.ok(idx >= 0); - // Assign me (Luigi) - triggerHotkey("alt+shift+i") - await legacyExtraNextTick(); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi") + // Assign me (Luigi) + triggerHotkey("alt+shift+i"); + await legacyExtraNextTick(); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi"); - // Unassign me - triggerHotkey("control+k"); - await nextTick(); - await click([...target.querySelectorAll(".o_command")][idx]) - await legacyExtraNextTick(); - assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "") - }); + // Unassign me + triggerHotkey("control+k"); + await nextTick(); + await click([...target.querySelectorAll(".o_command")][idx]); + await legacyExtraNextTick(); + assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, ""); + } + ); - QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { - assert.expect(4); + QUnit.test( + 'many2many_avatar_user widget edited by the smart action "Assign to..."', + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const [resUsersId1, resUsersId2] = pyEnv['res.users'].create( - [{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }], - ); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId2] }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Yoshi" }, + { name: "Luigi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ + user_ids: [resUsersId1, resUsersId2], + }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); - const views = { - 'm2x.avatar.user,false,form': '
', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi"]); + const views = { + "m2x.avatar.user,false,form": + '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); - triggerHotkey("control+k") - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I") - assert.ok(idx >= 0); + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign to ...ALT + I"); + assert.ok(idx >= 0); - await click([...target.querySelectorAll(".o_command")][idx]) - await nextTick(); - assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [ - "Your Company, Mitchell Admin", - "Public user", - "Luigi" - ]); + await click([...target.querySelectorAll(".o_command")][idx]); + await nextTick(); + assert.deepEqual( + [...target.querySelectorAll(".o_command")].map((el) => el.textContent), + ["Your Company, Mitchell Admin", "Public user", "Luigi"] + ); - await click(target, "#o_command_2"); - await legacyExtraNextTick(); - userNames = [...target.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent); - assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); - }); + await click(target, "#o_command_2"); + await legacyExtraNextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); + } + ); - QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { - assert.expect(4); + QUnit.test( + 'many2many_avatar_user widget edited by the smart action "Assign to me"', + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const [resUsersId1, resUsersId2, resUsersId3] = pyEnv['res.users'].create( - [{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }], - ); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId3] }); - patchWithCleanup(session, { user_id: [resUsersId2] }); - const legacyEnv = makeTestEnvironment({ bus: core.bus }); - const serviceRegistry = registry.category("services"); - serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2, resUsersId3] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Luigi" }, + { name: "Yoshi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ + user_ids: [resUsersId1, resUsersId3], + }); + patchWithCleanup(session, { user_id: [resUsersId2] }); + const legacyEnv = makeTestEnvironment({ bus: core.bus }); + const serviceRegistry = registry.category("services"); + serviceRegistry.add("legacy_command", makeLegacyCommandService(legacyEnv)); - const views = { - 'm2x.avatar.user,false,form': '
', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi"]); + const views = { + "m2x.avatar.user,false,form": + '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); - triggerHotkey("control+k"); - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I"); - assert.ok(idx >= 0); + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign/unassign to meALT + SHIFT + I"); + assert.ok(idx >= 0); - // Assign me (Luigi) - triggerHotkey("alt+shift+i"); - await legacyExtraNextTick(); - userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); + // Assign me (Luigi) + triggerHotkey("alt+shift+i"); + await legacyExtraNextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); - // Unassign me - triggerHotkey("control+k"); - await nextTick(); - await click([...target.querySelectorAll(".o_command")][idx]); - await legacyExtraNextTick(); - userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi"]); - }); + // Unassign me + triggerHotkey("control+k"); + await nextTick(); + await click([...target.querySelectorAll(".o_command")][idx]); + await legacyExtraNextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + } + ); - QUnit.test('avatar_user widget displays the appropriate user image in form view', async function (assert) { - assert.expect(1); + QUnit.test( + "avatar_user widget displays the appropriate user image in form view", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" }); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] }); - const views = { - 'm2x.avatar.user,false,form': '
', - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'm2x.avatar.user', - res_id: m2xAvatarUserId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelector('.o_field_many2manytags.avatar.o_field_widget .badge img').getAttribute('data-src'), - `/web/image/res.users/${resUsersId1}/avatar_128`, - 'Should have correct avatar image' - ); - }); + const pyEnv = await startServer(); + const resUsersId1 = pyEnv["res.users"].create({ name: "Mario" }); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_ids: [resUsersId1] }); + const views = { + "m2x.avatar.user,false,form": + '
', + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "m2x.avatar.user", + res_id: m2xAvatarUserId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document + .querySelector(".o_field_many2manytags.avatar.o_field_widget .badge img") + .getAttribute("data-src"), + `/web/image/res.users/${resUsersId1}/avatar_128`, + "Should have correct avatar image" + ); + } + ); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js index 3a3bf25a643..4a4b8603df3 100644 --- a/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/m2x_avatar_user_tests.js @@ -1,90 +1,96 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; import { click, getFixture, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils"; import { registry } from "@web/core/registry"; import { session } from "@web/session"; -import { dom, nextTick } from 'web.test_utils'; +import { dom, nextTick } from "web.test_utils"; import { popoverService } from "@web/core/popover/popover_service"; import { tooltipService } from "@web/core/tooltip/tooltip_service"; let target; -QUnit.module('mail', {}, function () { - QUnit.module('M2XAvatarUser', { +QUnit.module("mail", {}, function () { + QUnit.module("M2XAvatarUser", { beforeEach() { target = getFixture(); }, }); - QUnit.test('many2one_avatar_user widget in list view', async function (assert) { + QUnit.test("many2one_avatar_user widget in list view", async function (assert) { assert.expect(2); const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 }); - pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "Partner 1" }); + const resUsersId1 = pyEnv["res.users"].create({ name: "Mario", partner_id: resPartnerId1 }); + pyEnv["m2x.avatar.user"].create({ user_id: resUsersId1 }); const views = { - 'm2x.avatar.user,false,list': '', + "m2x.avatar.user,false,list": + '', }; const { openView } = await start({ serverData: { views }, }); await openView({ - res_model: 'm2x.avatar.user', + res_model: "m2x.avatar.user", views: [[false, "list"]], }); - await dom.click(document.querySelector('.o_data_cell .o_m2o_avatar > img')); - assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened'); + await dom.click(document.querySelector(".o_data_cell .o_m2o_avatar > img")); + assert.containsOnce(document.body, ".o_ChatWindow", "Chat window should be opened"); assert.strictEqual( - document.querySelector('.o_ChatWindowHeaderView_name').textContent, - 'Partner 1', - 'Chat window should be related to partner 1' + document.querySelector(".o_ChatWindowHeaderView_name").textContent, + "Partner 1", + "Chat window should be related to partner 1" ); }); - QUnit.test('many2many_avatar_user widget in form view', async function (assert) { + QUnit.test("many2many_avatar_user widget in form view", async function (assert) { assert.expect(2); const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: 'Partner 1' }); - const resUsersId1 = pyEnv['res.users'].create({ name: "Mario", partner_id: resPartnerId1 }); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] }); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "Partner 1" }); + const resUsersId1 = pyEnv["res.users"].create({ name: "Mario", partner_id: resPartnerId1 }); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_ids: [resUsersId1] }); const views = { - 'm2x.avatar.user,false,form': '
', + "m2x.avatar.user,false,form": + '
', }; const { openView } = await start({ serverData: { views }, }); await openView({ - res_model: 'm2x.avatar.user', + res_model: "m2x.avatar.user", res_id: m2xAvatarUserId1, - views: [[false, 'form']], + views: [[false, "form"]], }); - await dom.click(document.querySelector('.o_field_many2many_avatar_user .badge .o_m2m_avatar')); - assert.containsOnce(document.body, '.o_ChatWindow', 'Chat window should be opened'); + await dom.click( + document.querySelector(".o_field_many2many_avatar_user .badge .o_m2m_avatar") + ); + assert.containsOnce(document.body, ".o_ChatWindow", "Chat window should be opened"); assert.strictEqual( - document.querySelector('.o_ChatWindowHeaderView_name').textContent, - 'Partner 1', - 'First chat window should be related to partner 1' + document.querySelector(".o_ChatWindowHeaderView_name").textContent, + "Partner 1", + "First chat window should be related to partner 1" ); }); - QUnit.test('many2many_avatar_user in kanban view', async function (assert) { + QUnit.test("many2many_avatar_user in kanban view", async function (assert) { assert.expect(5); const pyEnv = await startServer(); - const resUsersIds = pyEnv['res.users'].create( - [{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }, { name: "Tapu" }], - ); - pyEnv['m2x.avatar.user'].create({ user_ids: resUsersIds }); + const resUsersIds = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Yoshi" }, + { name: "Luigi" }, + { name: "Tapu" }, + ]); + pyEnv["m2x.avatar.user"].create({ user_ids: resUsersIds }); registry.category("services").add("popover", popoverService); registry.category("services").add("tooltip", tooltipService); const views = { - 'm2x.avatar.user,false,kanban': - ` + "m2x.avatar.user,false,kanban": `
@@ -105,225 +111,311 @@ QUnit.module('mail', {}, function () { serverData: { views }, }); await openView({ - res_model: 'm2x.avatar.user', - views: [[false, 'kanban']], + res_model: "m2x.avatar.user", + views: [[false, "kanban"]], }); - assert.containsOnce(document.body, '.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty', - "should have o_m2m_avatar_empty span"); - assert.strictEqual(document.querySelector('.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty').innerText.trim(), "+2", - "should have +2 in o_m2m_avatar_empty"); - - document.querySelector('.o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty').dispatchEvent(new Event('mouseenter')); - await nextTick(); - assert.containsOnce(document.body, '.popover', - "should open a popover hover on o_m2m_avatar_empty"); - assert.strictEqual(document.querySelector('.popover .o-tooltip > div').innerText.trim(), 'Luigi', 'should have a right text in popover'); - assert.strictEqual(document.querySelectorAll('.popover .o-tooltip > div')[1].innerText.trim(), 'Tapu', 'should have a right text in popover'); - }); - - QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const [resUsersId1] = pyEnv['res.users'].create( - [{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }], + assert.containsOnce( + document.body, + ".o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty", + "should have o_m2m_avatar_empty span" ); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - - const views = { - 'm2x.avatar.user,false,form': '
', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user input").value, "Mario") - - triggerHotkey("control+k") - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I") - assert.ok(idx >= 0); - - await click([...target.querySelectorAll(".o_command")][idx]) - await nextTick(); - assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [ - "Your Company, Mitchell Admin", - "Public user", - "Mario", - "Luigi", - "Yoshi", - ]) - await click(target, "#o_command_3") - await nextTick(); - assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user input").value, "Luigi") - }); - - QUnit.test('many2one_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const [resUsersId1, resUsersId2] = pyEnv['res.users'].create([{ name: "Mario" }, { name: "Luigi" }]); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - patchWithCleanup(session, { uid: resUsersId2, name: "Luigi" }); - - const views = { - 'm2x.avatar.user,false,form': '
', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user input").value, "Mario") - triggerHotkey("control+k") - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/Unassign to meALT + SHIFT + I") - assert.ok(idx >= 0); - - // Assign me (Luigi) - triggerHotkey("alt+shift+i") - await nextTick(); - assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user input").value, "Luigi") - - // Unassign me - triggerHotkey("control+k"); - await nextTick(); - await click([...target.querySelectorAll(".o_command")][idx]) - await nextTick(); - assert.strictEqual(target.querySelector(".o_field_many2one_avatar_user input").value, "") - }); - - QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to..."', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const [resUsersId1, resUsersId2] = pyEnv['res.users'].create( - [{ name: "Mario" }, { name: "Yoshi" }, { name: "Luigi" }], - ); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId2] }); - - const views = { - 'm2x.avatar.user,false,form': '
', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi"]); - - triggerHotkey("control+k") - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I") - assert.ok(idx >= 0); - - await click([...target.querySelectorAll(".o_command")][idx]) - await nextTick(); - assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [ - "Your Company, Mitchell Admin", - "Public user", - "Luigi" - ]); - - await click(target, "#o_command_2"); - await nextTick(); - userNames = [...target.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent); - assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); - }); - - QUnit.test('many2many_avatar_user widget edited by the smart action "Assign to me"', async function (assert) { - assert.expect(4); - - const pyEnv = await startServer(); - const [resUsersId1, resUsersId2, resUsersId3] = pyEnv['res.users'].create( - [{ name: "Mario" }, { name: "Luigi" }, { name: "Yoshi" }], - ); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1, resUsersId3] }); - patchWithCleanup(session, { uid: resUsersId2, name: "Luigi" }); - - const views = { - 'm2x.avatar.user,false,form': '
', - }; - const { openView } = await start({ serverData: { views } }); - await openView({ - res_id: m2xAvatarUserId1, - type: 'ir.actions.act_window', - target: 'current', - res_model: 'm2x.avatar.user', - 'view_mode': 'form', - 'views': [[false, 'form']], - }); - let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi"]); - - triggerHotkey("control+k"); - await nextTick(); - const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/Unassign to meALT + SHIFT + I"); - assert.ok(idx >= 0); - - // Assign me (Luigi) - triggerHotkey("alt+shift+i"); - await nextTick(); - userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); - - // Unassign me - triggerHotkey("control+k"); - await nextTick(); - await click([...target.querySelectorAll(".o_command")][idx]); - await nextTick(); - userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent)); - assert.deepEqual(userNames, ["Mario", "Yoshi"]); - }); - - QUnit.test('avatar_user widget displays the appropriate user image in list view', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" }); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - const views = { - 'm2x.avatar.user,false,list': '', - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'm2x.avatar.user', - res_id: m2xAvatarUserId1, - views: [[false, 'list']], - }); assert.strictEqual( - document.querySelector('.o_m2o_avatar > img').getAttribute('data-src'), - `/web/image/res.users/${resUsersId1}/avatar_128`, - 'Should have correct avatar image' + document + .querySelector( + ".o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty" + ) + .innerText.trim(), + "+2", + "should have +2 in o_m2m_avatar_empty" + ); + + document + .querySelector(".o_kanban_record .o_field_many2many_avatar_user .o_m2m_avatar_empty") + .dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce( + document.body, + ".popover", + "should open a popover hover on o_m2m_avatar_empty" + ); + assert.strictEqual( + document.querySelector(".popover .o-tooltip > div").innerText.trim(), + "Luigi", + "should have a right text in popover" + ); + assert.strictEqual( + document.querySelectorAll(".popover .o-tooltip > div")[1].innerText.trim(), + "Tapu", + "should have a right text in popover" ); }); - QUnit.test('avatar_user widget displays the appropriate user image in kanban view', async function (assert) { - assert.expect(1); + QUnit.test( + 'many2one_avatar_user widget edited by the smart action "Assign to..."', + async function (assert) { + assert.expect(4); - const pyEnv = await startServer(); - const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" }); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_id: resUsersId1 }); - const views = { - 'm2x.avatar.user,false,kanban': - ` + const pyEnv = await startServer(); + const [resUsersId1] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Luigi" }, + { name: "Yoshi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_id: resUsersId1 }); + + const views = { + "m2x.avatar.user,false,form": + '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + assert.strictEqual( + target.querySelector(".o_field_many2one_avatar_user input").value, + "Mario" + ); + + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign to ...ALT + I"); + assert.ok(idx >= 0); + + await click([...target.querySelectorAll(".o_command")][idx]); + await nextTick(); + assert.deepEqual( + [...target.querySelectorAll(".o_command")].map((el) => el.textContent), + ["Your Company, Mitchell Admin", "Public user", "Mario", "Luigi", "Yoshi"] + ); + await click(target, "#o_command_3"); + await nextTick(); + assert.strictEqual( + target.querySelector(".o_field_many2one_avatar_user input").value, + "Luigi" + ); + } + ); + + QUnit.test( + 'many2one_avatar_user widget edited by the smart action "Assign to me"', + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Luigi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_id: resUsersId1 }); + patchWithCleanup(session, { uid: resUsersId2, name: "Luigi" }); + + const views = { + "m2x.avatar.user,false,form": + '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + assert.strictEqual( + target.querySelector(".o_field_many2one_avatar_user input").value, + "Mario" + ); + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign/Unassign to meALT + SHIFT + I"); + assert.ok(idx >= 0); + + // Assign me (Luigi) + triggerHotkey("alt+shift+i"); + await nextTick(); + assert.strictEqual( + target.querySelector(".o_field_many2one_avatar_user input").value, + "Luigi" + ); + + // Unassign me + triggerHotkey("control+k"); + await nextTick(); + await click([...target.querySelectorAll(".o_command")][idx]); + await nextTick(); + assert.strictEqual( + target.querySelector(".o_field_many2one_avatar_user input").value, + "" + ); + } + ); + + QUnit.test( + 'many2many_avatar_user widget edited by the smart action "Assign to..."', + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Yoshi" }, + { name: "Luigi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ + user_ids: [resUsersId1, resUsersId2], + }); + + const views = { + "m2x.avatar.user,false,form": + '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign to ...ALT + I"); + assert.ok(idx >= 0); + + await click([...target.querySelectorAll(".o_command")][idx]); + await nextTick(); + assert.deepEqual( + [...target.querySelectorAll(".o_command")].map((el) => el.textContent), + ["Your Company, Mitchell Admin", "Public user", "Luigi"] + ); + + await click(target, "#o_command_2"); + await nextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); + } + ); + + QUnit.test( + 'many2many_avatar_user widget edited by the smart action "Assign to me"', + async function (assert) { + assert.expect(4); + + const pyEnv = await startServer(); + const [resUsersId1, resUsersId2, resUsersId3] = pyEnv["res.users"].create([ + { name: "Mario" }, + { name: "Luigi" }, + { name: "Yoshi" }, + ]); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ + user_ids: [resUsersId1, resUsersId3], + }); + patchWithCleanup(session, { uid: resUsersId2, name: "Luigi" }); + + const views = { + "m2x.avatar.user,false,form": + '
', + }; + const { openView } = await start({ serverData: { views } }); + await openView({ + res_id: m2xAvatarUserId1, + type: "ir.actions.act_window", + target: "current", + res_model: "m2x.avatar.user", + view_mode: "form", + views: [[false, "form"]], + }); + let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + + triggerHotkey("control+k"); + await nextTick(); + const idx = [...target.querySelectorAll(".o_command")] + .map((el) => el.textContent) + .indexOf("Assign/Unassign to meALT + SHIFT + I"); + assert.ok(idx >= 0); + + // Assign me (Luigi) + triggerHotkey("alt+shift+i"); + await nextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]); + + // Unassign me + triggerHotkey("control+k"); + await nextTick(); + await click([...target.querySelectorAll(".o_command")][idx]); + await nextTick(); + userNames = [...target.querySelectorAll(".o_tag_badge_text")].map( + (el) => el.textContent + ); + assert.deepEqual(userNames, ["Mario", "Yoshi"]); + } + ); + + QUnit.test( + "avatar_user widget displays the appropriate user image in list view", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resUsersId1 = pyEnv["res.users"].create({ name: "Mario" }); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_id: resUsersId1 }); + const views = { + "m2x.avatar.user,false,list": + '', + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "m2x.avatar.user", + res_id: m2xAvatarUserId1, + views: [[false, "list"]], + }); + assert.strictEqual( + document.querySelector(".o_m2o_avatar > img").getAttribute("data-src"), + `/web/image/res.users/${resUsersId1}/avatar_128`, + "Should have correct avatar image" + ); + } + ); + + QUnit.test( + "avatar_user widget displays the appropriate user image in kanban view", + async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resUsersId1 = pyEnv["res.users"].create({ name: "Mario" }); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_id: resUsersId1 }); + const views = { + "m2x.avatar.user,false,kanban": `
@@ -332,43 +424,50 @@ QUnit.module('mail', {}, function () { `, - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'm2x.avatar.user', - res_id: m2xAvatarUserId1, - views: [[false, 'kanban']], - }); - assert.strictEqual( - document.querySelector('.o_m2o_avatar > img').getAttribute('data-src'), - `/web/image/res.users/${resUsersId1}/avatar_128`, - 'Should have correct avatar image' - ); - }); + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "m2x.avatar.user", + res_id: m2xAvatarUserId1, + views: [[false, "kanban"]], + }); + assert.strictEqual( + document.querySelector(".o_m2o_avatar > img").getAttribute("data-src"), + `/web/image/res.users/${resUsersId1}/avatar_128`, + "Should have correct avatar image" + ); + } + ); - QUnit.test('avatar_user widget displays the appropriate user image in form view', async function (assert) { - assert.expect(1); + QUnit.test( + "avatar_user widget displays the appropriate user image in form view", + async function (assert) { + assert.expect(1); - const pyEnv = await startServer(); - const resUsersId1 = pyEnv['res.users'].create({ name: "Mario" }); - const m2xAvatarUserId1 = pyEnv['m2x.avatar.user'].create({ user_ids: [resUsersId1] }); - const views = { - 'm2x.avatar.user,false,form': '
', - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'm2x.avatar.user', - res_id: m2xAvatarUserId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelector('.o_field_many2many_avatar_user.o_field_widget .badge img').getAttribute('data-src'), - `/web/image/res.users/${resUsersId1}/avatar_128`, - 'Should have correct avatar image' - ); - }); + const pyEnv = await startServer(); + const resUsersId1 = pyEnv["res.users"].create({ name: "Mario" }); + const m2xAvatarUserId1 = pyEnv["m2x.avatar.user"].create({ user_ids: [resUsersId1] }); + const views = { + "m2x.avatar.user,false,form": + '
', + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "m2x.avatar.user", + res_id: m2xAvatarUserId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document + .querySelector(".o_field_many2many_avatar_user.o_field_widget .badge img") + .getAttribute("data-src"), + `/web/image/res.users/${resUsersId1}/avatar_128`, + "Should have correct avatar image" + ); + } + ); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/clear_tests.js b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/clear_tests.js index 4bf648966d6..7b56728ec1d 100644 --- a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/clear_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/clear_tests.js @@ -1,123 +1,130 @@ /** @odoo-module **/ -import { clear } from '@mail/model'; -import { start } from '@mail/../tests/helpers/test_utils'; +import { clear } from "@mail/model"; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('model_field_commands', {}, function () { -QUnit.module('clear_tests.js'); -QUnit.test('clear: should set attribute field undefined if there is no default value', async function (assert) { - assert.expect(1); +QUnit.module("mail", {}, function () { + QUnit.module("model_field_commands", {}, function () { + QUnit.module("clear_tests.js"); + QUnit.test( + "clear: should set attribute field undefined if there is no default value", + async function (assert) { + assert.expect(1); - const { messaging } = await start(); - const task = messaging.models['TestTask'].insert({ - id: 1, - title: 'test title 1', + const { messaging } = await start(); + const task = messaging.models["TestTask"].insert({ + id: 1, + title: "test title 1", + }); + task.update({ title: clear() }); + assert.strictEqual( + task.title, + undefined, + "clear: should set attribute field undefined if there is no default value" + ); + } + ); + + QUnit.test("clear: should set attribute field the default value", async function (assert) { + assert.expect(1); + + const { messaging } = await start(); + const task = messaging.models["TestTask"].insert({ + id: 1, + difficulty: 5, + }); + task.update({ difficulty: clear() }); + assert.strictEqual( + task.difficulty, + 1, + "clear: should set attribute field the default value" + ); + }); + + QUnit.test( + "clear: should set x2one field undefined if no default value is given", + async function (assert) { + assert.expect(2); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + address: { id: 20 }, + }); + const address = messaging.models["TestAddress"].findFromIdentifyingData({ id: 20 }); + contact.update({ address: clear() }); + assert.strictEqual( + contact.address, + undefined, + "clear: should set x2one field undefined" + ); + assert.strictEqual( + address.contact, + undefined, + "the inverse relation should be cleared as well" + ); + } + ); + + QUnit.test("clear: should set x2one field the default value", async function (assert) { + assert.expect(1); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + favorite: { description: "pingpong" }, + id: 10, + }); + contact.update({ favorite: clear() }); + assert.strictEqual( + contact.favorite.description, + "football", + "clear: should set x2one field default value" + ); + }); + + QUnit.test( + "clear: should set x2many field empty array if no default value is given", + async function (assert) { + assert.expect(2); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + tasks: { id: 20 }, + }); + const task = messaging.models["TestTask"].findFromIdentifyingData({ id: 20 }); + contact.update({ tasks: clear() }); + assert.ok( + contact.tasks instanceof Array && contact.tasks.length === 0, + "clear: should set x2many field empty array" + ); + assert.strictEqual( + task.responsible, + undefined, + "the inverse relation should be cleared as well" + ); + } + ); + + QUnit.test("clear: should set x2many field the default value", async function (assert) { + assert.expect(1); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + hobbies: [ + { description: "basketball" }, + { description: "running" }, + { description: "photographing" }, + ], + }); + contact.update({ hobbies: clear() }); + const hobbyDescriptions = contact.hobbies.map((h) => h.description); + assert.deepEqual( + hobbyDescriptions, + ["hiking", "fishing"], + "clear: should set x2many field the default value" + ); + }); }); - task.update({ title: clear() }); - assert.strictEqual( - task.title, - undefined, - 'clear: should set attribute field undefined if there is no default value' - ); -}); - -QUnit.test('clear: should set attribute field the default value', async function (assert) { - assert.expect(1); - - const { messaging } = await start(); - const task = messaging.models['TestTask'].insert({ - id: 1, - difficulty: 5, - }); - task.update({ difficulty: clear() }); - assert.strictEqual( - task.difficulty, - 1, - 'clear: should set attribute field the default value' - ); -}); - -QUnit.test('clear: should set x2one field undefined if no default value is given', async function (assert) { - assert.expect(2); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - address: { id: 20 }, - }); - const address = messaging.models['TestAddress'].findFromIdentifyingData({ id: 20 }); - contact.update({ address: clear() }); - assert.strictEqual( - contact.address, - undefined, - 'clear: should set x2one field undefined' - ); - assert.strictEqual( - address.contact, - undefined, - 'the inverse relation should be cleared as well' - ); -}); - -QUnit.test('clear: should set x2one field the default value', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - favorite: { description: 'pingpong' }, - id: 10, - }); - contact.update({ favorite: clear() }); - assert.strictEqual( - contact.favorite.description, - 'football', - 'clear: should set x2one field default value' - ); -}); - -QUnit.test('clear: should set x2many field empty array if no default value is given', async function (assert) { - assert.expect(2); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - tasks: { id: 20 }, - }); - const task = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 }); - contact.update({ tasks: clear() }); - assert.ok( - contact.tasks instanceof Array && - contact.tasks.length === 0, - 'clear: should set x2many field empty array' - ); - assert.strictEqual( - task.responsible, - undefined, - 'the inverse relation should be cleared as well' - ); -}); - -QUnit.test('clear: should set x2many field the default value', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - hobbies: [ - { description: 'basketball' }, - { description: 'running' }, - { description: 'photographing' }, - ], - }); - contact.update({ hobbies: clear() }); - const hobbyDescriptions = contact.hobbies.map(h => h.description); - assert.deepEqual( - hobbyDescriptions, - ['hiking', 'fishing'], - 'clear: should set x2many field the default value', - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/insert_and_replace_tests.js b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/insert_and_replace_tests.js index f68bab4fe36..fa11a13c77b 100644 --- a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/insert_and_replace_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/insert_and_replace_tests.js @@ -1,161 +1,165 @@ /** @odoo-module **/ -import { start } from '@mail/../tests/helpers/test_utils'; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('model_field_commands', {}, function () { -QUnit.module('insert_and_replace_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("model_field_commands", {}, function () { + QUnit.module("insert_and_replace_tests.js"); -QUnit.test('insertAndReplace: should create and link a new record for an empty x2one field', async function (assert) { - assert.expect(2); - const { messaging } = await start(); + QUnit.test( + "insertAndReplace: should create and link a new record for an empty x2one field", + async function (assert) { + assert.expect(2); + const { messaging } = await start(); - const contact = messaging.models['TestContact'].insert({ id: 10 }); - contact.update({ address: { id: 10 } }); - const address = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 }); - assert.strictEqual( - contact.address, - address, - 'insertAndReplace: should create and link a record for an empty x2one field' - ); - assert.strictEqual( - address.contact, - contact, - 'the inverse relation should be set as well' - ); -}); + const contact = messaging.models["TestContact"].insert({ id: 10 }); + contact.update({ address: { id: 10 } }); + const address = messaging.models["TestAddress"].findFromIdentifyingData({ id: 10 }); + assert.strictEqual( + contact.address, + address, + "insertAndReplace: should create and link a record for an empty x2one field" + ); + assert.strictEqual( + address.contact, + contact, + "the inverse relation should be set as well" + ); + } + ); -QUnit.test('insertAndReplace: should create and replace a new record for a non-empty x2one field', async function (assert) { - assert.expect(3); - const { messaging } = await start(); + QUnit.test( + "insertAndReplace: should create and replace a new record for a non-empty x2one field", + async function (assert) { + assert.expect(3); + const { messaging } = await start(); - const contact = messaging.models['TestContact'].insert({ - id: 10, - address: { id: 10 }, + const contact = messaging.models["TestContact"].insert({ + id: 10, + address: { id: 10 }, + }); + const address10 = messaging.models["TestAddress"].findFromIdentifyingData({ + id: 10, + }); + contact.update({ address: { id: 20 } }); + const address20 = messaging.models["TestAddress"].findFromIdentifyingData({ + id: 20, + }); + assert.strictEqual( + contact.address, + address20, + "insertAndReplace: should create and replace a new record for a non-empty x2one field" + ); + assert.strictEqual( + address20.contact, + contact, + "the inverse relation should be set as well" + ); + assert.strictEqual( + address10.contact, + undefined, + "the original relation should be dropped" + ); + } + ); + + QUnit.test( + "insertAndReplace: should update the existing record for an x2one field", + async function (assert) { + assert.expect(2); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + address: { + id: 10, + addressInfo: "address 10", + }, + }); + const address10 = messaging.models["TestAddress"].findFromIdentifyingData({ + id: 10, + }); + contact.update({ + address: { + id: 10, + addressInfo: "address 10 updated", + }, + }); + assert.strictEqual( + contact.address, + address10, + "insertAndReplace: should not drop an existing record" + ); + assert.strictEqual( + address10.addressInfo, + "address 10 updated", + "insertAndReplace: should update the existing record for a x2one field" + ); + } + ); + + QUnit.test( + "insertAndReplace: should create and replace the records for an x2many field", + async function (assert) { + assert.expect(4); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + tasks: { id: 10 }, + }); + const task10 = messaging.models["TestTask"].findFromIdentifyingData({ id: 10 }); + contact.update({ tasks: { id: 20 } }); + const task20 = messaging.models["TestTask"].findFromIdentifyingData({ id: 20 }); + assert.strictEqual(contact.tasks.length, 1, "should have 1 record"); + assert.strictEqual( + contact.tasks[0], + task20, + "task should be replaced by the new record" + ); + assert.strictEqual( + task20.responsible, + contact, + "the inverse relation should be set" + ); + assert.strictEqual( + task10.responsible, + undefined, + "the original relation should be dropped" + ); + } + ); + + QUnit.test( + "insertAndReplace: should update and replace the records for an x2many field", + async function (assert) { + assert.expect(4); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + tasks: [ + { id: 10, title: "task 10" }, + { id: 20, title: "task 20" }, + ], + }); + const task10 = messaging.models["TestTask"].findFromIdentifyingData({ id: 10 }); + const task20 = messaging.models["TestTask"].findFromIdentifyingData({ id: 20 }); + contact.update({ + tasks: { + id: 10, + title: "task 10 updated", + }, + }); + assert.strictEqual(contact.tasks.length, 1, "should have 1 record"); + assert.strictEqual( + contact.tasks[0], + task10, + "tasks should be replaced by new record" + ); + assert.strictEqual(task10.title, "task 10 updated", "the record should be updated"); + assert.strictEqual(task20.responsible, undefined, "the record should be replaced"); + } + ); }); - const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 }); - contact.update({ address: { id: 20 } }); - const address20 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 20 }); - assert.strictEqual( - contact.address, - address20, - 'insertAndReplace: should create and replace a new record for a non-empty x2one field' - ); - assert.strictEqual( - address20.contact, - contact, - 'the inverse relation should be set as well' - ); - assert.strictEqual( - address10.contact, - undefined, - 'the original relation should be dropped' - ); -}); - -QUnit.test('insertAndReplace: should update the existing record for an x2one field', async function (assert) { - assert.expect(2); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - address: { - id: 10, - addressInfo: 'address 10', - }, - }); - const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 }); - contact.update({ - address: { - id: 10, - addressInfo: 'address 10 updated', - }, - }); - assert.strictEqual( - contact.address, - address10, - 'insertAndReplace: should not drop an existing record' - ); - assert.strictEqual( - address10.addressInfo, - 'address 10 updated', - 'insertAndReplace: should update the existing record for a x2one field' - ); -}); - -QUnit.test('insertAndReplace: should create and replace the records for an x2many field', async function (assert) { - assert.expect(4); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - tasks: { id: 10 }, - }); - const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 }); - contact.update({ tasks: { id: 20 } }); - const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 }); - assert.strictEqual( - contact.tasks.length, - 1, - "should have 1 record" - ); - assert.strictEqual( - contact.tasks[0], - task20, - 'task should be replaced by the new record' - ); - assert.strictEqual( - task20.responsible, - contact, - 'the inverse relation should be set' - ); - assert.strictEqual( - task10.responsible, - undefined, - 'the original relation should be dropped' - ); -}); - -QUnit.test('insertAndReplace: should update and replace the records for an x2many field', async function (assert) { - assert.expect(4); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - tasks: [ - { id: 10, title: 'task 10' }, - { id: 20, title: 'task 20' }, - ], - }); - const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 }); - const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 }); - contact.update({ - tasks: { - id: 10, - title: 'task 10 updated', - }, - }); - assert.strictEqual( - contact.tasks.length, - 1, - "should have 1 record" - ); - assert.strictEqual( - contact.tasks[0], - task10, - 'tasks should be replaced by new record' - ); - assert.strictEqual( - task10.title, - 'task 10 updated', - 'the record should be updated' - ); - assert.strictEqual( - task20.responsible, - undefined, - 'the record should be replaced' - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/insert_tests.js b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/insert_tests.js index cfdcda2fb7f..0eae5cb6d29 100644 --- a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/insert_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/insert_tests.js @@ -1,180 +1,175 @@ /** @odoo-module **/ -import { insert } from '@mail/model'; -import { start } from '@mail/../tests/helpers/test_utils'; +import { insert } from "@mail/model"; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('model_field_commands', {}, function () { -QUnit.module('insert_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("model_field_commands", {}, function () { + QUnit.module("insert_tests.js"); -QUnit.test('insert: should create and link a new record for an empty x2one field', async function (assert) { - assert.expect(2); - const { messaging } = await start(); + QUnit.test( + "insert: should create and link a new record for an empty x2one field", + async function (assert) { + assert.expect(2); + const { messaging } = await start(); - const contact = messaging.models['TestContact'].insert({ id: 10 }); - contact.update({ address: insert({ id: 10 }) }); - const address = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 }); - assert.strictEqual( - contact.address, - address, - 'insert: should create and link a record for an empty x2one field' - ); - assert.strictEqual( - address.contact, - contact, - 'the inverse relation should be set as well' - ); -}); + const contact = messaging.models["TestContact"].insert({ id: 10 }); + contact.update({ address: insert({ id: 10 }) }); + const address = messaging.models["TestAddress"].findFromIdentifyingData({ id: 10 }); + assert.strictEqual( + contact.address, + address, + "insert: should create and link a record for an empty x2one field" + ); + assert.strictEqual( + address.contact, + contact, + "the inverse relation should be set as well" + ); + } + ); -QUnit.test('insert: should create and replace a new record for a non-empty x2one field', async function (assert) { - assert.expect(3); - const { messaging } = await start(); + QUnit.test( + "insert: should create and replace a new record for a non-empty x2one field", + async function (assert) { + assert.expect(3); + const { messaging } = await start(); - const contact = messaging.models['TestContact'].insert({ - id: 10, - address: { id: 10 }, + const contact = messaging.models["TestContact"].insert({ + id: 10, + address: { id: 10 }, + }); + const address10 = messaging.models["TestAddress"].findFromIdentifyingData({ + id: 10, + }); + contact.update({ address: insert({ id: 20 }) }); + const address20 = messaging.models["TestAddress"].findFromIdentifyingData({ + id: 20, + }); + assert.strictEqual( + contact.address, + address20, + "insert: should create and replace a new record for a non-empty x2one field" + ); + assert.strictEqual( + address20.contact, + contact, + "the inverse relation should be set as well" + ); + assert.strictEqual( + address10.contact, + undefined, + "the original relation should be dropped" + ); + } + ); + + QUnit.test( + "insert: should update the existing record for an x2one field", + async function (assert) { + assert.expect(2); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + address: { + id: 10, + addressInfo: "address 10", + }, + }); + const address10 = messaging.models["TestAddress"].findFromIdentifyingData({ + id: 10, + }); + contact.update({ + address: insert({ + id: 10, + addressInfo: "address 10 updated", + }), + }); + assert.strictEqual( + contact.address, + address10, + "insert: should not drop an existing record" + ); + assert.strictEqual( + address10.addressInfo, + "address 10 updated", + "insert: should update the existing record for a x2one field" + ); + } + ); + + QUnit.test( + "insert: should create and link a new record for an x2many field", + async function (assert) { + assert.expect(3); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ id: 10 }); + contact.update({ tasks: insert({ id: 10 }) }); + const task = messaging.models["TestTask"].findFromIdentifyingData({ id: 10 }); + assert.strictEqual(contact.tasks.length, 1, "should have 1 record"); + assert.strictEqual(contact.tasks[0], task, "should link the new record"); + assert.strictEqual( + task.responsible, + contact, + "the inverse relation should be set as well" + ); + } + ); + + QUnit.test( + "insert: should create and add a new record for an x2many field", + async function (assert) { + assert.expect(4); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + tasks: { id: 10 }, + }); + const task10 = messaging.models["TestTask"].findFromIdentifyingData({ id: 10 }); + contact.update({ tasks: insert({ id: 20 }) }); + const task20 = messaging.models["TestTask"].findFromIdentifyingData({ id: 20 }); + assert.strictEqual(contact.tasks.length, 2, "should have 2 records"); + assert.strictEqual(contact.tasks[0], task10, "the original record should be kept"); + assert.strictEqual(contact.tasks[1], task20, "new record should be added"); + assert.strictEqual( + task20.responsible, + contact, + "the inverse relation should be set as well" + ); + } + ); + + QUnit.test( + "insert: should update existing records for an x2many field", + async function (assert) { + assert.expect(3); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + tasks: { + id: 10, + title: "task 10", + }, + }); + const task = messaging.models["TestTask"].findFromIdentifyingData({ id: 10 }); + contact.update({ + tasks: insert({ + id: 10, + title: "task 10 updated", + }), + }); + assert.strictEqual(contact.tasks.length, 1, "should have 1 record"); + assert.strictEqual(contact.tasks[0], task, "the original task should be kept"); + assert.strictEqual( + task.title, + "task 10 updated", + "should update the existing record" + ); + } + ); }); - const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 }); - contact.update({ address: insert({ id: 20 }) }); - const address20 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 20 }); - assert.strictEqual( - contact.address, - address20, - 'insert: should create and replace a new record for a non-empty x2one field' - ); - assert.strictEqual( - address20.contact, - contact, - 'the inverse relation should be set as well' - ); - assert.strictEqual( - address10.contact, - undefined, - 'the original relation should be dropped' - ); -}); - -QUnit.test('insert: should update the existing record for an x2one field', async function (assert) { - assert.expect(2); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - address: { - id: 10, - addressInfo: 'address 10', - }, - }); - const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 }); - contact.update({ - address: insert({ - id: 10, - addressInfo: 'address 10 updated', - }), - }); - assert.strictEqual( - contact.address, - address10, - 'insert: should not drop an existing record' - ); - assert.strictEqual( - address10.addressInfo, - 'address 10 updated', - 'insert: should update the existing record for a x2one field' - ); -}); - -QUnit.test('insert: should create and link a new record for an x2many field', async function (assert) { - assert.expect(3); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ id: 10 }); - contact.update({ tasks: insert({ id: 10 }) }); - const task = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 }); - assert.strictEqual( - contact.tasks.length, - 1, - 'should have 1 record' - ); - assert.strictEqual( - contact.tasks[0], - task, - "should link the new record" - ); - assert.strictEqual( - task.responsible, - contact, - 'the inverse relation should be set as well' - ); -}); - -QUnit.test('insert: should create and add a new record for an x2many field', async function (assert) { - assert.expect(4); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - tasks: { id: 10 }, - }); - const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 }); - contact.update({ tasks: insert({ id: 20 }) }); - const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 }); - assert.strictEqual( - contact.tasks.length, - 2, - "should have 2 records" - ); - assert.strictEqual( - contact.tasks[0], - task10, - "the original record should be kept" - ); - assert.strictEqual( - contact.tasks[1], - task20, - 'new record should be added' - ); - assert.strictEqual( - task20.responsible, - contact, - 'the inverse relation should be set as well' - ); -}); - -QUnit.test('insert: should update existing records for an x2many field', async function (assert) { - assert.expect(3); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - tasks: { - id: 10, - title: 'task 10', - }, - }); - const task = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 }); - contact.update({ - tasks: insert({ - id: 10, - title: 'task 10 updated', - }), - }); - assert.strictEqual( - contact.tasks.length, - 1, - "should have 1 record" - ); - assert.strictEqual( - contact.tasks[0], - task, - "the original task should be kept" - ); - assert.strictEqual( - task.title, - 'task 10 updated', - 'should update the existing record' - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/link_tests.js b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/link_tests.js index 9d36c8f6d30..5840c2b91bd 100644 --- a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/link_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/link_tests.js @@ -1,122 +1,109 @@ /** @odoo-module **/ -import { link } from '@mail/model'; -import { start } from '@mail/../tests/helpers/test_utils'; +import { link } from "@mail/model"; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('model_field_commands', {}, function () { -QUnit.module('field_command_link_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("model_field_commands", {}, function () { + QUnit.module("field_command_link_tests.js"); -QUnit.test('link: should link a record to an empty x2one field', async function (assert) { - assert.expect(2); - const { messaging } = await start(); + QUnit.test("link: should link a record to an empty x2one field", async function (assert) { + assert.expect(2); + const { messaging } = await start(); - const contact = messaging.models['TestContact'].insert({ id: 10 }); - const address = messaging.models['TestAddress'].insert({ id: 10 }); - contact.update({ address }); - assert.strictEqual( - contact.address, - address, - 'link: should link a record to an empty x2one field' - ); - assert.strictEqual( - address.contact, - contact, - 'the inverse relation should be set as well' - ); -}); + const contact = messaging.models["TestContact"].insert({ id: 10 }); + const address = messaging.models["TestAddress"].insert({ id: 10 }); + contact.update({ address }); + assert.strictEqual( + contact.address, + address, + "link: should link a record to an empty x2one field" + ); + assert.strictEqual( + address.contact, + contact, + "the inverse relation should be set as well" + ); + }); -QUnit.test('link: should replace a record to a non-empty x2one field', async function (assert) { - assert.expect(3); - const { messaging } = await start(); + QUnit.test( + "link: should replace a record to a non-empty x2one field", + async function (assert) { + assert.expect(3); + const { messaging } = await start(); - const contact = messaging.models['TestContact'].insert({ - id: 10, - address: { id: 10 }, + const contact = messaging.models["TestContact"].insert({ + id: 10, + address: { id: 10 }, + }); + const address10 = messaging.models["TestAddress"].findFromIdentifyingData({ + id: 10, + }); + const address20 = messaging.models["TestAddress"].insert({ id: 20 }); + contact.update({ address: address20 }); + assert.strictEqual( + contact.address, + address20, + "link: should replace a record to a non-empty x2one field" + ); + assert.strictEqual( + address20.contact, + contact, + "the inverse relation should be set as well" + ); + assert.strictEqual( + address10.contact, + undefined, + "the orginal relation should be dropped" + ); + } + ); + + QUnit.test("link: should link a record to an empty x2many field", async function (assert) { + assert.expect(3); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ id: 10 }); + const task = messaging.models["TestTask"].insert({ id: 10 }); + contact.update({ tasks: link(task) }); + assert.strictEqual(contact.tasks.length, 1, "should have 1 record"); + assert.strictEqual(contact.tasks[0], task, "the record should be linked"); + assert.strictEqual( + task.responsible, + contact, + "the inverse relation should be set as well" + ); + }); + + QUnit.test( + "link: should link and add a record to a non-empty x2many field", + async function (assert) { + assert.expect(5); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + tasks: { id: 10 }, + }); + const task10 = messaging.models["TestTask"].findFromIdentifyingData({ id: 10 }); + const task20 = messaging.models["TestTask"].insert({ id: 20 }); + contact.update({ tasks: link(task20) }); + assert.strictEqual(contact.tasks.length, 2, "should have 2 records"); + assert.strictEqual(contact.tasks[0], task10, "the original record should be kept"); + assert.strictEqual(contact.tasks[1], task20, "the new record should be added"); + assert.ok( + contact.tasks instanceof Array && + contact.tasks.length === 2 && + contact.tasks.includes(task10) && + contact.tasks.includes(task20), + "link: should link and add a record to a non-empty x2many field" + ); + assert.strictEqual( + task20.responsible, + contact, + "the inverse relation should be set as well" + ); + } + ); }); - const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 }); - const address20 = messaging.models['TestAddress'].insert({ id: 20 }); - contact.update({ address: address20 }); - assert.strictEqual( - contact.address, - address20, - 'link: should replace a record to a non-empty x2one field' - ); - assert.strictEqual( - address20.contact, - contact, - 'the inverse relation should be set as well' - ); - assert.strictEqual( - address10.contact, - undefined, - 'the orginal relation should be dropped' - ); -}); - -QUnit.test('link: should link a record to an empty x2many field', async function (assert) { - assert.expect(3); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ id: 10 }); - const task = messaging.models['TestTask'].insert({ id: 10 }); - contact.update({ tasks: link(task) }); - assert.strictEqual( - contact.tasks.length, - 1, - "should have 1 record" - ); - assert.strictEqual( - contact.tasks[0], - task, - 'the record should be linked' - ); - assert.strictEqual( - task.responsible, - contact, - 'the inverse relation should be set as well' - ); -}); - -QUnit.test('link: should link and add a record to a non-empty x2many field', async function (assert) { - assert.expect(5); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - tasks: { id: 10 }, - }); - const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 }); - const task20 = messaging.models['TestTask'].insert({ id: 20 }); - contact.update({ tasks: link(task20) }); - assert.strictEqual( - contact.tasks.length, - 2, - "should have 2 records" - ); - assert.strictEqual( - contact.tasks[0], - task10, - "the original record should be kept" - ); - assert.strictEqual( - contact.tasks[1], - task20, - "the new record should be added" - ); - assert.ok( - contact.tasks instanceof Array && - contact.tasks.length === 2 && - contact.tasks.includes(task10) && - contact.tasks.includes(task20), - 'link: should link and add a record to a non-empty x2many field', - ); - assert.strictEqual( - task20.responsible, - contact, - 'the inverse relation should be set as well' - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/replace_tests.js b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/replace_tests.js index f97fa9f9d2c..611fcd0976c 100644 --- a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/replace_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/replace_tests.js @@ -1,161 +1,143 @@ /** @odoo-module **/ -import { start } from '@mail/../tests/helpers/test_utils'; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('model_field_commands', {}, function () { -QUnit.module('replace_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("model_field_commands", {}, function () { + QUnit.module("replace_tests.js"); -QUnit.test('replace: should link a record for an empty x2one field', async function (assert) { - assert.expect(2); - const { messaging } = await start(); + QUnit.test( + "replace: should link a record for an empty x2one field", + async function (assert) { + assert.expect(2); + const { messaging } = await start(); - const contact = messaging.models['TestContact'].insert({ id: 10 }); - const address = messaging.models['TestAddress'].insert({ id: 10 }); - contact.update({ address }); - assert.strictEqual( - contact.address, - address, - 'replace: should link a record for an empty x2one field' - ); - assert.strictEqual( - address.contact, - contact, - 'the inverse relation should be set as well' - ); -}); + const contact = messaging.models["TestContact"].insert({ id: 10 }); + const address = messaging.models["TestAddress"].insert({ id: 10 }); + contact.update({ address }); + assert.strictEqual( + contact.address, + address, + "replace: should link a record for an empty x2one field" + ); + assert.strictEqual( + address.contact, + contact, + "the inverse relation should be set as well" + ); + } + ); -QUnit.test('replace: should replace a record for a non-empty x2one field', async function (assert) { - assert.expect(3); - const { messaging } = await start(); + QUnit.test( + "replace: should replace a record for a non-empty x2one field", + async function (assert) { + assert.expect(3); + const { messaging } = await start(); - const contact = messaging.models['TestContact'].insert({ - id: 10, - address: { id: 10 }, + const contact = messaging.models["TestContact"].insert({ + id: 10, + address: { id: 10 }, + }); + const address10 = messaging.models["TestAddress"].findFromIdentifyingData({ + id: 10, + }); + const address20 = messaging.models["TestAddress"].insert({ id: 20 }); + contact.update({ address: address20 }); + assert.strictEqual( + contact.address, + address20, + "replace: should replace a record for a non-empty x2one field" + ); + assert.strictEqual( + address20.contact, + contact, + "the inverse relation should be set as well" + ); + assert.strictEqual( + address10.contact, + undefined, + "the original relation should be dropped" + ); + } + ); + + QUnit.test( + "replace: should link a record for an empty x2many field", + async function (assert) { + assert.expect(4); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ id: 10 }); + const task = messaging.models["TestTask"].insert({ id: 10 }); + contact.update({ tasks: task }); + assert.strictEqual(contact.tasks.length, 1, "should have 1 record"); + assert.strictEqual(contact.tasks.length, 1, "should have 1 record"); + assert.strictEqual(contact.tasks[0], task, "the new record should be linked"); + assert.strictEqual( + task.responsible, + contact, + "the inverse relation should be dropped" + ); + } + ); + + QUnit.test( + "replace: should replace all records for a non-empty field", + async function (assert) { + assert.expect(5); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + tasks: [{ id: 10 }, { id: 20 }], + }); + const task10 = messaging.models["TestTask"].findFromIdentifyingData({ id: 10 }); + const task20 = messaging.models["TestTask"].findFromIdentifyingData({ id: 20 }); + const task30 = messaging.models["TestTask"].insert({ id: 30 }); + contact.update({ tasks: task30 }); + assert.strictEqual(contact.tasks.length, 1, "should have 1 record"); + assert.strictEqual( + contact.tasks[0], + task30, + "should be replaced with the new record" + ); + assert.strictEqual( + task30.responsible, + contact, + "the inverse relation should be set as well" + ); + assert.strictEqual( + task10.responsible, + undefined, + "the original relation should be dropped" + ); + assert.strictEqual( + task20.responsible, + undefined, + "the original relation should be dropped" + ); + } + ); + + QUnit.test( + "replace: should order the existing records for x2many field", + async function (assert) { + assert.expect(3); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + tasks: [{ id: 10 }, { id: 20 }], + }); + const task10 = messaging.models["TestTask"].findFromIdentifyingData({ id: 10 }); + const task20 = messaging.models["TestTask"].findFromIdentifyingData({ id: 20 }); + contact.update({ + tasks: [task20, task10], + }); + assert.strictEqual(contact.tasks.length, 2, "should have 2 records"); + assert.strictEqual(contact.tasks[0], task20, "records should be re-ordered"); + assert.strictEqual(contact.tasks[1], task10, "recprds should be re-ordered"); + } + ); }); - const address10 = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 }); - const address20 = messaging.models['TestAddress'].insert({ id: 20 }); - contact.update({ address: address20 }); - assert.strictEqual( - contact.address, - address20, - 'replace: should replace a record for a non-empty x2one field' - ); - assert.strictEqual( - address20.contact, - contact, - 'the inverse relation should be set as well' - ); - assert.strictEqual( - address10.contact, - undefined, - 'the original relation should be dropped' - ); -}); - -QUnit.test('replace: should link a record for an empty x2many field', async function (assert) { - assert.expect(4); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ id: 10 }); - const task = messaging.models['TestTask'].insert({ id: 10 }); - contact.update({ tasks: task }); - assert.strictEqual( - contact.tasks.length, - 1, - "should have 1 record" - ); - assert.strictEqual( - contact.tasks.length, - 1, - "should have 1 record" - ); - assert.strictEqual( - contact.tasks[0], - task, - "the new record should be linked" - ); - assert.strictEqual( - task.responsible, - contact, - 'the inverse relation should be dropped' - ); -}); - -QUnit.test('replace: should replace all records for a non-empty field', async function (assert) { - assert.expect(5); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - tasks: [ - { id: 10 }, - { id: 20 }, - ], - }); - const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 }); - const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 }); - const task30 = messaging.models['TestTask'].insert({ id: 30 }); - contact.update({ tasks: task30 }); - assert.strictEqual( - contact.tasks.length, - 1, - "should have 1 record" - ); - assert.strictEqual( - contact.tasks[0], - task30, - 'should be replaced with the new record' - ); - assert.strictEqual( - task30.responsible, - contact, - 'the inverse relation should be set as well' - ); - assert.strictEqual( - task10.responsible, - undefined, - 'the original relation should be dropped' - ); - assert.strictEqual( - task20.responsible, - undefined, - 'the original relation should be dropped' - ); -}); - -QUnit.test('replace: should order the existing records for x2many field', async function (assert) { - assert.expect(3); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - tasks: [ - { id: 10 }, - { id: 20 }, - ], - }); - const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 }); - const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 }); - contact.update({ - tasks: [task20, task10], - }); - assert.strictEqual( - contact.tasks.length, - 2, - "should have 2 records" - ); - assert.strictEqual( - contact.tasks[0], - task20, - 'records should be re-ordered' - ); - assert.strictEqual( - contact.tasks[1], - task10, - 'recprds should be re-ordered' - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/set_tests.js b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/set_tests.js index 032b4aa80a5..49c79d2d666 100644 --- a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/set_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/set_tests.js @@ -1,81 +1,72 @@ /** @odoo-module **/ -import { decrement, increment, set } from '@mail/model'; -import { start } from '@mail/../tests/helpers/test_utils'; +import { decrement, increment, set } from "@mail/model"; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('model_field_commands', {}, function () { -QUnit.module('set_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("model_field_commands", {}, function () { + QUnit.module("set_tests.js"); -QUnit.test('decrement: should decrease attribute field value', async function (assert) { - assert.expect(1); - const { messaging } = await start(); + QUnit.test("decrement: should decrease attribute field value", async function (assert) { + assert.expect(1); + const { messaging } = await start(); - const task = messaging.models['TestTask'].insert({ - id: 10, - difficulty: 5, + const task = messaging.models["TestTask"].insert({ + id: 10, + difficulty: 5, + }); + task.update({ difficulty: decrement(2) }); + assert.strictEqual( + task.difficulty, + 5 - 2, + "decrement: should decrease attribute field value" + ); + }); + + QUnit.test("increment: should increase attribute field value", async function (assert) { + assert.expect(1); + const { messaging } = await start(); + + const task = messaging.models["TestTask"].insert({ + id: 10, + difficulty: 5, + }); + task.update({ difficulty: increment(3) }); + assert.strictEqual( + task.difficulty, + 5 + 3, + "decrement: should increase attribute field value" + ); + }); + + QUnit.test("set: should set a value for attribute field", async function (assert) { + assert.expect(1); + const { messaging } = await start(); + + const task = messaging.models["TestTask"].insert({ + id: 10, + difficulty: 5, + }); + task.update({ difficulty: set(20) }); + assert.strictEqual(task.difficulty, 20, "set: should set a value for attribute field"); + }); + + QUnit.test("multiple attribute commands combination", async function (assert) { + assert.expect(1); + const { messaging } = await start(); + + const task = messaging.models["TestTask"].insert({ + id: 10, + difficulty: 5, + }); + task.update({ + difficulty: [set(20), increment(16), decrement(8)], + }); + assert.strictEqual( + task.difficulty, + 20 + 16 - 8, + "multiple attribute commands combination should work as expected" + ); + }); }); - task.update({ difficulty: decrement(2) }); - assert.strictEqual( - task.difficulty, - 5 - 2, - 'decrement: should decrease attribute field value' - ); -}); - -QUnit.test('increment: should increase attribute field value', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - - const task = messaging.models['TestTask'].insert({ - id: 10, - difficulty: 5, - }); - task.update({ difficulty: increment(3) }); - assert.strictEqual( - task.difficulty, - 5 + 3, - 'decrement: should increase attribute field value' - ); -}); - -QUnit.test('set: should set a value for attribute field', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - - const task = messaging.models['TestTask'].insert({ - id: 10, - difficulty: 5, - }); - task.update({ difficulty: set(20) }); - assert.strictEqual( - task.difficulty, - 20, - 'set: should set a value for attribute field' - ); -}); - -QUnit.test('multiple attribute commands combination', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - - const task = messaging.models['TestTask'].insert({ - id: 10, - difficulty: 5, - }); - task.update({ - difficulty: [ - set(20), - increment(16), - decrement(8), - ], - }); - assert.strictEqual( - task.difficulty, - 20 + 16 - 8, - 'multiple attribute commands combination should work as expected' - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/unlink_all_tests.js b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/unlink_all_tests.js index 9ede1721338..4c2f1935c7d 100644 --- a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/unlink_all_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/unlink_all_tests.js @@ -1,57 +1,56 @@ /** @odoo-module **/ -import { unlinkAll } from '@mail/model'; -import { start } from '@mail/../tests/helpers/test_utils'; +import { unlinkAll } from "@mail/model"; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('model_field_commands', {}, function () { -QUnit.module('unlink_all_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("model_field_commands", {}, function () { + QUnit.module("unlink_all_tests.js"); -QUnit.test('unlinkAll: should set x2one field undefined', async function (assert) { - assert.expect(2); - const { messaging } = await start(); + QUnit.test("unlinkAll: should set x2one field undefined", async function (assert) { + assert.expect(2); + const { messaging } = await start(); - const contact = messaging.models['TestContact'].insert({ - id: 10, - address: { id: 20 }, + const contact = messaging.models["TestContact"].insert({ + id: 10, + address: { id: 20 }, + }); + const address = messaging.models["TestAddress"].findFromIdentifyingData({ id: 20 }); + contact.update({ address: unlinkAll() }); + assert.strictEqual( + contact.address, + undefined, + "clear: should set x2one field undefined" + ); + assert.strictEqual( + address.contact, + undefined, + "the inverse relation should be cleared as well" + ); + }); + + QUnit.test("unlinkAll: should set x2many field an empty array", async function (assert) { + assert.expect(2); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + tasks: { + id: 20, + }, + }); + const task = messaging.models["TestTask"].findFromIdentifyingData({ id: 20 }); + contact.update({ tasks: unlinkAll() }); + assert.strictEqual( + contact.tasks.length, + 0, + "clear: should set x2many field empty array" + ); + assert.strictEqual( + task.responsible, + undefined, + "the inverse relation should be cleared as well" + ); + }); }); - const address = messaging.models['TestAddress'].findFromIdentifyingData({ id: 20 }); - contact.update({ address: unlinkAll() }); - assert.strictEqual( - contact.address, - undefined, - 'clear: should set x2one field undefined' - ); - assert.strictEqual( - address.contact, - undefined, - 'the inverse relation should be cleared as well' - ); -}); - -QUnit.test('unlinkAll: should set x2many field an empty array', async function (assert) { - assert.expect(2); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - tasks: { - id: 20, - }, - }); - const task = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 }); - contact.update({ tasks: unlinkAll() }); - assert.strictEqual( - contact.tasks.length, - 0, - 'clear: should set x2many field empty array' - ); - assert.strictEqual( - task.responsible, - undefined, - 'the inverse relation should be cleared as well' - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/unlink_tests.js b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/unlink_tests.js index 9b0f0f544f6..7cabbf233c1 100644 --- a/addons/mail/static/tests/qunit_suite_tests/model_field_commands/unlink_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/model_field_commands/unlink_tests.js @@ -1,61 +1,59 @@ /** @odoo-module **/ -import { clear, unlink } from '@mail/model'; -import { start } from '@mail/../tests/helpers/test_utils'; +import { clear, unlink } from "@mail/model"; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('model_field_commands', {}, function () { -QUnit.module('unlink_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("model_field_commands", {}, function () { + QUnit.module("unlink_tests.js"); + QUnit.test("unlink: should unlink the record for x2one field", async function (assert) { + assert.expect(2); + const { messaging } = await start(); -QUnit.test('unlink: should unlink the record for x2one field', async function (assert) { - assert.expect(2); - const { messaging } = await start(); + const contact = messaging.models["TestContact"].insert({ + id: 10, + address: { id: 10 }, + }); + const address = messaging.models["TestAddress"].findFromIdentifyingData({ id: 10 }); + contact.update({ address: clear() }); + assert.strictEqual( + contact.address, + undefined, + "unlink: should unlink the record for x2one field" + ); + assert.strictEqual( + address.contact, + undefined, + "the original relation should be dropped as well" + ); + }); - const contact = messaging.models['TestContact'].insert({ - id: 10, - address: { id: 10 }, + QUnit.test( + "unlink: should unlink the specified record for x2many field", + async function (assert) { + assert.expect(2); + const { messaging } = await start(); + + const contact = messaging.models["TestContact"].insert({ + id: 10, + tasks: [{ id: 10 }, { id: 20 }], + }); + const task10 = messaging.models["TestTask"].findFromIdentifyingData({ id: 10 }); + const task20 = messaging.models["TestTask"].findFromIdentifyingData({ id: 20 }); + contact.update({ tasks: unlink(task10) }); + assert.ok( + contact.tasks instanceof Array && + contact.tasks.length === 1 && + contact.tasks.includes(task20), + "unlink: should unlink the specified record for x2many field" + ); + assert.strictEqual( + task10.responsible, + undefined, + "the orignal relation should be dropped as well" + ); + } + ); }); - const address = messaging.models['TestAddress'].findFromIdentifyingData({ id: 10 }); - contact.update({ address: clear() }); - assert.strictEqual( - contact.address, - undefined, - 'unlink: should unlink the record for x2one field' - ); - assert.strictEqual( - address.contact, - undefined, - 'the original relation should be dropped as well' - ); -}); - -QUnit.test('unlink: should unlink the specified record for x2many field', async function (assert) { - assert.expect(2); - const { messaging } = await start(); - - const contact = messaging.models['TestContact'].insert({ - id: 10, - tasks: [ - { id: 10 }, - { id: 20 }, - ], - }); - const task10 = messaging.models['TestTask'].findFromIdentifyingData({ id: 10 }); - const task20 = messaging.models['TestTask'].findFromIdentifyingData({ id: 20 }); - contact.update({ tasks: unlink(task10) }); - assert.ok( - contact.tasks instanceof Array && - contact.tasks.length === 1 && - contact.tasks.includes(task20), - 'unlink: should unlink the specified record for x2many field' - ); - assert.strictEqual( - task10.responsible, - undefined, - 'the orignal relation should be dropped as well' - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/models/attachment_tests.js b/addons/mail/static/tests/qunit_suite_tests/models/attachment_tests.js index 8f3f60759e6..c8694964bae 100644 --- a/addons/mail/static/tests/qunit_suite_tests/models/attachment_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/models/attachment_tests.js @@ -1,124 +1,141 @@ /** @odoo-module **/ -import { start } from '@mail/../tests/helpers/test_utils'; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('models', {}, function () { -QUnit.module('attachment_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("models", {}, function () { + QUnit.module("attachment_tests.js"); -QUnit.test('create (txt)', async function (assert) { - assert.expect(9); + QUnit.test("create (txt)", async function (assert) { + assert.expect(9); - const { messaging } = await start(); - assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); + const { messaging } = await start(); + assert.notOk(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); - const attachment = messaging.models['Attachment'].insert({ - filename: "test.txt", - id: 750, - mimetype: 'text/plain', - name: "test.txt", + const attachment = messaging.models["Attachment"].insert({ + filename: "test.txt", + id: 750, + mimetype: "text/plain", + name: "test.txt", + }); + assert.ok(attachment); + assert.ok(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + assert.strictEqual( + messaging.models["Attachment"].findFromIdentifyingData({ id: 750 }), + attachment + ); + assert.strictEqual(attachment.filename, "test.txt"); + assert.strictEqual(attachment.id, 750); + assert.notOk(attachment.isUploading); + assert.strictEqual(attachment.mimetype, "text/plain"); + assert.strictEqual(attachment.name, "test.txt"); + }); + + QUnit.test("displayName", async function (assert) { + assert.expect(5); + + const { messaging } = await start(); + assert.notOk(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + + const attachment = messaging.models["Attachment"].insert({ + filename: "test.txt", + id: 750, + mimetype: "text/plain", + name: "test.txt", + }); + assert.ok(attachment); + assert.ok(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + assert.strictEqual( + attachment, + messaging.models["Attachment"].findFromIdentifyingData({ id: 750 }) + ); + assert.strictEqual(attachment.displayName, "test.txt"); + }); + + QUnit.test("extension", async function (assert) { + assert.expect(5); + + const { messaging } = await start(); + assert.notOk(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + + const attachment = messaging.models["Attachment"].insert({ + filename: "test.txt", + id: 750, + mimetype: "text/plain", + name: "test.txt", + }); + assert.ok(attachment); + assert.ok(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + assert.strictEqual( + attachment, + messaging.models["Attachment"].findFromIdentifyingData({ id: 750 }) + ); + assert.strictEqual(attachment.extension, "txt"); + }); + + QUnit.test("fileType", async function (assert) { + assert.expect(5); + + const { messaging } = await start(); + assert.notOk(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + + const attachment = messaging.models["Attachment"].insert({ + filename: "test.txt", + id: 750, + mimetype: "text/plain", + name: "test.txt", + }); + assert.ok(attachment); + assert.ok(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + assert.strictEqual( + attachment, + messaging.models["Attachment"].findFromIdentifyingData({ + id: 750, + }) + ); + assert.ok(attachment.isText); + }); + + QUnit.test("isTextFile", async function (assert) { + assert.expect(5); + + const { messaging } = await start(); + assert.notOk(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + + const attachment = messaging.models["Attachment"].insert({ + filename: "test.txt", + id: 750, + mimetype: "text/plain", + name: "test.txt", + }); + assert.ok(attachment); + assert.ok(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + assert.strictEqual( + attachment, + messaging.models["Attachment"].findFromIdentifyingData({ id: 750 }) + ); + assert.ok(attachment.isText); + }); + + QUnit.test("isViewable", async function (assert) { + assert.expect(5); + + const { messaging } = await start(); + assert.notOk(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + + const attachment = messaging.models["Attachment"].insert({ + filename: "test.txt", + id: 750, + mimetype: "text/plain", + name: "test.txt", + }); + assert.ok(attachment); + assert.ok(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + assert.strictEqual( + attachment, + messaging.models["Attachment"].findFromIdentifyingData({ id: 750 }) + ); + assert.ok(attachment.isViewable); + }); }); - assert.ok(attachment); - assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.strictEqual(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }), attachment); - assert.strictEqual(attachment.filename, "test.txt"); - assert.strictEqual(attachment.id, 750); - assert.notOk(attachment.isUploading); - assert.strictEqual(attachment.mimetype, 'text/plain'); - assert.strictEqual(attachment.name, "test.txt"); -}); - -QUnit.test('displayName', async function (assert) { - assert.expect(5); - - const { messaging } = await start(); - assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - - const attachment = messaging.models['Attachment'].insert({ - filename: "test.txt", - id: 750, - mimetype: 'text/plain', - name: "test.txt", - }); - assert.ok(attachment); - assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.strictEqual(attachment, messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.strictEqual(attachment.displayName, "test.txt"); -}); - -QUnit.test('extension', async function (assert) { - assert.expect(5); - - const { messaging } = await start(); - assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - - const attachment = messaging.models['Attachment'].insert({ - filename: "test.txt", - id: 750, - mimetype: 'text/plain', - name: "test.txt", - }); - assert.ok(attachment); - assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.strictEqual(attachment, messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.strictEqual(attachment.extension, 'txt'); -}); - -QUnit.test('fileType', async function (assert) { - assert.expect(5); - - const { messaging } = await start(); - assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - - const attachment = messaging.models['Attachment'].insert({ - filename: "test.txt", - id: 750, - mimetype: 'text/plain', - name: "test.txt", - }); - assert.ok(attachment); - assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.strictEqual(attachment, messaging.models['Attachment'].findFromIdentifyingData({ - id: 750, - })); - assert.ok(attachment.isText); -}); - -QUnit.test('isTextFile', async function (assert) { - assert.expect(5); - - const { messaging } = await start(); - assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - - const attachment = messaging.models['Attachment'].insert({ - filename: "test.txt", - id: 750, - mimetype: 'text/plain', - name: "test.txt", - }); - assert.ok(attachment); - assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.strictEqual(attachment, messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.ok(attachment.isText); -}); - -QUnit.test('isViewable', async function (assert) { - assert.expect(5); - - const { messaging } = await start(); - assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - - const attachment = messaging.models['Attachment'].insert({ - filename: "test.txt", - id: 750, - mimetype: 'text/plain', - name: "test.txt", - }); - assert.ok(attachment); - assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.strictEqual(attachment, messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.ok(attachment.isViewable); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/models/clock_tests.js b/addons/mail/static/tests/qunit_suite_tests/models/clock_tests.js index 0de2e8d9013..5394a3d2d0c 100644 --- a/addons/mail/static/tests/qunit_suite_tests/models/clock_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/models/clock_tests.js @@ -1,49 +1,54 @@ /** @odoo-module **/ -import { start } from '@mail/../tests/helpers/test_utils'; +import { start } from "@mail/../tests/helpers/test_utils"; -import { patchDate } from '@web/../tests/helpers/utils'; +import { patchDate } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('models', {}, function () { -QUnit.module('clock_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("models", {}, function () { + QUnit.module("clock_tests.js"); -QUnit.test('Deleting all the watchers of a clock should result in the deletion of the clock itself.', async function (assert) { - assert.expect(1); + QUnit.test( + "Deleting all the watchers of a clock should result in the deletion of the clock itself.", + async function (assert) { + assert.expect(1); - const { messaging } = await start(); - const watcher = messaging.models['ClockWatcher'].insert({ - clock: { frequency: 180 * 1000 }, - qunitTestOwner: {}, + const { messaging } = await start(); + const watcher = messaging.models["ClockWatcher"].insert({ + clock: { frequency: 180 * 1000 }, + qunitTestOwner: {}, + }); + const { clock } = watcher; + + watcher.delete(); + assert.notOk( + clock.exists(), + "deleting all the watchers of a clock should result in the deletion of the clock itself." + ); + } + ); + + QUnit.test( + "[technical] Before ticking for the first time, the clock should indicate the date of creation of the record.", + async function (assert) { + assert.expect(1); + + const { messaging } = await start(); + // The date is patched AFTER startup, so if the date field in Clock was set + // at initialization (which we don't want), it will now look completely + // different from the patched date. + patchDate(2016, 8, 8, 14, 55, 15, 352); + + const { clock } = messaging.models["ClockWatcher"].insert({ + clock: { frequency: 3600 * 1000 }, + qunitTestOwner: {}, + }); + assert.strictEqual( + clock.date.getFullYear(), // no need to be more precise than the year + 2016, + "before ticking for the first time, the clock should indicate the date of creation of the record." + ); + } + ); }); - const { clock } = watcher; - - watcher.delete(); - assert.notOk( - clock.exists(), - "deleting all the watchers of a clock should result in the deletion of the clock itself." - ); -}); - -QUnit.test('[technical] Before ticking for the first time, the clock should indicate the date of creation of the record.', async function (assert) { - assert.expect(1); - - const { messaging } = await start(); - // The date is patched AFTER startup, so if the date field in Clock was set - // at initialization (which we don't want), it will now look completely - // different from the patched date. - patchDate(2016, 8, 8, 14, 55, 15, 352); - - const { clock } = messaging.models['ClockWatcher'].insert({ - clock: { frequency: 3600 * 1000 }, - qunitTestOwner: {}, - }); - assert.strictEqual( - clock.date.getFullYear(), // no need to be more precise than the year - 2016, - "before ticking for the first time, the clock should indicate the date of creation of the record." - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/models/file_uploader_tests.js b/addons/mail/static/tests/qunit_suite_tests/models/file_uploader_tests.js index d4d36fbc351..2acf8edb09f 100644 --- a/addons/mail/static/tests/qunit_suite_tests/models/file_uploader_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/models/file_uploader_tests.js @@ -1,80 +1,83 @@ /** @odoo-module **/ -import { start, startServer, dropFiles, dragenterFiles } from '@mail/../tests/helpers/test_utils'; -import { dom, file } from 'web.test_utils'; +import { start, startServer, dropFiles, dragenterFiles } from "@mail/../tests/helpers/test_utils"; +import { dom, file } from "web.test_utils"; const { createFile } = file; -QUnit.module('mail', {}, function () { -QUnit.module('components', {}, function () { -QUnit.module('file_uploader', {}, function () { -QUnit.module('file_uploader_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("components", {}, function () { + QUnit.module("file_uploader", {}, function () { + QUnit.module("file_uploader_tests.js"); -QUnit.test('no conflicts between file uploaders', async function (assert) { - assert.expect(2); + QUnit.test("no conflicts between file uploaders", async function (assert) { + assert.expect(2); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const channelId = pyEnv['mail.channel'].create({}); - pyEnv['mail.message'].create({ - body: 'not empty', - model: 'mail.channel', - res_id: channelId, + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const channelId = pyEnv["mail.channel"].create({}); + pyEnv["mail.message"].create({ + body: "not empty", + model: "mail.channel", + res_id: channelId, + }); + const { afterNextRender, click, messaging, openView } = await start(); + + // Uploading file in the first thread: res.partner chatter. + await openView({ + res_id: resPartnerId1, + res_model: "res.partner", + views: [[false, "form"]], + }); + const file1 = await createFile({ + name: "text1.txt", + content: "hello, world", + contentType: "text/plain", + }); + await afterNextRender(() => dragenterFiles(document.querySelector(".o_Chatter"))); + await afterNextRender(() => + dropFiles(document.querySelector(".o_Chatter_dropZone"), [file1]) + ); + + // Uploading file in the second thread: mail.channel in chatWindow. + await click(`.o_MessagingMenu_toggler`); + await click(`.o_ChannelPreviewView[data-channel-id="${channelId}"]`); + const file2 = await createFile({ + name: "text2.txt", + content: "hello, world", + contentType: "text/plain", + }); + await afterNextRender(() => + dragenterFiles(document.querySelector(".o_ChatWindow")) + ); + await afterNextRender(() => + dropFiles(document.querySelector(".o_ChatWindow .o_DropZoneView"), [file2]) + ); + await afterNextRender(() => + dom.triggerEvent( + document.querySelector(".o_ChatWindow .o_ComposerTextInputView_textarea"), + "keydown", + { key: "Enter" } + ) + ); + const channelThread = messaging.models["Thread"].findFromIdentifyingData({ + id: channelId, + model: "mail.channel", + }); + const chatterThread = messaging.models["Thread"].findFromIdentifyingData({ + id: resPartnerId1, + model: "res.partner", + }); + assert.strictEqual( + chatterThread.allAttachments.length, + 1, + "Chatter thread should only have one attachment" + ); + assert.strictEqual( + channelThread.allAttachments.length, + 1, + "Channel thread should only have one attachment" + ); + }); + }); }); - const { afterNextRender, click, messaging, openView } = await start(); - - // Uploading file in the first thread: res.partner chatter. - await openView({ - res_id: resPartnerId1, - res_model: 'res.partner', - views: [[false, 'form']], - }); - const file1 = await createFile({ - name: 'text1.txt', - content: 'hello, world', - contentType: 'text/plain', - }); - await afterNextRender(() => - dragenterFiles(document.querySelector('.o_Chatter')) - ); - await afterNextRender(() => - dropFiles(document.querySelector('.o_Chatter_dropZone'), [file1]) - ); - - // Uploading file in the second thread: mail.channel in chatWindow. - await click(`.o_MessagingMenu_toggler`); - await click(`.o_ChannelPreviewView[data-channel-id="${channelId}"]`); - const file2 = await createFile({ - name: 'text2.txt', - content: 'hello, world', - contentType: 'text/plain', - }); - await afterNextRender(() => - dragenterFiles(document.querySelector('.o_ChatWindow')) - ); - await afterNextRender(() => - dropFiles(document.querySelector('.o_ChatWindow .o_DropZoneView'), [file2]) - ); - await afterNextRender(() => - dom.triggerEvent( - document.querySelector('.o_ChatWindow .o_ComposerTextInputView_textarea'), - 'keydown', - { key: 'Enter' }, - ) - ); - const channelThread = messaging.models['Thread'].findFromIdentifyingData({ id: channelId, model: 'mail.channel' }); - const chatterThread = messaging.models['Thread'].findFromIdentifyingData({ id: resPartnerId1, model: 'res.partner' }); - assert.strictEqual( - chatterThread.allAttachments.length, - 1, - 'Chatter thread should only have one attachment' - ); - assert.strictEqual( - channelThread.allAttachments.length, - 1, - 'Channel thread should only have one attachment' - ); -}); - -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/models/message_tests.js b/addons/mail/static/tests/qunit_suite_tests/models/message_tests.js index 904124db897..12da78b2787 100644 --- a/addons/mail/static/tests/qunit_suite_tests/models/message_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/models/message_tests.js @@ -1,168 +1,215 @@ /** @odoo-module **/ -import { insert } from '@mail/model'; -import { start } from '@mail/../tests/helpers/test_utils'; +import { insert } from "@mail/model"; +import { start } from "@mail/../tests/helpers/test_utils"; -import { str_to_datetime } from 'web.time'; +import { str_to_datetime } from "web.time"; -QUnit.module('mail', {}, function () { -QUnit.module('models', {}, function () { -QUnit.module('message_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("models", {}, function () { + QUnit.module("message_tests.js"); -QUnit.test('create', async function (assert) { - assert.expect(31); + QUnit.test("create", async function (assert) { + assert.expect(31); - const { messaging } = await start(); - assert.notOk(messaging.models['Partner'].findFromIdentifyingData({ id: 5 })); - assert.notOk(messaging.models['Thread'].findFromIdentifyingData({ - id: 100, - model: 'mail.channel', - })); - assert.notOk(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.notOk(messaging.models['Message'].findFromIdentifyingData({ id: 4000 })); + const { messaging } = await start(); + assert.notOk(messaging.models["Partner"].findFromIdentifyingData({ id: 5 })); + assert.notOk( + messaging.models["Thread"].findFromIdentifyingData({ + id: 100, + model: "mail.channel", + }) + ); + assert.notOk(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + assert.notOk(messaging.models["Message"].findFromIdentifyingData({ id: 4000 })); - const thread = messaging.models['Thread'].insert({ - id: 100, - model: 'mail.channel', - name: "General", + const thread = messaging.models["Thread"].insert({ + id: 100, + model: "mail.channel", + name: "General", + }); + const message = messaging.models["Message"].insert({ + attachments: { + filename: "test.txt", + id: 750, + mimetype: "text/plain", + name: "test.txt", + }, + author: insert({ id: 5, display_name: "Demo" }), + body: "

Test

", + date: moment(str_to_datetime("2019-05-05 10:00:00")), + id: 4000, + isNeedaction: true, + isStarred: true, + originThread: thread, + }); + + assert.ok(messaging.models["Partner"].findFromIdentifyingData({ id: 5 })); + assert.ok( + messaging.models["Thread"].findFromIdentifyingData({ + id: 100, + model: "mail.channel", + }) + ); + assert.ok(messaging.models["Attachment"].findFromIdentifyingData({ id: 750 })); + assert.ok(messaging.models["Message"].findFromIdentifyingData({ id: 4000 })); + + assert.ok(message); + assert.strictEqual( + messaging.models["Message"].findFromIdentifyingData({ id: 4000 }), + message + ); + assert.strictEqual(message.body, "

Test

"); + assert.ok(message.date instanceof moment); + assert.strictEqual( + moment(message.date).utc().format("YYYY-MM-DD hh:mm:ss"), + "2019-05-05 10:00:00" + ); + assert.strictEqual(message.id, 4000); + assert.strictEqual( + message.originThread, + messaging.models["Thread"].findFromIdentifyingData({ + id: 100, + model: "mail.channel", + }) + ); + assert.ok( + message.threads.includes( + messaging.models["Thread"].findFromIdentifyingData({ + id: 100, + model: "mail.channel", + }) + ) + ); + // from partnerId being in needaction_partner_ids + assert.ok(message.threads.includes(messaging.inbox.thread)); + // from partnerId being in starred_partner_ids + assert.ok(message.threads.includes(messaging.starred.thread)); + const attachment = messaging.models["Attachment"].findFromIdentifyingData({ id: 750 }); + assert.ok(attachment); + assert.strictEqual(attachment.filename, "test.txt"); + assert.strictEqual(attachment.id, 750); + assert.notOk(attachment.isUploading); + assert.strictEqual(attachment.mimetype, "text/plain"); + assert.strictEqual(attachment.name, "test.txt"); + const channel = messaging.models["Thread"].findFromIdentifyingData({ + id: 100, + model: "mail.channel", + }); + assert.ok(channel); + assert.strictEqual(channel.model, "mail.channel"); + assert.strictEqual(channel.id, 100); + assert.strictEqual(channel.name, "General"); + const partner = messaging.models["Partner"].findFromIdentifyingData({ id: 5 }); + assert.ok(partner); + assert.strictEqual(partner.displayName, "Demo"); + assert.strictEqual(partner.id, 5); + }); + + QUnit.test("message without body should be considered empty", async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ id: 11 }); + assert.ok(message.isEmpty); + }); + + QUnit.test('message with body "" should be considered empty', async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ body: "", id: 11 }); + assert.ok(message.isEmpty); + }); + + QUnit.test( + 'message with body "

" should be considered empty', + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ body: "

", id: 11 }); + assert.ok(message.isEmpty); + } + ); + + QUnit.test( + 'message with body "


" should be considered empty', + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ body: "


", id: 11 }); + assert.ok(message.isEmpty); + } + ); + + QUnit.test( + 'message with body "


" should be considered empty', + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ + body: "


", + id: 11, + }); + assert.ok(message.isEmpty); + } + ); + + QUnit.test( + String.raw`message with body "

\n

" should be considered empty`, + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ body: "

\n

", id: 11 }); + assert.ok(message.isEmpty); + } + ); + + QUnit.test( + String.raw`message with body "

\r\n\r\n

" should be considered empty`, + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ + body: "

\r\n\r\n

", + id: 11, + }); + assert.ok(message.isEmpty); + } + ); + + QUnit.test( + 'message with body "

" should be considered empty', + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ + body: "

", + id: 11, + }); + assert.ok(message.isEmpty); + } + ); + + QUnit.test( + `message with body "" should not be considered empty`, + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ + body: "", + id: 11, + }); + assert.notOk(message.isEmpty); + } + ); + + QUnit.test( + 'message with body "test" should not be considered empty', + async function (assert) { + assert.expect(1); + const { messaging } = await start(); + const message = messaging.models["Message"].insert({ body: "test", id: 11 }); + assert.notOk(message.isEmpty); + } + ); }); - const message = messaging.models['Message'].insert({ - attachments: { - filename: "test.txt", - id: 750, - mimetype: 'text/plain', - name: "test.txt", - }, - author: insert({ id: 5, display_name: "Demo" }), - body: "

Test

", - date: moment(str_to_datetime("2019-05-05 10:00:00")), - id: 4000, - isNeedaction: true, - isStarred: true, - originThread: thread, - }); - - assert.ok(messaging.models['Partner'].findFromIdentifyingData({ id: 5 })); - assert.ok(messaging.models['Thread'].findFromIdentifyingData({ - id: 100, - model: 'mail.channel', - })); - assert.ok(messaging.models['Attachment'].findFromIdentifyingData({ id: 750 })); - assert.ok(messaging.models['Message'].findFromIdentifyingData({ id: 4000 })); - - assert.ok(message); - assert.strictEqual(messaging.models['Message'].findFromIdentifyingData({ id: 4000 }), message); - assert.strictEqual(message.body, "

Test

"); - assert.ok(message.date instanceof moment); - assert.strictEqual( - moment(message.date).utc().format('YYYY-MM-DD hh:mm:ss'), - "2019-05-05 10:00:00" - ); - assert.strictEqual(message.id, 4000); - assert.strictEqual(message.originThread, messaging.models['Thread'].findFromIdentifyingData({ - id: 100, - model: 'mail.channel', - })); - assert.ok( - message.threads.includes(messaging.models['Thread'].findFromIdentifyingData({ - id: 100, - model: 'mail.channel', - })) - ); - // from partnerId being in needaction_partner_ids - assert.ok(message.threads.includes(messaging.inbox.thread)); - // from partnerId being in starred_partner_ids - assert.ok(message.threads.includes(messaging.starred.thread)); - const attachment = messaging.models['Attachment'].findFromIdentifyingData({ id: 750 }); - assert.ok(attachment); - assert.strictEqual(attachment.filename, "test.txt"); - assert.strictEqual(attachment.id, 750); - assert.notOk(attachment.isUploading); - assert.strictEqual(attachment.mimetype, 'text/plain'); - assert.strictEqual(attachment.name, "test.txt"); - const channel = messaging.models['Thread'].findFromIdentifyingData({ - id: 100, - model: 'mail.channel', - }); - assert.ok(channel); - assert.strictEqual(channel.model, 'mail.channel'); - assert.strictEqual(channel.id, 100); - assert.strictEqual(channel.name, "General"); - const partner = messaging.models['Partner'].findFromIdentifyingData({ id: 5 }); - assert.ok(partner); - assert.strictEqual(partner.displayName, "Demo"); - assert.strictEqual(partner.id, 5); -}); - -QUnit.test('message without body should be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test('message with body "" should be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test('message with body "

" should be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "

", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test('message with body "


" should be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "


", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test('message with body "


" should be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "


", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test(String.raw`message with body "

\n

" should be considered empty`, async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "

\n

", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test(String.raw`message with body "

\r\n\r\n

" should be considered empty`, async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "

\r\n\r\n

", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test('message with body "

" should be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "

", id: 11 }); - assert.ok(message.isEmpty); -}); - -QUnit.test(`message with body "" should not be considered empty`, async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "", id: 11 }); - assert.notOk(message.isEmpty); -}); - -QUnit.test('message with body "test" should not be considered empty', async function (assert) { - assert.expect(1); - const { messaging } = await start(); - const message = messaging.models['Message'].insert({ body: "test", id: 11 }); - assert.notOk(message.isEmpty); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/models/messaging_menu_tests.js b/addons/mail/static/tests/qunit_suite_tests/models/messaging_menu_tests.js index 7a64ecabaa6..e63e35399e0 100644 --- a/addons/mail/static/tests/qunit_suite_tests/models/messaging_menu_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/models/messaging_menu_tests.js @@ -1,39 +1,41 @@ /** @odoo-module **/ -import { start } from '@mail/../tests/helpers/test_utils'; +import { start } from "@mail/../tests/helpers/test_utils"; -import { browser } from '@web/core/browser/browser'; -import { patchWithCleanup } from '@web/../tests/helpers/utils'; +import { browser } from "@web/core/browser/browser"; +import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('models', {}, function () { -QUnit.module('messaging_menu_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("models", {}, function () { + QUnit.module("messaging_menu_tests.js"); -QUnit.test('messaging menu counter should ignore unread messages in channels that are unpinned', async function (assert) { - assert.expect(1); + QUnit.test( + "messaging menu counter should ignore unread messages in channels that are unpinned", + async function (assert) { + assert.expect(1); - patchWithCleanup(browser, { - Notification: { - ...browser.Notification, - permission: 'denied', - }, + patchWithCleanup(browser, { + Notification: { + ...browser.Notification, + permission: "denied", + }, + }); + const { messaging } = await start(); + messaging.models["Thread"].insert({ + channel: { + id: 31, + serverMessageUnreadCounter: 1, + }, + id: 31, + isServerPinned: false, + model: "mail.channel", + }); + assert.strictEqual( + messaging.messagingMenu.counter, + 0, + "messaging menu counter should ignore unread messages in channels that are unpinned" + ); + } + ); }); - const { messaging } = await start(); - messaging.models['Thread'].insert({ - channel: { - id: 31, - serverMessageUnreadCounter: 1, - }, - id: 31, - isServerPinned: false, - model: 'mail.channel', - }); - assert.strictEqual( - messaging.messagingMenu.counter, - 0, - "messaging menu counter should ignore unread messages in channels that are unpinned" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/models/messaging_tests.js b/addons/mail/static/tests/qunit_suite_tests/models/messaging_tests.js index 1d501a76a0a..bf5f5759fbd 100644 --- a/addons/mail/static/tests/qunit_suite_tests/models/messaging_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/models/messaging_tests.js @@ -1,311 +1,337 @@ /** @odoo-module **/ -import { start, startServer } from '@mail/../tests/helpers/test_utils'; +import { start, startServer } from "@mail/../tests/helpers/test_utils"; import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services"; -QUnit.module('mail', {}, function () { -QUnit.module('models', {}, function () { -QUnit.module('messaging_tests.js', {}, function () { +QUnit.module("mail", {}, function () { + QUnit.module("models", {}, function () { + QUnit.module("messaging_tests.js", {}, function () { + QUnit.test( + "openChat: display notification for partner without user", + async function (assert) { + assert.expect(2); -QUnit.test('openChat: display notification for partner without user', async function (assert) { - assert.expect(2); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const { messaging } = await start({ + services: { + notification: makeFakeNotificationService((message) => { + assert.ok( + true, + "should display a toast notification after failing to open chat" + ); + assert.strictEqual( + message, + "You can only chat with partners that have a dedicated user.", + "should display the correct information in the notification" + ); + }), + }, + }); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const { messaging } = await start({ - services: { - notification: makeFakeNotificationService(message => { - assert.ok( - true, - "should display a toast notification after failing to open chat" + await messaging.openChat({ partnerId: resPartnerId1 }); + } + ); + + QUnit.test("openChat: display notification for wrong user", async function (assert) { + assert.expect(2); + + const pyEnv = await startServer(); + pyEnv["res.users"].create({}); + const { messaging } = await start({ + services: { + notification: makeFakeNotificationService((message) => { + assert.ok( + true, + "should display a toast notification after failing to open chat" + ); + assert.strictEqual( + message, + "You can only chat with existing users.", + "should display the correct information in the notification" + ); + }), + }, + }); + + // userId not in the server data + await messaging.openChat({ userId: 4242 }); + }); + + QUnit.test("openChat: open new chat for user", async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + + const { messaging } = await start({ data: this.data }); + const partner = messaging.models["Partner"].findFromIdentifyingData({ + id: resPartnerId1, + }); + const existingChat = partner ? partner.dmChatWithCurrentPartner : undefined; + assert.notOk( + existingChat, + "a chat should not exist with the target partner initially" + ); + + await messaging.openChat({ partnerId: resPartnerId1 }); + const chat = messaging.models["Partner"].findFromIdentifyingData({ + id: resPartnerId1, + }).dmChatWithCurrentPartner; + assert.ok(chat, "a chat should exist with the target partner"); + assert.strictEqual( + chat.thread.threadViews.length, + 1, + "the chat should be displayed in a `ThreadView`" + ); + }); + + QUnit.test("openChat: open existing chat for user", async function (assert) { + assert.expect(5); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + pyEnv["res.users"].create({ partner_id: resPartnerId1 }); + const mailChannelId1 = pyEnv["mail.channel"].create({ + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: resPartnerId1 }], + ], + channel_type: "chat", + }); + const { messaging } = await start(); + const existingChat = messaging.models["Partner"].findFromIdentifyingData({ + id: resPartnerId1, + }).dmChatWithCurrentPartner; + assert.ok(existingChat, "a chat should initially exist with the target partner"); + assert.strictEqual( + existingChat.thread.threadViews.length, + 0, + "the chat should not be displayed in a `ThreadView`" + ); + + await messaging.openChat({ partnerId: resPartnerId1 }); + assert.ok(existingChat, "a chat should still exist with the target partner"); + assert.strictEqual( + existingChat.id, + mailChannelId1, + "the chat should be the existing chat" ); assert.strictEqual( - message, - "You can only chat with partners that have a dedicated user.", - "should display the correct information in the notification" + existingChat.thread.threadViews.length, + 1, + "the chat should now be displayed in a `ThreadView`" ); - }), - }, + }); + + QUnit.test("rpc: create from args", async function (assert) { + assert.expect(1); + + const { messaging, pyEnv } = await start(); + const partnerId = await messaging.rpc({ + method: "create", + model: "res.partner", + args: [[{ name: "foo" }]], + }); + const [partner] = pyEnv["res.partner"].searchRead([["id", "=", partnerId]]); + assert.strictEqual("foo", partner.name); + }); + + QUnit.test("rpc: create from kwargs", async function (assert) { + assert.expect(1); + + const { messaging, pyEnv } = await start(); + const partnerId = await messaging.rpc({ + method: "create", + model: "res.partner", + kwargs: { + vals_list: [{ name: "foo" }], + }, + }); + const [partner] = pyEnv["res.partner"].searchRead([["id", "=", partnerId]]); + assert.strictEqual("foo", partner.name); + }); + + QUnit.test("rpc: read from args", async function (assert) { + assert.expect(2); + + const { messaging, pyEnv } = await start(); + const partnerId = pyEnv["res.partner"].create({ name: "foo" }); + const [partner] = await messaging.rpc({ + method: "read", + model: "res.partner", + args: [[partnerId], ["id", "name"]], + }); + assert.strictEqual(partner.name, "foo"); + assert.strictEqual(Object.keys(partner).length, 2); + }); + + QUnit.test("rpc: read from kwargs", async function (assert) { + assert.expect(2); + + const { messaging, pyEnv } = await start(); + const partnerId = pyEnv["res.partner"].create({ name: "foo" }); + const [partner] = await messaging.rpc({ + method: "read", + model: "res.partner", + args: [[partnerId]], + kwargs: { + fields: ["id", "name"], + }, + }); + assert.strictEqual(partner.name, "foo"); + assert.strictEqual(Object.keys(partner).length, 2); + }); + + QUnit.test("rpc: readGroup from args", async function (assert) { + assert.expect(4); + + const { messaging, pyEnv } = await start(); + const partnerIds = pyEnv["res.partner"].create([ + { name: "foo" }, + { name: "foo" }, + { name: "bar" }, + { name: "bar" }, + ]); + const readGroupDomain = [["id", "in", partnerIds]]; + const readGroupFields = ["name"]; + const readGroupGroupBy = ["name"]; + + const [firstGroup, secondGroup] = await messaging.rpc({ + method: "read_group", + model: "res.partner", + args: [readGroupDomain, readGroupFields, readGroupGroupBy], + }); + assert.strictEqual(firstGroup.name, "bar"); + assert.strictEqual(firstGroup.name_count, 2); + assert.strictEqual(secondGroup.name, "foo"); + assert.strictEqual(secondGroup.name_count, 2); + }); + + QUnit.test("rpc: readGroup from kwargs", async function (assert) { + assert.expect(4); + + const { messaging, pyEnv } = await start(); + const partnerIds = pyEnv["res.partner"].create([ + { name: "foo" }, + { name: "foo" }, + { name: "bar" }, + { name: "bar" }, + ]); + const readGroupDomain = [["id", "in", partnerIds]]; + const readGroupFields = ["name"]; + const readGroupGroupBy = ["name"]; + + const [firstGroup, secondGroup] = await messaging.rpc({ + method: "read_group", + model: "res.partner", + kwargs: { + domain: readGroupDomain, + fields: readGroupFields, + groupBy: readGroupGroupBy, + }, + }); + assert.strictEqual(firstGroup.name, "bar"); + assert.strictEqual(firstGroup.name_count, 2); + assert.strictEqual(secondGroup.name, "foo"); + assert.strictEqual(secondGroup.name_count, 2); + }); + + QUnit.test("rpc: search from args", async function (assert) { + assert.expect(1); + + const { messaging, pyEnv } = await start(); + const partnerIds = pyEnv["res.partner"].create([{ name: "foo" }, { name: "bar" }]); + const searchDomain = [["id", "in", partnerIds]]; + + const serverPartnerIds = await messaging.rpc({ + model: "res.partner", + method: "search", + args: [searchDomain], + }); + assert.deepEqual(partnerIds, serverPartnerIds); + }); + + QUnit.test("rpc: search from kwargs", async function (assert) { + assert.expect(1); + + const { messaging, pyEnv } = await start(); + const partnerIds = pyEnv["res.partner"].create([{ name: "foo" }, { name: "bar" }]); + const searchDomain = [["id", "in", partnerIds]]; + + const serverPartnerIds = await messaging.rpc({ + model: "res.partner", + method: "search", + kwargs: { + domain: searchDomain, + }, + }); + assert.deepEqual(partnerIds, serverPartnerIds); + }); + + QUnit.test("rpc: searchRead from args", async function (assert) { + assert.expect(2); + + const { messaging, pyEnv } = await start(); + const partnerIds = pyEnv["res.partner"].create([{ name: "foo" }, { name: "bar" }]); + const searchReadDomain = [["id", "in", partnerIds]]; + const searchReadFields = ["id", "name"]; + + const [firstPartner, secondPartner] = await messaging.rpc({ + model: "res.partner", + method: "search_read", + args: [searchReadDomain, searchReadFields], + }); + assert.strictEqual(firstPartner.name, "foo"); + assert.strictEqual(secondPartner.name, "bar"); + }); + + QUnit.test("rpc: write from args", async function (assert) { + assert.expect(1); + + const { messaging, pyEnv } = await start(); + const partnerId = pyEnv["res.partner"].create({ name: "foo" }); + await messaging.rpc({ + method: "write", + model: "res.partner", + args: [[partnerId], { name: "bar" }], + }); + const [partner] = pyEnv["res.partner"].searchRead([["id", "=", partnerId]]); + assert.strictEqual(partner.name, "bar"); + }); + + QUnit.test("rpc: write from kwargs", async function (assert) { + assert.expect(1); + + const { messaging, pyEnv } = await start(); + const partnerId = pyEnv["res.partner"].create({ name: "foo" }); + await messaging.rpc({ + method: "write", + model: "res.partner", + args: [[partnerId]], + kwargs: { + vals: { name: "bar" }, + }, + }); + const [partner] = pyEnv["res.partner"].searchRead([["id", "=", partnerId]]); + assert.strictEqual(partner.name, "bar"); + }); + + QUnit.test("rpc: unlink", async function (assert) { + assert.expect(1); + + const { messaging, pyEnv } = await start(); + const partnerId = pyEnv["res.partner"].create({ name: "foo" }); + await messaging.rpc({ + method: "unlink", + model: "res.partner", + args: [[partnerId]], + }); + const searchReadResults = pyEnv["res.partner"].searchRead([["id", "=", partnerId]]); + assert.strictEqual(searchReadResults.length, 0); + }); + }); }); - - await messaging.openChat({ partnerId: resPartnerId1 }); -}); - -QUnit.test('openChat: display notification for wrong user', async function (assert) { - assert.expect(2); - - const pyEnv = await startServer(); - pyEnv['res.users'].create({}); - const { messaging } = await start({ - services: { - notification: makeFakeNotificationService(message => { - assert.ok( - true, - "should display a toast notification after failing to open chat" - ); - assert.strictEqual( - message, - "You can only chat with existing users.", - "should display the correct information in the notification" - ); - }), - }, - }); - - // userId not in the server data - await messaging.openChat({ userId: 4242 }); -}); - -QUnit.test('openChat: open new chat for user', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - - const { messaging } = await start({ data: this.data }); - const partner = messaging.models['Partner'].findFromIdentifyingData({ id: resPartnerId1 }); - const existingChat = partner ? partner.dmChatWithCurrentPartner : undefined; - assert.notOk(existingChat, 'a chat should not exist with the target partner initially'); - - await messaging.openChat({ partnerId: resPartnerId1 }); - const chat = messaging.models['Partner'].findFromIdentifyingData({ id: resPartnerId1 }).dmChatWithCurrentPartner; - assert.ok(chat, 'a chat should exist with the target partner'); - assert.strictEqual(chat.thread.threadViews.length, 1, 'the chat should be displayed in a `ThreadView`'); -}); - -QUnit.test('openChat: open existing chat for user', async function (assert) { - assert.expect(5); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - pyEnv['res.users'].create({ partner_id: resPartnerId1 }); - const mailChannelId1 = pyEnv['mail.channel'].create({ - channel_member_ids: [ - [0, 0, { partner_id: pyEnv.currentPartnerId }], - [0, 0, { partner_id: resPartnerId1 }], - ], - channel_type: "chat", - }); - const { messaging } = await start(); - const existingChat = messaging.models['Partner'].findFromIdentifyingData({ id: resPartnerId1 }).dmChatWithCurrentPartner; - assert.ok(existingChat, 'a chat should initially exist with the target partner'); - assert.strictEqual(existingChat.thread.threadViews.length, 0, 'the chat should not be displayed in a `ThreadView`'); - - await messaging.openChat({ partnerId: resPartnerId1 }); - assert.ok(existingChat, 'a chat should still exist with the target partner'); - assert.strictEqual(existingChat.id, mailChannelId1, 'the chat should be the existing chat'); - assert.strictEqual(existingChat.thread.threadViews.length, 1, 'the chat should now be displayed in a `ThreadView`'); -}); - -QUnit.test('rpc: create from args', async function (assert) { - assert.expect(1); - - const { messaging, pyEnv } = await start(); - const partnerId = await messaging.rpc({ - method: 'create', - model: 'res.partner', - args: [[{ name: 'foo' }]], - }); - const [partner] = pyEnv['res.partner'].searchRead([['id', '=', partnerId]]); - assert.strictEqual('foo', partner.name); -}); - -QUnit.test('rpc: create from kwargs', async function (assert) { - assert.expect(1); - - const { messaging, pyEnv } = await start(); - const partnerId = await messaging.rpc({ - method: 'create', - model: 'res.partner', - kwargs: { - vals_list: [{ name: 'foo' }], - }, - }); - const [partner] = pyEnv['res.partner'].searchRead([['id', '=', partnerId]]); - assert.strictEqual('foo', partner.name); -}); - -QUnit.test('rpc: read from args', async function (assert) { - assert.expect(2); - - const { messaging, pyEnv } = await start(); - const partnerId = pyEnv['res.partner'].create({ name: 'foo' }); - const [partner] = await messaging.rpc({ - method: 'read', - model: 'res.partner', - args: [[partnerId], ['id', 'name']], - }); - assert.strictEqual(partner.name, 'foo'); - assert.strictEqual(Object.keys(partner).length, 2); -}); - -QUnit.test('rpc: read from kwargs', async function (assert) { - assert.expect(2); - - const { messaging, pyEnv } = await start(); - const partnerId = pyEnv['res.partner'].create({ name: 'foo' }); - const [partner] = await messaging.rpc({ - method: 'read', - model: 'res.partner', - args: [[partnerId]], - kwargs: { - fields: ['id', 'name'], - } - }); - assert.strictEqual(partner.name, 'foo'); - assert.strictEqual(Object.keys(partner).length, 2); -}); - -QUnit.test('rpc: readGroup from args', async function (assert) { - assert.expect(4); - - const { messaging, pyEnv } = await start(); - const partnerIds = pyEnv['res.partner'].create([ - { name: 'foo' }, - { name: 'foo' }, - { name: 'bar' }, - { name: 'bar' }, - ]); - const readGroupDomain = [['id', 'in', partnerIds]]; - const readGroupFields = ['name']; - const readGroupGroupBy = ['name']; - - const [firstGroup, secondGroup] = await messaging.rpc({ - method: 'read_group', - model: 'res.partner', - args: [readGroupDomain, readGroupFields, readGroupGroupBy], - }); - assert.strictEqual(firstGroup.name, 'bar'); - assert.strictEqual(firstGroup.name_count, 2); - assert.strictEqual(secondGroup.name, 'foo'); - assert.strictEqual(secondGroup.name_count, 2); -}); - -QUnit.test('rpc: readGroup from kwargs', async function (assert) { - assert.expect(4); - - const { messaging, pyEnv } = await start(); - const partnerIds = pyEnv['res.partner'].create([ - { name: 'foo' }, - { name: 'foo' }, - { name: 'bar' }, - { name: 'bar' }, - ]); - const readGroupDomain = [['id', 'in', partnerIds]]; - const readGroupFields = ['name']; - const readGroupGroupBy = ['name']; - - const [firstGroup, secondGroup] = await messaging.rpc({ - method: 'read_group', - model: 'res.partner', - kwargs: { - domain: readGroupDomain, - fields: readGroupFields, - groupBy: readGroupGroupBy, - }, - }); - assert.strictEqual(firstGroup.name, 'bar'); - assert.strictEqual(firstGroup.name_count, 2); - assert.strictEqual(secondGroup.name, 'foo'); - assert.strictEqual(secondGroup.name_count, 2); -}); - -QUnit.test('rpc: search from args', async function (assert) { - assert.expect(1); - - const { messaging, pyEnv } = await start(); - const partnerIds = pyEnv['res.partner'].create([{ name: 'foo' }, { name: 'bar' }]); - const searchDomain = [['id', 'in', partnerIds]]; - - const serverPartnerIds = await messaging.rpc({ - model: 'res.partner', - method: 'search', - args: [searchDomain], - }); - assert.deepEqual(partnerIds, serverPartnerIds); -}); - -QUnit.test('rpc: search from kwargs', async function (assert) { - assert.expect(1); - - const { messaging, pyEnv } = await start(); - const partnerIds = pyEnv['res.partner'].create([{ name: 'foo' }, { name: 'bar' }]); - const searchDomain = [['id', 'in', partnerIds]]; - - const serverPartnerIds = await messaging.rpc({ - model: 'res.partner', - method: 'search', - kwargs: { - domain: searchDomain, - }, - }); - assert.deepEqual(partnerIds, serverPartnerIds); -}); - -QUnit.test('rpc: searchRead from args', async function (assert) { - assert.expect(2); - - const { messaging, pyEnv } = await start(); - const partnerIds = pyEnv['res.partner'].create([{ name: 'foo' }, { name: 'bar' }]); - const searchReadDomain = [['id', 'in', partnerIds]]; - const searchReadFields = ['id', 'name']; - - const [firstPartner, secondPartner] = await messaging.rpc({ - model: 'res.partner', - method: 'search_read', - args: [searchReadDomain, searchReadFields], - }); - assert.strictEqual(firstPartner.name, 'foo'); - assert.strictEqual(secondPartner.name, 'bar'); -}); - -QUnit.test('rpc: write from args', async function (assert) { - assert.expect(1); - - const { messaging, pyEnv } = await start(); - const partnerId = pyEnv['res.partner'].create({ name: 'foo' }); - await messaging.rpc({ - method: 'write', - model: 'res.partner', - args: [[partnerId], { name: 'bar' }], - }); - const [partner] = pyEnv['res.partner'].searchRead([['id', '=', partnerId]]); - assert.strictEqual(partner.name, 'bar'); -}); - -QUnit.test('rpc: write from kwargs', async function (assert) { - assert.expect(1); - - const { messaging, pyEnv } = await start(); - const partnerId = pyEnv['res.partner'].create({ name: 'foo' }); - await messaging.rpc({ - method: 'write', - model: 'res.partner', - args: [[partnerId]], - kwargs: { - vals: { name: 'bar' }, - }, - }); - const [partner] = pyEnv['res.partner'].searchRead([['id', '=', partnerId]]); - assert.strictEqual(partner.name, 'bar'); -}); - -QUnit.test('rpc: unlink', async function (assert) { - assert.expect(1); - - const { messaging, pyEnv } = await start(); - const partnerId = pyEnv['res.partner'].create({ name: 'foo' }); - await messaging.rpc({ - method: 'unlink', - model: 'res.partner', - args: [[partnerId]], - }); - const searchReadResults = pyEnv['res.partner'].searchRead([['id', '=', partnerId]]); - assert.strictEqual(searchReadResults.length, 0); -}); - -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/utils/mail_utils_tests.js b/addons/mail/static/tests/qunit_suite_tests/utils/mail_utils_tests.js index c6cff8891e8..9bc7c39b4e9 100644 --- a/addons/mail/static/tests/qunit_suite_tests/utils/mail_utils_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/utils/mail_utils_tests.js @@ -1,109 +1,111 @@ /** @odoo-module **/ -import * as utils from '@mail/js/utils'; +import * as utils from "@mail/js/utils"; -QUnit.module('mail', {}, function () { +QUnit.module("mail", {}, function () { + QUnit.module("Mail utils"); -QUnit.module('Mail utils'); + QUnit.test("add_link utility function", function (assert) { + assert.expect(19); -QUnit.test('add_link utility function', function (assert) { - assert.expect(19); + var testInputs = { + "http://admin:password@example.com:8/%2020": true, + "https://admin:password@example.com/test": true, + "www.example.com:8/test": true, + "https://127.0.0.5:8069": true, + "www.127.0.0.5": false, + "should.notmatch": false, + "fhttps://test.example.com/test": false, + "https://www.transifex.com/odoo/odoo-11/translate/#fr/lunch?q=text%3A'La+Tartiflette'": true, + "https://www.transifex.com/odoo/odoo-11/translate/#fr/$/119303430?q=text%3ATartiflette": true, + "https://tenor.com/view/chỗgiặt-dog-smile-gif-13860250": true, + "http://www.boîtenoire.be": true, + }; - var testInputs = { - 'http://admin:password@example.com:8/%2020': true, - 'https://admin:password@example.com/test': true, - 'www.example.com:8/test': true, - 'https://127.0.0.5:8069': true, - 'www.127.0.0.5': false, - 'should.notmatch': false, - 'fhttps://test.example.com/test': false, - "https://www.transifex.com/odoo/odoo-11/translate/#fr/lunch?q=text%3A'La+Tartiflette'": true, - 'https://www.transifex.com/odoo/odoo-11/translate/#fr/$/119303430?q=text%3ATartiflette': true, - 'https://tenor.com/view/chỗgiặt-dog-smile-gif-13860250': true, - 'http://www.boîtenoire.be': true, - }; + _.each(testInputs, function (willLinkify, content) { + var output = utils.parseAndTransform(content, utils.addLink); + if (willLinkify) { + assert.strictEqual(output.indexOf(""), + output.length - 4, + "Link should match the whole text" + ); + } else { + assert.strictEqual(output.indexOf("'), (output.length - 4), "Link should match the whole text"); - } else { - assert.strictEqual(output.indexOf('some text ' tag" + ); + assert.ok( + linkified.endsWith("

"), + "linkified text should end with closing '' tag" + ); + + // linkify may add some attributes. Since we do not care of their exact + // stringified representation, we continue deeper assertion with query + // selectors. + const fragment = document.createDocumentFragment(); + const div = document.createElement("div"); + fragment.appendChild(div); + div.innerHTML = linkified; + assert.strictEqual( + div.textContent, + "some text https://somelink.com", + "linkified text should have same text content as non-linkified version" + ); + assert.strictEqual( + div.querySelectorAll(":scope a").length, + 1, + "linkified text should have an tag" + ); + assert.strictEqual( + div.querySelector(":scope a").textContent, + "https://somelink.com", + "text content of link should be equivalent of its non-linkified version" + ); + }); + + QUnit.test("addLink: linkify inside text node (2 occurrences)", function (assert) { + assert.expect(4); + + // linkify may add some attributes. Since we do not care of their exact + // stringified representation, we continue deeper assertion with query + // selectors. + const content = "

some text https://somelink.com and again https://somelink2.com ...

"; + const linkified = utils.parseAndTransform(content, utils.addLink); + const fragment = document.createDocumentFragment(); + const div = document.createElement("div"); + fragment.appendChild(div); + div.innerHTML = linkified; + assert.strictEqual( + div.textContent, + "some text https://somelink.com and again https://somelink2.com ...", + "linkified text should have same text content as non-linkified version" + ); + assert.strictEqual( + div.querySelectorAll(":scope a").length, + 2, + "linkified text should have 2
tags" + ); + assert.strictEqual( + div.querySelectorAll(":scope a")[0].textContent, + "https://somelink.com", + "text content of 1st link should be equivalent to its non-linkified version" + ); + assert.strictEqual( + div.querySelectorAll(":scope a")[1].textContent, + "https://somelink2.com", + "text content of 2nd link should be equivalent to its non-linkified version" + ); }); }); - -QUnit.test('addLink: linkify inside text node (1 occurrence)', function (assert) { - assert.expect(5); - - const content = '

some text https://somelink.com

'; - const linkified = utils.parseAndTransform(content, utils.addLink); - assert.ok( - linkified.startsWith('

some text ' tag" - ); - assert.ok( - linkified.endsWith('

'), - "linkified text should end with closing '' tag" - ); - - // linkify may add some attributes. Since we do not care of their exact - // stringified representation, we continue deeper assertion with query - // selectors. - const fragment = document.createDocumentFragment(); - const div = document.createElement('div'); - fragment.appendChild(div); - div.innerHTML = linkified; - assert.strictEqual( - div.textContent, - 'some text https://somelink.com', - "linkified text should have same text content as non-linkified version" - ); - assert.strictEqual( - div.querySelectorAll(':scope a').length, - 1, - "linkified text should have an tag" - ); - assert.strictEqual( - div.querySelector(':scope a').textContent, - 'https://somelink.com', - "text content of link should be equivalent of its non-linkified version" - ); -}); - -QUnit.test('addLink: linkify inside text node (2 occurrences)', function (assert) { - assert.expect(4); - - // linkify may add some attributes. Since we do not care of their exact - // stringified representation, we continue deeper assertion with query - // selectors. - const content = '

some text https://somelink.com and again https://somelink2.com ...

'; - const linkified = utils.parseAndTransform(content, utils.addLink); - const fragment = document.createDocumentFragment(); - const div = document.createElement('div'); - fragment.appendChild(div); - div.innerHTML = linkified; - assert.strictEqual( - div.textContent, - 'some text https://somelink.com and again https://somelink2.com ...', - "linkified text should have same text content as non-linkified version" - ); - assert.strictEqual( - div.querySelectorAll(':scope a').length, - 2, - "linkified text should have 2
tags" - ); - assert.strictEqual( - div.querySelectorAll(':scope a')[0].textContent, - 'https://somelink.com', - "text content of 1st link should be equivalent to its non-linkified version" - ); - assert.strictEqual( - div.querySelectorAll(':scope a')[1].textContent, - 'https://somelink2.com', - "text content of 2nd link should be equivalent to its non-linkified version" - ); -}); - -}); diff --git a/addons/mail/static/tests/qunit_suite_tests/utils/throttle_tests.js b/addons/mail/static/tests/qunit_suite_tests/utils/throttle_tests.js index 76de0fa74da..6258e2241d9 100644 --- a/addons/mail/static/tests/qunit_suite_tests/utils/throttle_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/utils/throttle_tests.js @@ -1,174 +1,173 @@ /** @odoo-module **/ -import { start } from '@mail/../tests/helpers/test_utils'; -import { nextTick } from '@mail/utils/utils'; +import { start } from "@mail/../tests/helpers/test_utils"; +import { nextTick } from "@mail/utils/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('utils', {}, function () { -QUnit.module('throttle', {}, function () { -QUnit.module('throttle_tests.js', {}); +QUnit.module("mail", {}, function () { + QUnit.module("utils", {}, function () { + QUnit.module("throttle", {}, function () { + QUnit.module("throttle_tests.js", {}); -QUnit.test('single call', async function (assert) { - assert.expect(3); + QUnit.test("single call", async function (assert) { + assert.expect(3); - const { advanceTime, messaging } = await start({ - hasTimeControl: true, + const { advanceTime, messaging } = await start({ + hasTimeControl: true, + }); + let hasInvokedFunc = false; + const throttle = messaging.models["Throttle"].insert({ + func: () => (hasInvokedFunc = true), + qunitTestOwner1: {}, + }); + assert.notOk( + hasInvokedFunc, + "func should not have been invoked on immediate throttle initialization" + ); + + await advanceTime(0); + assert.notOk( + hasInvokedFunc, + "func should not have been invoked from throttle initialization after 0ms" + ); + + throttle.do(); + await nextTick(); + assert.ok( + hasInvokedFunc, + "func should have been immediately invoked on first throttle call" + ); + }); + + QUnit.test("2nd (throttled) call", async function (assert) { + assert.expect(4); + + const { advanceTime, messaging } = await start({ + hasTimeControl: true, + }); + let funcCalledAmount = 0; + const throttle = messaging.models["Throttle"].insert({ + func: () => funcCalledAmount++, + qunitTestOwner2: {}, + }); + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "throttle call return should forward result of inner func 1" + ); + + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" + ); + + await advanceTime(999); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been invoked after 999ms of 2nd call (throttled with 1s internal clock)" + ); + + await advanceTime(1); + assert.strictEqual( + funcCalledAmount, + 2, + "throttle call return should forward result of inner func 2" + ); + }); + + QUnit.test("throttled call reinvocation", async function (assert) { + assert.expect(4); + + const { advanceTime, messaging } = await start({ + hasTimeControl: true, + }); + let funcCalledAmount = 0; + const throttle = messaging.models["Throttle"].insert({ + func: () => funcCalledAmount++, + qunitTestOwner2: {}, + }); + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "throttle call return should forward result of inner func 1" + ); + + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" + ); + + await advanceTime(999); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been invoked after 999ms of 2nd call (throttled with 1s internal clock)" + ); + + throttle.do(); + await nextTick(); + await advanceTime(1); + assert.strictEqual( + funcCalledAmount, + 2, + "throttle call return should forward result of inner func 2" + ); + }); + + QUnit.test("clear throttled call", async function (assert) { + assert.expect(4); + + const { advanceTime, messaging } = await start({ + hasTimeControl: true, + }); + let funcCalledAmount = 0; + const throttle = messaging.models["Throttle"].insert({ + func: () => funcCalledAmount++, + qunitTestOwner2: {}, + }); + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should have been invoked on 1st call (immediate return)" + ); + + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" + ); + + await advanceTime(500); + assert.strictEqual( + funcCalledAmount, + 1, + "inner function of throttle should not have been invoked after 500ms of 2nd call (throttled with 1s internal clock)" + ); + + throttle.clear(); + await nextTick(); + throttle.do(); + await nextTick(); + assert.strictEqual( + funcCalledAmount, + 2, + "3rd throttle function call should have invoke inner function immediately (`.clear()` flushes throttle)" + ); + }); + }); }); - let hasInvokedFunc = false; - const throttle = messaging.models['Throttle'].insert({ - func: () => hasInvokedFunc = true, - qunitTestOwner1: {}, - }); - assert.notOk( - hasInvokedFunc, - "func should not have been invoked on immediate throttle initialization" - ); - - await advanceTime(0); - assert.notOk( - hasInvokedFunc, - "func should not have been invoked from throttle initialization after 0ms" - ); - - throttle.do(); - await nextTick(); - assert.ok( - hasInvokedFunc, - "func should have been immediately invoked on first throttle call" - ); -}); - -QUnit.test('2nd (throttled) call', async function (assert) { - assert.expect(4); - - const { advanceTime, messaging } = await start({ - hasTimeControl: true, - }); - let funcCalledAmount = 0; - const throttle = messaging.models['Throttle'].insert({ - func: () => funcCalledAmount++, - qunitTestOwner2: {}, - }); - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "throttle call return should forward result of inner func 1" - ); - - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" - ); - - await advanceTime(999); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been invoked after 999ms of 2nd call (throttled with 1s internal clock)" - ); - - await advanceTime(1); - assert.strictEqual( - funcCalledAmount, - 2, - "throttle call return should forward result of inner func 2" - ); -}); - -QUnit.test('throttled call reinvocation', async function (assert) { - assert.expect(4); - - const { advanceTime, messaging } = await start({ - hasTimeControl: true, - }); - let funcCalledAmount = 0; - const throttle = messaging.models['Throttle'].insert({ - func: () => funcCalledAmount++, - qunitTestOwner2: {}, - }); - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "throttle call return should forward result of inner func 1" - ); - - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" - ); - - await advanceTime(999); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been invoked after 999ms of 2nd call (throttled with 1s internal clock)" - ); - - throttle.do(); - await nextTick(); - await advanceTime(1); - assert.strictEqual( - funcCalledAmount, - 2, - "throttle call return should forward result of inner func 2" - ); -}); - -QUnit.test('clear throttled call', async function (assert) { - assert.expect(4); - - const { advanceTime, messaging } = await start({ - hasTimeControl: true, - }); - let funcCalledAmount = 0; - const throttle = messaging.models['Throttle'].insert({ - func: () => funcCalledAmount++, - qunitTestOwner2: {}, - }); - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should have been invoked on 1st call (immediate return)" - ); - - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been immediately invoked after 2nd call immediately after 1st call (throttled with 1s internal clock)" - ); - - await advanceTime(500); - assert.strictEqual( - funcCalledAmount, - 1, - "inner function of throttle should not have been invoked after 500ms of 2nd call (throttled with 1s internal clock)" - ); - - throttle.clear(); - await nextTick(); - throttle.do(); - await nextTick(); - assert.strictEqual( - funcCalledAmount, - 2, - "3rd throttle function call should have invoke inner function immediately (`.clear()` flushes throttle)" - ); -}); - -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js b/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js index 3c02afdd39e..9bced8aba05 100644 --- a/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/utils/timer_tests.js @@ -1,68 +1,58 @@ /** @odoo-module **/ -import { start } from '@mail/../tests/helpers/test_utils'; +import { start } from "@mail/../tests/helpers/test_utils"; -QUnit.module('mail', {}, function () { -QUnit.module('utils', {}, function () { -QUnit.module('timer', {}, function () { -QUnit.module('timer_tests.js', {}); +QUnit.module("mail", {}, function () { + QUnit.module("utils", {}, function () { + QUnit.module("timer", {}, function () { + QUnit.module("timer_tests.js", {}); -QUnit.test('timer insert (duration: 0ms)', async function (assert) { - assert.expect(2); + QUnit.test("timer insert (duration: 0ms)", async function (assert) { + assert.expect(2); - const { advanceTime, messaging } = await start({ hasTimeControl: true }); - const timer = messaging.models['Timer'].insert({ - qunitTestOwner1: {}, + const { advanceTime, messaging } = await start({ hasTimeControl: true }); + const timer = messaging.models["Timer"].insert({ + qunitTestOwner1: {}, + }); + assert.ok( + timer.timeoutId, + "timer should not have timed out immediately after insert" + ); + + await advanceTime(0); + assert.notOk(timer.timeoutId, "timer should have timed out on insert after 0ms"); + }); + + QUnit.test("timer insert (duration: 1000s)", async function (assert) { + assert.expect(5); + + const { advanceTime, messaging } = await start({ hasTimeControl: true }); + const timer = messaging.models["Timer"].insert({ + qunitTestOwner2: {}, + }); + assert.ok( + timer.timeoutId, + "timer should not have timed out immediately after insert" + ); + + await advanceTime(0); + assert.ok(timer.timeoutId, "timer should not have timed out on insert after 0ms"); + + await advanceTime(1000); + assert.ok( + timer.timeoutId, + "timer should not have timed out on insert after 1000ms" + ); + + await advanceTime(998 * 1000 + 999); + assert.ok( + timer.timeoutId, + "timer should not have timed out on insert after 9999ms" + ); + + await advanceTime(1); + assert.notOk(timer.timeoutId, "timer should have timed out on insert after 10s"); + }); + }); }); - assert.ok( - timer.timeoutId, - "timer should not have timed out immediately after insert" - ); - - await advanceTime(0); - assert.notOk( - timer.timeoutId, - "timer should have timed out on insert after 0ms" - ); -}); - -QUnit.test('timer insert (duration: 1000s)', async function (assert) { - assert.expect(5); - - const { advanceTime, messaging } = await start({ hasTimeControl: true }); - const timer = messaging.models['Timer'].insert({ - qunitTestOwner2: {}, - }); - assert.ok( - timer.timeoutId, - "timer should not have timed out immediately after insert" - ); - - await advanceTime(0); - assert.ok( - timer.timeoutId, - "timer should not have timed out on insert after 0ms" - ); - - await advanceTime(1000); - assert.ok( - timer.timeoutId, - "timer should not have timed out on insert after 1000ms" - ); - - await advanceTime(998 * 1000 + 999); - assert.ok( - timer.timeoutId, - "timer should not have timed out on insert after 9999ms" - ); - - await advanceTime(1); - assert.notOk( - timer.timeoutId, - "timer should have timed out on insert after 10s" - ); -}); - -}); -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js b/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js index 408cab6ae13..31cb7d27f36 100644 --- a/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/webclient/commands/mail_providers_tests.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import { afterNextRender, start, startServer } from '@mail/../tests/helpers/test_utils'; -import { editSearchBar } from '@web/../tests/core/commands/command_service_tests'; +import { afterNextRender, start, startServer } from "@mail/../tests/helpers/test_utils"; +import { editSearchBar } from "@web/../tests/core/commands/command_service_tests"; import { click, nextTick, triggerHotkey } from "@web/../tests/helpers/utils"; import { commandService } from "@web/core/commands/command_service"; import { registry } from "@web/core/registry"; @@ -9,66 +9,78 @@ import { registry } from "@web/core/registry"; const serviceRegistry = registry.category("services"); const commandSetupRegistry = registry.category("command_setup"); -QUnit.module('mail', {}, function () { - QUnit.module('webclient', function () { - QUnit.module('commands', function () { - QUnit.module('mail_providers_tests.js', { - beforeEach() { - serviceRegistry.add("command", commandService); - registry.category("command_categories").add("default", { label: ("default") }); - }, - }); +QUnit.module("mail", {}, function () { + QUnit.module("webclient", function () { + QUnit.module("commands", function () { + QUnit.module("mail_providers_tests.js", { + beforeEach() { + serviceRegistry.add("command", commandService); + registry.category("command_categories").add("default", { label: "default" }); + }, + }); - QUnit.test('open the chatWindow of a user from the command palette', async function (assert) { - assert.expect(1); + QUnit.test( + "open the chatWindow of a user from the command palette", + async function (assert) { + assert.expect(1); - const { advanceTime } = await start({ - hasTimeControl: true, + const { advanceTime } = await start({ + hasTimeControl: true, + }); + triggerHotkey("control+k"); + await nextTick(); + + // Switch to partners + await editSearchBar("@"); + await afterNextRender(() => + advanceTime(commandSetupRegistry.get("@").debounceDelay) + ); + + await afterNextRender(() => click(document.body, ".o_command.focused")); + assert.containsOnce(document.body, ".o_ChatWindow"); + } + ); + + QUnit.test( + "open the chatWindow of a channel from the command palette", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + pyEnv["mail.channel"].create({ + name: "general", + }); + pyEnv["mail.channel"].create({ + name: "project", + }); + const { advanceTime } = await start({ + hasTimeControl: true, + }); + triggerHotkey("control+k"); + await nextTick(); + + // Switch to channels + await editSearchBar("#"); + await afterNextRender(() => + advanceTime(commandSetupRegistry.get("#").debounceDelay) + ); + + assert.deepEqual( + [...document.querySelectorAll(".o_command_palette .o_command")].map( + (el) => el.textContent + ), + ["general", "project"] + ); + + await afterNextRender(() => click(document.body, ".o_command.focused")); + assert.containsOnce(document.body, ".o_ChatWindow"); + assert.strictEqual( + document.querySelector(".o_ChatWindow .o_ChatWindowHeaderView_name") + .textContent, + "general" + ); + } + ); }); - triggerHotkey("control+k"); - await nextTick(); - - // Switch to partners - await editSearchBar("@"); - await afterNextRender(() => advanceTime(commandSetupRegistry.get('@').debounceDelay)); - - await afterNextRender(() => click(document.body, ".o_command.focused")); - assert.containsOnce(document.body, ".o_ChatWindow"); - }); - - QUnit.test('open the chatWindow of a channel from the command palette', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - pyEnv['mail.channel'].create({ - name: "general", - }); - pyEnv['mail.channel'].create({ - name: "project", - }); - const { advanceTime } = await start({ - hasTimeControl: true, - }); - triggerHotkey("control+k"); - await nextTick(); - - // Switch to channels - await editSearchBar("#"); - await afterNextRender(() => advanceTime(commandSetupRegistry.get('#').debounceDelay)); - - assert.deepEqual( - [...document.querySelectorAll(".o_command_palette .o_command")].map((el) => el.textContent), - [ - "general", - "project" - ], - ); - - await afterNextRender(() => click(document.body, ".o_command.focused")); - assert.containsOnce(document.body, ".o_ChatWindow"); - assert.strictEqual(document.querySelector(".o_ChatWindow .o_ChatWindowHeaderView_name").textContent, "general"); - }); - - }); }); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js b/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js index e5ad9ce941d..0b93da4fa89 100644 --- a/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/widgets/form_renderer_tests.js @@ -1,503 +1,440 @@ /** @odoo-module **/ -import { makeDeferred } from '@mail/utils/deferred'; -import { patchUiSize, SIZES } from '@mail/../tests/helpers/patch_ui_size'; +import { makeDeferred } from "@mail/utils/deferred"; +import { patchUiSize, SIZES } from "@mail/../tests/helpers/patch_ui_size"; import { afterNextRender, isScrolledToBottom, nextAnimationFrame, start, startServer, -} from '@mail/../tests/helpers/test_utils'; +} from "@mail/../tests/helpers/test_utils"; -import fieldRegistry from 'web.field_registry'; -import { dom, makeTestPromise, nextTick } from 'web.test_utils'; +import fieldRegistry from "web.field_registry"; +import { dom, makeTestPromise, nextTick } from "web.test_utils"; import { registerCleanup } from "@web/../tests/helpers/cleanup"; const { triggerEvent } = dom; -QUnit.module('mail', {}, function () { -QUnit.module('widgets', {}, function () { -QUnit.module('form_renderer_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("widgets", {}, function () { + QUnit.module("form_renderer_tests.js"); -QUnit.test('[technical] spinner when messaging is not created', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - */ - assert.expect(3); + QUnit.test("[technical] spinner when messaging is not created", async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(3); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "second partner", - }); - const views = { - 'res.partner,false,form': - `
+ const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "second partner", + }); + const views = { + "res.partner,false,form": `
`, - }; - const { openView } = await start({ - messagingBeforeCreationDeferred: makeDeferred(), // block messaging creation - serverData: { views }, - waitUntilMessagingCondition: 'none', - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ChatterContainer', - "should display chatter container even when messaging is not created yet" - ); - assert.containsOnce( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should not display any chatter when messaging not created" - ); - assert.strictEqual( - document.querySelector('.o_ChatterContainer').textContent, - "Please wait...", - "chatter container should display spinner when messaging not yet created" - ); -}); - -QUnit.test('[technical] keep spinner on transition from messaging non-created to messaging created (and non-initialized)', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - */ - assert.expect(4); - - const messaginginitializedDeferred = makeTestPromise(); - const pyEnv = await startServer(); - const messagingBeforeCreationDeferred = makeDeferred(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "second partner", - }); - const views = { - 'res.partner,false,form': - `
- - - -
-
`, - }; - const { openView } = await start({ - messagingBeforeCreationDeferred, - async mockRPC(route, args) { - if (route === '/mail/init_messaging') { - await messaginginitializedDeferred; // simulate messaging never initialized - } - }, - serverData: { views }, - waitUntilMessagingCondition: 'none', - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelector('.o_ChatterContainer').textContent, - "Please wait...", - "chatter container should display spinner when messaging not yet created" - ); - assert.containsOnce( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should not display any chatter when messaging not created" - ); - - // simulate messaging become created - messagingBeforeCreationDeferred.resolve(); - await nextAnimationFrame(); - assert.strictEqual( - document.querySelector('.o_ChatterContainer').textContent, - "Please wait...", - "chatter container should still display spinner when messaging is created but not initialized" - ); - assert.containsOnce( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should still not display any chatter when messaging not initialized" - ); - messaginginitializedDeferred.resolve(); // ensure proper teardown -}); - -QUnit.test('spinner when messaging is created but not initialized', async function (assert) { - assert.expect(3); - - const messaginginitializedDeferred = makeTestPromise(); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "second partner", - }); - const views = { - 'res.partner,false,form': - `
- - - -
-
`, - }; - const { openView } = await start({ - async mockRPC(route, args) { - if (route === '/mail/init_messaging') { - await messaginginitializedDeferred; // simulate messaging never initialized - } - }, - serverData: { views }, - waitUntilMessagingCondition: 'created', - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_ChatterContainer', - "should display chatter container even when messaging is not fully initialized" - ); - assert.containsOnce( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should not display any chatter when messaging not initialized" - ); - assert.strictEqual( - document.querySelector('.o_ChatterContainer').textContent, - "Please wait...", - "chatter container should display spinner when messaging not yet initialized" - ); - messaginginitializedDeferred.resolve(); // ensure proper teardown -}); - -QUnit.test('transition non-initialized messaging to initialized messaging: display spinner then chatter', async function (assert) { - assert.expect(3); - - const pyEnv = await startServer(); - const messagingBeforeInitializationDeferred = makeDeferred(); - const resPartnerId1 = pyEnv['res.partner'].create({ - display_name: "second partner", - }); - const views = { - 'res.partner,false,form': - `
- - - -
-
`, - }; - const { openView } = await start({ - async mockRPC(route, args) { - if (route === '/mail/init_messaging') { - await messagingBeforeInitializationDeferred; - } - }, - serverData: { views }, - waitUntilMessagingCondition: 'created', - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelector('.o_ChatterContainer').textContent, - "Please wait...", - "chatter container should display spinner when messaging not yet initialized" - ); - assert.containsOnce( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should not display any chatter when messaging not initialized" - ); - - // Simulate messaging becomes initialized - await afterNextRender(() => messagingBeforeInitializationDeferred.resolve()); - assert.containsNone( - document.body, - '.o_ChatterContainer_noChatter', - "chatter container should now display chatter when messaging becomes initialized" - ); -}); - -QUnit.test('basic chatter rendering', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - const views = { - 'res.partner,false,form': - `
- - - -
-
`, - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "there should be a chatter" - ); -}); - -QUnit.test('basic chatter rendering without followers', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - const views = { - 'res.partner,false,form': - `
- - - -
- - -
-
`, - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_Chatter', - "there should be a chatter" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar', - "there should be a chatter topbar" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonAddAttachments', - "there should be an attachment button" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonScheduleActivity', - "there should be a schedule activity button" - ); - assert.containsNone( - document.body, - '.o_FollowerListMenuView', - "there should be no followers menu because the 'message_follower_ids' field is not present in 'oe_chatter'" - ); - assert.containsOnce( - document.body, - '.o_Chatter_thread', - "there should be a thread" - ); -}); - -QUnit.test('basic chatter rendering without activities', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - const views = { - 'res.partner,false,form': - `
- - - -
- - -
-
`, - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_Chatter', - "there should be a chatter" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar', - "there should be a chatter topbar" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonAddAttachments', - "there should be an attachment button" - ); - assert.containsNone( - document.body, - '.o_ChatterTopbar_buttonScheduleActivity', - "there should be no schedule activity button because the 'activity_ids' field is not present in 'oe_chatter'" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "there should be a followers menu" - ); - assert.containsOnce( - document.body, - '.o_Chatter_thread', - "there should be a thread" - ); -}); - -QUnit.test('basic chatter rendering without messages', async function (assert) { - assert.expect(6); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "second partner" }); - const views = { - 'res.partner,false,form': - `
- - - -
- - -
-
`, - }; - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_Chatter', - "there should be a chatter" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar', - "there should be a chatter topbar" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonAddAttachments', - "there should be an attachment button" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonScheduleActivity', - "there should be a schedule activity button" - ); - assert.containsOnce( - document.body, - '.o_FollowerListMenuView', - "there should be a followers menu" - ); - assert.containsNone( - document.body, - '.o_Chatter_thread', - "there should be no thread because the 'message_ids' field is not present in 'oe_chatter'" - ); -}); - -QUnit.test('chatter updating', async function (assert) { - assert.expect(1); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([ - { display_name: "first partner" }, - { display_name: "second partner" }, - ]); - pyEnv['mail.message'].create({ body: "not empty", model: 'res.partner', res_id: resPartnerId2 }); - const views = { - 'res.partner,false,form': - `
- - - -
- -
-
`, - }; - const { afterEvent, openFormView } = await start({ - serverData: { views }, - }); - await openFormView( - { - res_model: 'res.partner', - res_id: resPartnerId1, - }, - { - props: { - resIds: [resPartnerId1, resPartnerId2], - }, - } - ); - await afterNextRender(() => afterEvent({ - eventName: 'o-thread-view-hint-processed', - func: () => document.querySelector('.o_pager_next').click(), - message: "should wait until partner 12 thread loaded messages after clicking on next", - predicate: ({ hint, threadViewer }) => { - return ( - hint.type === 'messages-loaded' && - threadViewer.thread.model === 'res.partner' && - threadViewer.thread.id === resPartnerId2 + }; + const { openView } = await start({ + messagingBeforeCreationDeferred: makeDeferred(), // block messaging creation + serverData: { views }, + waitUntilMessagingCondition: "none", + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ChatterContainer", + "should display chatter container even when messaging is not created yet" ); - }, - })); - assert.containsOnce( - document.body, - '.o_MessageView', - "there should be a message in partner 12 thread" - ); -}); + assert.containsOnce( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should not display any chatter when messaging not created" + ); + assert.strictEqual( + document.querySelector(".o_ChatterContainer").textContent, + "Please wait...", + "chatter container should display spinner when messaging not yet created" + ); + }); -QUnit.test('chatter should become enabled when creation done', async function (assert) { - assert.expect(10); + QUnit.test( + "[technical] keep spinner on transition from messaging non-created to messaging created (and non-initialized)", + async function (assert) { + /** + * Creation of messaging in env is async due to generation of models being + * async. Generation of models is async because it requires parsing of all + * JS modules that contain pieces of model definitions. + * + * Time of having no messaging is very short, almost imperceptible by user + * on UI, but the display should not crash during this critical time period. + */ + assert.expect(4); - const views = { - 'res.partner,false,form': - `
+ const messaginginitializedDeferred = makeTestPromise(); + const pyEnv = await startServer(); + const messagingBeforeCreationDeferred = makeDeferred(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "second partner", + }); + const views = { + "res.partner,false,form": ` + + + +
+
`, + }; + const { openView } = await start({ + messagingBeforeCreationDeferred, + async mockRPC(route, args) { + if (route === "/mail/init_messaging") { + await messaginginitializedDeferred; // simulate messaging never initialized + } + }, + serverData: { views }, + waitUntilMessagingCondition: "none", + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelector(".o_ChatterContainer").textContent, + "Please wait...", + "chatter container should display spinner when messaging not yet created" + ); + assert.containsOnce( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should not display any chatter when messaging not created" + ); + + // simulate messaging become created + messagingBeforeCreationDeferred.resolve(); + await nextAnimationFrame(); + assert.strictEqual( + document.querySelector(".o_ChatterContainer").textContent, + "Please wait...", + "chatter container should still display spinner when messaging is created but not initialized" + ); + assert.containsOnce( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should still not display any chatter when messaging not initialized" + ); + messaginginitializedDeferred.resolve(); // ensure proper teardown + } + ); + + QUnit.test( + "spinner when messaging is created but not initialized", + async function (assert) { + assert.expect(3); + + const messaginginitializedDeferred = makeTestPromise(); + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "second partner", + }); + const views = { + "res.partner,false,form": `
+ + + +
+
`, + }; + const { openView } = await start({ + async mockRPC(route, args) { + if (route === "/mail/init_messaging") { + await messaginginitializedDeferred; // simulate messaging never initialized + } + }, + serverData: { views }, + waitUntilMessagingCondition: "created", + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.containsOnce( + document.body, + ".o_ChatterContainer", + "should display chatter container even when messaging is not fully initialized" + ); + assert.containsOnce( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should not display any chatter when messaging not initialized" + ); + assert.strictEqual( + document.querySelector(".o_ChatterContainer").textContent, + "Please wait...", + "chatter container should display spinner when messaging not yet initialized" + ); + messaginginitializedDeferred.resolve(); // ensure proper teardown + } + ); + + QUnit.test( + "transition non-initialized messaging to initialized messaging: display spinner then chatter", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const messagingBeforeInitializationDeferred = makeDeferred(); + const resPartnerId1 = pyEnv["res.partner"].create({ + display_name: "second partner", + }); + const views = { + "res.partner,false,form": `
+ + + +
+
`, + }; + const { openView } = await start({ + async mockRPC(route, args) { + if (route === "/mail/init_messaging") { + await messagingBeforeInitializationDeferred; + } + }, + serverData: { views }, + waitUntilMessagingCondition: "created", + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelector(".o_ChatterContainer").textContent, + "Please wait...", + "chatter container should display spinner when messaging not yet initialized" + ); + assert.containsOnce( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should not display any chatter when messaging not initialized" + ); + + // Simulate messaging becomes initialized + await afterNextRender(() => messagingBeforeInitializationDeferred.resolve()); + assert.containsNone( + document.body, + ".o_ChatterContainer_noChatter", + "chatter container should now display chatter when messaging becomes initialized" + ); + } + ); + + QUnit.test("basic chatter rendering", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "second partner" }); + const views = { + "res.partner,false,form": `
+ + + +
+
`, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "there should be a chatter" + ); + }); + + QUnit.test("basic chatter rendering without followers", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "second partner" }); + const views = { + "res.partner,false,form": `
+ + + +
+ + +
+
`, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.containsOnce(document.body, ".o_Chatter", "there should be a chatter"); + assert.containsOnce( + document.body, + ".o_ChatterTopbar", + "there should be a chatter topbar" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonAddAttachments", + "there should be an attachment button" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonScheduleActivity", + "there should be a schedule activity button" + ); + assert.containsNone( + document.body, + ".o_FollowerListMenuView", + "there should be no followers menu because the 'message_follower_ids' field is not present in 'oe_chatter'" + ); + assert.containsOnce(document.body, ".o_Chatter_thread", "there should be a thread"); + }); + + QUnit.test("basic chatter rendering without activities", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "second partner" }); + const views = { + "res.partner,false,form": `
+ + + +
+ + +
+
`, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.containsOnce(document.body, ".o_Chatter", "there should be a chatter"); + assert.containsOnce( + document.body, + ".o_ChatterTopbar", + "there should be a chatter topbar" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonAddAttachments", + "there should be an attachment button" + ); + assert.containsNone( + document.body, + ".o_ChatterTopbar_buttonScheduleActivity", + "there should be no schedule activity button because the 'activity_ids' field is not present in 'oe_chatter'" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "there should be a followers menu" + ); + assert.containsOnce(document.body, ".o_Chatter_thread", "there should be a thread"); + }); + + QUnit.test("basic chatter rendering without messages", async function (assert) { + assert.expect(6); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "second partner" }); + const views = { + "res.partner,false,form": `
+ + + +
+ + +
+
`, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.containsOnce(document.body, ".o_Chatter", "there should be a chatter"); + assert.containsOnce( + document.body, + ".o_ChatterTopbar", + "there should be a chatter topbar" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonAddAttachments", + "there should be an attachment button" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonScheduleActivity", + "there should be a schedule activity button" + ); + assert.containsOnce( + document.body, + ".o_FollowerListMenuView", + "there should be a followers menu" + ); + assert.containsNone( + document.body, + ".o_Chatter_thread", + "there should be no thread because the 'message_ids' field is not present in 'oe_chatter'" + ); + }); + + QUnit.test("chatter updating", async function (assert) { + assert.expect(1); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { display_name: "first partner" }, + { display_name: "second partner" }, + ]); + pyEnv["mail.message"].create({ + body: "not empty", + model: "res.partner", + res_id: resPartnerId2, + }); + const views = { + "res.partner,false,form": `
@@ -505,73 +442,120 @@ QUnit.test('chatter should become enabled when creation done', async function (a
`, - }; - const { click, openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - views: [[false, 'form']], - }); - assert.containsOnce( - document.body, - '.o_Chatter', - "there should be a chatter" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonSendMessage', - "there should be a send message button" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonLogNote', - "there should be a log note button" - ); - assert.containsOnce( - document.body, - '.o_ChatterTopbar_buttonLogNote', - "there should be an attachments button" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled, - "send message button should be disabled" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled, - "log note button should be disabled" - ); - assert.ok( - document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).disabled, - "attachments button should be disabled" - ); + }; + const { afterEvent, openFormView } = await start({ + serverData: { views }, + }); + await openFormView( + { + res_model: "res.partner", + res_id: resPartnerId1, + }, + { + props: { + resIds: [resPartnerId1, resPartnerId2], + }, + } + ); + await afterNextRender(() => + afterEvent({ + eventName: "o-thread-view-hint-processed", + func: () => document.querySelector(".o_pager_next").click(), + message: + "should wait until partner 12 thread loaded messages after clicking on next", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === "messages-loaded" && + threadViewer.thread.model === "res.partner" && + threadViewer.thread.id === resPartnerId2 + ); + }, + }) + ); + assert.containsOnce( + document.body, + ".o_MessageView", + "there should be a message in partner 12 thread" + ); + }); - document.querySelectorAll('.o_field_char')[0].focus(); - document.execCommand('insertText', false, "hello"); - await click('.o_form_button_save'); - assert.notOk( - document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled, - "send message button should now be enabled" - ); - assert.notOk( - document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled, - "log note button should now be enabled" - ); - assert.notOk( - document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).disabled, - "attachments button should now be enabled" - ); -}); + QUnit.test("chatter should become enabled when creation done", async function (assert) { + assert.expect(10); -QUnit.test('read more/less links are not duplicated when switching from read to edit mode', async function (assert) { - assert.expect(3); + const views = { + "res.partner,false,form": `
+ + + +
+ +
+
`, + }; + const { click, openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + views: [[false, "form"]], + }); + assert.containsOnce(document.body, ".o_Chatter", "there should be a chatter"); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonSendMessage", + "there should be a send message button" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonLogNote", + "there should be a log note button" + ); + assert.containsOnce( + document.body, + ".o_ChatterTopbar_buttonLogNote", + "there should be an attachments button" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled, + "send message button should be disabled" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled, + "log note button should be disabled" + ); + assert.ok( + document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).disabled, + "attachments button should be disabled" + ); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create({ - author_id: resPartnerId1, - // "data-o-mail-quote" added by server is intended to be compacted in read more/less blocks - body: ` + document.querySelectorAll(".o_field_char")[0].focus(); + document.execCommand("insertText", false, "hello"); + await click(".o_form_button_save"); + assert.notOk( + document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).disabled, + "send message button should now be enabled" + ); + assert.notOk( + document.querySelector(`.o_ChatterTopbar_buttonLogNote`).disabled, + "log note button should now be enabled" + ); + assert.notOk( + document.querySelector(`.o_ChatterTopbar_buttonAddAttachments`).disabled, + "attachments button should now be enabled" + ); + }); + + QUnit.test( + "read more/less links are not duplicated when switching from read to edit mode", + async function (assert) { + assert.expect(3); + + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create({ + author_id: resPartnerId1, + // "data-o-mail-quote" added by server is intended to be compacted in read more/less blocks + body: `
Dear Joel Willis,
Thank you for your enquiry.
@@ -583,12 +567,11 @@ QUnit.test('read more/less links are not duplicated when switching from read to
`, - model: 'res.partner', - res_id: resPartnerId1, - }); - const views = { - 'res.partner,false,form': - `
+ model: "res.partner", + res_id: resPartnerId1, + }); + const views = { + "res.partner,false,form": ` @@ -596,47 +579,43 @@ QUnit.test('read more/less links are not duplicated when switching from read to
`, - }; - const { afterEvent, openView } = await start({ - serverData: { views }, - }); - const openViewAction = { - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }; - await afterEvent({ - func: () => openView(openViewAction), - eventName: 'o-component-message-read-more-less-inserted', - message: "should wait until read more/less is inserted initially", - predicate: ({ message }) => message.id === mailMessageId1, - }); - assert.containsOnce( - document.body, - '.o_Chatter', - "there should be a chatter" - ); - assert.containsOnce( - document.body, - '.o_MessageView', - "there should be a message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_readMoreLess', - "there should be only one read more" - ); -}); + }; + const { afterEvent, openView } = await start({ + serverData: { views }, + }); + const openViewAction = { + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }; + await afterEvent({ + func: () => openView(openViewAction), + eventName: "o-component-message-read-more-less-inserted", + message: "should wait until read more/less is inserted initially", + predicate: ({ message }) => message.id === mailMessageId1, + }); + assert.containsOnce(document.body, ".o_Chatter", "there should be a chatter"); + assert.containsOnce(document.body, ".o_MessageView", "there should be a message"); + assert.containsOnce( + document.body, + ".o_MessageView_readMoreLess", + "there should be only one read more" + ); + } + ); -QUnit.test('read more links becomes read less after being clicked', async function (assert) { - assert.expect(5); + QUnit.test( + "read more links becomes read less after being clicked", + async function (assert) { + assert.expect(5); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const mailMessageId1 = pyEnv['mail.message'].create([{ - author_id: resPartnerId1, - // "data-o-mail-quote" added by server is intended to be compacted in read more/less blocks - body: ` + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const mailMessageId1 = pyEnv["mail.message"].create([ + { + author_id: resPartnerId1, + // "data-o-mail-quote" added by server is intended to be compacted in read more/less blocks + body: `
Dear Joel Willis,
Thank you for your enquiry.
@@ -648,12 +627,12 @@ QUnit.test('read more links becomes read less after being clicked', async functi
`, - model: 'res.partner', - res_id: resPartnerId1, - }]); - const views = { - 'res.partner,false,form': - `
+ model: "res.partner", + res_id: resPartnerId1, + }, + ]); + const views = { + "res.partner,false,form": ` @@ -661,74 +640,66 @@ QUnit.test('read more links becomes read less after being clicked', async functi
`, - }; - const { afterEvent, openView } = await start({ - serverData: { views }, - }); - const openViewAction = { - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }; - await afterEvent({ - func: () => openView(openViewAction), - eventName: 'o-component-message-read-more-less-inserted', - message: "should wait until read more/less is inserted initially", - predicate: ({ message }) => message.id === mailMessageId1, - }); - assert.containsOnce( - document.body, - '.o_Chatter', - "there should be a chatter" - ); - assert.containsOnce( - document.body, - '.o_MessageView', - "there should be a message" - ); - assert.containsOnce( - document.body, - '.o_MessageView_readMoreLess', - "there should be a read more" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_readMoreLess').textContent, - 'Read More', - "Read More/Less link should contain 'Read More' as text" - ); + }; + const { afterEvent, openView } = await start({ + serverData: { views }, + }); + const openViewAction = { + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }; + await afterEvent({ + func: () => openView(openViewAction), + eventName: "o-component-message-read-more-less-inserted", + message: "should wait until read more/less is inserted initially", + predicate: ({ message }) => message.id === mailMessageId1, + }); + assert.containsOnce(document.body, ".o_Chatter", "there should be a chatter"); + assert.containsOnce(document.body, ".o_MessageView", "there should be a message"); + assert.containsOnce( + document.body, + ".o_MessageView_readMoreLess", + "there should be a read more" + ); + assert.strictEqual( + document.querySelector(".o_MessageView_readMoreLess").textContent, + "Read More", + "Read More/Less link should contain 'Read More' as text" + ); - document.querySelector('.o_MessageView_readMoreLess').click(); - assert.strictEqual( - document.querySelector('.o_MessageView_readMoreLess').textContent, - 'Read Less', - "Read Less/Less link should contain 'Read Less' as text after it has been clicked" - ); -}); + document.querySelector(".o_MessageView_readMoreLess").click(); + assert.strictEqual( + document.querySelector(".o_MessageView_readMoreLess").textContent, + "Read Less", + "Read Less/Less link should contain 'Read Less' as text after it has been clicked" + ); + } + ); -QUnit.test('Form view not scrolled when switching record', async function (assert) { - assert.expect(6); + QUnit.test("Form view not scrolled when switching record", async function (assert) { + assert.expect(6); - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([ - { - description: [...Array(60).keys()].join('\n'), - display_name: "Partner 1", - }, - { - display_name: "Partner 2", - }, - ]); + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { + description: [...Array(60).keys()].join("\n"), + display_name: "Partner 1", + }, + { + display_name: "Partner 2", + }, + ]); - const messages = [...Array(60).keys()].map(id => { - return { - model: 'res.partner', - res_id: id % 2 ? resPartnerId1 : resPartnerId2, - }; - }); - pyEnv['mail.message'].create(messages); - const views = { - 'res.partner,false,form': - `
+ const messages = [...Array(60).keys()].map((id) => { + return { + model: "res.partner", + res_id: id % 2 ? resPartnerId1 : resPartnerId2, + }; + }); + pyEnv["mail.message"].create(messages); + const views = { + "res.partner,false,form": ` @@ -737,87 +708,143 @@ QUnit.test('Form view not scrolled when switching record', async function (asser
`, - }; - patchUiSize({ size: SIZES.LG }); - const { click, openView } = await start({ - serverData: { views }, - }); - await openView( - { - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }, - { - resIds: [resPartnerId1, resPartnerId2], - } - ); + }; + patchUiSize({ size: SIZES.LG }); + const { click, openView } = await start({ + serverData: { views }, + }); + await openView( + { + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }, + { + resIds: [resPartnerId1, resPartnerId2], + } + ); - const controllerContentEl = document.querySelector('.o_content'); + const controllerContentEl = document.querySelector(".o_content"); - assert.strictEqual( - document.querySelector('.breadcrumb-item.active').textContent, - 'Partner 1', - "Form view should display partner 'Partner 1'" - ); - assert.strictEqual(controllerContentEl.scrollTop, 0, - "The top of the form view is visible" - ); + assert.strictEqual( + document.querySelector(".breadcrumb-item.active").textContent, + "Partner 1", + "Form view should display partner 'Partner 1'" + ); + assert.strictEqual( + controllerContentEl.scrollTop, + 0, + "The top of the form view is visible" + ); - await afterNextRender(async () => { - controllerContentEl.scrollTop = controllerContentEl.scrollHeight - controllerContentEl.clientHeight; - await triggerEvent( - document.querySelector('.o_ThreadView_messageList'), - 'scroll' + await afterNextRender(async () => { + controllerContentEl.scrollTop = + controllerContentEl.scrollHeight - controllerContentEl.clientHeight; + await triggerEvent(document.querySelector(".o_ThreadView_messageList"), "scroll"); + }); + assert.ok( + isScrolledToBottom(controllerContentEl), + "The controller container should be scrolled to its bottom" + ); + + await click(".o_pager_next"); + assert.strictEqual( + document.querySelector(".breadcrumb-item.active").textContent, + "Partner 2", + "The form view should display partner 'Partner 2'" + ); + assert.strictEqual( + controllerContentEl.scrollTop, + 0, + "The top of the form view should be visible when switching record from pager" + ); + + await click(".o_pager_previous"); + assert.strictEqual( + controllerContentEl.scrollTop, + 0, + "Form view's scroll position should have been reset when switching back to first record" + ); + }); + + QUnit.test( + "Attachments that have been unlinked from server should be visually unlinked from record", + async function (assert) { + // Attachments that have been fetched from a record at certain time and then + // removed from the server should be reflected on the UI when the current + // partner accesses this record again. + assert.expect(2); + + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { display_name: "Partner1" }, + { display_name: "Partner2" }, + ]); + const [irAttachmentId1] = pyEnv["ir.attachment"].create([ + { + mimetype: "text.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + { + mimetype: "text.txt", + res_id: resPartnerId1, + res_model: "res.partner", + }, + ]); + const views = { + "res.partner,false,form": `
+ + + +
+ +
+
`, + }; + const { click, openView } = await start({ + serverData: { views }, + }); + await openView( + { + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }, + { + resId: resPartnerId1, + resIds: [resPartnerId1, resPartnerId2], + } + ); + assert.strictEqual( + document.querySelector(".o_ChatterTopbar_buttonCount").textContent, + "2", + "Partner1 should have 2 attachments initially" + ); + + // The attachment links are updated on (re)load, + // so using pager is a way to reload the record "Partner1". + await click(".o_pager_next"); + // Simulate unlinking attachment 1 from Partner 1. + pyEnv["ir.attachment"].write([irAttachmentId1], { res_id: 0 }); + await click(".o_pager_previous"); + assert.strictEqual( + document.querySelector(".o_ChatterTopbar_buttonCount").textContent, + "1", + "Partner1 should now have 1 attachment after it has been unlinked from server" + ); + } ); - }); - assert.ok( - isScrolledToBottom(controllerContentEl), - "The controller container should be scrolled to its bottom" - ); - await click('.o_pager_next'); - assert.strictEqual( - document.querySelector('.breadcrumb-item.active').textContent, - 'Partner 2', - "The form view should display partner 'Partner 2'" - ); - assert.strictEqual(controllerContentEl.scrollTop, 0, - "The top of the form view should be visible when switching record from pager" - ); + QUnit.test( + 'chatter just contains "creating a new record" message during the creation of a new record after having displayed a chatter for an existing record', + async function (assert) { + assert.expect(2); - await click('.o_pager_previous'); - assert.strictEqual(controllerContentEl.scrollTop, 0, - "Form view's scroll position should have been reset when switching back to first record" - ); -}); - -QUnit.test('Attachments that have been unlinked from server should be visually unlinked from record', async function (assert) { - // Attachments that have been fetched from a record at certain time and then - // removed from the server should be reflected on the UI when the current - // partner accesses this record again. - assert.expect(2); - - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([ - { display_name: "Partner1" }, - { display_name: "Partner2" }, - ]); - const [irAttachmentId1] = pyEnv['ir.attachment'].create([ - { - mimetype: 'text.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - { - mimetype: 'text.txt', - res_id: resPartnerId1, - res_model: 'res.partner', - }, - ]); - const views = { - 'res.partner,false,form': - `
+ const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({}); + const views = { + "res.partner,false,form": ` @@ -825,95 +852,49 @@ QUnit.test('Attachments that have been unlinked from server should be visually u
`, - }; - const { click, openView } = await start({ - serverData: { views }, - }); - await openView( - { - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }, - { - resId: resPartnerId1, - resIds: [resPartnerId1, resPartnerId2], - }, - ); - assert.strictEqual( - document.querySelector('.o_ChatterTopbar_buttonCount').textContent, - '2', - "Partner1 should have 2 attachments initially" - ); + }; + const { click, openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); - // The attachment links are updated on (re)load, - // so using pager is a way to reload the record "Partner1". - await click('.o_pager_next'); - // Simulate unlinking attachment 1 from Partner 1. - pyEnv['ir.attachment'].write([irAttachmentId1], { res_id: 0 }); - await click('.o_pager_previous'); - assert.strictEqual( - document.querySelector('.o_ChatterTopbar_buttonCount').textContent, - '1', - "Partner1 should now have 1 attachment after it has been unlinked from server" - ); -}); + await click(".o_form_button_create"); + assert.containsOnce( + document.body, + ".o_MessageView", + "Should have a single message when creating a new record" + ); + assert.strictEqual( + document.querySelector(".o_MessageView_content").textContent, + "Creating a new record...", + "the message content should be in accord to the creation of this record" + ); + } + ); -QUnit.test('chatter just contains "creating a new record" message during the creation of a new record after having displayed a chatter for an existing record', async function (assert) { - assert.expect(2); + QUnit.test( + "[TECHNICAL] unfolded read more/less links should not fold on message click besides those button links", + async function (assert) { + // message click triggers a re-render. Before writing of this test, the + // insertion of read more/less links were done during render. This meant + // any re-render would re-insert the read more/less links. If some button + // links were unfolded, any re-render would fold them again. + // + // This previous behavior is undesirable, and results to bothersome UX + // such as inability to copy/paste unfolded message content due to click + // from text selection automatically folding all read more/less links. + assert.expect(3); - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({}); - const views = { - 'res.partner,false,form': - `
- - - -
- -
-
`, - }; - const { click, openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - - await click('.o_form_button_create'); - assert.containsOnce( - document.body, - '.o_MessageView', - "Should have a single message when creating a new record" - ); - assert.strictEqual( - document.querySelector('.o_MessageView_content').textContent, - 'Creating a new record...', - "the message content should be in accord to the creation of this record" - ); -}); - -QUnit.test('[TECHNICAL] unfolded read more/less links should not fold on message click besides those button links', async function (assert) { - // message click triggers a re-render. Before writing of this test, the - // insertion of read more/less links were done during render. This meant - // any re-render would re-insert the read more/less links. If some button - // links were unfolded, any re-render would fold them again. - // - // This previous behavior is undesirable, and results to bothersome UX - // such as inability to copy/paste unfolded message content due to click - // from text selection automatically folding all read more/less links. - assert.expect(3); - - const pyEnv = await startServer(); - const resPartnerId1 = pyEnv['res.partner'].create({ display_name: "Someone" }); - pyEnv['mail.message'].create({ - author_id: resPartnerId1, - // "data-o-mail-quote" added by server is intended to be compacted in read more/less blocks - body: ` + const pyEnv = await startServer(); + const resPartnerId1 = pyEnv["res.partner"].create({ display_name: "Someone" }); + pyEnv["mail.message"].create({ + author_id: resPartnerId1, + // "data-o-mail-quote" added by server is intended to be compacted in read more/less blocks + body: `
Dear Joel Willis,
Thank you for your enquiry.
@@ -925,12 +906,11 @@ QUnit.test('[TECHNICAL] unfolded read more/less links should not fold on message
`, - model: 'res.partner', - res_id: resPartnerId1, - }); - const views = { - 'res.partner,false,form': - `
+ model: "res.partner", + res_id: resPartnerId1, + }); + const views = { + "res.partner,false,form": ` @@ -938,96 +918,101 @@ QUnit.test('[TECHNICAL] unfolded read more/less links should not fold on message
`, - }; - const { click, openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }); - assert.strictEqual( - document.querySelector('.o_MessageView_readMoreLess').textContent, - "Read More", - "Read More/Less link on message should be folded initially (Read More)" - ); + }; + const { click, openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }); + assert.strictEqual( + document.querySelector(".o_MessageView_readMoreLess").textContent, + "Read More", + "Read More/Less link on message should be folded initially (Read More)" + ); - document.querySelector('.o_MessageView_readMoreLess').click(), - assert.strictEqual( - document.querySelector('.o_MessageView_readMoreLess').textContent, - "Read Less", - "Read More/Less link on message should be unfolded after a click from initial rendering (read less)" - ); + document.querySelector(".o_MessageView_readMoreLess").click(), + assert.strictEqual( + document.querySelector(".o_MessageView_readMoreLess").textContent, + "Read Less", + "Read More/Less link on message should be unfolded after a click from initial rendering (read less)" + ); - await click('.o_MessageView'); - assert.strictEqual( - document.querySelector('.o_MessageView_readMoreLess').textContent, - "Read Less", - "Read More/Less link on message should still be unfolded after a click on message aside of this button click (Read Less)" - ); -}); + await click(".o_MessageView"); + assert.strictEqual( + document.querySelector(".o_MessageView_readMoreLess").textContent, + "Read Less", + "Read More/Less link on message should still be unfolded after a click on message aside of this button click (Read Less)" + ); + } + ); -QUnit.test('chatter does not flicker when the form view is re-rendered', async function (assert) { - const pyEnv = await startServer(); - const [resPartnerId1, resPartnerId2] = pyEnv['res.partner'].create([ - { display_name: "first partner" }, - { display_name: "second partner" }, - ]); + QUnit.test( + "chatter does not flicker when the form view is re-rendered", + async function (assert) { + const pyEnv = await startServer(); + const [resPartnerId1, resPartnerId2] = pyEnv["res.partner"].create([ + { display_name: "first partner" }, + { display_name: "second partner" }, + ]); - // define an asynchronous field and use it in the form to ease testing - let def; - const FieldChar = fieldRegistry.get("char"); - const AsyncWidget = FieldChar.extend({ - willStart() { - return Promise.resolve(def); - }, - }); - fieldRegistry.add("async_widget", AsyncWidget); - registerCleanup(() => { - delete fieldRegistry.map.async_widget; - }); + // define an asynchronous field and use it in the form to ease testing + let def; + const FieldChar = fieldRegistry.get("char"); + const AsyncWidget = FieldChar.extend({ + willStart() { + return Promise.resolve(def); + }, + }); + fieldRegistry.add("async_widget", AsyncWidget); + registerCleanup(() => { + delete fieldRegistry.map.async_widget; + }); - const views = { - 'res.partner,false,form': - `
+ const views = { + "res.partner,false,form": `
`, - }; - const { openView } = await start({ - serverData: { views }, + }; + const { openView } = await start({ + serverData: { views }, + }); + await openView( + { + res_model: "res.partner", + res_id: resPartnerId1, + views: [[false, "form"]], + }, + { resIds: [resPartnerId1, resPartnerId2] } + ); + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "there should be a chatter" + ); + def = makeDeferred(); + document.querySelector(".o_pager_next").click(); + await nextTick(); + + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "there should be a chatter" + ); + def.resolve(); + await nextTick(); + + assert.strictEqual( + document.querySelectorAll(`.o_Chatter`).length, + 1, + "there should be a chatter" + ); + } + ); }); - await openView({ - res_model: 'res.partner', - res_id: resPartnerId1, - views: [[false, 'form']], - }, { resIds: [resPartnerId1, resPartnerId2]}); - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "there should be a chatter" - ); - def = makeDeferred(); - document.querySelector('.o_pager_next').click(); - await nextTick(); - - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "there should be a chatter" - ); - def.resolve(); - await nextTick(); - - assert.strictEqual( - document.querySelectorAll(`.o_Chatter`).length, - 1, - "there should be a chatter" - ); -}); - -}); }); diff --git a/addons/mail/static/tests/qunit_suite_tests/widgets/notification_alert_tests.js b/addons/mail/static/tests/qunit_suite_tests/widgets/notification_alert_tests.js index 153586779d2..8018419effe 100644 --- a/addons/mail/static/tests/qunit_suite_tests/widgets/notification_alert_tests.js +++ b/addons/mail/static/tests/qunit_suite_tests/widgets/notification_alert_tests.js @@ -1,92 +1,100 @@ /** @odoo-module **/ -import { start } from '@mail/../tests/helpers/test_utils'; +import { start } from "@mail/../tests/helpers/test_utils"; -import { browser } from '@web/core/browser/browser'; +import { browser } from "@web/core/browser/browser"; import { patchWithCleanup } from "@web/../tests/helpers/utils"; -QUnit.module('mail', {}, function () { -QUnit.module('widgets', {}, function () { -QUnit.module('notification_alert_tests.js'); +QUnit.module("mail", {}, function () { + QUnit.module("widgets", {}, function () { + QUnit.module("notification_alert_tests.js"); -QUnit.test('notification_alert widget: display blocked notification alert', async function (assert) { - assert.expect(1); + QUnit.test( + "notification_alert widget: display blocked notification alert", + async function (assert) { + assert.expect(1); - const views = { - 'mail.message,false,form': `
`, - }; - patchWithCleanup(browser, { - Notification: { - ...browser.Notification, - permission: 'denied', - }, - }); - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'mail.message', - views: [[false, 'form']], - }); + const views = { + "mail.message,false,form": `
`, + }; + patchWithCleanup(browser, { + Notification: { + ...browser.Notification, + permission: "denied", + }, + }); + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "mail.message", + views: [[false, "form"]], + }); - assert.containsOnce( - document.body, - '.o_NotificationAlert', - "Blocked notification alert should be displayed" - ); -}); - -QUnit.test('notification_alert widget: no notification alert when granted', async function (assert) { - assert.expect(1); - - const views = { - 'mail.message,false,form': `
`, - }; - patchWithCleanup(browser, { - Notification: { - permission: 'granted', - }, - }); - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'mail.message', - views: [[false, 'form']], - }); - - assert.containsNone( - document.body, - '.o_NotificationAlert', - "Blocked notification alert should not be displayed" - ); -}); - -QUnit.test('notification_alert widget: no notification alert when default', async function (assert) { - assert.expect(1); - - const views = { - 'mail.message,false,form': `
`, - }; - patchWithCleanup(browser, { - Notification: { - permission: 'default', - }, - }); - const { openView } = await start({ - serverData: { views }, - }); - await openView({ - res_model: 'mail.message', - views: [[false, 'form']], - }); - - assert.containsNone( - document.body, - '.o_NotificationAlert', - "Blocked notification alert should not be displayed" - ); -}); - -}); + assert.containsOnce( + document.body, + ".o_NotificationAlert", + "Blocked notification alert should be displayed" + ); + } + ); + + QUnit.test( + "notification_alert widget: no notification alert when granted", + async function (assert) { + assert.expect(1); + + const views = { + "mail.message,false,form": `
`, + }; + patchWithCleanup(browser, { + Notification: { + permission: "granted", + }, + }); + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "mail.message", + views: [[false, "form"]], + }); + + assert.containsNone( + document.body, + ".o_NotificationAlert", + "Blocked notification alert should not be displayed" + ); + } + ); + + QUnit.test( + "notification_alert widget: no notification alert when default", + async function (assert) { + assert.expect(1); + + const views = { + "mail.message,false,form": `
`, + }; + patchWithCleanup(browser, { + Notification: { + permission: "default", + }, + }); + const { openView } = await start({ + serverData: { views }, + }); + await openView({ + res_model: "mail.message", + views: [[false, "form"]], + }); + + assert.containsNone( + document.body, + ".o_NotificationAlert", + "Blocked notification alert should not be displayed" + ); + } + ); + }); }); diff --git a/addons/mail/static/tests/tours/discuss_public_tour.js b/addons/mail/static/tests/tours/discuss_public_tour.js index 118cbea51cf..c6c7ff9844e 100644 --- a/addons/mail/static/tests/tours/discuss_public_tour.js +++ b/addons/mail/static/tests/tours/discuss_public_tour.js @@ -1,30 +1,41 @@ /** @odoo-module **/ -import tour from 'web_tour.tour'; +import tour from "web_tour.tour"; -tour.register('mail/static/tests/tours/discuss_public_tour.js', { - test: true, -}, [{ - trigger: '.o_DiscussPublicView', - extraTrigger: '.o_ThreadView', -}, { - content: "Check that we are on channel page", - trigger: '.o_ThreadView', - run() { - if (!window.location.pathname.startsWith('/discuss/channel')) { - console.error('Did not automatically redirect to channel page'); - } - // Wait for modules to be loaded or failed for the next step - odoo.__DEBUG__.didLogInfo.then(() => { - const { missing, failed, unloaded } = odoo.__DEBUG__.jsModules; - if ([missing, failed, unloaded].some(arr => arr.length)) { - console.error("Couldn't load all JS modules.", JSON.stringify({ missing, failed, unloaded })); - } - document.body.classList.add('o_mail_channel_public_modules_loaded'); - }); +tour.register( + "mail/static/tests/tours/discuss_public_tour.js", + { + test: true, }, - extraTrigger: '.o_mail_channel_public_modules_loaded', -}, { - content: "Wait for all modules loaded check in previous step", - trigger: '.o_mail_channel_public_modules_loaded', -}]); + [ + { + trigger: ".o_DiscussPublicView", + extraTrigger: ".o_ThreadView", + }, + { + content: "Check that we are on channel page", + trigger: ".o_ThreadView", + run() { + if (!window.location.pathname.startsWith("/discuss/channel")) { + console.error("Did not automatically redirect to channel page"); + } + // Wait for modules to be loaded or failed for the next step + odoo.__DEBUG__.didLogInfo.then(() => { + const { missing, failed, unloaded } = odoo.__DEBUG__.jsModules; + if ([missing, failed, unloaded].some((arr) => arr.length)) { + console.error( + "Couldn't load all JS modules.", + JSON.stringify({ missing, failed, unloaded }) + ); + } + document.body.classList.add("o_mail_channel_public_modules_loaded"); + }); + }, + extraTrigger: ".o_mail_channel_public_modules_loaded", + }, + { + content: "Wait for all modules loaded check in previous step", + trigger: ".o_mail_channel_public_modules_loaded", + }, + ] +); diff --git a/addons/mail/static/tests/tours/mail_channel_as_guest_tour.js b/addons/mail/static/tests/tours/mail_channel_as_guest_tour.js index b63e7760f65..3e2908d00e2 100644 --- a/addons/mail/static/tests/tours/mail_channel_as_guest_tour.js +++ b/addons/mail/static/tests/tours/mail_channel_as_guest_tour.js @@ -1,31 +1,42 @@ /** @odoo-module **/ -import tour from 'web_tour.tour'; +import tour from "web_tour.tour"; -tour.register('mail/static/tests/tours/mail_channel_as_guest_tour.js', { - test: true, -}, [{ - content: "Click join", - trigger: '.o_WelcomeView_joinButton', - extraTrigger: '.o_ThreadView', -}, { - content: "Check that we are on channel page", - trigger: '.o_ThreadView', - run() { - if (!window.location.pathname.startsWith('/discuss/channel')) { - console.error('Clicking on join button did not redirect to channel page'); - } - // Wait for modules to be loaded or failed for the next step - odoo.__DEBUG__.didLogInfo.then(() => { - const { missing, failed, unloaded } = odoo.__DEBUG__.jsModules; - if ([missing, failed, unloaded].some(arr => arr.length)) { - console.error("Couldn't load all JS modules.", JSON.stringify({ missing, failed, unloaded })); - } - document.body.classList.add('o_mail_channel_as_guest_tour_modules_loaded'); - }); +tour.register( + "mail/static/tests/tours/mail_channel_as_guest_tour.js", + { + test: true, }, - extraTrigger: '.o_mail_channel_as_guest_tour_modules_loaded', -}, { - content: "Wait for all modules loaded check in previous step", - trigger: '.o_mail_channel_as_guest_tour_modules_loaded', -}]); + [ + { + content: "Click join", + trigger: ".o_WelcomeView_joinButton", + extraTrigger: ".o_ThreadView", + }, + { + content: "Check that we are on channel page", + trigger: ".o_ThreadView", + run() { + if (!window.location.pathname.startsWith("/discuss/channel")) { + console.error("Clicking on join button did not redirect to channel page"); + } + // Wait for modules to be loaded or failed for the next step + odoo.__DEBUG__.didLogInfo.then(() => { + const { missing, failed, unloaded } = odoo.__DEBUG__.jsModules; + if ([missing, failed, unloaded].some((arr) => arr.length)) { + console.error( + "Couldn't load all JS modules.", + JSON.stringify({ missing, failed, unloaded }) + ); + } + document.body.classList.add("o_mail_channel_as_guest_tour_modules_loaded"); + }); + }, + extraTrigger: ".o_mail_channel_as_guest_tour_modules_loaded", + }, + { + content: "Wait for all modules loaded check in previous step", + trigger: ".o_mail_channel_as_guest_tour_modules_loaded", + }, + ] +); diff --git a/addons/mail/static/tests/tours/mail_full_composer_test_tour.js b/addons/mail/static/tests/tours/mail_full_composer_test_tour.js index 1d4ef927ef1..2f0c3621764 100644 --- a/addons/mail/static/tests/tours/mail_full_composer_test_tour.js +++ b/addons/mail/static/tests/tours/mail_full_composer_test_tour.js @@ -1,11 +1,8 @@ /** @odoo-module **/ -import { - createFile, - inputFiles, -} from 'web.test_utils_file'; +import { createFile, inputFiles } from "web.test_utils_file"; -import tour from 'web_tour.tour'; +import tour from "web_tour.tour"; /** * This tour depends on data created by python test in charge of launching it. @@ -13,76 +10,91 @@ import tour from 'web_tour.tour'; * an action (action manager) which is not possible to test with QUnit. * @see mail/tests/test_mail_full_composer.py */ -tour.register('mail/static/tests/tours/mail_full_composer_test_tour.js', { - test: true, -}, [{ - content: "Click on Send Message", - trigger: '.o_ChatterTopbar_buttonSendMessage', -}, { - content: "Write something in composer", - trigger: '.o_ComposerTextInputView_textarea', - run: 'text blahblah', -}, { - content: "Add one file in composer", - trigger: '.o_ComposerView_buttonAttachment', - async run() { - const file = await createFile({ - content: 'hello, world', - contentType: 'text/plain', - name: 'text.txt', - }); - const messaging = await odoo.__DEBUG__.messaging; - const uploader = messaging.models['ComposerView'].all()[0].fileUploader; - inputFiles( - uploader.fileInput, - [file] - ); +tour.register( + "mail/static/tests/tours/mail_full_composer_test_tour.js", + { + test: true, }, -}, { - content: "Open full composer", - trigger: '.o_ComposerView_buttonFullComposer', - extra_trigger: '.o_AttachmentCard:not(.o-isUploading)' // waiting the attachment to be uploaded -}, { - content: "Check the earlier provided attachment is listed", - trigger: '.o_AttachmentCard[title="text.txt"]', - run() {}, -}, { - content: "Check subject is autofilled", - trigger: '[name="subject"] input', - run() { - const subjectValue = document.querySelector('[name="subject"] input').value; - if (subjectValue !== "Re: Test User") { - console.error( - `Full composer should have "Re: Test User" in subject input (actual: ${subjectValue})` - ); - } - }, -}, { - content: "Check composer content is kept", - trigger: '.o_field_html[name="body"]', - run() { - const bodyContent = document.querySelector('.o_field_html[name="body"]').textContent; - if (!bodyContent.includes("blahblah")) { - console.error( - `Full composer should contain text from small composer ("blahblah") in body input (actual: ${bodyContent})` - ); - } - }, -}, { - content: "Open templates", - trigger: '.o_field_widget[name="template_id"] input', -}, { - content: "Check a template is listed", - in_modal: false, - trigger: '.ui-autocomplete .ui-menu-item a:contains("Test template")', - run() {}, -}, { - content: "Send message", - trigger: '.o_mail_send', -}, { - content: "Check message is shown", - trigger: '.o_MessageView:contains("blahblah")', -}, { - content: "Check message contains the attachment", - trigger: '.o_MessageView .o_AttachmentCard_filename:contains("text.txt")', -}]); + [ + { + content: "Click on Send Message", + trigger: ".o_ChatterTopbar_buttonSendMessage", + }, + { + content: "Write something in composer", + trigger: ".o_ComposerTextInputView_textarea", + run: "text blahblah", + }, + { + content: "Add one file in composer", + trigger: ".o_ComposerView_buttonAttachment", + async run() { + const file = await createFile({ + content: "hello, world", + contentType: "text/plain", + name: "text.txt", + }); + const messaging = await odoo.__DEBUG__.messaging; + const uploader = messaging.models["ComposerView"].all()[0].fileUploader; + inputFiles(uploader.fileInput, [file]); + }, + }, + { + content: "Open full composer", + trigger: ".o_ComposerView_buttonFullComposer", + extra_trigger: ".o_AttachmentCard:not(.o-isUploading)", // waiting the attachment to be uploaded + }, + { + content: "Check the earlier provided attachment is listed", + trigger: '.o_AttachmentCard[title="text.txt"]', + run() {}, + }, + { + content: "Check subject is autofilled", + trigger: '[name="subject"] input', + run() { + const subjectValue = document.querySelector('[name="subject"] input').value; + if (subjectValue !== "Re: Test User") { + console.error( + `Full composer should have "Re: Test User" in subject input (actual: ${subjectValue})` + ); + } + }, + }, + { + content: "Check composer content is kept", + trigger: '.o_field_html[name="body"]', + run() { + const bodyContent = document.querySelector('.o_field_html[name="body"]') + .textContent; + if (!bodyContent.includes("blahblah")) { + console.error( + `Full composer should contain text from small composer ("blahblah") in body input (actual: ${bodyContent})` + ); + } + }, + }, + { + content: "Open templates", + trigger: '.o_field_widget[name="template_id"] input', + }, + { + content: "Check a template is listed", + in_modal: false, + trigger: '.ui-autocomplete .ui-menu-item a:contains("Test template")', + run() {}, + }, + { + content: "Send message", + trigger: ".o_mail_send", + }, + { + content: "Check message is shown", + trigger: '.o_MessageView:contains("blahblah")', + }, + { + content: "Check message contains the attachment", + trigger: '.o_MessageView .o_AttachmentCard_filename:contains("text.txt")', + }, + ] +); diff --git a/addons/web/tooling/_eslintignore b/addons/web/tooling/_eslintignore index 8f89e3746a6..ba3f1b7b2ed 100644 --- a/addons/web/tooling/_eslintignore +++ b/addons/web/tooling/_eslintignore @@ -147,3 +147,7 @@ addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js !addons/auth_password_policy !addons/auth_password_policy/**/* + +# Whitelist mail +!addons/mail/ +!addons/mail/**/* diff --git a/addons/web/tooling/_prettierignore b/addons/web/tooling/_prettierignore index edde8208dcf..7409a91c2f7 100644 --- a/addons/web/tooling/_prettierignore +++ b/addons/web/tooling/_prettierignore @@ -151,3 +151,7 @@ addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js !addons/auth_password_policy !addons/auth_password_policy/**/* + +# Whitelist mail +!addons/mail/ +!addons/mail/**/*