Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,551 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { clear, insert, link } from '@mail/model/model_field_command';
import { makeDeferred } from '@mail/utils/deferred';
const getThreadNextTemporaryId = (function () {
let tmpId = 0;
return () => {
tmpId -= 1;
return tmpId;
};
})();
const getMessageNextTemporaryId = (function () {
let tmpId = 0;
return () => {
tmpId -= 1;
return tmpId;
};
})();
registerModel({
name: 'Chatter',
recordMethods: {
focus() {
if (this.composerView) {
this.composerView.update({ doFocus: true });
}
},
async doSaveRecord() {
const saved = await this.saveRecord();
if (!saved) {
return saved;
}
let composerData = null;
if (this.composerView) {
const {
attachments,
isLog,
rawMentionedChannels,
rawMentionedPartners,
textInputContent,
textInputCursorEnd,
textInputCursorStart,
textInputSelectionDirection,
} = this.composerView.composer;
composerData = {
attachments,
isLog,
rawMentionedChannels,
rawMentionedPartners,
textInputContent,
textInputCursorEnd,
textInputCursorStart,
textInputSelectionDirection,
};
}
// Wait for next render from chatter_container,
// So that it changes to composer of new thread
this.update({
createNewRecordComposerData: composerData,
createNewRecordDeferred: composerData ? makeDeferred() : null,
});
await this.createNewRecordDeferred;
// Give some time to chatter model being updated by save.
await new Promise((resolve) => setTimeout(() => requestAnimationFrame(resolve)));
return saved;
},
onAttachmentsLoadingTimeout() {
this.update({
attachmentsLoaderTimer: clear(),
isShowingAttachmentsLoading: true,
});
},
/**
* Handles click on the attach button.
*/
async onClickButtonAddAttachments() {
if (this.isTemporary) {
const saved = await this.doSaveRecord();
if (!saved) {
return;
}
}
this.fileUploader.openBrowserFileUploader();
},
/**
* Handles click on the attachments button.
*/
async onClickButtonToggleAttachments() {
if (this.isTemporary) {
const saved = await this.doSaveRecord();
if (!saved) {
return;
}
}
this.update({ attachmentBoxView: this.attachmentBoxView ? clear() : {} });
if (this.attachmentBoxView) {
this.scrollPanelRef.el.scrollTop = 0;
}
},
/**
* Handles click on top bar close button.
*
* @param {MouseEvent} ev
*/
onClickChatterTopbarClose(ev) {
this.component.trigger('o-close-chatter');
},
/**
* Handles click on "log note" button.
*
* @param {MouseEvent} ev
*/
onClickLogNote() {
if (this.composerView && this.composerView.composer.isLog) {
this.update({ composerView: clear() });
} else {
this.showLogNote();
}
},
/**
* Handles click on "schedule activity" button.
*
* @param {MouseEvent} ev
*/
async onClickScheduleActivity(ev) {
if (this.isTemporary) {
const saved = await this.doSaveRecord();
if (!saved) {
return;
}
}
await this.messaging.openActivityForm({ thread: this.thread });
if (this.exists()) {
this.reloadParentView();
}
},
/**
* Handles click on "send message" button.
*
* @param {MouseEvent} ev
*/
onClickSendMessage(ev) {
if (this.composerView && !this.composerView.composer.isLog) {
this.update({ composerView: clear() });
} else {
this.showSendMessage();
}
},
/**
* Handles scroll on this scroll panel.
*
* @param {Event} ev
*/
onScrollScrollPanel(ev) {
if (!this.threadView || !this.threadView.messageListView || !this.threadView.messageListView.component) {
return;
}
this.threadView.messageListView.component.onScroll(ev);
},
openAttachmentBoxView() {
this.update({ attachmentBoxView: {} });
},
/**
* Open a dialog to add partners as followers.
*/
promptAddPartnerFollower() {
const action = {
type: 'ir.actions.act_window',
res_model: 'mail.wizard.invite',
view_mode: 'form',
views: [[false, 'form']],
name: this.env._t("Invite Follower"),
target: 'new',
context: {
default_res_model: this.thread.model,
default_res_id: this.thread.id,
},
};
this.env.services.action.doAction(
action,
{
onClose: async () => {
if (!this.exists() && !this.thread) {
return;
}
await this.thread.fetchData(['followers']);
if (this.exists() && this.hasParentReloadOnFollowersUpdate) {
this.reloadParentView();
}
},
}
);
},
async refresh() {
const requestData = ['activities', 'followers', 'suggestedRecipients'];
if (this.hasMessageList) {
requestData.push('attachments', 'messages');
}
this.thread.fetchData(requestData);
},
/**
* @param {Object} [param0={}]
* @param {string[]} [fieldNames]
*/
async reloadParentView({ fieldNames } = {}) {
if (this.saveRecord) {
await this.saveRecord();
}
if (this.webRecord) {
await this.webRecord.model.root.load({ resId: this.threadId }, { keepChanges: true });
this.webRecord.model.notify();
return;
}
if (this.component) {
const options = { keepChanges: true };
if (fieldNames) {
options.fieldNames = fieldNames;
}
this.component.trigger('reload', options);
}
},
showLogNote() {
this.update({ composerView: {} });
this.composerView.composer.update({ isLog: true });
this.focus();
},
showSendMessage() {
this.update({ composerView: {} });
this.composerView.composer.update({ isLog: false });
this.focus();
},
/**
* @private
*/
_onThreadIdOrThreadModelChanged() {
if (!this.threadModel) {
return;
}
if (this.threadId) {
if (this.thread && this.thread.isTemporary) {
this.thread.delete();
}
this.update({
attachmentBoxView: this.isAttachmentBoxVisibleInitially ? {} : clear(),
thread: insert({
// If the thread was considered to have the activity
// mixin once, it will have it forever.
hasActivities: this.hasActivities ? true : undefined,
id: this.threadId,
model: this.threadModel,
}),
});
} else if (!this.thread || !this.thread.isTemporary) {
const currentPartner = this.messaging.currentPartner;
const message = this.messaging.models['Message'].insert({
author: currentPartner,
body: this.env._t("Creating a new record..."),
id: getMessageNextTemporaryId(),
isTemporary: true,
});
const nextId = getThreadNextTemporaryId();
this.update({
attachmentBoxView: clear(),
thread: insert({
areAttachmentsLoaded: true,
id: nextId,
isTemporary: true,
model: this.threadModel,
}),
});
this.thread.cache.update({ temporaryMessages: link(message) });
}
// continuation of saving new record: restore composer state
if (this.createNewRecordComposerData) {
this.update({
composerView: {
composer: {
...this.createNewRecordComposerData,
thread: this.thread,
},
},
});
this.createNewRecordDeferred.resolve();
}
this.update({
createNewRecordComposerData: clear(),
createNewRecordDeferred: clear(),
});
},
/**
* @private
*/
_onThreadIsLoadingAttachmentsChanged() {
if (!this.thread || !this.thread.isLoadingAttachments) {
this.update({
attachmentsLoaderTimer: clear(),
isShowingAttachmentsLoading: false,
});
return;
}
if (this.isPreparingAttachmentsLoading || this.isShowingAttachmentsLoading) {
return;
}
this._prepareAttachmentsLoading();
},
/**
* @private
*/
_prepareAttachmentsLoading() {
this.update({ attachmentsLoaderTimer: {} });
},
},
fields: {
activityBoxView: one('ActivityBoxView', {
compute() {
if (this.thread && this.thread.hasActivities && this.thread.activities.length > 0) {
return {};
}
return clear();
},
inverse: 'chatter',
}),
attachmentBoxView: one('AttachmentBoxView', {
inverse: 'chatter',
}),
attachmentsLoaderTimer: one('Timer', {
inverse: 'chatterOwnerAsAttachmentsLoader',
}),
canPostMessage: attr({
compute() {
return Boolean(this.isTemporary || this.hasWriteAccess ||
(this.hasReadAccess && this.thread && this.thread.canPostOnReadonly));
},
}),
/**
* States the OWL Chatter component of this chatter.
*/
component: attr(),
/**
* Determines the composer view used to post in this chatter (if any).
*/
composerView: one('ComposerView', {
inverse: 'chatter',
}),
context: attr({
default: {},
}),
dropZoneView: one('DropZoneView', {
compute() {
if (!this.thread) {
return clear();
}
if (this.useDragVisibleDropZone.isVisible) {
return {};
}
return clear();
},
inverse: 'chatterOwner',
}),
fileUploader: one('FileUploader', {
compute() {
return this.thread ? {} : clear();
},
inverse: 'chatterOwner',
}),
followButtonView: one('FollowButtonView', {
compute() {
if (this.hasFollowers && this.thread && (!this.thread.channel || this.thread.channel.channel_type !== 'chat')) {
return {};
}
return clear();
},
inverse: 'chatterOwner',
}),
followerListMenuView: one('FollowerListMenuView', {
compute() {
if (this.hasFollowers && this.thread) {
return {};
}
return clear();
},
inverse: 'chatterOwner',
}),
/**
* Determines whether `this` should display an activity box.
*/
hasActivities: attr({
default: true,
}),
hasExternalBorder: attr({
default: true,
}),
/**
* Determines whether `this` should display followers menu.
*/
hasFollowers: attr({
default: true,
}),
/**
* Determines whether `this` should display a message list.
*/
hasMessageList: attr({
default: true,
}),
/**
* Whether the message list should manage its scroll.
* In particular, when the chatter is on the form view's side,
* then the scroll is managed by the message list.
* Also, the message list shoud not manage the scroll if it shares it
* with the rest of the page.
*/
hasMessageListScrollAdjust: attr({
default: false,
}),
hasParentReloadOnAttachmentsChanged: attr({
default: false,
}),
hasParentReloadOnFollowersUpdate: attr({
default: false,
}),
hasParentReloadOnMessagePosted: attr({
default: false,
}),
hasReadAccess: attr({
compute() {
return Boolean(this.thread && !this.thread.isTemporary && this.thread.hasReadAccess);
},
}),
/**
* Determines whether `this.thread` should be displayed.
*/
hasThreadView: attr({
compute() {
return Boolean(this.thread && this.hasMessageList);
},
}),
hasWriteAccess: attr({
compute() {
return Boolean(this.thread && !this.thread.isTemporary && this.thread.hasWriteAccess);
},
}),
hasTopbarCloseButton: attr({
default: false,
}),
/**
* States the id of this chatter. This id does not correspond to any
* specific value, it is just a unique identifier given by the creator
* of this record.
*/
id: attr({
identifying: true,
}),
/**
* Determiners whether the attachment box is visible initially.
*/
isAttachmentBoxVisibleInitially: attr({
default: false,
}),
isInFormSheetBg: attr({
default: false,
}),
isPreparingAttachmentsLoading: attr({
compute() {
return Boolean(this.attachmentsLoaderTimer);
},
default: false,
}),
isShowingAttachmentsLoading: attr({
default: false,
}),
isTemporary: attr({
compute() {
return Boolean(!this.thread || this.thread.isTemporary);
},
}),
saveRecord: attr(),
scrollPanelRef: attr(),
/**
* Determines whether the view should reload after file changed in this chatter,
* such as from a file upload.
*/
shouldReloadParentFromFileChanged: attr({
compute() {
return this.hasParentReloadOnAttachmentsChanged;
},
}),
/**
* Determines the `Thread` that should be displayed by `this`.
*/
thread: one('Thread'),
/**
* Determines the id of the thread that will be displayed by `this`.
*/
threadId: attr(),
/**
* Determines the model of the thread that will be displayed by `this`.
*/
threadModel: attr(),
/**
* States the `ThreadView` displaying `this.thread`.
*/
threadView: one('ThreadView', {
related: 'threadViewer.threadView',
}),
/**
* Determines the `ThreadViewer` managing the display of `this.thread`.
*/
threadViewer: one('ThreadViewer', {
compute() {
if (!this.thread) {
return clear();
}
return {
hasThreadView: this.hasThreadView,
order: 'desc',
thread: this.thread ? this.thread : clear(),
};
},
inverse: 'chatter',
}),
topbar: one('ChatterTopbar', {
compute() {
return this.thread ? {} : clear();
},
inverse: 'chatter',
}),
useDragVisibleDropZone: one('UseDragVisibleDropZone', {
default: {},
inverse: 'chatterOwner',
readonly: true,
required: true,
}),
webRecord: attr(),
createNewRecordComposerData: attr(),
createNewRecordDeferred: attr(),
},
onChanges: [
{
dependencies: ['threadId', 'threadModel'],
methodName: '_onThreadIdOrThreadModelChanged',
},
{
dependencies: ['thread.isLoadingAttachments'],
methodName: '_onThreadIsLoadingAttachmentsChanged',
},
],
});