[IMP] mail: introduce view models (step 5)

This commit introduces models that define records being 1:1 map with components,
as a step to move further to having essentially all business code in models.

Having code in models is desirable to have very maintainable code, thanks to
robust and declarative code with an ORM-like architecture.

Task-2831082

closes odoo/odoo#90427

Related: odoo/enterprise#26928
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-05-04 16:43:44 +02:00
parent 23962f3d4b
commit 5c1e7a6f5d
26 changed files with 328 additions and 157 deletions
@@ -34,16 +34,16 @@ QUnit.test('livechats should be in "chat" filter', async function (assert) {
await afterNextRender(() => document.querySelector('.o_MessagingMenu_toggler').click());
assert.containsOnce(
document.body,
'.o_MessagingMenu_tabButton[data-tab-id="all"]',
'.o_MessagingMenuTab[data-tab-id="all"]',
"should have a tab/filter 'all' in messaging menu"
);
assert.containsOnce(
document.body,
'.o_MessagingMenu_tabButton[data-tab-id="chat"]',
'.o_MessagingMenuTab[data-tab-id="chat"]',
"should have a tab/filter 'chat' in messaging menu"
);
assert.hasClass(
document.querySelector('.o_MessagingMenu_tabButton[data-tab-id="all"]'),
document.querySelector('.o_MessagingMenuTab[data-tab-id="all"]'),
'o-active',
"tab/filter 'all' of messaging menu should be active initially"
);
@@ -59,10 +59,10 @@ QUnit.test('livechats should be in "chat" filter', async function (assert) {
);
await afterNextRender(() =>
document.querySelector('.o_MessagingMenu_tabButton[data-tab-id="chat"]').click()
document.querySelector('.o_MessagingMenuTab[data-tab-id="chat"]').click()
);
assert.hasClass(
document.querySelector('.o_MessagingMenu_tabButton[data-tab-id="chat"]'),
document.querySelector('.o_MessagingMenuTab[data-tab-id="chat"]'),
'o-active',
"tab/filter 'chat' of messaging menu should become active after click"
);
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import Popover from "web.Popover";
@@ -14,6 +15,7 @@ export class Activity extends LegacyComponent {
setup() {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'ActivityView' });
useRefToModel({ fieldName: 'markDoneButtonRef', modelName: 'ActivityView', refName: 'markDoneButton', });
}
/**
@@ -119,16 +119,10 @@
<t t-if="activityView.activity.canWrite">
<div name="tools" class="o_Activity_tools d-flex">
<t t-if="!activityView.fileUploader">
<Popover position="'right'" title="activityView.markDoneText">
<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-slot="opened">
<ActivityMarkDonePopover localId="activityView.activityMarkDonePopoverView.localId"/>
</t>
</Popover>
</t>
<button class="o_Activity_toolButton o_Activity_markDoneButton btn btn-link btn-primary pt-0 pl-0" t-att-title="activityView.markDoneText" t-ref="markDoneButton" t-on-click="activityView.onClickMarkDoneButton">
<i class="fa fa-check"/> Mark Done
</button>
<PopoverView t-if="activityView.markDonePopoverView" localId="activityView.markDonePopoverView.localId"/>
<t t-if="activityView.fileUploader">
<button class="o_Activity_toolButton o_Activity_uploadButton btn btn-link btn-primary pt-0 pl-0" t-on-click="activityView.onClickUploadDocument">
<i class="fa fa-upload"/> Upload Document
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityMarkDonePopover" owl="1">
<t t-if="activityMarkDonePopoverView">
<div class="o_ActivityMarkDonePopover" t-attf-class="{{ className }}" t-on-keydown="activityMarkDonePopoverView.onKeydown" t-ref="root">
<textarea class="form-control o_ActivityMarkDonePopover_feedback mb-2" rows="3" placeholder="Write Feedback" t-on-blur="activityMarkDonePopoverView.onBlur" t-ref="feedbackTextarea"/>
<div class="o_ActivityMarkDonePopover_buttons">
<button type="button" class="o_ActivityMarkDonePopover_doneScheduleNextButton btn btn-sm btn-primary" t-on-click="activityMarkDonePopoverView.onClickDoneAndScheduleNext">
Done &amp; Schedule Next
</button>
<t t-if="activityMarkDonePopoverView.activityViewOwner.activity.chaining_type === 'suggest'">
<button type="button" class="o_ActivityMarkDonePopover_doneButton btn btn-sm btn-primary mx-2" t-on-click="activityMarkDonePopoverView.onClickDone">
Done
</button>
</t>
<button type="button" class="o_ActivityMarkDonePopover_discardButton btn btn-sm btn-link" t-on-click="activityMarkDonePopoverView.onClickDiscard">
Discard
</button>
</div>
</div>
</t>
</t>
</templates>
@@ -7,15 +7,15 @@ import { LegacyComponent } from "@web/legacy/legacy_component";
const { onMounted, useRef } = owl;
export class ActivityMarkDonePopover extends LegacyComponent {
export class ActivityMarkDonePopoverContent extends LegacyComponent {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'ActivityMarkDonePopoverView' });
useRefToModel({ fieldName: 'feedbackTextareaRef', modelName: 'ActivityMarkDonePopoverView', refName: 'feedbackTextarea' });
useComponentToModel({ fieldName: 'component', modelName: 'ActivityMarkDonePopoverContentView' });
useRefToModel({ fieldName: 'feedbackTextareaRef', modelName: 'ActivityMarkDonePopoverContentView', refName: 'feedbackTextarea' });
this._feedbackTextareaRef = useRef('feedbackTextarea');
onMounted(() => this._mounted());
}
@@ -26,23 +26,23 @@ export class ActivityMarkDonePopover extends LegacyComponent {
_mounted() {
this._feedbackTextareaRef.el.focus();
if (this.activityMarkDonePopoverView.activityViewOwner.activity.feedbackBackup) {
this._feedbackTextareaRef.el.value = this.activityMarkDonePopoverView.activityViewOwner.activity.feedbackBackup;
if (this.activityMarkDonePopoverContentView.activityViewOwner.activity.feedbackBackup) {
this._feedbackTextareaRef.el.value = this.activityMarkDonePopoverContentView.activityViewOwner.activity.feedbackBackup;
}
}
/**
* @returns {ActivityMarkDonePopoverView}
* @returns {ActivityMarkDonePopoverContentView}
*/
get activityMarkDonePopoverView() {
return this.messaging && this.messaging.models['ActivityMarkDonePopoverView'].get(this.props.localId);
get activityMarkDonePopoverContentView() {
return this.messaging && this.messaging.models['ActivityMarkDonePopoverContentView'].get(this.props.localId);
}
}
Object.assign(ActivityMarkDonePopover, {
Object.assign(ActivityMarkDonePopoverContent, {
props: { localId: String },
template: 'mail.ActivityMarkDonePopover',
template: 'mail.ActivityMarkDonePopoverContent',
});
registerMessagingComponent(ActivityMarkDonePopover);
registerMessagingComponent(ActivityMarkDonePopoverContent);
@@ -2,6 +2,6 @@
// Layout
// ------------------------------------------------------------------
.o_ActivityMarkDonePopover_feedback {
.o_ActivityMarkDonePopoverContent_feedback {
min-height: 70px;
}
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityMarkDonePopoverContent" owl="1">
<t t-if="activityMarkDonePopoverContentView">
<div class="o_ActivityMarkDonePopoverContent" t-attf-class="{{ className }}" t-on-keydown="activityMarkDonePopoverContentView.onKeydown" t-ref="root">
<h6 class="o_ActivityMarkDonePopoverContent_header p-2 font-weight-bolder bg-200 border-bottom" t-esc="activityMarkDonePopoverContentView.headerText"/>
<div class="o_ActivityMarkDonePopoverContent_content py-2 px-3">
<textarea class="form-control o_ActivityMarkDonePopoverContent_feedback" rows="3" placeholder="Write Feedback" t-on-blur="activityMarkDonePopoverContentView.onBlur" t-ref="feedbackTextarea"/>
<div class="o_ActivityMarkDonePopoverContent_buttons">
<button type="button" class="o_ActivityMarkDonePopoverContent_doneScheduleNextButton btn btn-sm btn-primary" t-on-click="activityMarkDonePopoverContentView.onClickDoneAndScheduleNext">
Done &amp; Schedule Next
</button>
<t t-if="activityMarkDonePopoverContentView.activityViewOwner.activity.chaining_type === 'suggest'">
<button type="button" class="o_ActivityMarkDonePopoverContent_doneButton btn btn-sm btn-primary mx-2" t-on-click="activityMarkDonePopoverContentView.onClickDone">
Done
</button>
</t>
<button type="button" class="o_ActivityMarkDonePopoverContent_discardButton btn btn-sm btn-link" t-on-click="activityMarkDonePopoverContentView.onClickDiscard">
Discard
</button>
</div>
</div>
</div>
</t>
</t>
</templates>
@@ -115,18 +115,3 @@
border-radius: 5px;
outline: none;
}
.o_MessagingMenu_tabButton.o-desktop {
&.o-active {
font-weight: bold;
}
&:not(:hover) {
&:not(.o-active) {
color: gray('500');
}
}
}
@@ -23,13 +23,9 @@
<t t-else="">
<div class="o_MessagingMenu_dropdownMenuHeader border-bottom" t-att-class="{ 'o-mobile': messaging.device.isSmall }">
<t t-if="!messaging.device.isSmall">
<t t-foreach="['all', 'chat', 'channel']" t-as="tabId" t-key="tabId">
<button class="o_MessagingMenu_tabButton o-desktop btn btn-link" t-att-class="{ 'o-active': messagingMenu.activeTabId === tabId, }" t-on-click="messagingMenu.onClickDesktopTabButton" type="button" role="tab" t-att-data-tab-id="tabId">
<t t-if="tabId === 'all'">All</t>
<t t-elif="tabId === 'chat'">Chat</t>
<t t-elif="tabId === 'channel'">Channels</t>
</button>
</t>
<MessagingMenuTab messagingMenuLocalId="messagingMenu.localId" tabId="'all'"/>
<MessagingMenuTab messagingMenuLocalId="messagingMenu.localId" tabId="'chat'"/>
<MessagingMenuTab messagingMenuLocalId="messagingMenu.localId" tabId="'channel'"/>
</t>
<t t-if="messaging.device.isSmall">
<t t-call="mail.MessagingMenu.newMessageButton"/>
@@ -0,0 +1,30 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class MessagingMenuTab extends Component {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {MessagingMenu}
*/
get messagingMenu() {
return this.messaging && this.messaging.models['MessagingMenu'].get(this.props.messagingMenuLocalId);
}
}
Object.assign(MessagingMenuTab, {
props: {
messagingMenuLocalId: String,
tabId: String,
},
template: 'mail.MessagingMenuTab',
});
registerMessagingComponent(MessagingMenuTab);
@@ -0,0 +1,13 @@
.o_MessagingMenuTab.o-desktop {
&.o-active {
font-weight: bold;
}
&:not(:hover) {
&:not(.o-active) {
color: gray('500');
}
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.MessagingMenuTab" owl="1">
<button class="o_MessagingMenuTab o-desktop btn btn-link" t-att-class="{ 'o-active': messagingMenu.activeTabId === props.tabId, }" t-on-click="messagingMenu.onClickDesktopTabButton" type="button" role="tab" t-att-data-tab-id="props.tabId" t-attf-class="{{ className }}" t-ref="root">
<t t-if="props.tabId === 'all'">All</t>
<t t-elif="props.tabId === 'chat'">Chat</t>
<t t-elif="props.tabId === 'channel'">Channels</t>
</button>
</t>
</templates>
@@ -19,8 +19,8 @@
t-on-click="callParticipantCard.onClick"
>
<!-- card -->
<t t-if="callParticipantCard.rtcSession and callParticipantCard.rtcSession.videoStream">
<RtcVideo rtcSessionLocalId="callParticipantCard.rtcSession.localId"/>
<t t-if="callParticipantCard.rtcVideoView">
<RtcVideo localId="callParticipantCard.rtcVideoView.localId"/>
</t>
<t t-else="">
<div class="o_RtcCallParticipantCard_avatarFrame mh-100 mw-100 h-100 align-items-center justify-content-center" t-att-class="{ 'o-isMinimized': callParticipantCard.isMinimized }" draggable="false">
@@ -10,9 +10,7 @@
<select name="inputDevice" class="o_RtcConfigurationMenu_optionDeviceSelect" t-att-value="messaging.userSetting.audioInputDeviceId" t-on-change="rtcConfigurationMenu.onChangeSelectAudioInput">
<option value="">Browser default</option>
<t t-if="state.userDevices" t-foreach="state.userDevices" t-as="device" t-key="device_index">
<t t-if="device.kind === 'audioinput'">
<option t-att-value="device.deviceId"><t t-esc="device.label"/></option>
</t>
<RtcConfigurationMenuDeviceComponent device="device"/>
</t>
</select>
</div>
@@ -0,0 +1,14 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class RtcConfigurationMenuDevice extends Component {}
Object.assign(RtcConfigurationMenuDevice, {
props: { device: Object },
template: 'mail.RtcConfigurationMenuDevice',
});
registerMessagingComponent(RtcConfigurationMenuDevice);
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.RtcConfigurationMenuDeviceComponent" owl="1">
<t t-if="props.device.kind === 'audioinput'">
<option t-att-value="props.device.deviceId" t-attf-class="{{ className }}" t-ref="root"><t t-esc="props.device.label"/></option>
</t>
</t>
</templates>
@@ -20,12 +20,10 @@ export class RtcVideo extends Component {
//--------------------------------------------------------------------------
/**
* @returns {RtcSession|undefined}
* @returns {RtcVideoView}
*/
get rtcSession() {
return this.messaging.models['RtcSession'].get(
this.props.rtcSessionLocalId
);
get rtcVideoView() {
return this.messaging && this.messaging.models['RtcVideoView'].get(this.props.localId);
}
//--------------------------------------------------------------------------
@@ -46,10 +44,13 @@ export class RtcVideo extends Component {
if (!this.root.el) {
return;
}
if (!this.rtcSession || !this.rtcSession.videoStream) {
if (!this.rtcVideoView) {
return;
}
if (!this.rtcVideoView.rtcSession || !this.rtcVideoView.rtcSession.videoStream) {
this.root.el.srcObject = undefined;
} else {
this.root.el.srcObject = this.rtcSession.videoStream;
this.root.el.srcObject = this.rtcVideoView.rtcSession.videoStream;
}
this.root.el.load();
}
@@ -57,7 +58,7 @@ export class RtcVideo extends Component {
}
Object.assign(RtcVideo, {
props: { rtcSessionLocalId: String },
props: { localId: String },
template: 'mail.RtcVideo',
});
@@ -2,13 +2,13 @@
<templates xml:space="preserve">
<t t-name="mail.RtcVideo" owl="1">
<t t-if="rtcSession">
<t t-if="rtcVideoView">
<video class="o_RtcVideo"
t-attf-class="{{ className }}"
playsinline="true"
autoplay="true"
muted="true"
t-on-loadedmetadata="rtcSession.onVideoLoadedMetaData"
t-on-loadedmetadata="rtcVideoView.rtcSession.onVideoLoadedMetaData"
t-ref="root"
/>
</t>
@@ -2,10 +2,11 @@
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear, replace } from '@mail/model/model_field_command';
registerModel({
name: 'ActivityMarkDonePopoverView',
identifyingFields: ['activityViewOwner'],
name: 'ActivityMarkDonePopoverContentView',
identifyingFields: ['popoverViewOwner'],
recordMethods: {
/**
* Handles blur on this feedback textarea.
@@ -69,16 +70,44 @@ registerModel({
*/
_close() {
this._backupFeedback();
this.component.trigger('o-popover-close');
this.activityViewOwner.update({ markDonePopoverView: clear() });
},
/**
* @private
* @returns {FieldCommand}
*/
_computeActivityViewOwner() {
if (this.popoverViewOwner.activityViewOwnerAsMarkDone) {
return replace(this.popoverViewOwner.activityViewOwnerAsMarkDone);
}
return clear();
},
/**
* @private
* @returns {string|FieldCommand}
*/
_computeHeaderText() {
if (this.activityViewOwner) {
return this.activityViewOwner.markDoneText;
}
return clear();
},
},
fields: {
activityViewOwner: one('ActivityView', {
inverse: 'activityMarkDonePopoverView',
compute: '_computeActivityViewOwner',
readonly: true,
required: true,
}),
component: attr(),
feedbackTextareaRef: attr(),
headerText: attr({
compute: '_computeHeaderText',
default: '',
}),
popoverViewOwner: one('PopoverView', {
inverse: 'activityMarkDonePopoverContentView',
readonly: true,
required: true,
}),
},
});
@@ -50,6 +50,9 @@ registerModel({
onClickEdit() {
this.activity.edit();
},
onClickMarkDoneButton() {
this.update({ markDonePopoverView: this.markDonePopoverView ? clear() : insertAndReplace() });
},
/**
* Handles the click on the upload document button. This open the file
* explorer for upload.
@@ -165,11 +168,6 @@ registerModel({
readonly: true,
required: true,
}),
activityMarkDonePopoverView: one('ActivityMarkDonePopoverView', {
default: insertAndReplace(),
inverse: 'activityViewOwner',
isCausal: true,
}),
/**
* Determines whether the details are visible.
*/
@@ -223,6 +221,11 @@ registerModel({
inverse: 'activityViewOwner',
isCausal: true,
}),
markDoneButtonRef: attr(),
markDonePopoverView: one('PopoverView', {
inverse: 'activityViewOwnerAsMarkDone',
isCausal: true,
}),
/**
* Label for mark as done. This is just for translations purpose.
*/
+33 -1
View File
@@ -6,7 +6,7 @@ import { clear, insertAndReplace, replace } from '@mail/model/model_field_comman
registerModel({
name: 'PopoverView',
identifyingFields: [['composerViewOwnerAsEmoji', 'messageActionListOwnerAsReaction', 'threadViewTopbarOwnerAsInvite']],
identifyingFields: [['activityViewOwnerAsMarkDone', 'composerViewOwnerAsEmoji', 'messageActionListOwnerAsReaction', 'threadViewTopbarOwnerAsInvite']],
lifecycleHooks: {
_created() {
document.addEventListener('click', this._onClickCaptureGlobal, true);
@@ -26,11 +26,24 @@ registerModel({
contains(element) {
return Boolean(this.component && this.component.root.el.contains(element));
},
/**
* @private
* @returns {FieldCommand}
*/
_computeActivityMarkDonePopoverContentView() {
if (this.activityViewOwnerAsMarkDone) {
return insertAndReplace();
}
return clear();
},
/**
* @private
* @returns {Ref}
*/
_computeAnchorRef() {
if (this.activityViewOwnerAsMarkDone) {
return this.activityViewOwnerAsMarkDone.markDoneButtonRef;
}
if (this.threadViewTopbarOwnerAsInvite) {
return this.threadViewTopbarOwnerAsInvite.inviteButtonRef;
}
@@ -57,6 +70,9 @@ registerModel({
* @returns {FieldCommand}
*/
_computeContent() {
if (this.activityMarkDonePopoverContentView) {
return replace(this.activityMarkDonePopoverContentView);
}
if (this.channelInvitationForm) {
return replace(this.channelInvitationForm);
}
@@ -83,6 +99,9 @@ registerModel({
* @returns {string|FieldCommand}
*/
_computeContentComponentName() {
if (this.activityMarkDonePopoverContentView) {
return 'ActivityMarkDonePopoverContent';
}
if (this.channelInvitationForm) {
return 'ChannelInvitationForm';
}
@@ -109,6 +128,9 @@ registerModel({
* @returns {string}
*/
_computePosition() {
if (this.activityViewOwnerAsMarkDone) {
return 'right';
}
if (this.threadViewTopbarOwnerAsInvite) {
return 'bottom';
}
@@ -140,6 +162,16 @@ registerModel({
},
},
fields: {
activityMarkDonePopoverContentView: one('ActivityMarkDonePopoverContentView', {
compute: '_computeActivityMarkDonePopoverContentView',
inverse: 'popoverViewOwner',
isCausal: true,
readonly: true,
}),
activityViewOwnerAsMarkDone: one('ActivityView', {
inverse: 'markDonePopoverView',
readonly: true,
}),
/**
* HTML element that is used as anchor position for this popover view.
*/
@@ -2,6 +2,7 @@
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear, insertAndReplace } from '@mail/model/model_field_command';
import { isEventHandled, markEventHandled } from '@mail/utils/utils';
import { sprintf } from '@web/core/utils/strings';
@@ -158,6 +159,16 @@ registerModel({
},
);
},
/**
* @private
* @returns {FieldCommand}
*/
_computeRtcVideoView() {
if (this.rtcSession && this.rtcSession.videoStream) {
return insertAndReplace();
}
return clear();
},
},
fields: {
/**
@@ -249,6 +260,11 @@ registerModel({
* If set, this card represents a rtcSession.
*/
rtcSession: one('RtcSession'),
rtcVideoView: one('RtcVideoView', {
compute: '_computeRtcVideoView',
inverse: 'rtcCallParticipantCardOwner',
isCausal: true,
}),
volumeMenuAnchorRef: attr(),
},
});
@@ -0,0 +1,33 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
import { clear, replace } from '@mail/model/model_field_command';
registerModel({
name: 'RtcVideoView',
identifyingFields: ['rtcCallParticipantCardOwner'],
recordMethods: {
/**
* @private
* @returns {FieldCommand}
*/
_computeRtcSession() {
if (this.rtcCallParticipantCardOwner.rtcSession) {
return replace(this.rtcCallParticipantCardOwner.rtcSession);
}
return clear();
},
},
fields: {
rtcCallParticipantCardOwner: one('RtcCallParticipantCard', {
inverse: 'rtcVideoView',
readonly: true,
required: true,
}),
rtcSession: one('RtcSession', {
compute: '_computeRtcSession',
readonly: true,
}),
},
});
@@ -28,32 +28,32 @@ QUnit.test('activity mark done popover simplest layout', async function (assert)
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover',
'.o_ActivityMarkDonePopoverContent',
"Popover component should be present"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover_feedback',
'.o_ActivityMarkDonePopoverContent_feedback',
"Popover component should contain the feedback textarea"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover_buttons',
'.o_ActivityMarkDonePopoverContent_buttons',
"Popover component should contain the action buttons"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover_doneScheduleNextButton',
'.o_ActivityMarkDonePopoverContent_doneScheduleNextButton',
"Popover component should contain the done & schedule next button"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover_doneButton',
'.o_ActivityMarkDonePopoverContent_doneButton',
"Popover component should contain the done button"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover_discardButton',
'.o_ActivityMarkDonePopoverContent_discardButton',
"Popover component should contain the discard button"
);
});
@@ -79,32 +79,32 @@ QUnit.test('activity with force next mark done popover simplest layout', async f
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover',
'.o_ActivityMarkDonePopoverContent',
"Popover component should be present"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover_feedback',
'.o_ActivityMarkDonePopoverContent_feedback',
"Popover component should contain the feedback textarea"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover_buttons',
'.o_ActivityMarkDonePopoverContent_buttons',
"Popover component should contain the action buttons"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover_doneScheduleNextButton',
'.o_ActivityMarkDonePopoverContent_doneScheduleNextButton',
"Popover component should contain the done & schedule next button"
);
assert.containsNone(
document.body,
'.o_ActivityMarkDonePopover_doneButton',
'.o_ActivityMarkDonePopoverContent_doneButton',
"Popover component should NOT contain the done button"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover_discardButton',
'.o_ActivityMarkDonePopoverContent_discardButton',
"Popover component should contain the discard button"
);
});
@@ -143,7 +143,7 @@ QUnit.test('activity mark done popover mark done without feedback', async functi
threadModel: 'res.partner',
});
await click('.o_Activity_markDoneButton');
await click('.o_ActivityMarkDonePopover_doneButton');
await click('.o_ActivityMarkDonePopoverContent_doneButton');
assert.verifySteps(
['action_feedback'],
"Mark done and schedule next button should call the right rpc"
@@ -185,10 +185,10 @@ QUnit.test('activity mark done popover mark done with feedback', async function
});
await click('.o_Activity_markDoneButton');
let feedbackTextarea = document.querySelector('.o_ActivityMarkDonePopover_feedback');
let feedbackTextarea = document.querySelector('.o_ActivityMarkDonePopoverContent_feedback');
feedbackTextarea.focus();
document.execCommand('insertText', false, 'This task is done');
document.querySelector('.o_ActivityMarkDonePopover_doneButton').click();
document.querySelector('.o_ActivityMarkDonePopoverContent_doneButton').click();
assert.verifySteps(
['action_feedback'],
"Mark done and schedule next button should call the right rpc"
@@ -235,10 +235,10 @@ QUnit.test('activity mark done popover mark done and schedule next', async funct
});
await click('.o_Activity_markDoneButton');
let feedbackTextarea = document.querySelector('.o_ActivityMarkDonePopover_feedback');
let feedbackTextarea = document.querySelector('.o_ActivityMarkDonePopoverContent_feedback');
feedbackTextarea.focus();
document.execCommand('insertText', false, 'This task is done');
await click('.o_ActivityMarkDonePopover_doneScheduleNextButton');
await click('.o_ActivityMarkDonePopoverContent_doneScheduleNextButton');
assert.verifySteps(
['action_feedback_schedule_next'],
"Mark done and schedule next button should call the right rpc and not trigger an action"
@@ -280,7 +280,7 @@ QUnit.test('[technical] activity mark done & schedule next with new action', asy
});
await click('.o_Activity_markDoneButton');
await click('.o_ActivityMarkDonePopover_doneScheduleNextButton');
await click('.o_ActivityMarkDonePopoverContent_doneScheduleNextButton');
assert.verifySteps(
['activity_action'],
"The action returned by the route should be executed"
@@ -771,14 +771,14 @@ QUnit.test('activity click on mark as done', async function (assert) {
await click('.o_Activity_markDoneButton');
assert.strictEqual(
document.querySelectorAll('.o_ActivityMarkDonePopover').length,
document.querySelectorAll('.o_ActivityMarkDonePopoverContent').length,
1,
"should have opened the mark done popover"
);
await click('.o_Activity_markDoneButton');
assert.strictEqual(
document.querySelectorAll('.o_ActivityMarkDonePopover').length,
document.querySelectorAll('.o_ActivityMarkDonePopoverContent').length,
0,
"should have closed the mark done popover"
);
@@ -815,7 +815,7 @@ QUnit.test('activity mark as done popover should focus feedback input on open [R
await click('.o_Activity_markDoneButton');
assert.strictEqual(
document.querySelector('.o_ActivityMarkDonePopover_feedback'),
document.querySelector('.o_ActivityMarkDonePopoverContent_feedback'),
document.activeElement,
"the popover textarea should have the focus"
);
@@ -1051,17 +1051,17 @@ QUnit.test('activity mark done popover close on ESCAPE', async function (assert)
await click('.o_Activity_markDoneButton');
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover',
'.o_ActivityMarkDonePopoverContent',
"Popover component should be present"
);
await afterNextRender(() => {
const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" });
document.querySelector(`.o_ActivityMarkDonePopover`).dispatchEvent(ev);
document.querySelector(`.o_ActivityMarkDonePopoverContent`).dispatchEvent(ev);
});
assert.containsNone(
document.body,
'.o_ActivityMarkDonePopover',
'.o_ActivityMarkDonePopoverContent',
"ESCAPE pressed should have closed the mark done popover"
);
});
@@ -1090,18 +1090,18 @@ QUnit.test('activity mark done popover click on discard', async function (assert
await click('.o_Activity_markDoneButton');
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover',
'.o_ActivityMarkDonePopoverContent',
"Popover component should be present"
);
assert.containsOnce(
document.body,
'.o_ActivityMarkDonePopover_discardButton',
'.o_ActivityMarkDonePopoverContent_discardButton',
"Popover component should contain the discard button"
);
await click('.o_ActivityMarkDonePopover_discardButton');
await click('.o_ActivityMarkDonePopoverContent_discardButton');
assert.containsNone(
document.body,
'.o_ActivityMarkDonePopover',
'.o_ActivityMarkDonePopoverContent',
"Discard button clicked should have closed the mark done popover"
);
});
@@ -160,41 +160,41 @@ QUnit.test('basic rendering', async function (assert) {
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenuHeader
.o_MessagingMenu_tabButton
.o_MessagingMenuTab
`).length,
3,
"should have 3 tab buttons to filter items in the header"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_tabButton[data-tab-id="all"]`).length,
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="all"]`).length,
1,
"1 tab button should be 'All'"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_tabButton[data-tab-id="chat"]`).length,
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="chat"]`).length,
1,
"1 tab button should be 'Chat'"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_tabButton[data-tab-id="channel"]`).length,
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="channel"]`).length,
1,
"1 tab button should be 'Channels'"
);
assert.ok(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="all"]
.o_MessagingMenuTab[data-tab-id="all"]
`).classList.contains('o-active'),
"'all' tab button should be active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="chat"]
.o_MessagingMenuTab[data-tab-id="chat"]
`).classList.contains('o-active'),
"'chat' tab button should not be active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="channel"]
.o_MessagingMenuTab[data-tab-id="channel"]
`).classList.contains('o-active'),
"'channel' tab button should not be active"
);
@@ -268,95 +268,95 @@ QUnit.test('switch tab', async function (assert) {
await click(`.o_MessagingMenu_toggler`);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_tabButton[data-tab-id="all"]`).length,
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="all"]`).length,
1,
"1 tab button should be 'All'"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_tabButton[data-tab-id="chat"]`).length,
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="chat"]`).length,
1,
"1 tab button should be 'Chat'"
);
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_tabButton[data-tab-id="channel"]`).length,
document.querySelectorAll(`.o_MessagingMenuTab[data-tab-id="channel"]`).length,
1,
"1 tab button should be 'Channels'"
);
assert.ok(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="all"]
.o_MessagingMenuTab[data-tab-id="all"]
`).classList.contains('o-active'),
"'all' tab button should be active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="chat"]
.o_MessagingMenuTab[data-tab-id="chat"]
`).classList.contains('o-active'),
"'chat' tab button should not be active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="channel"]
.o_MessagingMenuTab[data-tab-id="channel"]
`).classList.contains('o-active'),
"'channel' tab button should not be active"
);
await click(`.o_MessagingMenu_tabButton[data-tab-id="chat"]`);
await click(`.o_MessagingMenuTab[data-tab-id="chat"]`);
assert.notOk(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="all"]
.o_MessagingMenuTab[data-tab-id="all"]
`).classList.contains('o-active'),
"'all' tab button should become inactive"
);
assert.ok(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="chat"]
.o_MessagingMenuTab[data-tab-id="chat"]
`).classList.contains('o-active'),
"'chat' tab button should not become active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="channel"]
.o_MessagingMenuTab[data-tab-id="channel"]
`).classList.contains('o-active'),
"'channel' tab button should stay inactive"
);
await click(`.o_MessagingMenu_tabButton[data-tab-id="channel"]`);
await click(`.o_MessagingMenuTab[data-tab-id="channel"]`);
assert.notOk(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="all"]
.o_MessagingMenuTab[data-tab-id="all"]
`).classList.contains('o-active'),
"'all' tab button should stay active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="chat"]
.o_MessagingMenuTab[data-tab-id="chat"]
`).classList.contains('o-active'),
"'chat' tab button should become inactive"
);
assert.ok(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="channel"]
.o_MessagingMenuTab[data-tab-id="channel"]
`).classList.contains('o-active'),
"'channel' tab button should become active"
);
await click(`.o_MessagingMenu_tabButton[data-tab-id="all"]`);
await click(`.o_MessagingMenuTab[data-tab-id="all"]`);
assert.ok(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="all"]
.o_MessagingMenuTab[data-tab-id="all"]
`).classList.contains('o-active'),
"'all' tab button should become active"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="chat"]
.o_MessagingMenuTab[data-tab-id="chat"]
`).classList.contains('o-active'),
"'chat' tab button should stay inactive"
);
assert.notOk(
document.querySelector(`
.o_MessagingMenu_tabButton[data-tab-id="channel"]
.o_MessagingMenuTab[data-tab-id="channel"]
`).classList.contains('o-active'),
"'channel' tab button should become inactive"
);
@@ -563,7 +563,7 @@ QUnit.test('filtered previews', async function (assert) {
"should have preview of channel"
);
await click('.o_MessagingMenu_tabButton[data-tab-id="chat"]');
await click('.o_MessagingMenuTab[data-tab-id="chat"]');
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ThreadPreview`).length,
1,
@@ -596,7 +596,7 @@ QUnit.test('filtered previews', async function (assert) {
"should not have preview of channel"
);
await click('.o_MessagingMenu_tabButton[data-tab-id="channel"]');
await click('.o_MessagingMenuTab[data-tab-id="channel"]');
assert.strictEqual(
document.querySelectorAll(`
.o_MessagingMenu_dropdownMenu
@@ -632,7 +632,7 @@ QUnit.test('filtered previews', async function (assert) {
"should have preview of channel"
);
await click('.o_MessagingMenu_tabButton[data-tab-id="all"]');
await click('.o_MessagingMenuTab[data-tab-id="all"]');
assert.strictEqual(
document.querySelectorAll(`.o_MessagingMenu_dropdownMenu .o_ThreadPreview`).length,
2,
@@ -954,7 +954,7 @@ QUnit.test('Group chat should be displayed inside the chat section of the messag
await createMessagingMenuComponent();
await click('.o_MessagingMenu_toggler');
await click(`.o_MessagingMenu_tabButton[data-tab-id="chat"]`);
await click(`.o_MessagingMenuTab[data-tab-id="chat"]`);
assert.strictEqual(
document.querySelectorAll(`