[IMP] mail: make messaging and models available in models without env

Part of task-2582313
This commit is contained in:
Sébastien Theys
2021-08-18 12:20:51 +00:00
parent d08b16a49e
commit 5d0b8cdfc9
21 changed files with 245 additions and 228 deletions
@@ -32,7 +32,7 @@ registerInstancePatchModel('mail.partner', 'hr_holidays/static/src/models/partne
if (!this.outOfOfficeDateEnd) {
return clear();
}
if (!this.env.messaging.locale.language) {
if (!this.messaging.locale || !this.messaging.locale.language) {
return clear();
}
const currentDate = new Date();
@@ -112,6 +112,9 @@ export class Composer extends Component {
* @returns {boolean}
*/
get hasFooter() {
if (!this.composer) {
return false;
}
return (
this.props.hasThreadTyping ||
this.composer.attachments.length > 0 ||
@@ -209,6 +212,9 @@ export class Composer extends Component {
if (this.contains(ev.target)) {
return;
}
if (!this.composer) {
return;
}
this.composer.discard();
}
@@ -37,6 +37,9 @@ export class Discuss extends Component {
}
patched() {
if (!this.discuss) {
return;
}
if (this.discuss.thread) {
this.trigger('o-push-state-action-manager');
}
@@ -235,6 +235,9 @@ export class DiscussSidebar extends Component {
*/
_onHideAddingItem(ev) {
ev.stopPropagation();
if (!this.discuss) {
return;
}
this.discuss.clearIsAddingItem();
}
@@ -2,79 +2,81 @@
<templates xml:space="preserve">
<t t-name="mail.DiscussSidebar" owl="1">
<div name="root" class="o_DiscussSidebar">
<div class="o_DiscussSidebar_group o_DiscussSidebar_groupMailbox">
<t t-foreach="orderedMailboxes" t-as="mailbox" t-key="mailbox.localId">
<DiscussSidebarItem
class="o_DiscussSidebar_item"
threadLocalId="mailbox.localId"
/>
<div class="o_DiscussSidebar">
<t t-if="discuss" name="root">
<div class="o_DiscussSidebar_group o_DiscussSidebar_groupMailbox">
<t t-foreach="orderedMailboxes" t-as="mailbox" t-key="mailbox.localId">
<DiscussSidebarItem
class="o_DiscussSidebar_item"
threadLocalId="mailbox.localId"
/>
</t>
</div>
<hr class="o_DiscussSidebar_separator"/>
<t t-if="env.models['mail.thread'].all(thread => thread.isPinned and thread.model === 'mail.channel').length > 19">
<input class="o_DiscussSidebar_quickSearch" t-on-input="_onInputQuickSearch" placeholder="Quick search..." t-ref="quickSearchInput" t-esc="discuss.sidebarQuickSearchValue"/>
</t>
</div>
<hr class="o_DiscussSidebar_separator"/>
<t t-if="env.models['mail.thread'].all(thread => thread.isPinned and thread.model === 'mail.channel').length > 19">
<input class="o_DiscussSidebar_quickSearch" t-on-input="_onInputQuickSearch" placeholder="Quick search..." t-ref="quickSearchInput" t-esc="discuss.sidebarQuickSearchValue"/>
<div class="o_DiscussSidebar_group o_DiscussSidebar_groupChannel">
<div class="o_DiscussSidebar_groupHeader">
<div class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupTitle o-clickable" t-on-click="_onClickChannelTitle">
Channels
</div>
<div class="o-autogrow"/>
<div class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupHeaderItemAdd fa fa-plus" t-on-click="_onClickChannelAdd" title="Add or join a channel"/>
</div>
<div class="o_DiscussSidebar_list">
<t t-if="discuss.isAddingChannel">
<div class="o_DiscussSidebar_item o_DiscussSidebar_itemNew">
<AutocompleteInput
class="o_DiscussSidebar_itemNewInput"
customClass="'o_DiscussSidebar_newChannelAutocompleteSuggestions'"
isFocusOnMount="true"
isHtml="true"
placeholder="FIND_OR_CREATE_CHANNEL"
select="_onAddChannelAutocompleteSelect"
source="_onAddChannelAutocompleteSource"
t-on-o-hide="_onHideAddingItem"
/>
</div>
</t>
<t t-foreach="quickSearchOrderedAndPinnedMultiUserChannels" t-as="channel" t-key="channel.localId">
<DiscussSidebarItem
class="o_DiscussSidebar_item"
threadLocalId="channel.localId"
/>
</t>
</div>
</div>
<div class="o_DiscussSidebar_group o_DiscussSidebar_groupChat">
<div class="o_DiscussSidebar_groupHeader">
<div class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupTitle">
Direct Messages
</div>
<div class="o-autogrow"/>
<div class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupHeaderItemAdd fa fa-plus" t-on-click="_onClickChatAdd" title="Start a conversation"/>
</div>
<div class="o_DiscussSidebar_list">
<t t-if="discuss.isAddingChat">
<div class="o_DiscussSidebar_item o_DiscussSidebar_itemNew">
<AutocompleteInput
class="o_DiscussSidebar_itemNewInput"
isFocusOnMount="true"
placeholder="'Find or start a conversation...'"
select="_onAddChatAutocompleteSelect"
source="_onAddChatAutocompleteSource"
t-on-o-hide="_onHideAddingItem"
/>
</div>
</t>
<t t-foreach="quickSearchPinnedAndOrderedChats" t-as="chat" t-key="chat.localId">
<DiscussSidebarItem
class="o_DiscussSidebar_item"
threadLocalId="chat.localId"
/>
</t>
</div>
</div>
</t>
<div class="o_DiscussSidebar_group o_DiscussSidebar_groupChannel">
<div class="o_DiscussSidebar_groupHeader">
<div class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupTitle o-clickable" t-on-click="_onClickChannelTitle">
Channels
</div>
<div class="o-autogrow"/>
<div class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupHeaderItemAdd fa fa-plus" t-on-click="_onClickChannelAdd" title="Add or join a channel"/>
</div>
<div class="o_DiscussSidebar_list">
<t t-if="discuss.isAddingChannel">
<div class="o_DiscussSidebar_item o_DiscussSidebar_itemNew">
<AutocompleteInput
class="o_DiscussSidebar_itemNewInput"
customClass="'o_DiscussSidebar_newChannelAutocompleteSuggestions'"
isFocusOnMount="true"
isHtml="true"
placeholder="FIND_OR_CREATE_CHANNEL"
select="_onAddChannelAutocompleteSelect"
source="_onAddChannelAutocompleteSource"
t-on-o-hide="_onHideAddingItem"
/>
</div>
</t>
<t t-foreach="quickSearchOrderedAndPinnedMultiUserChannels" t-as="channel" t-key="channel.localId">
<DiscussSidebarItem
class="o_DiscussSidebar_item"
threadLocalId="channel.localId"
/>
</t>
</div>
</div>
<div class="o_DiscussSidebar_group o_DiscussSidebar_groupChat">
<div class="o_DiscussSidebar_groupHeader">
<div class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupTitle">
Direct Messages
</div>
<div class="o-autogrow"/>
<div class="o_DiscussSidebar_groupHeaderItem o_DiscussSidebar_groupHeaderItemAdd fa fa-plus" t-on-click="_onClickChatAdd" title="Start a conversation"/>
</div>
<div class="o_DiscussSidebar_list">
<t t-if="discuss.isAddingChat">
<div class="o_DiscussSidebar_item o_DiscussSidebar_itemNew">
<AutocompleteInput
class="o_DiscussSidebar_itemNewInput"
isFocusOnMount="true"
placeholder="'Find or start a conversation...'"
select="_onAddChatAutocompleteSelect"
source="_onAddChatAutocompleteSource"
t-on-o-hide="_onHideAddingItem"
/>
</div>
</t>
<t t-foreach="quickSearchPinnedAndOrderedChats" t-as="chat" t-key="chat.localId">
<DiscussSidebarItem
class="o_DiscussSidebar_item"
threadLocalId="chat.localId"
/>
</t>
</div>
</div>
</div>
</t>
+4 -21
View File
@@ -9,9 +9,8 @@ const { EventBus } = owl.core;
* @param {Object} [providedEnv={}]
* @returns {Object}
*/
function addMessagingToEnv(providedEnv = {}) {
const env = Object.assign(providedEnv);
export function addMessagingToEnv(providedEnv = {}) {
const env = { ...providedEnv };
/**
* Registry of models.
*/
@@ -31,12 +30,6 @@ function addMessagingToEnv(providedEnv = {}) {
},
}, (env.browser && env.browser.Notification) || {}),
}, env.browser),
destroyMessaging() {
if (env.modelManager) {
env.modelManager.deleteAll();
env.messaging = undefined;
}
},
disableAnimation: true,
isMessagingInitialized() {
if (!this.messaging) {
@@ -52,13 +45,10 @@ function addMessagingToEnv(providedEnv = {}) {
*/
isQUnitTest: true,
loadingBaseDelayDuration: providedEnv.loadingBaseDelayDuration || 0,
messaging: undefined,
messagingCreatedPromise: makeDeferred(),
messagingInitializedDeferred: makeDeferred(),
messagingBus: new EventBus(),
modelManager: undefined,
});
return env;
}
@@ -66,10 +56,8 @@ function addMessagingToEnv(providedEnv = {}) {
* @param {Object} [providedEnv={}]
* @returns {Object}
*/
function addTimeControlToEnv(providedEnv = {}) {
let env = Object.assign({}, providedEnv);
export function addTimeControlToEnv(providedEnv = {}) {
const env = { ...providedEnv };
if (!env.browser) {
env.browser = {};
}
@@ -126,8 +114,3 @@ function addTimeControlToEnv(providedEnv = {}) {
});
return env;
}
export {
addMessagingToEnv,
addTimeControlToEnv,
}
+12 -17
View File
@@ -33,33 +33,19 @@ async function createMessaging() {
await env.session.is_bound;
env.modelManager.start();
/**
* Create the messaging singleton record.
*/
env.messaging = env.models['mail.messaging'].create();
}
/**
* Registry of models.
*/
env.models = {};
/**
* Environment keys used in messaging.
*/
Object.assign(env, {
autofetchPartnerImStatus: true,
destroyMessaging() {
if (env.modelManager) {
env.modelManager.deleteAll();
env.messaging = undefined;
}
},
disableAnimation: false,
isMessagingInitialized() {
if (!this.messaging) {
if (!this.modelManager.messaging) {
return false;
}
return this.messaging.isInitialized;
return this.modelManager.messaging.isInitialized;
},
/**
* States whether the environment is in QUnit test or not.
@@ -69,7 +55,6 @@ Object.assign(env, {
*/
isQUnitTest: false,
loadingBaseDelayDuration: 400,
messaging: undefined,
messagingBus: new EventBus(),
/**
* Promise which becomes resolved when messaging is created.
@@ -80,5 +65,15 @@ Object.assign(env, {
messagingCreatedPromise: createMessaging(),
modelManager: new ModelManager(env),
});
Object.defineProperty(env, 'messaging', {
get() {
return this.modelManager.messaging;
},
});
Object.defineProperty(env, 'models', {
get() {
return this.modelManager.models;
},
});
serviceRegistry.add('messaging', MessagingService);
+19 -19
View File
@@ -16,11 +16,11 @@ export class ModelField {
constructor({
compute,
default: def,
env,
fieldName,
fieldType,
inverse,
isCausal = false,
modelManager,
readonly = false,
related,
relationType,
@@ -39,10 +39,6 @@ export class ModelField {
* set a value by default.
*/
this.default = def;
/**
* The messaging env.
*/
this.env = env;
/**
* Name of the field in the definition of fields on model.
*/
@@ -68,6 +64,10 @@ export class ModelField {
* relation is removed, the related record is automatically deleted.
*/
this.isCausal = isCausal;
/**
* Reference to the manager of this field.
*/
this.modelManager = modelManager;
/**
* Determines whether the field is read only. Read only field
* can't be updated once the record is created.
@@ -482,8 +482,8 @@ export class ModelField {
* @returns {boolean} whether the value changed for the current field
*/
_setRelationCreate(record, data, options) {
const OtherModel = this.env.models[this.to];
const other = this.env.modelManager._create(OtherModel, data);
const OtherModel = this.modelManager.models[this.to];
const other = this.modelManager._create(OtherModel, data);
return this._setRelationLink(record, other, options);
}
@@ -500,8 +500,8 @@ export class ModelField {
* @returns {boolean} whether the value changed for the current field
*/
_setRelationInsert(record, data, options) {
const OtherModel = this.env.models[this.to];
const other = this.env.modelManager._insert(OtherModel, data);
const OtherModel = this.modelManager.models[this.to];
const other = this.modelManager._insert(OtherModel, data);
return this._setRelationLink(record, other, options);
}
@@ -517,8 +517,8 @@ export class ModelField {
* @returns {boolean} whether the value changed for the current field
*/
_setRelationInsertAndReplace(record, data, options) {
const OtherModel = this.env.models[this.to];
const newValue = this.env.modelManager._insert(OtherModel, data);
const OtherModel = this.modelManager.models[this.to];
const newValue = this.modelManager._insert(OtherModel, data);
return this._setRelationReplace(record, newValue, options);
}
@@ -569,7 +569,7 @@ export class ModelField {
otherRecords.add(recordToLink);
// link current record to other records
if (hasToUpdateInverse) {
this.env.modelManager._update(
this.modelManager._update(
recordToLink,
{ [this.inverse]: link(record) },
{ allowWriteReadonly: true, hasToUpdateInverse: false }
@@ -605,7 +605,7 @@ export class ModelField {
record.__values[this.fieldName] = recordToLink;
// link current record to other record
if (hasToUpdateInverse) {
this.env.modelManager._update(
this.modelManager._update(
recordToLink,
{ [this.inverse]: link(record) },
{ allowWriteReadonly: true, hasToUpdateInverse: false }
@@ -713,7 +713,7 @@ export class ModelField {
if (!otherRecord) {
throw Error(`Record ${record.localId} cannot update undefined relational field ${this.fieldName}.`);
}
this.env.modelManager._update(otherRecord, data);
this.modelManager._update(otherRecord, data);
return false;
}
@@ -756,9 +756,9 @@ export class ModelField {
}
// apply causality
if (this.isCausal) {
this.env.modelManager._delete(recordToUnlink);
this.modelManager._delete(recordToUnlink);
} else {
this.env.modelManager._update(
this.modelManager._update(
recordToUnlink,
{ [this.inverse]: unlink(record) },
{ allowWriteReadonly: true, hasToUpdateInverse: false }
@@ -800,9 +800,9 @@ export class ModelField {
}
// apply causality
if (this.isCausal) {
this.env.modelManager._delete(otherRecord);
this.modelManager._delete(otherRecord);
} else {
this.env.modelManager._update(
this.modelManager._update(
otherRecord,
{ [this.inverse]: unlink(record) },
{ allowWriteReadonly: true, hasToUpdateInverse: false }
@@ -822,7 +822,7 @@ export class ModelField {
* @throws {Error} if record does not satisfy related model
*/
_verifyRelationalValue(record) {
const OtherModel = this.env.models[this.to];
const OtherModel = this.modelManager.models[this.to];
if (!OtherModel.get(record.localId, { isCheckingInheritance: true })) {
throw Error(`Record ${record.localId} is not valid for relational field ${this.fieldName}.`);
}
+45 -17
View File
@@ -4,7 +4,7 @@ import { registry } from '@mail/model/model_core';
import { ModelField } from '@mail/model/model_field';
import { Listener } from '@mail/model/model_listener';
import { patchClassMethods, patchInstanceMethods } from '@mail/utils/utils';
import { unlinkAll } from '@mail/model/model_field_command';
import { link, unlinkAll } from '@mail/model/model_field_command';
/**
* Object that manage models and records, notably their update cycle: whenever
@@ -83,6 +83,14 @@ export class ModelManager {
* iterate all localId to be able to find which are using it.
*/
this._localIdsObservedByListener = new Map();
/**
* The messaging singleton associated to this model manager.
*/
this._messaging = undefined;
/**
* All generated models. Keys are model name, values are model class.
*/
this.models = {};
/**
* Set of records that have been updated during the current update
* cycle.
@@ -98,7 +106,11 @@ export class ModelManager {
/**
* Generate the models.
*/
Object.assign(this.env.models, this._generateModels());
this.models = this._generateModels();
/**
* Create the messaging singleton record.
*/
this.models['mail.messaging'].create();
}
//--------------------------------------------------------------------------
@@ -150,18 +162,6 @@ export class ModelManager {
this._flushUpdateCycle();
}
/**
* Delete all records.
*/
deleteAll() {
for (const Model of Object.values(this.env.models)) {
for (const record of Object.values(Model.__records)) {
this._delete(record);
}
}
this._flushUpdateCycle();
}
/**
* Returns whether the given record still exists.
*
@@ -232,7 +232,7 @@ export class ModelManager {
return;
}
// support for inherited models (eg. relation targeting `mail.model`)
for (const SubModel of Object.values(this.env.models)) {
for (const SubModel of Object.values(this.models)) {
if (!(SubModel.prototype instanceof Model)) {
continue;
}
@@ -260,6 +260,20 @@ export class ModelManager {
return res;
}
/**
* Returns the messaging singleton associated to this model manager.
*
* @returns {mail.messaging}
*/
get messaging() {
if (!this.models || !this._messaging) {
return undefined;
}
// Use "get" specifically to ensure the record still exists and to
// ensure listeners are properly notified of this access.
return this.get(this.models['mail.messaging'], this._messaging.localId);
}
/**
* Removes a listener, with the same object reference as given to `startListening`.
* Removing the listener effectively makes its `onChange` function no longer
@@ -605,6 +619,12 @@ export class ModelManager {
* expected to be found on every record.
*/
const record = new Model({ valid: true });
if (Model.modelName === 'mail.messaging') {
if (this._messaging) {
throw Error('messaging should be a singleton');
}
this._messaging = record;
}
Object.assign(record, {
// The messaging env.
env: this.env,
@@ -651,6 +671,7 @@ export class ModelManager {
data2[field.fieldName] = field.default;
}
}
data2.messaging = link(this._messaging);
this._update(record, data2, { allowWriteReadonly: true });
for (const field of Model.__fieldList) {
if (field.compute) {
@@ -724,6 +745,9 @@ export class ModelManager {
this._markListenerToNotify(listener);
}
delete Model.__records[record.localId];
if (record === this._messaging) {
this._messaging = undefined;
}
}
/**
@@ -812,6 +836,12 @@ export class ModelManager {
// Make environment accessible from Model.
const Model = generatable.factory(Models);
Model.env = this.env;
const modelManager = this;
Object.defineProperty(Model, 'messaging', {
get() {
return modelManager.messaging;
},
});
/**
* Contains all records. key is local id, while value is the record.
*/
@@ -900,7 +930,6 @@ export class ModelManager {
{},
relFunc(Model.modelName, { inverse: field.fieldName }),
{
env: this.env,
fieldName: `_inverse_${Model.modelName}/${field.fieldName}`,
modelManager: this,
}
@@ -983,7 +1012,6 @@ export class ModelManager {
// Make fields aware of their field name.
for (const [fieldName, fieldData] of Object.entries(Model.fields)) {
Model.fields[fieldName] = new ModelField(Object.assign({}, fieldData, {
env: this.env,
fieldName,
modelManager: this,
}));
@@ -297,7 +297,7 @@ function factory(dependencies) {
*/
_computeVisual() {
let visual = JSON.parse(JSON.stringify(BASE_VISUAL));
if (!this.messaging) {
if (!this.messaging || !this.messaging.device) {
return visual;
}
const device = this.messaging.device;
@@ -391,9 +391,6 @@ function factory(dependencies) {
lastVisible: many2one('mail.chat_window', {
compute: '_computeLastVisible',
}),
messaging: one2one('mail.messaging', {
inverse: 'chatWindowManager',
}),
newMessageChatWindow: one2one('mail.chat_window', {
compute: '_computeNewMessageChatWindow',
}),
@@ -161,6 +161,7 @@ function factory(dependencies) {
_onThreadIsLoadingAttachmentsChanged() {
if (!this.thread || !this.thread.isLoadingAttachments) {
this._stopAttachmentsLoading();
this.update({ isShowingAttachmentsLoading: false });
return;
}
if (this._isPreparingAttachmentsLoading || this.isShowingAttachmentsLoading) {
@@ -186,7 +187,6 @@ function factory(dependencies) {
_stopAttachmentsLoading() {
this.env.browser.clearTimeout(this._attachmentsLoaderTimeout);
this._attachmentsLoaderTimeout = null;
this.update({ isShowingAttachmentsLoading: false });
this._isPreparingAttachmentsLoading = false;
}
@@ -2,7 +2,7 @@
import { registerNewModel } from '@mail/model/model_core';
import { attr, many2one, one2many, one2one } from '@mail/model/model_field';
import { clear, create, link, replace, unlink, unlinkAll, update } from '@mail/model/model_field_command';
import { clear, create, link, unlink, unlinkAll, update } from '@mail/model/model_field_command';
function factory(dependencies) {
@@ -343,23 +343,9 @@ function factory(dependencies) {
* @returns {mail.thread|undefined}
*/
_computeThread() {
let thread = this.thread;
if (this.env.messaging &&
this.env.messaging.inbox &&
this.env.messaging.device.isMobile &&
this.activeMobileNavbarTabId === 'mailbox' &&
this.initActiveId !== 'mail.box_inbox' &&
!thread
) {
// After loading Discuss from an arbitrary tab other then 'mailbox',
// switching to 'mailbox' requires to also set its inner-tab ;
// by default the 'inbox'.
return replace(this.env.messaging.inbox);
}
if (!thread || !thread.isPinned) {
if (!this.thread || !this.thread.isPinned) {
return unlink();
}
return;
}
/**
@@ -454,9 +440,6 @@ function factory(dependencies) {
menu_id: attr({
default: null,
}),
messaging: one2one('mail.messaging', {
inverse: 'discuss',
}),
renamingThreads: one2many('mail.thread'),
/**
* The message that is currently selected as being replied to in Inbox.
@@ -55,7 +55,7 @@ function factory(dependencies) {
data2.email_from = data.email_from;
}
if ('history_partner_ids' in data) {
data2.isHistory = data.history_partner_ids.includes(this.env.messaging.currentPartner.id);
data2.isHistory = data.history_partner_ids.includes(this.messaging.currentPartner.id);
}
if ('id' in data) {
data2.id = data.id;
@@ -171,6 +171,9 @@ function factory(dependencies) {
limit,
},
}, { shadow: true });
if (!this.messaging) {
return;
}
const messages = this.env.models['mail.message'].insert(messagesData.map(
messageData => this.env.models['mail.message'].convertData(messageData)
));
@@ -427,14 +430,14 @@ function factory(dependencies) {
*/
_computeThreads() {
const threads = [];
if (this.isHistory) {
threads.push(this.env.messaging.history);
if (this.isHistory && this.messaging.history) {
threads.push(this.messaging.history);
}
if (this.isNeedaction) {
threads.push(this.env.messaging.inbox);
if (this.isNeedaction && this.messaging.inbox) {
threads.push(this.messaging.inbox);
}
if (this.isStarred) {
threads.push(this.env.messaging.starred);
if (this.isStarred && this.messaging.starred) {
threads.push(this.messaging.starred);
}
if (this.originThread) {
threads.push(this.originThread);
@@ -179,10 +179,17 @@ function factory(dependencies) {
}
Messaging.fields = {
/**
* Inverse of the messaging field present on all models. This field
* therefore contains all existing records.
*/
allRecords: one2many('mail.model', {
inverse: 'messaging',
isCausal: true,
}),
cannedResponses: one2many('mail.canned_response'),
chatWindowManager: one2one('mail.chat_window_manager', {
default: create(),
inverse: 'messaging',
isCausal: true,
readonly: true,
}),
@@ -201,7 +208,6 @@ function factory(dependencies) {
}),
discuss: one2one('mail.discuss', {
default: create(),
inverse: 'messaging',
isCausal: true,
readonly: true,
}),
@@ -223,7 +229,6 @@ function factory(dependencies) {
}),
initializer: one2one('mail.messaging_initializer', {
default: create(),
inverse: 'messaging',
isCausal: true,
readonly: true,
}),
@@ -245,7 +250,6 @@ function factory(dependencies) {
}),
messagingMenu: one2one('mail.messaging_menu', {
default: create(),
inverse: 'messaging',
isCausal: true,
readonly: true,
}),
@@ -256,7 +260,6 @@ function factory(dependencies) {
}),
notificationHandler: one2one('mail.messaging_notification_handler', {
default: create(),
inverse: 'messaging',
isCausal: true,
readonly: true,
}),
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { registerNewModel } from '@mail/model/model_core';
import { one2one } from '@mail/model/model_field';
import { executeGracefully } from '@mail/utils/utils';
import { create, link, insert } from '@mail/model/model_field_command';
@@ -237,12 +236,6 @@ function factory(dependencies) {
}
MessagingInitializer.fields = {
messaging: one2one('mail.messaging', {
inverse: 'initializer',
}),
};
MessagingInitializer.modelName = 'mail.messaging_initializer';
return MessagingInitializer;
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { registerNewModel } from '@mail/model/model_core';
import { attr, one2one } from '@mail/model/model_field';
import { attr } from '@mail/model/model_field';
function factory(dependencies) {
@@ -96,9 +96,6 @@ function factory(dependencies) {
isOpen: attr({
default: false,
}),
messaging: one2one('mail.messaging', {
inverse: 'messagingMenu',
}),
};
MessagingMenu.modelName = 'mail.messaging_menu';
@@ -1,7 +1,6 @@
/** @odoo-module **/
import { registerNewModel } from '@mail/model/model_core';
import { one2one } from '@mail/model/model_field';
import { decrement, increment, insert, link } from '@mail/model/model_field_command';
import { htmlToTextContentInline } from '@mail/js/utils';
@@ -712,12 +711,6 @@ function factory(dependencies) {
}
MessagingNotificationHandler.fields = {
messaging: one2one('mail.messaging', {
inverse: 'notificationHandler',
}),
};
MessagingNotificationHandler.modelName = 'mail.messaging_notification_handler';
return MessagingNotificationHandler;
+12 -1
View File
@@ -2,6 +2,7 @@
import { registerNewModel } from '@mail/model/model_core';
import { RecordDeletedError } from '@mail/model/model_errors';
import { many2one } from '@mail/model/model_field';
/**
* This function generates a class that represent a model. Instances of such
@@ -242,7 +243,17 @@ function factory() {
* Note: fields of super-class are automatically inherited, therefore a
* sub-class should (re-)define fields without copying ancestors' fields.
*/
Model.fields = {};
Model.fields = {
/**
* States the messaging singleton. Automatically assigned by the model
* manager at creation.
*/
messaging: many2one('mail.messaging', {
inverse: 'allRecords',
required: true,
readonly: true,
}),
};
/**
* Name of the model. Important to refer to appropriate model class
@@ -27,14 +27,17 @@ function factory(dependencies) {
let fetchedMessages;
let success;
try {
fetchedMessages = await this.async(() => this._loadMessages({
fetchedMessages = await this._loadMessages({
extraDomain: [['id', '<', Math.min(...messageIds)]],
limit,
}));
});
success = true;
} catch (e) {
} catch (e) {
success = false;
}
if (!this.exists()) {
return;
}
if (success) {
if (fetchedMessages.length < limit) {
this.update({ isAllHistoryLoaded: true });
@@ -254,20 +257,23 @@ function factory(dependencies) {
const context = this.env.session.user_context;
let messages;
try {
messages = await this.async(() =>
this.env.models['mail.message'].performRpcMessageFetch(
domain,
limit,
context,
)
messages = await this.env.models['mail.message'].performRpcMessageFetch(
domain,
limit,
context,
);
} catch(e) {
this.update({
hasLoadingFailed: true,
isLoading: false,
});
} catch (e) {
if (this.exists()) {
this.update({
hasLoadingFailed: true,
isLoading: false,
});
}
throw e;
}
if (!this.exists()) {
return;
}
this.update({
fetchedMessages: link(messages),
hasLoadingFailed: false,
@@ -339,7 +345,7 @@ function factory(dependencies) {
if (!this.hasToLoadMessages) {
return;
}
const fetchedMessages = await this.async(() => this._loadMessages());
const fetchedMessages = await this._loadMessages();
for (const threadView of this.threadViews) {
threadView.addComponentHint('messages-loaded', { fetchedMessages });
}
@@ -94,11 +94,14 @@ function factory(dependencies) {
if (!this.thread) {
return;
}
if (!this.messaging.device) {
return;
}
// Actually in mobile there is a send button, so we need there 'enter' to allow new line.
// Hence, we want to use a different shortcut 'ctrl/meta enter' to send for small screen
// size with a non-mailing channel.
// here send will be done on clicking the button or using the 'ctrl/meta enter' shortcut.
if (this.env.messaging.device.isMobile) {
if (this.messaging.device.isMobile) {
return ['ctrl-enter', 'meta-enter'];
}
return ['enter'];
+19 -11
View File
@@ -536,8 +536,8 @@ async function start(param0 = {}) {
destroyCallbacks.forEach(callback => callback({ widget }));
this._super(...arguments);
legacyUnpatch(widget);
if (testEnv) {
testEnv.destroyMessaging();
if (testEnv && testEnv.modelManager.messaging) {
testEnv.modelManager.messaging.delete();
}
}
});
@@ -587,8 +587,8 @@ async function start(param0 = {}) {
destroyCallbacks.forEach(callback => callback({ widget }));
this._super(...arguments);
legacyUnpatch(widget);
if (testEnv) {
testEnv.destroyMessaging();
if (testEnv && testEnv.modelManager.messaging) {
testEnv.modelManager.messaging.delete();
}
}
});
@@ -603,13 +603,26 @@ async function start(param0 = {}) {
delete widget.destroy;
destroyCallbacks.forEach(callback => callback({ widget }));
parent.destroy();
if (testEnv) {
testEnv.destroyMessaging();
if (testEnv && testEnv.modelManager.messaging) {
testEnv.modelManager.messaging.delete();
}
},
});
}
// get the final test env after execution of createView/createWebClient/addMockEnvironment
testEnv = Component.env;
const modelManager = new ModelManager(testEnv);
Object.assign(testEnv, { modelManager });
Object.defineProperty(testEnv, 'messaging', {
get() {
return this.modelManager.messaging;
},
});
Object.defineProperty(testEnv, 'models', {
get() {
return this.modelManager.models;
},
});
/**
* Returns a promise resolved after the expected event is received.
*
@@ -668,12 +681,7 @@ async function start(param0 = {}) {
*/
await env.session.is_bound;
testEnv.modelManager = new ModelManager(testEnv);
testEnv.modelManager.start();
/**
* Create the messaging singleton record.
*/
testEnv.messaging = testEnv.models['mail.messaging'].create();
testEnv.messaging.start().then(() =>
testEnv.messagingInitializedDeferred.resolve()
);