[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:
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
@@ -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',
|
||||
|
||||
+3
-5
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
-1
@@ -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',
|
||||
});
|
||||
|
||||
+3
-3
@@ -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;
|
||||
|
||||
+18
-6
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
},
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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"/>
|
||||
|
||||
+6
-1
@@ -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}
|
||||
*/
|
||||
|
||||
+1
-1
@@ -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()
|
||||
);
|
||||
|
||||
+2
-2
@@ -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();
|
||||
}
|
||||
|
||||
+1
-1
@@ -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',
|
||||
});
|
||||
|
||||
+2
-2
@@ -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();
|
||||
}
|
||||
|
||||
+3
@@ -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,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',
|
||||
|
||||
+3
-2
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user