[REF] mail,*: adapt code to owl 2

*im_livechat,sms,snailmail

Owl 2 changelog: https://github.com/odoo/owl/blob/a9f29c4caad4f32d06be1ec4780572825781cd9b/CHANGELOG.md

Part-of: odoo/odoo#80156
Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Bruno Boi <boi@odoo.com>
Co-authored-by: Géry Debongnie <ged@odoo.com>
Co-authored-by: Samuel Degueldre <sad@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Co-authored-by: Simon Genin (ges) <ges@odoo.com>
Co-authored-by: Francois (fge) <fge@odoo.com>
Co-authored-by: Michael Mattiello (mcm) <mcm@odoo.com>
Co-authored-by: Julien Mougenot <jum@odoo.com>
Co-authored-by: luvi <luvi@odoo.com>
Co-authored-by: Lucas Perais (lpe) <lpe@odoo.com>
Co-authored-by: Jorge Pinna Puissant <jpp@odoo.com>
This commit is contained in:
Samuel Degueldre
2022-02-10 07:46:13 +00:00
co-authored by Aaron Bohy Bruno Boi Géry Debongnie Mathieu Duckerts-Antoine Simon Genin Francois Michael Mattiello Julien Mougenot luvi Lucas Perais Jorge Pinna Puissant
parent 0c7910529d
commit edd79ff560
76 changed files with 445 additions and 362 deletions
+2 -1
View File
@@ -80,7 +80,7 @@ Help your customers with this chat, and analyse their feedback.
'web/static/lib/qweb/qweb2.js',
# Odoo JS Framework
'web/static/lib/owl/owl.js',
'web/static/src/owl_next_adapter.js',
'web/static/src/owl2_compatibility/*.js',
'web/static/src/legacy/js/promise_extension.js',
'web/static/src/boot.js',
'web/static/src/core/assets.js',
@@ -101,6 +101,7 @@ Help your customers with this chat, and analyse their feedback.
'web/static/src/core/notifications/notification_container.js',
'web/static/src/core/notifications/notification_service.js',
'web/static/src/core/registry.js',
'web/static/src/core/transition.js',
'web/static/src/core/ui/block_ui.js',
'web/static/src/core/ui/ui_service.js',
'web/static/src/core/user_service.js',
@@ -2,7 +2,7 @@
import { clear } from '@mail/model/model_field_command';
const { onWillUpdateProps, useComponent } = owl;
const { onWillDestroy, onWillUpdateProps, useComponent } = owl;
/**
* This hook provides support for saving the reference of the component directly
@@ -30,12 +30,10 @@ export function useComponentToModel({ fieldName, modelName }) {
nextRecord.update({ [fieldName]: component });
}
});
const __destroy = component.__destroy;
component.__destroy = parent => {
onWillDestroy(() => {
const record = modelManager.models[modelName].get(component.props.localId);
if (record) {
record.update({ [fieldName]: clear() });
}
__destroy.call(component, parent);
};
});
}
@@ -2,7 +2,7 @@
import { Listener } from '@mail/model/model_listener';
const { useComponent } = owl;
const { onRendered, onWillDestroy, onWillRender, useComponent } = owl;
/**
* This hook provides support for automatically re-rendering when used records
@@ -24,23 +24,21 @@ export function useModels() {
name: `useModels() of ${component}`,
onChange: () => component.render(),
});
const __render = component.__render;
component.__render = fiber => {
onWillRender(() => {
if (modelManager) {
modelManager.startListening(listener);
}
__render.call(component, fiber);
});
onRendered(() => {
if (modelManager) {
modelManager.stopListening(listener);
}
};
const __destroy = component.__destroy;
component.__destroy = parent => {
});
onWillDestroy(() => {
if (modelManager) {
modelManager.removeListener(listener);
}
__destroy.call(component, parent);
};
});
modelManager.messagingCreatedPromise.then(() => {
component.render();
});
@@ -2,7 +2,7 @@
import { clear } from '@mail/model/model_field_command';
const { onWillUpdateProps, useComponent, useRef } = owl;
const { onWillDestroy, onWillUpdateProps, useComponent, useRef } = owl;
/**
* This hook provides support for saving the result of useRef directly into the
@@ -32,12 +32,10 @@ export function useRefToModel({ fieldName, modelName, refName }) {
nextRecord.update({ [fieldName]: ref });
}
});
const __destroy = component.__destroy;
component.__destroy = parent => {
onWillDestroy(() => {
const record = modelManager.models[modelName].get(component.props.localId);
if (record) {
record.update({ [fieldName]: clear() });
}
__destroy.call(component, parent);
};
});
}
@@ -2,7 +2,7 @@
import { Listener } from '@mail/model/model_listener';
const { onMounted, onPatched, useComponent } = owl;
const { onMounted, onPatched, onWillDestroy, onWillRender, useComponent } = owl;
/**
* This hooks provides support for accessing the values returned by the given
@@ -22,22 +22,16 @@ export function useRenderedValues(selector) {
name: `useRenderedValues() of ${component}`,
onChange: () => component.render(),
});
const __render = component.__render.bind(component);
component.__render = function () {
onWillRender(() => {
modelManager.startListening(listener);
renderedValues = selector();
modelManager.stopListening(listener);
return __render(...arguments);
};
})
onMounted(onUpdate);
onPatched(onUpdate);
function onUpdate() {
patchedValues = renderedValues;
}
const __destroy = component.__destroy;
component.__destroy = parent => {
modelManager.removeListener(listener);
__destroy.call(component, parent);
};
onWillDestroy(() => modelManager.removeListener(listener));
return () => patchedValues;
}
@@ -2,7 +2,7 @@
import { Listener } from '@mail/model/model_listener';
const { onMounted, onPatched, useComponent } = owl;
const { onMounted, onPatched, onWillDestroy, useComponent } = owl;
/**
* This hook provides support for executing code after update (render or patch).
@@ -29,11 +29,9 @@ export function useUpdate({ func }) {
}
onMounted(onUpdate);
onPatched(onUpdate);
const __destroy = component.__destroy;
component.__destroy = parent => {
onWillDestroy(() => {
if (modelManager) {
modelManager.removeListener(listener);
}
__destroy.call(component, parent);
};
});
}
@@ -2,10 +2,16 @@
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
import Popover from "web.Popover";
const { Component, markup } = owl;
export class Activity extends Component {
get noteAsMarkup() {
return markup(this.activityView.activity.note);
}
/**
* @returns {ActivityView}
*/
@@ -18,6 +24,7 @@ export class Activity extends Component {
Object.assign(Activity, {
props: { localId: String },
template: 'mail.Activity',
components: { Popover },
});
registerMessagingComponent(Activity);
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="mail.Activity" owl="1">
<div class="o_Activity d-flex p-2" t-attf-class="{{ className }}" t-on-click="activityView.onClickActivity" t-ref="root">
<div class="o_Activity d-flex p-2" t-attf-class="{{ className }}" t-on-click="activityView and activityView.onClickActivity" t-ref="root">
<t t-if="activityView">
<div class="o_Activity_sidebar mr-3">
<div class="o_Activity_user position-relative">
@@ -102,7 +102,7 @@
<t t-if="activityView.activity.note">
<div class="o_Activity_note">
<t t-raw="activityView.activity.note"/>
<t t-out="noteAsMarkup"/>
</div>
</t>
@@ -125,7 +125,7 @@
<button class="o_Activity_toolButton o_Activity_markDoneButton btn btn-link btn-primary pt-0 pl-0" t-att-title="activityView.markDoneText">
<i class="fa fa-check"/> Mark Done
</button>
<t t-set="opened">
<t t-set-slot="opened">
<ActivityMarkDonePopover localId="activityView.activityMarkDonePopoverView.localId"/>
</t>
</Popover>
@@ -14,8 +14,8 @@ QUnit.module('activity_tests.js', {
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { components, env, widget } = res;
this.components = components;
const { apps, env, widget } = res;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;
@@ -13,8 +13,8 @@ QUnit.module('activity_mark_done_popover_tests.js', {
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { components, env, widget } = res;
this.components = components;
const { apps, env, widget } = res;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;
@@ -11,7 +11,6 @@ export class AttachmentDeleteConfirm extends Component {
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'AttachmentDeleteConfirmView' });
}
@@ -16,9 +16,9 @@ QUnit.module('attachment_image_tests.js', {
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { afterEvent, components, env, widget } = res;
const { afterEvent, apps, env, widget } = res;
this.afterEvent = afterEvent;
this.components = components;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;
@@ -18,9 +18,9 @@ QUnit.module('attachment_list_tests.js', {
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { afterEvent, components, env, widget } = res;
const { afterEvent, apps, env, widget } = res;
this.afterEvent = afterEvent;
this.components = components;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;
@@ -2,7 +2,7 @@
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component, onMounted, onWillUnmount } = owl;
const { Component, onMounted, onWillUnmount, useEffect } = owl;
export class AutocompleteInput extends Component {
@@ -13,6 +13,12 @@ export class AutocompleteInput extends Component {
super.setup();
onMounted(() => this._mounted());
onWillUnmount(() => this._willUnmount());
useEffect(() => {
if (this.props.inputRef) {
this.props.inputRef.el = this.root.el;
return () => this.props.inputRef.el = null;
}
})
}
_mounted() {
@@ -73,13 +79,6 @@ export class AutocompleteInput extends Component {
return element.contains(node);
}
focus() {
if (!this.root.el) {
return;
}
this.root.el.focus();
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -147,6 +146,7 @@ Object.assign(AutocompleteInput, {
isFocusOnMount: false,
isHtml: false,
placeholder: '',
onFocusin: () => {},
},
props: {
customClass: {
@@ -157,8 +157,14 @@ Object.assign(AutocompleteInput, {
type: Function,
optional: true,
},
isFocusOnMount: Boolean,
isHtml: Boolean,
isFocusOnMount: {
type: Boolean,
optional: true,
},
isHtml: {
type: Boolean,
optional: true,
},
onFocusin: {
type: Function,
optional: true,
@@ -167,7 +173,10 @@ Object.assign(AutocompleteInput, {
type: Function,
optional: true,
},
placeholder: String,
placeholder: {
type: String,
optional: true,
},
select: {
type: Function,
optional: true,
@@ -176,6 +185,10 @@ Object.assign(AutocompleteInput, {
type: Function,
optional: true,
},
inputRef: {
type: { el: Object },
optional: true,
},
},
template: 'mail.AutocompleteInput',
});
@@ -11,7 +11,7 @@
</div>
<div class="d-flex flex-column flex-grow-1 mx-0 py-2 overflow-auto">
<t t-foreach="channelInvitationForm.selectablePartners" t-as="selectablePartner" t-key="selectablePartner.localId">
<div class="o_ChannelInvitationForm_selectablePartner d-flex align-items-center px-3 py-1" t-on-click="channelInvitationForm.onClickSelectablePartner.bind(channelInvitationForm, selectablePartner)" t-att-data-partner-id="selectablePartner.id">
<div class="o_ChannelInvitationForm_selectablePartner d-flex align-items-center px-3 py-1" t-on-click="() => channelInvitationForm.onClickSelectablePartner(selectablePartner)" t-att-data-partner-id="selectablePartner.id">
<div class="o_ChannelInvitationForm_selectablePartnerAvatarContainer flex-shrink-0 position-relative">
<img class="o_ChannelInvitationForm_selectablePartnerAvatar w-100 h-100 rounded-circle" t-att-src="selectablePartner.avatarUrl" alt="Avatar"/>
<t t-if="selectablePartner.im_status and selectablePartner.im_status !== 'im_partner'">
@@ -29,7 +29,7 @@
<t t-esc="selectablePartner.nameOrDisplayName"/>
</span>
<div class="o_ChannelInvitationForm_selectablePartnerNameSelectionSeparator flex-grow-1 flex-shrink-0"/>
<input class="o_ChannelInvitationForm_selectablePartnerCheckbox flex-shrink-0" type="checkbox" t-att-checked="channelInvitationForm.selectedPartners.includes(selectablePartner) ? 'checked' : undefined" t-on-input="channelInvitationForm.onInputPartnerCheckbox.bind(channelInvitationForm, selectablePartner)" t-ref="selection-status"/>
<input class="o_ChannelInvitationForm_selectablePartnerCheckbox flex-shrink-0" type="checkbox" t-att-checked="channelInvitationForm.selectedPartners.includes(selectablePartner) ? 'checked' : undefined" t-on-input="ev => channelInvitationForm.onInputPartnerCheckbox(selectablePartner, ev)" t-ref="selection-status"/>
</div>
</t>
<t t-if="channelInvitationForm.selectablePartners.length === 0">
@@ -46,7 +46,7 @@
<h4>Selected users:</h4>
<div class="o_ChannelInvitationForm_selectedPartners overflow-auto">
<t t-foreach="channelInvitationForm.selectedPartners" t-as="selectedPartner" t-key="selectedPartner.localId">
<button class="btn btn-secondary" t-on-click="channelInvitationForm.onClickSelectedPartner.bind(channelInvitationForm, selectedPartner)">
<button class="btn btn-secondary" t-on-click="() => channelInvitationForm.onClickSelectedPartner(selectedPartner)">
<t t-esc="selectedPartner.nameOrDisplayName"/> <i class="fa fa-times"/>
</button>
</t>
@@ -33,7 +33,7 @@
<h6 class="m-2"><t t-esc="title"/> - <t t-esc="members.length"/></h6>
<t t-foreach="members" t-as="member" t-key="member.localId">
<div class="o_ChannelMemberList_member d-flex align-items-center mx-3 my-1">
<div class="o_ChannelMemberList_avatarContainer position-relative flex-shrink-0" t-on-click="channel.onClickMemberAvatar(member)">
<div class="o_ChannelMemberList_avatarContainer position-relative flex-shrink-0" t-on-click="() => channel.onClickMemberAvatar(member)">
<img class="o_ChannelMemberList_avatar rounded-circle w-100 h-100" t-attf-src="/mail/channel/{{ channel.id }}/partner/{{ member.id }}/avatar_128" alt="Avatar"/>
<t t-if="member.im_status and member.im_status !== 'im_partner'">
@@ -46,7 +46,7 @@
/>
</t>
</div>
<span class="o_ChannelMemberList_name ml-2 flex-column-1 text-truncate" t-on-click="channel.onClickMemberName(member)">
<span class="o_ChannelMemberList_name ml-2 flex-column-1 text-truncate" t-on-click="() => channel.onClickMemberName(member)">
<t t-esc="member.nameOrDisplayName"/>
</span>
</div>
@@ -19,7 +19,7 @@ export class ChatWindow extends Component {
* Useful when focusing this chat window, which consists of focusing
* this input.
*/
this._inputRef = useRef('input');
this._inputRef = { el: null };
// the following are passed as props to children
this._onAutocompleteSelect = this._onAutocompleteSelect.bind(this);
this._onAutocompleteSource = this._onAutocompleteSource.bind(this);
@@ -109,8 +109,8 @@ export class ChatWindow extends Component {
}
if (this.chatWindow.isDoFocus) {
this.chatWindow.update({ isDoFocus: false });
if (this._inputRef.comp) {
this._inputRef.comp.focus();
if (this._inputRef.el) {
this._inputRef.el.focus();
}
}
this._applyVisibleOffset();
@@ -254,9 +254,9 @@ Object.assign(ChatWindow, {
},
props: {
localId: String,
hasCloseAsBackButton: Boolean,
isExpandable: Boolean,
isFullscreen: Boolean,
hasCloseAsBackButton: { type: Boolean, optional: true },
isExpandable: { type: Boolean, optional: true },
isFullscreen: { type: Boolean, optional: true },
},
template: 'mail.ChatWindow',
});
@@ -45,7 +45,7 @@
select="_onAutocompleteSelect"
source="_onAutocompleteSource"
onFocusin="chatWindow.onFocusInNewMessageFormInput"
t-ref="input"
inputRef="_inputRef"
/>
</div>
</t>
@@ -137,8 +137,14 @@ Object.assign(ChatWindowHeader, {
},
props: {
chatWindowLocalId: String,
hasCloseAsBackButton: Boolean,
isExpandable: Boolean,
hasCloseAsBackButton: {
type: Boolean,
optional: true,
},
isExpandable: {
type: Boolean,
optional: true,
},
onClicked: {
type: Function,
optional: true,
@@ -3,7 +3,7 @@
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { clear } from '@mail/model/model_field_command';
const { Component, onWillUpdateProps } = owl;
const { Component, onWillDestroy, onWillUpdateProps } = owl;
const getChatterNextTemporaryId = (function () {
let tmpId = 0;
@@ -32,6 +32,7 @@ export class ChatterContainer extends Component {
this.chatterId = getChatterNextTemporaryId();
this._insertFromProps(this.props);
onWillUpdateProps(nextProps => this._willUpdateProps(nextProps));
onWillDestroy(this._onWillDestroy);
}
_willUpdateProps(nextProps) {
@@ -41,8 +42,7 @@ export class ChatterContainer extends Component {
/**
* @override
*/
destroy() {
super.destroy();
_onWillDestroy() {
if (this.chatter) {
this.chatter.delete();
}
@@ -219,16 +219,34 @@ Object.assign(Composer, {
},
props: {
localId: String,
hasCurrentPartnerAvatar: Boolean,
hasDiscardButton: Boolean,
hasFollowers: Boolean,
hasCurrentPartnerAvatar: {
type: Boolean,
optional: true,
},
hasDiscardButton: {
type: Boolean,
optional: true,
},
hasFollowers: {
type: Boolean,
optional: true,
},
hasMentionSuggestionsBelowPosition: {
type: Boolean,
optional: true,
},
hasSendButton: Boolean,
hasThreadName: Boolean,
hasThreadTyping: Boolean,
hasSendButton: {
type: Boolean,
optional: true,
},
hasThreadName: {
type: Boolean,
optional: true,
},
hasThreadTyping: {
type: Boolean,
optional: true,
},
showAttachmentsExtensions: {
type: Boolean,
optional: true,
@@ -237,8 +255,14 @@ Object.assign(Composer, {
type: Boolean,
optional: true,
},
isCompact: Boolean,
isExpandable: Boolean,
isCompact: {
type: Boolean,
optional: true,
},
isExpandable: {
type: Boolean,
optional: true,
},
},
template: 'mail.Composer',
});
@@ -28,9 +28,9 @@ QUnit.module('composer_tests.js', {
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { afterEvent, components, env, widget } = res;
const { afterEvent, apps, env, widget } = res;
this.afterEvent = afterEvent;
this.components = components;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;
@@ -10,13 +10,23 @@ const { Component, useRef } = owl;
class FormViewDialogComponentAdapter extends ComponentAdapter {
renderWidget() {
async renderWidget() {
// Ensure the dialog is properly reconstructed. Without this line, it is
// impossible to open the dialog again after having it closed a first
// time, because the DOM of the dialog has disappeared.
return this.willStart();
await this.onWillStart();
this.props.setFormViewDialogWidget(this.widget);
}
updateWidget() {
// This component should never be re-rendered but because shouldUpdate was removed,
// when the Composer is rerendered, so is the ComposerSuggestedRecipients even
// though its props haven't changed and there is nothing to do.
}
get widgetArgs() {
return [this.props.params];
}
}
export class ComposerSuggestedRecipient extends Component {
@@ -42,7 +52,9 @@ export class ComposerSuggestedRecipient extends Component {
* Reference of the partner creation dialog. Useful to open it, for
* compatibility with old code.
*/
this._dialogRef = useRef('dialog');
this.setFormViewDialogWidget = (widget) => {
this._dialogWidget = widget;
};
/**
* Whether the dialog is currently open. `_dialogRef` cannot be trusted
* to know if the dialog is open due to manually calling `open` and
@@ -110,12 +122,12 @@ export class ComposerSuggestedRecipient extends Component {
// Recipients must always be partners. On selecting a suggested
// recipient that does not have a partner, the partner creation form
// should be opened.
if (isChecked && this._dialogRef && !this._isDialogOpen) {
if (isChecked && this._dialogWidget && !this._isDialogOpen) {
this._isDialogOpen = true;
this._dialogRef.comp.widget.on('closed', this, () => {
this._dialogWidget.on('closed', this, () => {
this._isDialogOpen = false;
});
this._dialogRef.comp.widget.open();
this._dialogWidget.open();
}
}
}
@@ -17,6 +17,7 @@
<t t-if="!suggestedRecipientInfo.partner">
<FormViewDialogComponentAdapter
Component="FormViewDialog"
setFormViewDialogWidget="setFormViewDialogWidget"
params="{
context: {
active_id: suggestedRecipientInfo.thread.id,
@@ -33,7 +34,6 @@
res_model: 'res.partner',
title: PLEASE_COMPLETE_CUSTOMER_S_INFORMATION,
}"
t-ref="dialog"
/>
</t>
</t>
@@ -116,7 +116,7 @@ Object.assign(ComposerSuggestion, {
},
props: {
composerViewLocalId: String,
isActive: Boolean,
isActive: { type: Boolean, optional: true },
modelName: String,
recordLocalId: String,
},
@@ -25,7 +25,7 @@ Object.assign(ComposerSuggestionList, {
},
props: {
composerViewLocalId: String,
isBelow: Boolean,
isBelow: { type: Boolean, optional: true },
},
template: 'mail.ComposerSuggestionList',
});
@@ -404,7 +404,10 @@ Object.assign(ComposerTextInput, {
},
props: {
localId: String,
hasMentionSuggestionsBelowPosition: Boolean,
hasMentionSuggestionsBelowPosition: {
type: Boolean,
optional: true,
},
isCompact: Boolean,
onComposerTextInputSendShortcut: {
type: Function,
@@ -11,7 +11,6 @@ export class Discuss extends Component {
* @override
*/
setup() {
super.setup();
this._updateLocalStoreProps();
// bind since passed as props
this._onMobileAddItemHeaderInputSelect = this._onMobileAddItemHeaderInputSelect.bind(this);
@@ -28,9 +28,9 @@ QUnit.module('discuss_tests.js', {
data: this.data,
hasDiscuss: true,
}));
const { afterEvent, components, env, widget } = res;
const { afterEvent, apps, env, widget } = res;
this.afterEvent = afterEvent;
this.components = components;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;
@@ -10,7 +10,7 @@ export class DiscussSidebarCategory extends Component {
* @returns {DiscussSidebarCategory}
*/
get category() {
return this.messaging.models['DiscussSidebarCategory'].get(this.props.localId);
return this.messaging && this.messaging.models['DiscussSidebarCategory'].get(this.props.localId);
}
}
@@ -4,7 +4,7 @@
<t t-name="mail.EmojiList" owl="1">
<div class="o_EmojiList" t-attf-class="{{ className }}" t-ref="root">
<t t-foreach="emojis" t-as="emoji" t-key="emoji.unicode">
<span class="o_EmojiList_emoji" t-on-click="emojiListView.onClickEmoji" t-att-title="emoji.description" t-att-data-source="emoji.sources[0]" t-att-data-unicode="emoji.unicode">
<span class="o_EmojiList_emoji" t-on-click="ev => emojiListView.onClickEmoji(ev)" t-att-title="emoji.description" t-att-data-source="emoji.sources[0]" t-att-data-unicode="emoji.unicode">
<t t-esc="emoji.unicode"/>
</span>
</t>
@@ -11,13 +11,13 @@ QUnit.module('file_uploader', {}, function () {
QUnit.module('file_uploader_tests.js', {
beforeEach() {
beforeEach(this);
this.components = [];
this.apps = [];
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { components, env, widget } = res;
const { apps, env, widget } = res;
this.env = env;
this.components = components;
this.apps = apps;
this.widget = widget;
return res;
};
@@ -73,7 +73,7 @@ Object.assign(FollowButton, {
isDisabled: false,
},
props: {
isDisabled: Boolean,
isDisabled: { type: Boolean, optional: true },
threadLocalId: String,
},
template: 'mail.FollowButton',
@@ -51,6 +51,9 @@ export class Follower extends Component {
}
Object.assign(Follower, {
defaultProps: {
onClick: () => {},
},
props: {
followerLocalId: String,
onClick: {
@@ -117,7 +117,7 @@ Object.assign(FollowerListMenu, {
isDisabled: false,
},
props: {
isDisabled: Boolean,
isDisabled: { type: Boolean, optional: true },
threadLocalId: String,
},
template: 'mail.FollowerListMenu',
@@ -8,6 +8,7 @@ import { isEventHandled, markEventHandled } from '@mail/utils/utils';
import { _lt } from 'web.core';
import { format } from 'web.field_utils';
import Popover from "web.Popover";
import { getLangDatetimeFormat } from 'web.time';
const { Component, onWillUnmount, useRef, useState } = owl;
@@ -557,6 +558,7 @@ export class Message extends Component {
Object.assign(Message, {
props: { localId: String },
template: 'mail.Message',
components: { Popover },
});
registerMessagingComponent(Message);
@@ -16,7 +16,7 @@
'o-squashed': messageView and messageView.isSquashed,
'o-starred': messageView and messageView.message.isStarred,
'mt-3': messageView and !messageView.isSquashed and messageView.threadView,
}" t-attf-class="{{ messageView and messageView.extraClass }} {{ className }}" t-on-click="_onClick" t-on-mouseenter="state.isHovered = true" t-on-mouseleave="state.isHovered = false" t-att-data-message-local-id="messageView and messageView.message.localId"
}" t-attf-class="{{ messageView and messageView.extraClass }} {{ className }}" t-on-click="_onClick" t-on-mouseenter="() => state.isHovered = true" t-on-mouseleave="() => state.isHovered = false" t-att-data-message-local-id="messageView and messageView.message.localId"
t-ref="root"
>
<t t-if="messageView" name="rootCondition">
@@ -111,7 +111,7 @@
<span class="o_Message_notificationIconClickable">
<i name="notificationIcon" class="o_Message_notificationIcon fa fa-envelope-o"/>
</span>
<t t-set="opened">
<t t-set-slot="opened">
<NotificationPopover
messageViewLocalId="messageView.localId"
/>
@@ -22,9 +22,9 @@ QUnit.module('message_tests.js', {
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { afterEvent, components, env, widget } = res;
const { afterEvent, apps, env, widget } = res;
this.afterEvent = afterEvent;
this.components = components;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="mail.MessageActionList" owl="1">
<div class="o_MessageActionList d-flex" t-attf-class="{{ className }}" t-on-click="messageActionList and messageActionList.onClick" t-ref="root">
<div class="o_MessageActionList d-flex" t-attf-class="{{ className }}" t-on-click="(ev) => messageActionList and messageActionList.onClick(ev)" t-ref="root">
<t t-if="messageActionList">
<i t-if="messageActionList.message.hasReactionIcon" class="o_MessageActionList_action o_MessageActionList_actionReaction p-2 fa fa-lg fa-smile-o" t-att-title="ADD_A_REACTION" t-on-click="messageActionList.onClickActionReaction" t-ref="actionReaction"/>
<PopoverView t-if="messageActionList.reactionPopoverView" localId="messageActionList.reactionPopoverView.localId"/>
@@ -2,9 +2,14 @@
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
const { Component, markup } = owl;
export class MessageInReplyToView extends Component {
get contentAsMarkup() {
return markup(this.messageInReplyToView.messageView.message.parentMessage.prettyBody);
}
/**
* @returns {MessageInReplyToView}
*/
@@ -7,7 +7,7 @@
<b class="o_MessageInReplyToView_author text-muted ml-2">@<t t-esc="messageInReplyToView.messageView.message.parentMessage.authorName"/></b>
<span class="o_MessageInReplyToView_body ml-1" t-on-click="messageInReplyToView.onClickReply">
<t t-if="messageInReplyToView.hasBodyBackLink">
<t t-raw="messageInReplyToView.messageView.message.parentMessage.prettyBody"/>
<t t-out="contentAsMarkup"/>
</t>
<t t-if="messageInReplyToView.hasAttachmentBackLink">
<span class="font-italic mr-2">Click to see the attachments</span>
@@ -5,6 +5,8 @@ import { useComponentToModel } from '@mail/component_hooks/use_component_to_mode
import { useRenderedValues } from '@mail/component_hooks/use_rendered_values/use_rendered_values';
import { useUpdate } from '@mail/component_hooks/use_update/use_update';
import { Transition } from "@web/core/transition";
const { Component, onWillPatch, useRef } = owl;
export class MessageList extends Component {
@@ -385,6 +387,7 @@ export class MessageList extends Component {
}
Object.assign(MessageList, {
components: { Transition },
defaultProps: {
hasScrollAdjust: true,
},
@@ -397,7 +400,10 @@ Object.assign(MessageList, {
type: Function,
optional: true,
},
hasScrollAdjust: Boolean,
hasScrollAdjust: {
type: Boolean,
optional: true,
},
localId: String,
},
template: 'mail.MessageList',
@@ -123,11 +123,8 @@
// ------------------------------------------------------------------
.o_MessageList_separatorNewMessages:not(.o-disable-animation) {
&.fade-leave-active {
transition: opacity 0.5s;
}
&.fade-leave-to {
transition: opacity 0.5s;
&.o-fade-leave {
opacity: 0;
}
}
@@ -48,11 +48,11 @@
<!-- MESSAGES -->
<t t-set="current_day" t-value="0"/>
<t t-foreach="messageListView.threadViewOwner.messageViews" t-as="messageView" t-key="messageView.localId">
<t t-if="messageView.message === messageListView.threadViewOwner.thread.messageAfterNewMessageSeparator">
<div class="o_MessageList_separator o_MessageList_separatorNewMessages o_MessageList_item" t-att-class="{ 'o-disable-animation': messaging.disableAnimation }" t-transition="fade">
<Transition visible="messageView.message === messageListView.threadViewOwner.thread.messageAfterNewMessageSeparator" name="'o-fade'" t-slot-scope="transition">
<div class="o_MessageList_separator o_MessageList_separatorNewMessages o_MessageList_item" t-att-class="{ 'o-disable-animation': messaging.disableAnimation }" t-attf-class="{{transition.className}}">
<hr class="o_MessageList_separatorLine o_MessageList_separatorLineNewMessages"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelNewMessages">New messages</span>
</div>
</t>
</Transition>
<t t-if="!messageView.message.isEmpty">
<t t-set="message_day" t-value="messageView.message.dateDay"/>
<t t-if="current_day !== message_day">
@@ -397,7 +397,6 @@ QUnit.test('new message', async function (assert) {
hasChatWindow: true,
});
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_toggler`).click()
);
@@ -3,7 +3,7 @@
import { useModels } from "@mail/component_hooks/use_models/use_models";
import { getMessagingComponent } from "@mail/utils/messaging_component";
const { Component } = owl;
const { Component, useSubEnv } = owl;
export class MessagingMenuContainer extends Component {
@@ -13,7 +13,7 @@ export class MessagingMenuContainer extends Component {
setup() {
// for now, the legacy env is needed for internal functions such as
// `useModels` to work
this.env = Component.env;
useSubEnv(Component.env);
useModels();
super.setup();
}
@@ -5,7 +5,7 @@
<div class="o_MobileMessagingNavbar" t-attf-class="{{ className }}" t-ref="root">
<t t-if="mobileMessagingNavbarView">
<t t-foreach="mobileMessagingNavbarView.tabs" t-as="tab" t-key="tab.id">
<div class="o_MobileMessagingNavbar_tab" t-att-class="{ 'o-active': mobileMessagingNavbarView.activeTabId === tab.id }" t-on-click="mobileMessagingNavbarView.onClick.bind(mobileMessagingNavbarView, tab.id)" t-att-data-tab-id="tab.id">
<div class="o_MobileMessagingNavbar_tab" t-att-class="{ 'o-active': mobileMessagingNavbarView.activeTabId === tab.id }" t-on-click="ev => mobileMessagingNavbarView.onClick(tab.id, ev)" t-att-data-tab-id="tab.id">
<t t-if="tab.icon">
<span class="o_MobileMessagingNavbar_tabIcon" t-att-class="tab.icon"/>
</t>
@@ -41,12 +41,12 @@ Object.assign(PartnerImStatusIcon, {
},
props: {
partnerLocalId: String,
hasBackground: Boolean,
hasBackground: { type: Boolean, optional: true },
/**
* Determines whether a click on `this` should open a chat with
* `this.partner`.
*/
hasOpenChat: Boolean,
hasOpenChat: { type: Boolean, optional: true },
},
template: 'mail.PartnerImStatusIcon',
});
@@ -3,7 +3,7 @@
import { useModels } from "@mail/component_hooks/use_models/use_models";
import { getMessagingComponent } from "@mail/utils/messaging_component";
const { Component } = owl;
const { Component, useSubEnv } = owl;
export class RtcActivityNoticeContainer extends Component {
@@ -13,7 +13,7 @@ export class RtcActivityNoticeContainer extends Component {
setup() {
// for now, the legacy env is needed for internal functions such as
// `useModels` to work
this.env = Component.env;
useSubEnv(Component.env);
useModels();
super.setup();
}
@@ -2,6 +2,8 @@
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import Popover from "web.Popover";
const { Component, useRef } = owl;
export class RtcCallParticipantCard extends Component {
@@ -48,6 +50,7 @@ export class RtcCallParticipantCard extends Component {
Object.assign(RtcCallParticipantCard, {
props: { localId: String },
template: 'mail.RtcCallParticipantCard',
components: { Popover },
});
registerMessagingComponent(RtcCallParticipantCard);
@@ -2,6 +2,8 @@
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import Popover from "web.Popover";
const { Component } = owl;
export class RtcController extends Component {
@@ -18,6 +20,7 @@ export class RtcController extends Component {
Object.assign(RtcController, {
props: { localId: String },
template: 'mail.RtcController',
components: { Popover },
});
registerMessagingComponent(RtcController);
@@ -15,10 +15,12 @@ Object.assign(ThreadTypingIcon, {
animation: {
type: String,
validate: prop => ['bounce', 'none', 'pulse'].includes(prop),
optional: true,
},
size: {
type: String,
validate: prop => ['small', 'medium'].includes(prop),
optional: true,
},
title: {
type: String,
@@ -18,9 +18,9 @@ QUnit.module('thread_view_tests.js', {
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { afterEvent, components, env, widget } = res;
const { afterEvent, apps, env, widget } = res;
this.afterEvent = afterEvent;
this.components = components;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;
@@ -42,6 +42,7 @@ Object.assign(ThreadView, {
hasComposerThreadName: false,
showComposerAttachmentsExtensions: true,
showComposerAttachmentsFilenames: true,
onFocusin: () => {},
},
props: {
/**
@@ -86,8 +87,14 @@ Object.assign(ThreadView, {
type: Function,
optional: true,
},
showComposerAttachmentsExtensions: Boolean,
showComposerAttachmentsFilenames: Boolean,
showComposerAttachmentsExtensions: {
type: Boolean,
optional: true,
},
showComposerAttachmentsFilenames: {
type: Boolean,
optional: true,
},
},
template: 'mail.ThreadView',
});
+1 -2
View File
@@ -1,8 +1,7 @@
/** @odoo-module **/
import core from 'web.core';
const { escape } = owl;
import { escape } from '@web/core/utils/strings';
var _t = core._t;
@@ -14,7 +14,6 @@ import utils from 'web.utils';
const { useState } = owl;
const _t = core._t;
/**
* Owl Component Adapter for ActivityRecord which is KanbanRecord (Odoo Widget)
* TODO: Remove this adapter when ActivityRecord is a Component
@@ -70,16 +69,23 @@ class KanbanColumnProgressBarAdapter extends ComponentAdapter {
// KanbanColumnProgressBar triggers 3 events before being mounted
// 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,
}));
}
super._trigger_up(ev);
}
}
}
class ActivityRenderer extends AbstractRendererOwl {
constructor(parent, props) {
super(...arguments);
setup() {
super.setup(...arguments);
this.qweb = new QWeb(this.env.isDebug(), {_s: session.origin});
this.qweb.add_template(utils.json_node_to_xml(props.templates));
this.qweb.add_template(utils.json_node_to_xml(this.props.templates));
this.activeFilter = useState({
state: null,
activityTypeId: null,
@@ -192,7 +198,11 @@ class ActivityRenderer extends AbstractRendererOwl {
} else {
this.activeFilter.state = null;
this.activeFilter.activityTypeId = null;
this.activeFilter.resIds = [];
if (this.activeFilter.resIds.length > 0) {
// writing a new array is a state mutation which triggers a rerender
// only replace resIds with empty array if it's not already empty
this.activeFilter.resIds = [];
}
}
}
}
@@ -4,6 +4,8 @@ import { registerModel } from '@mail/model/model_core';
import { attr, many, one } from '@mail/model/model_field';
import { clear, insert, unlink, unlinkAll } from '@mail/model/model_field_command';
const { markup } = owl;
registerModel({
name: 'Activity',
identifyingFields: ['id'],
@@ -53,9 +53,8 @@ registerModel({
},
/**
* @param {Partner} partner
* @param {MouseEvent} ev
*/
onClickSelectablePartner(partner, ev) {
onClickSelectablePartner(partner) {
if (this.selectedPartners.includes(partner)) {
this.update({ selectedPartners: unlink(partner) });
return;
@@ -64,9 +63,8 @@ registerModel({
},
/**
* @param {Partner} partner
* @param {MouseEvent} ev
*/
onClickSelectedPartner(partner, ev) {
onClickSelectedPartner(partner) {
this.update({ selectedPartners: unlink(partner) });
},
/**
@@ -8,7 +8,7 @@ import { OnChange } from '@mail/model/model_onchange';
import { addLink, escapeAndCompactTextContent, parseAndTransform } from '@mail/js/utils';
import { isEventHandled, markEventHandled } from '@mail/utils/utils';
const { escape } = owl;
import { escape } from '@web/core/utils/strings';
registerModel({
name: 'ComposerView',
@@ -3,8 +3,7 @@
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear, insertAndReplace, link, unlink } from '@mail/model/model_field_command';
const { escape } = owl;
import { escape } from '@web/core/utils/strings';
registerModel({
name: 'Discuss',
@@ -4,10 +4,11 @@ import { registerModel } from '@mail/model/model_core';
import { decrement, increment, insert, insertAndReplace, link, replace, unlink } from '@mail/model/model_field_command';
import { htmlToTextContentInline } from '@mail/js/utils';
import { escape } from '@web/core/utils/strings';
import { str_to_datetime } from 'web.time';
import { Markup } from 'web.utils';
import { renderToString } from "@web/core/utils/render";
const { escape } = owl;
const PREVIEW_MSG_MAX_SIZE = 350; // optimal for native English speakers
registerModel({
@@ -678,7 +679,7 @@ registerModel({
// hack: notification template does not support OWL components,
// so we simply use their template to make HTML as if it comes
// from component
const channelIcon = this.env.qweb.renderToString('mail.ThreadIcon', {
const channelIcon = renderToString('mail.ThreadIcon', {
env: this.env,
thread: channel,
});
@@ -24,13 +24,12 @@ import {
} from '@web/legacy/utils';
import * as legacySession from 'web.session';
const { Component, config, mount, whenReady } = owl;
const { Component, mount, whenReady } = owl;
Component.env = legacyEnv;
(async function boot() {
await whenReady();
config.mode = Component.env.isDebug() ? 'dev' : 'prod';
AbstractService.prototype.deployServices(Component.env);
const serviceRegistry = registry.category('services');
serviceRegistry.add('legacy_rpc', makeLegacyRpcService(Component.env));
@@ -39,7 +38,6 @@ Component.env = legacyEnv;
serviceRegistry.add('legacy_crash_manager', makeLegacyCrashManagerService(Component.env));
serviceRegistry.add('legacy_dialog_mapping', makeLegacyDialogMappingService(Component.env));
await legacySession.is_bound;
Component.env.qweb.addTemplates(legacySession.owlTemplates);
Object.assign(odoo, {
info: {
db: session.db,
@@ -54,7 +52,6 @@ Component.env = legacyEnv;
startServices(env),
odoo.loadTemplatesPromise.then(processTemplates),
]);
env.qweb.addTemplates(templates);
mapLegacyEnvToWowlEnv(Component.env, env);
odoo.isReady = true;
legacyServiceRegistry.add('messaging', MessagingService.extend({
@@ -62,16 +59,19 @@ Component.env = legacyEnv;
autofetchPartnerImStatus: false,
},
}));
await mount(MainComponentsContainer, { env, target: document.body });
createAndMountDiscussPublicView();
await mount(MainComponentsContainer, document.body, { env, templates, dev: env.debug });
createAndMountDiscussPublicView(templates);
})();
async function createAndMountDiscussPublicView() {
async function createAndMountDiscussPublicView(templates) {
const messaging = await Component.env.services.messaging.get();
// needed by the attachment viewer
const DialogManager = getMessagingComponent('DialogManager');
const dialogManagerComponent = new DialogManager(null, {});
await dialogManagerComponent.mount(document.body);
await mount(DialogManager, document.body, {
templates,
env: Component.env,
dev: Component.env.isDebug(),
});
messaging.models['Thread'].insert(messaging.models['Thread'].convertData(data.channelData));
const discussPublicView = messaging.models['DiscussPublicView'].create(data.discussPublicViewData);
if (discussPublicView.shouldDisplayWelcomeViewInitially) {
@@ -80,8 +80,12 @@ async function createAndMountDiscussPublicView() {
discussPublicView.switchToThreadView();
}
const DiscussPublicView = getMessagingComponent('DiscussPublicView');
const discussPublicViewComponent = new DiscussPublicView(null, {
localId: discussPublicView.localId,
await mount(DiscussPublicView, document.body, {
templates,
env: Component.env,
dev: Component.env.isDebug(),
props: {
localId: discussPublicView.localId,
},
});
await discussPublicViewComponent.mount(document.body);
}
+5 -4
View File
@@ -3,9 +3,10 @@
* TourManager so it can get access to _trigger_up.
*/
odoo.define("root.widget", function (require) {
const { ComponentAdapter } = require("web.OwlCompatibility");
// need to wait for owl.Component.env to be set by discuss_public_boot before
// we spawn the component adapter
require("@mail/public/discuss_public_boot");
const { standaloneAdapter } = require("web.OwlCompatibility");
const { Component } = owl;
return new ComponentAdapter(null, { Component });
return standaloneAdapter({ Component });
});
@@ -5,6 +5,8 @@ import { getMessagingComponent } from "@mail/utils/messaging_component";
import AbstractService from 'web.AbstractService';
import { bus } from 'web.core';
const { App } = owl;
export const ChatWindowService = AbstractService.extend({
dependencies: ['messaging'],
/**
@@ -18,9 +20,9 @@ export const ChatWindowService = AbstractService.extend({
* @private
*/
destroy() {
if (this.component) {
this.component.destroy();
this.component = undefined;
if (this.app) {
this.app.destroy();
this.app = undefined;
}
},
@@ -45,14 +47,20 @@ export const ChatWindowService = AbstractService.extend({
* @private
*/
async _mount() {
if (this.component) {
this.component.destroy();
this.component = undefined;
if (this.app) {
this.app.destroy();
this.app = undefined;
}
const ChatWindowManagerComponent = getMessagingComponent("ChatWindowManager");
this.component = new ChatWindowManagerComponent(null);
this.app = new App(ChatWindowManagerComponent, {
templates: window.__OWL_TEMPLATES__,
env: owl.Component.env,
dev: owl.Component.env.isDebug(),
translateFn: owl.Component.env._t,
translatableAttributes: ["data-tooltip"],
});
const parentNode = this._getParentNode();
await this.component.mount(parentNode);
await this.app.mount(parentNode);
},
//--------------------------------------------------------------------------
@@ -5,6 +5,8 @@ import { getMessagingComponent } from "@mail/utils/messaging_component";
import AbstractService from 'web.AbstractService';
import { bus } from 'web.core';
const { App } = owl;
export const DialogService = AbstractService.extend({
dependencies: ['messaging'],
/**
@@ -18,9 +20,9 @@ export const DialogService = AbstractService.extend({
* @private
*/
destroy() {
if (this.component) {
this.component.destroy();
this.component = undefined;
if (this.app) {
this.app.destroy();
this.app = undefined;
}
},
@@ -45,14 +47,20 @@ export const DialogService = AbstractService.extend({
* @private
*/
async _mount() {
if (this.component) {
this.component.destroy();
this.component = undefined;
if (this.app) {
this.app.destroy();
this.app = undefined;
}
const DialogManagerComponent = getMessagingComponent("DialogManager");
this.component = new DialogManagerComponent(null);
this.app = new App(DialogManagerComponent, {
templates: window.__OWL_TEMPLATES__,
env: owl.Component.env,
dev: owl.Component.env.isDebug(),
translateFn: owl.Component.env._t,
translatableAttributes: ["data-tooltip"],
});
const parentNode = this._getParentNode();
await this.component.mount(parentNode);
await this.app.mount(parentNode);
},
//--------------------------------------------------------------------------
+55 -97
View File
@@ -25,11 +25,13 @@ import {
mock,
} from 'web.test_utils';
import Widget from 'web.Widget';
import { getFixture } from "@web/../tests/helpers/utils";
import { createWebClient, getActionManagerServerData } from "@web/../tests/webclient/helpers";
import LegacyRegistry from "web.Registry";
const { Component, EventBus } = owl;
const { App, Component, EventBus } = owl;
const { afterNextRender } = App;
const {
addMockEnvironment,
patch: legacyPatch,
@@ -162,74 +164,11 @@ function _useDiscuss(callbacks, { afterNextRender }) {
* @returns {Promise}
*/
function nextAnimationFrame() {
const requestAnimationFrame = Component.scheduler.requestAnimationFrame;
return new Promise(function (resolve) {
setTimeout(() => requestAnimationFrame(() => resolve()));
});
}
/**
* Returns a promise resolved the next time OWL stops rendering.
*
* @param {function} func function which, when called, is
* expected to trigger OWL render(s).
* @param {number} [timeoutDelay=5000] in ms
* @returns {Promise}
*/
const afterNextRender = (function () {
const stop = Component.scheduler.stop;
const stopPromises = [];
Component.scheduler.stop = function () {
const wasRunning = this.isRunning;
stop.call(this);
if (wasRunning) {
while (stopPromises.length) {
stopPromises.pop().resolve();
}
}
};
async function afterNextRender(func, timeoutDelay = 5000) {
// Define the potential errors outside of the promise to get a proper
// trace if they happen.
const startError = new Error("Timeout: the render didn't start.");
const stopError = new Error("Timeout: the render didn't stop.");
// Set up the timeout to reject if no render happens.
let timeoutNoRender;
const timeoutProm = new Promise((resolve, reject) => {
timeoutNoRender = setTimeout(() => {
let error = startError;
if (Component.scheduler.isRunning) {
error = stopError;
}
console.error(error);
reject(error);
}, timeoutDelay);
});
// Set up the promise to resolve if a render happens.
const prom = makeTestPromise();
stopPromises.push(prom);
// Start the function expected to trigger a render after the promise
// has been registered to not miss any potential render.
const funcRes = func();
// Make them race (first to resolve/reject wins).
await Promise.race([prom, timeoutProm]);
clearTimeout(timeoutNoRender);
// Wait the end of the function to ensure all potential effects are
// taken into account during the following verification step.
await funcRes;
// Wait one more frame to make sure no new render has been queued.
await nextAnimationFrame();
if (Component.scheduler.isRunning) {
await afterNextRender(() => {}, timeoutDelay);
}
}
return afterNextRender;
})();
//------------------------------------------------------------------------------
// Public: test lifecycle
//------------------------------------------------------------------------------
@@ -290,7 +229,7 @@ function beforeEach(self) {
}
Object.assign(self, {
components: [],
apps: [],
data,
unpatch,
widget: undefined
@@ -310,9 +249,8 @@ function afterEach(self) {
// The components must be destroyed before the widget, because the
// widget might destroy the models before destroying the components,
// and the components might still rely on messaging (or other) record(s).
while (self.components.length > 0) {
const component = self.components.pop();
component.destroy();
while (self.apps.length > 0) {
self.apps.pop().destroy();
}
if (self.widget) {
self.widget.destroy();
@@ -386,11 +324,17 @@ function getAfterEvent({ messagingBus }) {
* @returns {Component}
*/
async function createRootComponent(self, Component, { props = {}, target }) {
Component.env = self.env;
const component = new Component(null, props);
delete Component.env;
self.components.push(component);
await afterNextRender(() => component.mount(target));
const app = new App(Component, {
props,
templates: window.__OWL_TEMPLATES__,
env: self.env,
test: true,
});
self.apps.push(app);
let component;
await afterNextRender(() => {
component = app.mount(target);
});
return component;
}
@@ -418,11 +362,11 @@ function getClick({ afterNextRender }) {
};
}
function getCreateChatterContainerComponent({ afterEvent, components, env, widget }) {
function getCreateChatterContainerComponent({ afterEvent, apps, env, widget }) {
return async function createChatterContainerComponent(props, { waitUntilMessagesLoaded = true } = {}) {
let chatterContainerComponent;
async function func() {
chatterContainerComponent = await createRootMessagingComponent({ components, env }, "ChatterContainer", {
chatterContainerComponent = await createRootMessagingComponent({ apps, env }, "ChatterContainer", {
props,
target: widget.el,
});
@@ -449,67 +393,67 @@ function getCreateChatterContainerComponent({ afterEvent, components, env, widge
};
}
function getCreateComposerComponent({ components, env, modelManager, widget }) {
function getCreateComposerComponent({ apps, env, modelManager, widget }) {
return async function createComposerComponent(composer, props) {
const composerView = modelManager.messaging.models['ComposerView'].create({
qunitTest: insertAndReplace({
composer: replace(composer),
}),
});
return await createRootMessagingComponent({ components, env }, "Composer", {
return await createRootMessagingComponent({ apps, env }, "Composer", {
props: { localId: composerView.localId, ...props },
target: widget.el,
});
};
}
function getCreateComposerSuggestionComponent({ components, env, modelManager, widget }) {
function getCreateComposerSuggestionComponent({ apps, env, modelManager, widget }) {
return async function createComposerSuggestionComponent(composer, props) {
const composerView = modelManager.messaging.models['ComposerView'].create({
qunitTest: insertAndReplace({
composer: replace(composer),
}),
});
await createRootMessagingComponent({ components, env }, "ComposerSuggestion", {
await createRootMessagingComponent({ apps, env }, "ComposerSuggestion", {
props: { ...props, composerViewLocalId: composerView.localId },
target: widget.el,
});
};
}
function getCreateMessageComponent({ components, env, modelManager, widget }) {
function getCreateMessageComponent({ apps, env, modelManager, widget }) {
return async function createMessageComponent(message) {
const messageView = modelManager.messaging.models['MessageView'].create({
message: replace(message),
qunitTest: insertAndReplace(),
});
await createRootMessagingComponent({ components, env }, "Message", {
await createRootMessagingComponent({ apps, env }, "Message", {
props: { localId: messageView.localId },
target: widget.el,
});
};
}
function getCreateMessagingMenuComponent({ components, env, widget }) {
function getCreateMessagingMenuComponent({ apps, env, widget }) {
return async function createMessagingMenuComponent() {
return await createRootComponent({ components, env }, MessagingMenuContainer, { target: widget.el });
return await createRootComponent({ apps, env }, MessagingMenuContainer, { target: widget.el });
};
}
function getCreateNotificationListComponent({ components, env, modelManager, widget }) {
function getCreateNotificationListComponent({ apps, env, modelManager, widget }) {
return async function createNotificationListComponent({ filter = 'all' } = {}) {
const notificationListView = modelManager.messaging.models['NotificationListView'].create({
filter,
qunitTestOwner: insertAndReplace(),
});
await createRootMessagingComponent({ components, env }, "NotificationList", {
await createRootMessagingComponent({ apps, env }, "NotificationList", {
props: { localId: notificationListView.localId },
target: widget.el,
});
};
}
function getCreateThreadViewComponent({ afterEvent, components, env, widget }) {
function getCreateThreadViewComponent({ afterEvent, apps, env, widget }) {
return async function createThreadViewComponent(threadView, otherProps = {}, { isFixedSize = false, waitUntilMessagesLoaded = true } = {}) {
let target;
if (isFixedSize) {
@@ -526,7 +470,7 @@ function getCreateThreadViewComponent({ afterEvent, components, env, widget }) {
target = widget.el;
}
async function func() {
return createRootMessagingComponent({ components, env }, "ThreadView", { props: { localId: threadView.localId, ...otherProps }, target });
return createRootMessagingComponent({ apps, env }, "ThreadView", { props: { localId: threadView.localId, ...otherProps }, target });
}
if (waitUntilMessagesLoaded) {
await afterNextRender(() => afterEvent({
@@ -549,6 +493,16 @@ function getCreateThreadViewComponent({ afterEvent, components, env, widget }) {
function getOpenDiscuss({ afterNextRender, discussWidget }) {
return async function openDiscuss() {
await afterNextRender(() => discussWidget.on_attach_callback());
// Some changes in the models are made on mount, but these changes don't
// cause a rerender directly, they cause the model to fetch more data
// but we cannot wait for that data to come back as the model manager
// doesn't expose it. This means that in the following microticks, the
// data will come back from the server and cause a render. The following
// is a way for us to catch the render cascade caused by the data coming
// back and wait for it.
await afterNextRender(() => {
discussWidget.app.root.render();
});
};
}
@@ -592,6 +546,8 @@ function getOpenDiscuss({ afterNextRender, discussWidget }) {
* the res_id to use in createView.
* @param {Object} [param0.services]
* @param {Object} [param0.session]
* @param {Element} [param0.target] if provided, the component will be mounted inside
* that element (only used if `params0.hasWebClient` is true)
* @param {Object} [param0.View] makes only sense when `param0.hasView` is set:
* the View class to use in createView.
* @param {Object} [param0.viewOptions] makes only sense when `param0.hasView`
@@ -636,6 +592,7 @@ async function start(param0 = {}) {
hasView = false,
loadingBaseDelayDuration = 0,
messagingBeforeCreationDeferred = Promise.resolve(),
target = getFixture(),
waitUntilEvent,
waitUntilMessagingCondition = 'initialized',
} = param0;
@@ -649,6 +606,7 @@ async function start(param0 = {}) {
delete param0.hasDiscuss;
delete param0.hasTimeControl;
delete param0.hasView;
delete param0.target;
if (hasChatWindow) {
callbacks = _useChatWindow(callbacks, { afterNextRender });
}
@@ -800,7 +758,7 @@ async function start(param0 = {}) {
legacyParams.withLegacyMockServer = true;
legacyParams.env = env;
widget = await createWebClient({ serverData, mockRPC, legacyParams });
widget = await createWebClient({ target, serverData, mockRPC, legacyParams });
legacyPatch(widget, {
destroy() {
@@ -833,10 +791,10 @@ async function start(param0 = {}) {
testSetupDoneDeferred.resolve();
waitUntilEventPromise = Promise.resolve();
}
const components = [];
const apps = [];
const result = {
afterEvent,
components,
apps,
env: testEnv,
mockServer,
widget,
@@ -862,13 +820,13 @@ async function start(param0 = {}) {
...result,
afterNextRender,
click: getClick({ afterNextRender }),
createChatterContainerComponent: getCreateChatterContainerComponent({ afterEvent, components, env: testEnv, widget }),
createComposerComponent: getCreateComposerComponent({ components, env: testEnv, modelManager, widget }),
createComposerSuggestionComponent: getCreateComposerSuggestionComponent({ components, env: testEnv, modelManager, widget }),
createMessageComponent: getCreateMessageComponent({ components, env: testEnv, modelManager, widget }),
createMessagingMenuComponent: getCreateMessagingMenuComponent({ components, env: testEnv, widget }),
createNotificationListComponent: getCreateNotificationListComponent({ components, env: testEnv, modelManager, widget }),
createThreadViewComponent: getCreateThreadViewComponent({ afterEvent, components, env: testEnv, widget }),
createChatterContainerComponent: getCreateChatterContainerComponent({ afterEvent, apps, env: testEnv, widget }),
createComposerComponent: getCreateComposerComponent({ apps, env: testEnv, modelManager, widget }),
createComposerSuggestionComponent: getCreateComposerSuggestionComponent({ apps, env: testEnv, modelManager, widget }),
createMessageComponent: getCreateMessageComponent({ apps, env: testEnv, modelManager, widget }),
createMessagingMenuComponent: getCreateMessagingMenuComponent({ apps, env: testEnv, widget }),
createNotificationListComponent: getCreateNotificationListComponent({ apps, env: testEnv, modelManager, widget }),
createThreadViewComponent: getCreateThreadViewComponent({ afterEvent, apps, env: testEnv, widget }),
messaging: modelManager.messaging,
openDiscuss,
};
@@ -4,7 +4,7 @@ import { DiscussContainer } from "@mail/components/discuss_container/discuss_con
import AbstractAction from 'web.AbstractAction';
const { Component } = owl;
const { App, Component } = owl;
export const DiscussWidget = AbstractAction.extend({
template: 'mail.widgets.Discuss',
@@ -27,7 +27,7 @@ export const DiscussWidget = AbstractAction.extend({
this.discuss = undefined;
this.options = options;
this.component = undefined;
this.app = undefined;
this._lastPushStateActiveThread = null;
this.env = Component.env;
@@ -45,9 +45,9 @@ export const DiscussWidget = AbstractAction.extend({
* @override {web.AbstractAction}
*/
destroy() {
if (this.component) {
this.component.destroy();
this.component = undefined;
if (this.app) {
this.app.destroy();
this.app = undefined;
}
if (this.$buttons) {
this.$buttons.off().remove();
@@ -57,13 +57,12 @@ export const DiscussWidget = AbstractAction.extend({
/**
* @override {web.AbstractAction}
*/
on_attach_callback() {
async on_attach_callback() {
this._super(...arguments);
if (this.component) {
if (this.app) {
// prevent twice call to on_attach_callback (FIXME)
return;
}
this.component = new DiscussContainer();
this._pushStateActionManagerEventListener = ev => {
ev.stopPropagation();
if (this._lastPushStateActiveThread === this.discuss.thread) {
@@ -84,17 +83,25 @@ export const DiscussWidget = AbstractAction.extend({
'o-show-rainbow-man',
this._showRainbowManEventListener
);
return this.component.mount(this.el);
this.app = new App(DiscussContainer, {
templates: window.__OWL_TEMPLATES__,
env: owl.Component.env,
dev: owl.Component.env.isDebug(),
translateFn: owl.Component.env._t,
translatableAttributes: ["data-tooltip"],
});
await this.app.mount(this.el);
},
/**
* @override {web.AbstractAction}
*/
on_detach_callback() {
this._super(...arguments);
if (this.component) {
this.component.destroy();
if (this.app) {
this.app.destroy();
}
this.component = undefined;
this.app = undefined;
this.el.removeEventListener(
'o-push-state-action-manager',
this._pushStateActionManagerEventListener
@@ -105,6 +105,9 @@ FormRenderer.include({
* @returns {jQuery.Element}
*/
_makeChatterContainerTarget() {
if (this._chatterContainerTarget) {
return $(this._chatterContainerTarget);
}
const $el = $('<div class="o_FormRenderer_chatterContainer"/>');
this._chatterContainerTarget = $el[0];
return $el;
@@ -151,7 +154,7 @@ FormRenderer.include({
if (!this._chatterContainerComponent) {
this._makeChatterContainerComponent();
} else {
await this._updateChatterContainerComponent();
return this._updateChatterContainerComponent();
}
await this._mountChatterContainerComponent();
}
+2 -2
View File
@@ -28,7 +28,7 @@
<KanbanColumnProgressBarAdapter Component="widgetComponents.KanbanColumnProgressBar"
widgetArgs="[getProgressBarOptions(type[0]), getProgressBarColumnState(type[0])]"
t-if="activityTypeIds.includes(type[0])"
t-on-set-progress-bar-state="_onSetProgressBarState"/>
onSetProgressBarState.bind="_onSetProgressBarState"/>
<div t-else="" class="mt24"/>
</th>
</tr>
@@ -75,7 +75,7 @@
<t t-name="mail.ActivityViewFooter" owl="1">
<tfoot>
<tr class="o_data_row">
<td class="o_record_selector p-3" t-on-click.prevent.stop="trigger('schedule_activity')">
<td class="o_record_selector p-3" t-on-click.prevent.stop="() => this.trigger('schedule_activity')">
<span class="fa fa-plus pr-2"/><span>Schedule activity</span>
</td>
</tr>
+30 -25
View File
@@ -5,12 +5,13 @@ import ActivityView from '@mail/js/views/activity/activity_view';
import testUtils from 'web.test_utils';
import domUtils from 'web.dom';
import { legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { getFixture, legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { session } from '@web/session';
import { click } from "@web/../tests/helpers/utils";
let serverData;
let target;
var createView = testUtils.createView;
@@ -116,6 +117,7 @@ QUnit.module('activity view', {
},
};
serverData = { models: this.data };
target = getFixture();
}
});
@@ -402,17 +404,17 @@ QUnit.test("activity view: no group_by_menu and no comparison_menu", async funct
await doAction(webClient, 1);
assert.containsN(
webClient,
target,
".o_search_options .dropdown button:visible",
2,
"only two elements should be available in view search"
);
assert.isVisible(
$(webClient.el).find(".o_search_options .dropdown.o_filter_menu > button"),
target.querySelector(".o_search_options .dropdown.o_filter_menu > button"),
"filter should be available in view search"
);
assert.isVisible(
$(webClient.el).find(".o_search_options .dropdown.o_favorite_menu > button"),
target.querySelector(".o_search_options .dropdown.o_favorite_menu > button"),
"favorites should be available in view search"
);
});
@@ -522,7 +524,7 @@ QUnit.test("Activity view: discard an activity creation dialog", async function
await doAction(webClient, 1);
await testUtils.dom.click(
$(webClient.el).find(".o_activity_view .o_data_row .o_activity_empty_cell")[0]
target.querySelector(".o_activity_view .o_data_row .o_activity_empty_cell")
);
await legacyExtraNextTick();
assert.containsOnce($, ".modal.o_technical_modal", "Activity Modal should be opened");
@@ -578,10 +580,10 @@ QUnit.test('Activity view: many2one_avatar_user widget in activity view', async
await doAction(webClient, 1);
await legacyExtraNextTick();
assert.containsN(webClient, '.o_m2o_avatar', 2);
assert.containsOnce(webClient, 'tr[data-res-id=13] .o_m2o_avatar > img[src="/web/image/res.users/1/avatar_128"]',
assert.containsN(target, '.o_m2o_avatar', 2);
assert.containsOnce(target, 'tr[data-res-id=13] .o_m2o_avatar > img[src="/web/image/res.users/1/avatar_128"]',
"should have m2o avatar image");
assert.containsNone(webClient, '.o_m2o_avatar > span',
assert.containsNone(target, '.o_m2o_avatar > span',
"should not have text on many2one_avatar_user if onlyImage node option is passed");
});
@@ -602,11 +604,14 @@ QUnit.test("Activity view: on_destroy_callback doesn't crash", async function (a
};
patchWithCleanup(ActivityRenderer.prototype, {
mounted() {
assert.step('mounted');
},
willUnmount() {
assert.step('willUnmount');
setup() {
this._super();
owl.onMounted(() => {
assert.step('mounted');
});
owl.onWillUnmount(() => {
assert.step('willUnmount');
});
}
});
@@ -660,7 +665,7 @@ QUnit.test("Schedule activity dialog uses the same search view as activity view"
])
// click on "Schedule activity"
await click(webClient.el.querySelector(".o_activity_view .o_record_selector"));
await click(target.querySelector(".o_activity_view .o_record_selector"));
assert.verifySteps([
'[[false,"list"],[false,"search"]]',
@@ -680,7 +685,7 @@ QUnit.test("Schedule activity dialog uses the same search view as activity view"
])
// click on "Schedule activity"
await click(webClient.el.querySelector(".o_activity_view .o_record_selector"));
await click(target.querySelector(".o_activity_view .o_record_selector"));
assert.verifySteps([
'[[false,"list"],[1,"search"]]',
@@ -715,28 +720,28 @@ QUnit.test('Activity view: apply progressbar filter', async function (assert) {
await doAction(webClient, 1);
assert.containsNone(webClient.el.querySelector('.o_activity_view thead'),
assert.containsNone(target.querySelector('.o_activity_view thead'),
'.o_activity_filter_planned,.o_activity_filter_today,.o_activity_filter_overdue,.o_activity_filter___false',
"should not have active filter");
assert.containsNone(webClient.el.querySelector('.o_activity_view tbody'),
assert.containsNone(target.querySelector('.o_activity_view tbody'),
'.o_activity_filter_planned,.o_activity_filter_today,.o_activity_filter_overdue,.o_activity_filter___false',
"should not have active filter");
assert.strictEqual(webClient.el.querySelector('.o_activity_view tbody .o_activity_record').textContent,
assert.strictEqual(target.querySelector('.o_activity_view tbody .o_activity_record').textContent,
'Office planning', "'Office planning' should be first record");
assert.containsOnce(webClient.el.querySelector('.o_activity_view tbody'), '.planned',
assert.containsOnce(target.querySelector('.o_activity_view tbody'), '.planned',
"other records should be available");
await testUtils.dom.click(webClient.el.querySelector('.o_kanban_counter_progress .progress-bar[data-filter="planned"]'));
assert.containsOnce(webClient.el.querySelector('.o_activity_view thead'), '.o_activity_filter_planned',
await testUtils.dom.click(target.querySelector('.o_kanban_counter_progress .progress-bar[data-filter="planned"]'));
assert.containsOnce(target.querySelector('.o_activity_view thead'), '.o_activity_filter_planned',
"planned should be active filter");
assert.containsN(webClient.el.querySelector('.o_activity_view tbody'), '.o_activity_filter_planned', 5,
assert.containsN(target.querySelector('.o_activity_view tbody'), '.o_activity_filter_planned', 5,
"planned should be active filter");
assert.strictEqual(webClient.el.querySelector('.o_activity_view tbody .o_activity_record').textContent,
assert.strictEqual(target.querySelector('.o_activity_view tbody .o_activity_record').textContent,
'Meeting Room Furnitures', "'Office planning' should be first record");
const tr = webClient.el.querySelectorAll('.o_activity_view tbody tr')[1];
const tr = target.querySelectorAll('.o_activity_view tbody tr')[1];
assert.hasClass(tr.querySelectorAll('td')[1], 'o_activity_empty_cell',
"other records should be hidden");
assert.containsNone(webClient.el.querySelector('.o_activity_view tbody'), 'planned',
assert.containsNone(target.querySelector('.o_activity_view tbody'), 'planned',
"other records should be hidden");
});
@@ -2,7 +2,7 @@
import { Many2OneAvatarUser } from '@mail/js/m2x_avatar_user';
import { afterEach, beforeEach, start } from '@mail/utils/test_utils';
import { click, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils";
import { click, getFixture, legacyExtraNextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils";
import { createWebClient, doAction } from '@web/../tests/webclient/helpers';
import { registry } from "@web/core/registry";
import { makeLegacyCommandService } from "@web/legacy/utils";
@@ -14,6 +14,7 @@ import session from 'web.session';
import makeTestEnvironment from "web.test_env";
import { dom, mock, nextTick } from 'web.test_utils';
let target;
QUnit.module('mail', {}, function () {
QUnit.module('M2XAvatarUser', {
@@ -48,6 +49,8 @@ QUnit.module('mail', {}, function () {
{ id: 7, name: "Luigi", partner_id: 12 },
{ id: 23, name: "Yoshi", partner_id: 13 }
);
target = getFixture();
},
afterEach() {
afterEach(this);
@@ -431,7 +434,7 @@ QUnit.module('mail', {}, function () {
'res.users': this.data['res.users'],
}
const serverData = { models, views}
const webClient = await createWebClient({serverData});
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
res_id: 1,
type: 'ir.actions.act_window',
@@ -440,25 +443,25 @@ QUnit.module('mail', {}, function () {
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.strictEqual(webClient.el.querySelector(".o_m2o_avatar > span").textContent, "Mario")
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario")
triggerHotkey("control+k")
await nextTick();
const idx = [...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I")
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I")
assert.ok(idx >= 0);
await click([...webClient.el.querySelectorAll(".o_command")][idx])
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.deepEqual([...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent), [
assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [
"Your Company, Mitchell Admin",
"Public user",
"Mario",
"Luigi",
"Yoshi",
])
await click(webClient.el, "#o_command_3")
await click(target, "#o_command_3")
await legacyExtraNextTick();
assert.strictEqual(webClient.el.querySelector(".o_m2o_avatar > span").textContent, "Luigi")
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) {
@@ -479,7 +482,7 @@ QUnit.module('mail', {}, function () {
'res.users': this.data['res.users'],
}
const serverData = { models, views}
const webClient = await createWebClient({serverData});
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
res_id: 1,
type: 'ir.actions.act_window',
@@ -488,23 +491,23 @@ QUnit.module('mail', {}, function () {
'view_mode': 'form',
'views': [[false, 'form']],
});
assert.strictEqual(webClient.el.querySelector(".o_m2o_avatar > span").textContent, "Mario")
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Mario")
triggerHotkey("control+k")
await nextTick();
const idx = [...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I")
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(webClient.el.querySelector(".o_m2o_avatar > span").textContent, "Luigi")
assert.strictEqual(target.querySelector(".o_m2o_avatar > span").textContent, "Luigi")
// Unassign me
triggerHotkey("control+k");
await nextTick();
await click([...webClient.el.querySelectorAll(".o_command")][idx])
await click([...target.querySelectorAll(".o_command")][idx])
await legacyExtraNextTick();
assert.strictEqual(webClient.el.querySelector(".o_m2o_avatar > span").textContent, "")
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) {
@@ -524,7 +527,7 @@ QUnit.module('mail', {}, function () {
'res.users': this.data['res.users'],
}
const serverData = { models, views}
const webClient = await createWebClient({serverData});
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
res_id: 1,
type: 'ir.actions.act_window',
@@ -533,25 +536,25 @@ QUnit.module('mail', {}, function () {
'view_mode': 'form',
'views': [[false, 'form']],
});
let userNames = [...webClient.el.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
triggerHotkey("control+k")
await nextTick();
const idx = [...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I")
const idx = [...target.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign to ...ALT + I")
assert.ok(idx >= 0);
await click([...webClient.el.querySelectorAll(".o_command")][idx])
await click([...target.querySelectorAll(".o_command")][idx])
await nextTick();
assert.deepEqual([...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent), [
assert.deepEqual([...target.querySelectorAll(".o_command")].map(el => el.textContent), [
"Your Company, Mitchell Admin",
"Public user",
"Luigi"
]);
await click(webClient.el, "#o_command_2");
await click(target, "#o_command_2");
await legacyExtraNextTick();
userNames = [...webClient.el.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent);
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map(el => el.textContent);
assert.deepEqual(userNames, ["Mario", "Yoshi", "Luigi"]);
});
@@ -573,7 +576,7 @@ QUnit.module('mail', {}, function () {
'res.users': this.data['res.users'],
}
const serverData = { models, views}
const webClient = await createWebClient({serverData});
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
res_id: 1,
type: 'ir.actions.act_window',
@@ -582,26 +585,26 @@ QUnit.module('mail', {}, function () {
'view_mode': 'form',
'views': [[false, 'form']],
});
let userNames = [...webClient.el.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
let userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
triggerHotkey("control+k");
await nextTick();
const idx = [...webClient.el.querySelectorAll(".o_command")].map(el => el.textContent).indexOf("Assign/unassign to meALT + SHIFT + I");
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 = [...webClient.el.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
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([...webClient.el.querySelectorAll(".o_command")][idx]);
await click([...target.querySelectorAll(".o_command")][idx]);
await legacyExtraNextTick();
userNames = [...webClient.el.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
userNames = [...target.querySelectorAll(".o_tag_badge_text")].map((el => el.textContent));
assert.deepEqual(userNames, ["Mario", "Yoshi"]);
});
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { manageMessages } from "@mail/js/tools/debug_manager";
import { click, legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
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";
@@ -37,12 +37,13 @@ QUnit.test("Manage Messages", async function (assert) {
}
}
const target = getFixture();
const wc = await createWebClient({ serverData, mockRPC });
await doAction(wc, 3, { viewType: "form", props: { resId: 5 } });
await legacyExtraNextTick();
await click(wc.el, ".o_debug_manager .dropdown-toggle");
await click(target, ".o_debug_manager .dropdown-toggle");
const dropdownItems = wc.el.querySelectorAll(
const dropdownItems = target.querySelectorAll(
".o_debug_manager .dropdown-menu .dropdown-item"
);
assert.strictEqual(dropdownItems.length, 1);
@@ -56,7 +57,7 @@ QUnit.test("Manage Messages", async function (assert) {
await legacyExtraNextTick();
assert.strictEqual(
wc.el.querySelector(".breadcrumb-item.active").innerText.trim(),
target.querySelector(".breadcrumb-item.active").innerText.trim(),
"Manage Messages"
);
});
@@ -2,7 +2,7 @@
import { afterEach, afterNextRender, beforeEach, start } from '@mail/utils/test_utils';
import { editSearchBar } from '@web/../tests/core/commands/command_service_tests';
import { click, nextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils";
import { click, getFixture, nextTick, patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils";
import { browser } from '@web/core/browser/browser';
import { commandService } from "@web/core/commands/command_service";
import { registry } from "@web/core/registry";
@@ -41,10 +41,12 @@ QUnit.module('mail', {}, function () {
{ id: 23, name: "User 3", partner_id: 13 },
);
const target = getFixture();
const { widget: webClient } = await start({
data: this.data,
hasChatWindow: true,
hasWebClient: true,
target,
});
triggerHotkey("control+k");
await nextTick();
@@ -52,7 +54,7 @@ QUnit.module('mail', {}, function () {
// Switch to partners
await editSearchBar("@");
assert.deepEqual(
[...webClient.el.querySelectorAll(".o_command_palette .o_command")].map((el) => el.textContent),
[...target.querySelectorAll(".o_command_palette .o_command")].map((el) => el.textContent),
[
"Partner 1p1@odoo.com",
"Partner 2p2@odoo.com",
@@ -63,8 +65,6 @@ QUnit.module('mail', {}, function () {
await afterNextRender(() => click(document.body, ".o_command.focused"));
assert.containsOnce(document.body, ".o_ChatWindow");
assert.strictEqual(document.querySelector(".o_ChatWindow .o_ChatWindowHeader_name").textContent, "Partner 1");
webClient.destroy();
});
QUnit.test('open the chatWindow of a channel from the command palette', async function (assert) {
@@ -80,10 +80,12 @@ QUnit.module('mail', {}, function () {
name: "project",
members: [this.data.currentPartnerId],
});
const target = getFixture();
const { widget: webClient } = await start({
data: this.data,
hasChatWindow: true,
hasWebClient: true,
target,
});
triggerHotkey("control+k");
await nextTick();
@@ -91,7 +93,7 @@ QUnit.module('mail', {}, function () {
// Switch to channels
await editSearchBar("#");
assert.deepEqual(
[...webClient.el.querySelectorAll(".o_command_palette .o_command")].map((el) => el.textContent),
[...target.querySelectorAll(".o_command_palette .o_command")].map((el) => el.textContent),
[
"general",
"project"
@@ -101,7 +103,5 @@ QUnit.module('mail', {}, function () {
await afterNextRender(() => click(document.body, ".o_command.focused"));
assert.containsOnce(document.body, ".o_ChatWindow");
assert.strictEqual(document.querySelector(".o_ChatWindow .o_ChatWindowHeader_name").textContent, "general");
webClient.destroy();
});
});
@@ -20,9 +20,9 @@ QUnit.module('message_tests.js', {
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { afterEvent, components, env, widget } = res;
const { afterEvent, apps, env, widget } = res;
this.afterEvent = afterEvent;
this.components = components;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;
@@ -19,9 +19,9 @@ QUnit.module('message_tests.js', {
this.start = async params => {
const res = await start({ ...params, data: this.data });
const { afterEvent, components, env, widget } = res;
const { afterEvent, apps, env, widget } = res;
this.afterEvent = afterEvent;
this.components = components;
this.apps = apps;
this.env = env;
this.widget = widget;
return res;
@@ -11,7 +11,6 @@ export class SnailmailError extends Component {
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'SnailmailErrorView' });
}