[IMP] web, mail: add mail module to prettier and linter

closes odoo/odoo#105677

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-11-14 16:21:39 +01:00
committed by Sébastien Theys
parent 424b77b6a8
commit b0116e4523
359 changed files with 35487 additions and 30877 deletions
@@ -1,9 +1,9 @@
/** @odoo-module **/
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { insert } from '@mail/model';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { insert } from "@mail/model";
import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl';
import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl";
const getNextId = (function () {
let tmpId = 0;
@@ -18,7 +18,6 @@ const getNextId = (function () {
* records are ready before rendering ActivityCellView component.
*/
export class ActivityCellViewContainer extends Component {
/**
* @override
*/
@@ -28,7 +27,7 @@ export class ActivityCellViewContainer extends Component {
this.activityCellView = undefined;
this.activityCellViewId = getNextId();
this._insertFromProps(this.props);
onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
onWillUpdateProps((nextProps) => this._insertFromProps(nextProps));
onWillDestroy(() => this._deleteRecord());
}
@@ -53,7 +52,7 @@ export class ActivityCellViewContainer extends Component {
this._deleteRecord();
return;
}
const activityCellView = messaging.models['ActivityCellView'].insert({
const activityCellView = messaging.models["ActivityCellView"].insert({
activityType: {
id: props.activityTypeId,
},
@@ -61,14 +60,16 @@ export class ActivityCellViewContainer extends Component {
id: this.activityCellViewId,
reloadFunc: props.reloadFunc,
thread: {
activities: insert(props.activityIds.map(activityId => {
return {
id: activityId,
type: {
id: props.activityTypeId,
},
};
})),
activities: insert(
props.activityIds.map((activityId) => {
return {
id: activityId,
type: {
id: props.activityTypeId,
},
};
})
),
hasActivities: true,
id: props.resId,
model: props.resModel,
@@ -80,7 +81,6 @@ export class ActivityCellViewContainer extends Component {
}
this.render();
}
}
Object.assign(ActivityCellViewContainer, {
@@ -95,5 +95,5 @@ Object.assign(ActivityCellViewContainer, {
resId: Number,
resModel: String,
},
template: 'mail.ActivityCellViewContainer',
template: "mail.ActivityCellViewContainer",
});
@@ -3,26 +3,26 @@
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
class ActivityException extends Component {
get textClass() {
if (this.props.value) {
return 'text-' + this.props.value + ' fa ' + this.props.record.data.activity_exception_icon;
return (
"text-" + this.props.value + " fa " + this.props.record.data.activity_exception_icon
);
}
return undefined;
}
}
Object.assign(ActivityException, {
props: standardFieldProps,
template: 'mail.ActivityException',
template: "mail.ActivityException",
fieldDependencies: {
activity_exception_icon: { type: 'char' },
activity_exception_icon: { type: "char" },
},
noLabel: true,
});
registry.category('fields').add('activity_exception', ActivityException);
registry.category("fields").add("activity_exception", ActivityException);
@@ -1,12 +1,12 @@
/** @odoo-module **/
// ensure components are registered beforehand.
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl';
import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl";
const getNextId = (function () {
let tmpId = 0;
@@ -21,7 +21,6 @@ const getNextId = (function () {
* records are ready before rendering KanbanFieldActivityView component.
*/
export class KanbanFieldActivityViewContainer extends Component {
/**
* @override
*/
@@ -31,7 +30,7 @@ export class KanbanFieldActivityViewContainer extends Component {
this.kanbanFieldActivityView = undefined;
this.kanbanFieldActivityViewId = getNextId();
this._insertFromProps(this.props);
onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
onWillUpdateProps((nextProps) => this._insertFromProps(nextProps));
onWillDestroy(() => this._deleteRecord());
}
@@ -56,10 +55,10 @@ export class KanbanFieldActivityViewContainer extends Component {
this._deleteRecord();
return;
}
const kanbanFieldActivityView = messaging.models['KanbanFieldActivityView'].insert({
const kanbanFieldActivityView = messaging.models["KanbanFieldActivityView"].insert({
id: this.kanbanFieldActivityViewId,
thread: {
activities: props.value.records.map(activityData => {
activities: props.value.records.map((activityData) => {
return {
id: activityData.resId,
};
@@ -76,22 +75,21 @@ export class KanbanFieldActivityViewContainer extends Component {
}
this.render();
}
}
Object.assign(KanbanFieldActivityViewContainer, {
fieldDependencies: {
activity_exception_decoration: { type: 'selection' },
activity_exception_icon: { type: 'char' },
activity_state: { type: 'selection' },
activity_summary: { type: 'char' },
activity_type_icon: { type: 'char' },
activity_type_id: { type: 'many2one', relation: 'mail.activity.type' },
activity_exception_decoration: { type: "selection" },
activity_exception_icon: { type: "char" },
activity_state: { type: "selection" },
activity_summary: { type: "char" },
activity_type_icon: { type: "char" },
activity_type_id: { type: "many2one", relation: "mail.activity.type" },
},
props: {
...standardFieldProps,
},
template: 'mail.KanbanFieldActivityViewContainer',
template: "mail.KanbanFieldActivityViewContainer",
});
registry.category('fields').add('kanban_activity', KanbanFieldActivityViewContainer);
registry.category("fields").add("kanban_activity", KanbanFieldActivityViewContainer);
@@ -1,12 +1,12 @@
/** @odoo-module **/
// ensure components are registered beforehand.
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl';
import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl";
const getNextId = (function () {
let tmpId = 0;
@@ -21,7 +21,6 @@ const getNextId = (function () {
* records are ready before rendering ListFieldActivityView component.
*/
export class ListFieldActivityViewContainer extends Component {
/**
* @override
*/
@@ -31,7 +30,7 @@ export class ListFieldActivityViewContainer extends Component {
this.listFieldActivityView = undefined;
this.listFieldActivityViewId = getNextId();
this._insertFromProps(this.props);
onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
onWillUpdateProps((nextProps) => this._insertFromProps(nextProps));
onWillDestroy(() => this._deleteRecord());
}
@@ -56,10 +55,10 @@ export class ListFieldActivityViewContainer extends Component {
this._deleteRecord();
return;
}
const listFieldActivityView = messaging.models['ListFieldActivityView'].insert({
const listFieldActivityView = messaging.models["ListFieldActivityView"].insert({
id: this.listFieldActivityViewId,
thread: {
activities: props.value.records.map(activityData => {
activities: props.value.records.map((activityData) => {
return {
id: activityData.resId,
};
@@ -76,22 +75,21 @@ export class ListFieldActivityViewContainer extends Component {
}
this.render();
}
}
Object.assign(ListFieldActivityViewContainer, {
fieldDependencies: {
activity_exception_decoration: { type: 'selection' },
activity_exception_icon: { type: 'char' },
activity_state: { type: 'selection' },
activity_summary: { type: 'char' },
activity_type_icon: { type: 'char' },
activity_type_id: { type: 'many2one', relation: 'mail.activity.type' },
activity_exception_decoration: { type: "selection" },
activity_exception_icon: { type: "char" },
activity_state: { type: "selection" },
activity_summary: { type: "char" },
activity_type_icon: { type: "char" },
activity_type_id: { type: "many2one", relation: "mail.activity.type" },
},
props: {
...standardFieldProps,
},
template: 'mail.ListFieldActivityViewContainer',
template: "mail.ListFieldActivityViewContainer",
});
registry.category('fields').add('list_activity', ListFieldActivityViewContainer);
registry.category("fields").add("list_activity", ListFieldActivityViewContainer);
@@ -1,8 +1,8 @@
/** @odoo-module **/
import { clear } from '@mail/model';
import { clear } from "@mail/model";
import { onWillUpdateProps, useComponent } from '@odoo/owl';
import { onWillUpdateProps, useComponent } from "@odoo/owl";
/**
* This hook provides support for saving the reference of the component directly
@@ -15,7 +15,7 @@ import { onWillUpdateProps, useComponent } from '@odoo/owl';
export function useComponentToModel({ fieldName }) {
const component = useComponent();
component.props.record.update({ [fieldName]: component });
onWillUpdateProps(nextProps => {
onWillUpdateProps((nextProps) => {
const currentRecord = component.props.record;
const nextRecord = nextProps.record;
if (currentRecord.exists() && currentRecord !== nextRecord) {
@@ -1,16 +1,16 @@
/** @odoo-module **/
import { useModels } from '@mail/component_hooks/use_models';
import { useModels } from "@mail/component_hooks/use_models";
import { componentRegistry, getMessagingComponent } from "@mail/utils/messaging_component";
import { useComponent } from '@odoo/owl';
import { useComponent } from "@odoo/owl";
export function useMessagingContainer() {
const component = useComponent();
component.isLoaded = false;
useModels();
component.env.services.messaging.modelManager.messagingCreatedPromise.then(() => {
let components = { ...component.constructor.components };
const components = { ...component.constructor.components };
for (const name in componentRegistry) {
Object.assign(components, { [name]: getMessagingComponent(name) });
}
@@ -1,8 +1,8 @@
/** @odoo-module **/
import { Listener } from '@mail/model/model_listener';
import { Listener } from "@mail/model/model_listener";
import { onRendered, onWillDestroy, onWillRender, useComponent } from '@odoo/owl';
import { onRendered, onWillDestroy, onWillRender, useComponent } from "@odoo/owl";
/**
* This hook provides support for automatically re-rendering when used records
@@ -1,8 +1,8 @@
/** @odoo-module **/
import { clear } from '@mail/model';
import { clear } from "@mail/model";
import { onWillUpdateProps, useComponent, useRef } from '@odoo/owl';
import { onWillUpdateProps, useComponent, useRef } from "@odoo/owl";
/**
* This hook provides support for saving the result of useRef directly into the
@@ -17,7 +17,7 @@ export function useRefToModel({ fieldName, refName }) {
const component = useComponent();
const ref = useRef(refName);
component.props.record.update({ [fieldName]: ref });
onWillUpdateProps(nextProps => {
onWillUpdateProps((nextProps) => {
const currentRecord = component.props.record;
const nextRecord = nextProps.record;
if (currentRecord.exists() && currentRecord !== nextRecord) {
@@ -1,8 +1,8 @@
/** @odoo-module **/
import { Listener } from '@mail/model/model_listener';
import { Listener } from "@mail/model/model_listener";
import { onMounted, onPatched, onWillDestroy, useComponent } from '@odoo/owl';
import { onMounted, onPatched, onWillDestroy, useComponent } from "@odoo/owl";
/**
* This hook provides support for executing code after update (render or patch).
@@ -1,8 +1,8 @@
/** @odoo-module **/
import { useUpdate } from '@mail/component_hooks/use_update';
import { useUpdate } from "@mail/component_hooks/use_update";
import { useComponent } from '@odoo/owl';
import { useComponent } from "@odoo/owl";
/**
* This hook provides support for binding the onMounted/onPatched hooks to the
@@ -13,7 +13,9 @@ import { useComponent } from '@odoo/owl';
*/
export function useUpdateToModel({ methodName }) {
const component = useComponent();
useUpdate({ func: () => {
component.props.record[methodName]();
} });
useUpdate({
func: () => {
component.props.record[methodName]();
},
});
}
@@ -1,28 +1,28 @@
/** @odoo-module **/
// ensure components are registered beforehand.
import '@mail/components/activity_menu_view/activity_menu_view';
import "@mail/components/activity_menu_view/activity_menu_view";
import { getMessagingComponent } from "@mail/utils/messaging_component";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class ActivityMenuContainer extends Component {
/**
* @override
*/
setup() {
super.setup();
this.env.services.messaging.modelManager.messagingCreatedPromise.then(() => {
this.activityMenuView = this.env.services.messaging.modelManager.messaging.models['ActivityMenuView'].insert();
this.activityMenuView = this.env.services.messaging.modelManager.messaging.models[
"ActivityMenuView"
].insert();
this.render();
});
}
}
ActivityMenuContainer.props = {};
Object.assign(ActivityMenuContainer, {
components: { ActivityMenuView: getMessagingComponent('ActivityMenuView') },
template: 'mail.ActivityMenuContainer',
components: { ActivityMenuView: getMessagingComponent("ActivityMenuView") },
template: "mail.ActivityMenuContainer",
});
@@ -1,17 +1,17 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class ActivityMenuView extends Component {
/**
* @override
*/
setup() {
setup() {
super.setup();
useComponentToModel({ fieldName: 'component' });
useComponentToModel({ fieldName: "component" });
}
/**
* @returns {ActivityMenuView}
@@ -23,7 +23,7 @@ export class ActivityMenuView extends Component {
Object.assign(ActivityMenuView, {
props: { record: Object },
template: 'mail.ActivityMenuView',
template: "mail.ActivityMenuView",
});
registerMessagingComponent(ActivityMenuView);
@@ -1,24 +1,23 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
import { useRefToModel } from "@mail/component_hooks/use_ref_to_model";
import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model";
import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { Component, onMounted } from '@odoo/owl';
import { Component, onMounted } from "@odoo/owl";
export class AttachmentViewer extends Component {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component' });
useRefToModel({ fieldName: 'imageRef', refName: 'image' });
useRefToModel({ fieldName: 'zoomerRef', refName: 'zoomer' });
useRefToModel({ fieldName: 'iframeViewerPdfRef', refName: 'iframeViewerPdf' });
useUpdateToModel({ methodName: 'onComponentUpdate' });
useComponentToModel({ fieldName: "component" });
useRefToModel({ fieldName: "imageRef", refName: "image" });
useRefToModel({ fieldName: "zoomerRef", refName: "zoomer" });
useRefToModel({ fieldName: "iframeViewerPdfRef", refName: "iframeViewerPdf" });
useUpdateToModel({ methodName: "onComponentUpdate" });
onMounted(() => this._mounted());
}
@@ -39,12 +38,11 @@ export class AttachmentViewer extends Component {
get attachmentViewer() {
return this.props.record;
}
}
Object.assign(AttachmentViewer, {
props: { record: Object },
template: 'mail.AttachmentViewer',
template: "mail.AttachmentViewer",
});
registerMessagingComponent(AttachmentViewer);
@@ -1,14 +1,14 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class CallInviteRequestPopupList extends Component {}
Object.assign(CallInviteRequestPopupList, {
props: {},
template: 'mail.CallInviteRequestPopupList',
template: "mail.CallInviteRequestPopupList",
});
registerMessagingComponent(CallInviteRequestPopupList);
@@ -1,14 +1,14 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class CallSettingsMenuDevice extends Component {}
Object.assign(CallSettingsMenuDevice, {
props: { device: Object },
template: 'mail.CallSettingsMenuDevice',
template: "mail.CallSettingsMenuDevice",
});
registerMessagingComponent(CallSettingsMenuDevice);
@@ -1,11 +1,10 @@
/** @odoo-module **/
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class CallSystrayMenuContainer extends Component {
/**
* @override
*/
@@ -16,10 +15,9 @@ export class CallSystrayMenuContainer extends Component {
get messaging() {
return this.env.services.messaging.modelManager.messaging;
}
}
CallSystrayMenuContainer.props = {};
Object.assign(CallSystrayMenuContainer, {
template: 'mail.CallSystrayMenuContainer',
template: "mail.CallSystrayMenuContainer",
});
@@ -1,11 +1,10 @@
/** @odoo-module **/
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class ChatWindowManagerContainer extends Component {
/**
* @override
*/
@@ -20,5 +19,5 @@ export class ChatWindowManagerContainer extends Component {
ChatWindowManagerContainer.props = {};
Object.assign(ChatWindowManagerContainer, {
template: 'mail.ChatWindowManagerContainer',
template: "mail.ChatWindowManagerContainer",
});
@@ -1,9 +1,9 @@
/** @odoo-module **/
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { clear } from '@mail/model';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { clear } from "@mail/model";
import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl';
import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl";
export const getChatterNextTemporaryId = (function () {
let tmpId = 0;
@@ -22,7 +22,6 @@ export const getChatterNextTemporaryId = (function () {
* this component delays the mounting of chatter until it becomes initialized.
*/
export class ChatterContainer extends Component {
/**
* @override
*/
@@ -31,7 +30,7 @@ export class ChatterContainer extends Component {
super.setup();
this.localChatter = undefined;
this._insertFromProps(this.props);
onWillUpdateProps(nextProps => {
onWillUpdateProps((nextProps) => {
this._insertFromProps(nextProps);
});
onWillDestroy(() => this.deleteLocalChatter());
@@ -67,7 +66,10 @@ export class ChatterContainer extends Component {
}
const hasToCreateChatter = !props.chatter && !this.localChatter;
if (hasToCreateChatter) {
this.localChatter = messaging.models['Chatter'].insert({ id: getChatterNextTemporaryId(), ...values });
this.localChatter = messaging.models["Chatter"].insert({
id: getChatterNextTemporaryId(),
...values,
});
}
const chatter = props.chatter || this.localChatter;
if (!hasToCreateChatter) {
@@ -98,7 +100,6 @@ export class ChatterContainer extends Component {
}
this.render();
}
}
Object.assign(ChatterContainer, {
@@ -163,7 +164,7 @@ Object.assign(ChatterContainer, {
webRecord: {
type: Object,
optional: true,
}
},
},
template: 'mail.ChatterContainer',
template: "mail.ChatterContainer",
});
@@ -1,28 +1,27 @@
/** @odoo-module **/
import { useUpdate } from '@mail/component_hooks/use_update';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useUpdate } from "@mail/component_hooks/use_update";
import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { useService } from "@web/core/utils/hooks";
import { FormViewDialog } from '@web/views/view_dialogs/form_view_dialog';
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
import { Component, useRef } from '@odoo/owl';
import { Component, useRef } from "@odoo/owl";
export class ComposerSuggestedRecipientView extends Component {
/**
* @override
*/
setup() {
super.setup();
this.id = _.uniqueId('o_ComposerSuggestedRecipientView_');
this.id = _.uniqueId("o_ComposerSuggestedRecipientView_");
useUpdate({ func: () => this._update() });
/**
* Reference of the checkbox. Useful to know whether it was checked or
* not, to properly update the corresponding state in the record or to
* prompt the user with the partner creation dialog.
*/
this._checkboxRef = useRef('checkbox');
this._checkboxRef = useRef("checkbox");
this.dialogService = useService("dialog");
}
@@ -70,13 +69,17 @@ export class ComposerSuggestedRecipientView extends Component {
if (isChecked) {
this.dialogService.add(FormViewDialog, {
context: {
active_id: this.composerSuggestedRecipientView.suggestedRecipientInfo.thread.id,
active_model: 'mail.compose.message',
default_email: this.composerSuggestedRecipientView.suggestedRecipientInfo.email,
default_name: this.composerSuggestedRecipientView.suggestedRecipientInfo.name,
default_lang: this.composerSuggestedRecipientView.suggestedRecipientInfo.lang,
active_id: this.composerSuggestedRecipientView.suggestedRecipientInfo.thread
.id,
active_model: "mail.compose.message",
default_email: this.composerSuggestedRecipientView.suggestedRecipientInfo
.email,
default_name: this.composerSuggestedRecipientView.suggestedRecipientInfo
.name,
default_lang: this.composerSuggestedRecipientView.suggestedRecipientInfo
.lang,
force_email: true,
ref: 'compound_context',
ref: "compound_context",
},
onRecordSaved: () => this._onDialogSaved(),
resModel: "res.partner",
@@ -93,20 +96,19 @@ export class ComposerSuggestedRecipientView extends Component {
if (!this.composerSuggestedRecipientView.exists()) {
return;
}
const thread = (
const thread =
this.composerSuggestedRecipientView.suggestedRecipientInfo &&
this.composerSuggestedRecipientView.suggestedRecipientInfo.thread
);
this.composerSuggestedRecipientView.suggestedRecipientInfo.thread;
if (!thread) {
return;
}
thread.fetchData(['suggestedRecipients']);
thread.fetchData(["suggestedRecipients"]);
}
}
Object.assign(ComposerSuggestedRecipientView, {
props: { record: Object },
template: 'mail.ComposerSuggestedRecipientView',
template: "mail.ComposerSuggestedRecipientView",
});
registerMessagingComponent(ComposerSuggestedRecipientView);
@@ -1,15 +1,14 @@
/** @odoo-module **/
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class DialogManagerContainer extends Component {
/**
* @override
*/
setup() {
setup() {
useMessagingContainer();
}
@@ -20,5 +19,5 @@ export class DialogManagerContainer extends Component {
DialogManagerContainer.props = {};
Object.assign(DialogManagerContainer, {
template: 'mail.DialogManagerContainer',
template: "mail.DialogManagerContainer",
});
@@ -1,11 +1,10 @@
/** @odoo-module **/
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { Component, onWillDestroy } from '@odoo/owl';
import { Component, onWillDestroy } from "@odoo/owl";
export class DiscussContainer extends Component {
/**
* @override
*/
@@ -18,7 +17,7 @@ export class DiscussContainer extends Component {
const initActiveId =
(action.context && action.context.active_id) ||
(action.params && action.params.default_active_id) ||
'mail.box_inbox';
"mail.box_inbox";
this.discuss = this.messaging.discuss;
this.discuss.update({
discussView: {
@@ -58,7 +57,6 @@ export class DiscussContainer extends Component {
this.discuss.close();
}
}
}
Object.assign(DiscussContainer, {
@@ -68,5 +66,5 @@ Object.assign(DiscussContainer, {
className: String,
globalState: { type: Object, optional: 1 },
},
template: 'mail.DiscussContainer',
template: "mail.DiscussContainer",
});
@@ -1,20 +1,23 @@
/** @odoo-module **/
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class DiscussPublicViewContainer extends Component {
/**
* @override
*/
setup() {
super.setup();
useMessagingContainer();
this.env.services.messaging.get().then(messaging => {
messaging.models['Thread'].insert(messaging.models['Thread'].convertData(this.props.data.channelData));
this.discussPublicView = messaging.models['DiscussPublicView'].insert(this.props.data.discussPublicViewData);
this.env.services.messaging.get().then((messaging) => {
messaging.models["Thread"].insert(
messaging.models["Thread"].convertData(this.props.data.channelData)
);
this.discussPublicView = messaging.models["DiscussPublicView"].insert(
this.props.data.discussPublicViewData
);
if (this.discussPublicView.shouldDisplayWelcomeViewInitially) {
this.discussPublicView.switchToWelcomeView();
} else {
@@ -23,11 +26,10 @@ export class DiscussPublicViewContainer extends Component {
this.render();
});
}
}
Object.assign(DiscussPublicViewContainer, {
template: 'mail.DiscussPublicViewContainer',
template: "mail.DiscussPublicViewContainer",
props: {
data: Object,
},
@@ -1,24 +1,23 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
import { useRefToModel } from "@mail/component_hooks/use_ref_to_model";
import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model";
import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { Transition } from "@web/core/transition";
import { Component, onWillPatch } from '@odoo/owl';
import { Component, onWillPatch } from "@odoo/owl";
export class MessageListView extends Component {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component' });
useRefToModel({ fieldName: 'loadMoreRef', refName: 'loadMore' });
useUpdateToModel({ methodName: 'onComponentUpdate' });
useComponentToModel({ fieldName: "component" });
useRefToModel({ fieldName: "loadMoreRef", refName: "loadMore" });
useUpdateToModel({ methodName: "onComponentUpdate" });
/**
* Snapshot computed during willPatch, which is used by patched.
*/
@@ -46,13 +45,12 @@ export class MessageListView extends Component {
get messageListView() {
return this.props.record;
}
}
Object.assign(MessageListView, {
components: { Transition },
props: { record: Object },
template: 'mail.MessageListView',
template: "mail.MessageListView",
});
registerMessagingComponent(MessageListView);
@@ -1,11 +1,10 @@
/** @odoo-module **/
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class MessagingMenuContainer extends Component {
/**
* @override
*/
@@ -20,5 +19,5 @@ export class MessagingMenuContainer extends Component {
MessagingMenuContainer.props = {};
Object.assign(MessagingMenuContainer, {
template: 'mail.MessagingMenuContainer',
template: "mail.MessagingMenuContainer",
});
@@ -1,11 +1,11 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class NotificationAlert extends Component {}
Object.assign(NotificationAlert, {
template: 'mail.NotificationAlert',
template: "mail.NotificationAlert",
});
registerMessagingComponent(NotificationAlert);
@@ -3,19 +3,16 @@
import { registry } from "@web/core/registry";
import { getMessagingComponent } from "@mail/utils/messaging_component";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class NotificationAlertContainer extends Component {}
Object.assign(NotificationAlertContainer, {
components: { NotificationAlert: getMessagingComponent('NotificationAlert') },
template: 'mail.NotificationAlertContainer',
components: { NotificationAlert: getMessagingComponent("NotificationAlert") },
template: "mail.NotificationAlertContainer",
props: {
// Should call standard_widget_props.js
readonly: { type: Boolean, optional: true },
record: { type: Object },
},
});
registry.category("view_widgets").add(
"notification_alert",
NotificationAlertContainer
);
registry.category("view_widgets").add("notification_alert", NotificationAlertContainer);
@@ -1,11 +1,10 @@
/** @odoo-module **/
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class PopoverManagerContainer extends Component {
/**
* @override
*/
@@ -20,5 +19,5 @@ export class PopoverManagerContainer extends Component {
PopoverManagerContainer.props = {};
Object.assign(PopoverManagerContainer, {
template: 'mail.PopoverManagerContainer',
template: "mail.PopoverManagerContainer",
});
@@ -1,18 +1,17 @@
/** @odoo-module **/
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useRefToModel } from "@mail/component_hooks/use_ref_to_model";
import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class ThreadNeedactionPreviewView extends Component {
/**
* @override
*/
setup() {
super.setup();
useRefToModel({ fieldName: 'markAsReadRef', refName: 'markAsRead' });
useRefToModel({ fieldName: "markAsReadRef", refName: "markAsRead" });
}
//--------------------------------------------------------------------------
@@ -29,7 +28,7 @@ export class ThreadNeedactionPreviewView extends Component {
return this.threadNeedactionPreviewView.thread.moduleIcon;
}
if (!this.threadNeedactionPreviewView.thread.channel) {
return '/mail/static/src/img/smiley/avatar.jpg';
return "/mail/static/src/img/smiley/avatar.jpg";
}
if (this.threadNeedactionPreviewView.thread.channel.correspondent) {
return this.threadNeedactionPreviewView.thread.channel.correspondent.avatarUrl;
@@ -43,12 +42,11 @@ export class ThreadNeedactionPreviewView extends Component {
get threadNeedactionPreviewView() {
return this.props.record;
}
}
Object.assign(ThreadNeedactionPreviewView, {
props: { record: Object },
template: 'mail.ThreadNeedactionPreviewView',
template: "mail.ThreadNeedactionPreviewView",
});
registerMessagingComponent(ThreadNeedactionPreviewView);
@@ -1,23 +1,21 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { registerMessagingComponent } from "@mail/utils/messaging_component";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
export class TrackingValue extends Component {
/**
* @returns {TrackingValue}
*/
get trackingValue() {
return this.props.value;
}
}
Object.assign(TrackingValue, {
props: { value: Object },
template: 'mail.TrackingValue',
template: "mail.TrackingValue",
});
registerMessagingComponent(TrackingValue);
@@ -1,8 +1,8 @@
/** @odoo-module **/
import { useMessagingContainer } from '@mail/component_hooks/use_messaging_container';
import { useMessagingContainer } from "@mail/component_hooks/use_messaging_container";
import { Component, onWillDestroy, onWillUpdateProps } from '@odoo/owl';
import { Component, onWillDestroy, onWillUpdateProps } from "@odoo/owl";
const getNextId = (function () {
let tmpId = 0;
@@ -22,7 +22,6 @@ const getNextId = (function () {
* mounting of attachment view until it becomes initialized.
*/
export class WebClientViewAttachmentViewContainer extends Component {
/**
* @override
*/
@@ -32,7 +31,7 @@ export class WebClientViewAttachmentViewContainer extends Component {
this.webClientViewAttachmentView = undefined;
this.webClientViewAttachmentViewId = getNextId();
this._insertFromProps(this.props);
onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
onWillUpdateProps((nextProps) => this._insertFromProps(nextProps));
onWillDestroy(() => this._deleteRecord());
}
@@ -61,17 +60,16 @@ export class WebClientViewAttachmentViewContainer extends Component {
this._deleteRecord();
return;
}
const thread = messaging.models['Thread'].insert({
const thread = messaging.models["Thread"].insert({
id: props.threadId,
model: props.threadModel,
});
this.webClientViewAttachmentView = messaging.models['WebClientViewAttachmentView'].insert({
this.webClientViewAttachmentView = messaging.models["WebClientViewAttachmentView"].insert({
id: this.webClientViewAttachmentViewId,
thread,
});
this.render();
}
}
Object.assign(WebClientViewAttachmentViewContainer, {
@@ -82,5 +80,5 @@ Object.assign(WebClientViewAttachmentViewContainer, {
},
threadModel: String,
},
template: 'mail.WebClientViewAttachmentViewContainer',
template: "mail.WebClientViewAttachmentViewContainer",
});
+9 -6
View File
@@ -1,19 +1,19 @@
/** @odoo-module **/
import { attr, Model } from '@mail/model';
import { attr, Model } from "@mail/model";
import { browser } from "@web/core/browser/browser";
import { isMobileOS } from "@web/core/browser/feature_detection";
import { SIZES } from '@web/core/ui/ui_service';
import { SIZES } from "@web/core/ui/ui_service";
Model({
name: 'Device',
name: "Device",
lifecycleHooks: {
_created() {
this._refresh();
this._onResize = _.debounce(() => this._refresh(), 100);
},
_willDelete() {
browser.removeEventListener('resize', this._onResize);
browser.removeEventListener("resize", this._onResize);
},
},
recordMethods: {
@@ -21,7 +21,7 @@ Model({
* Called when messaging is started.
*/
start() {
browser.addEventListener('resize', this._onResize);
browser.addEventListener("resize", this._onResize);
},
/**
* @private
@@ -39,7 +39,10 @@ Model({
fields: {
globalWindowInnerHeight: attr(),
globalWindowInnerWidth: attr(),
hasCanvasFilterSupport: attr({ default: typeof document.createElement('canvas').getContext('2d').filter !== 'undefined' }),
hasCanvasFilterSupport: attr({
default:
typeof document.createElement("canvas").getContext("2d").filter !== "undefined",
}),
hasRtcSupport: attr({
compute() {
return Boolean(window.RTCPeerConnection && window.MediaStream);
+3 -3
View File
@@ -1,11 +1,11 @@
/** @odoo-module **/
import { attr, Model } from '@mail/model';
import { attr, Model } from "@mail/model";
import { localization } from '@web/core/l10n/localization';
import { localization } from "@web/core/l10n/localization";
Model({
name: 'Locale',
name: "Locale",
fields: {
/**
* Language used by interface, formatted like {language ISO 2}_{country ISO 2} (eg: fr_FR).
+34 -20
View File
@@ -1,14 +1,14 @@
/** @odoo-module **/
import { attr, many, one, Model } from '@mail/model';
import { makeDeferred } from '@mail/utils/deferred';
import { attr, many, one, Model } from "@mail/model";
import { makeDeferred } from "@mail/utils/deferred";
import { browser } from '@web/core/browser/browser';
import { browser } from "@web/core/browser/browser";
import { EventBus } from '@odoo/owl';
import { EventBus } from "@odoo/owl";
Model({
name: 'Messaging',
name: "Messaging",
lifecycleHooks: {
_created() {
odoo.__DEBUG__.messaging = this;
@@ -33,23 +33,35 @@ Model({
const { args = [], method, model, kwargs = {} } = params;
const { domain, fields, groupBy, ...remainingKwargs } = kwargs;
const ormService = 'shadow' in options ? this.env.services.orm.silent : this.env.services.orm;
const ormService =
"shadow" in options ? this.env.services.orm.silent : this.env.services.orm;
switch (method) {
case 'create': {
case "create": {
const { vals_list, ...createKwargs } = kwargs;
return ormService.create(model, args[0] || vals_list, createKwargs);
}
case 'read':
case "read":
return ormService.read(model, args[0], args[1] || fields, remainingKwargs);
case 'read_group':
return ormService.readGroup(model, args[0] || domain, args[1] || fields, args[2] || groupBy, remainingKwargs);
case 'search':
case "read_group":
return ormService.readGroup(
model,
args[0] || domain,
args[1] || fields,
args[2] || groupBy,
remainingKwargs
);
case "search":
return ormService.search(model, args[0] || domain, remainingKwargs);
case 'search_read':
return ormService.searchRead(model, args[0] || domain, args[1] || fields, remainingKwargs);
case 'unlink':
case "search_read":
return ormService.searchRead(
model,
args[0] || domain,
args[1] || fields,
remainingKwargs
);
case "unlink":
return ormService.unlink(model, args[0], kwargs);
case 'write': {
case "write": {
const { vals, ...writeKwargs } = kwargs;
return ormService.write(model, args[0], args[1] || vals, writeKwargs);
}
@@ -71,27 +83,29 @@ Model({
* Inverse of the messaging field present on all models. This field
* therefore contains all existing records.
*/
allRecords: many('Record', { inverse: 'messaging', isCausal: true }),
allRecords: many("Record", { inverse: "messaging", isCausal: true }),
browser: attr({
compute() {
return browser;
},
}),
device: one('Device', { default: {}, isCausal: true, readonly: true }),
device: one("Device", { default: {}, isCausal: true, readonly: true }),
/**
* Promise that will be resolved when messaging is initialized.
*/
initializedPromise: attr({ required: true,
initializedPromise: attr({
required: true,
compute() {
return makeDeferred();
},
}),
isInitialized: attr({ default: false }),
locale: one('Locale', { default: {}, isCausal: true, readonly: true }),
locale: one("Locale", { default: {}, isCausal: true, readonly: true }),
/**
* Determines the bus that is used to communicate messaging events.
*/
messagingBus: attr({ required: true,
messagingBus: attr({
required: true,
compute() {
if (this.messagingBus) {
return; // avoid overwrite if already provided (example in tests)
+8 -3
View File
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { one, Model } from '@mail/model';
import { one, Model } from "@mail/model";
/**
* The object below defines a model. Instances of such model (or inherited
@@ -13,7 +13,7 @@ Model({
* Name of the model. Important to refer to appropriate model like in
* relational fields. Name of models must be unique.
*/
name: 'Record',
name: "Record",
lifecycleHooks: {
/**
* This function is called after the record has been created, more
@@ -178,6 +178,11 @@ Model({
* States the messaging singleton. Automatically assigned by the model
* manager at creation.
*/
messaging: one('Messaging', { default: {}, inverse: 'allRecords', readonly: true, required: true }),
messaging: one("Messaging", {
default: {},
inverse: "allRecords",
readonly: true,
required: true,
}),
},
});
+9 -6
View File
@@ -1,13 +1,15 @@
/** @odoo-module **/
import { attr, clear, Model } from '@mail/model';
import { attr, clear, Model } from "@mail/model";
Model({
name: 'Timer',
identifyingMode: 'xor',
name: "Timer",
identifyingMode: "xor",
lifecycleHooks: {
_created() {
this.update({ timeoutId: this.messaging.browser.setTimeout(this._onTimeout, this.duration) });
this.update({
timeoutId: this.messaging.browser.setTimeout(this._onTimeout, this.duration),
});
},
_willDelete() {
this.messaging.browser.clearTimeout(this.timeoutId);
@@ -39,7 +41,8 @@ Model({
* Duration, in milliseconds, until timer times out and calls the
* timeout function.
*/
duration: attr({ required: true,
duration: attr({
required: true,
compute() {
return clear();
},
@@ -53,7 +56,7 @@ Model({
onChanges: [
{
dependencies: ["doReset"],
methodName: '_onChangeDoReset',
methodName: "_onChangeDoReset",
},
],
});
+210 -177
View File
@@ -1,13 +1,13 @@
/** @odoo-module **/
import AbstractField from 'web.AbstractField';
import config from 'web.config';
import core from 'web.core';
import field_registry from 'web.field_registry';
import session from 'web.session';
import { sprintf } from '@web/core/utils/strings';
import framework from 'web.framework';
import time from 'web.time';
import AbstractField from "web.AbstractField";
import config from "web.config";
import core from "web.core";
import field_registry from "web.field_registry";
import session from "web.session";
import { sprintf } from "@web/core/utils/strings";
import framework from "web.framework";
import time from "web.time";
const QWeb = core.qweb;
const _t = core._t;
@@ -17,21 +17,21 @@ const _lt = core._lt;
// Activities Widget for Kanban views ('kanban_activity' widget)
// -----------------------------------------------------------------------------
const KanbanActivity = AbstractField.extend({
template: 'mail.KanbanActivity',
template: "mail.KanbanActivity",
events: {
'change input.o_input_file': '_onFileChanged',
'click .o_activity_template_preview': '_onPreviewMailTemplate',
'click .o_activity_template_send': '_onSendMailTemplate',
'click .o_edit_activity': '_onEditActivity',
'click .o_mark_as_done': '_onMarkActivityDone',
'click .o_mark_as_done_upload_file': '_onMarkActivityDoneUploadFile',
'click .o_schedule_activity': '_onScheduleActivity',
'show.bs.dropdown': '_onDropdownShow',
"change input.o_input_file": "_onFileChanged",
"click .o_activity_template_preview": "_onPreviewMailTemplate",
"click .o_activity_template_send": "_onSendMailTemplate",
"click .o_edit_activity": "_onEditActivity",
"click .o_mark_as_done": "_onMarkActivityDone",
"click .o_mark_as_done_upload_file": "_onMarkActivityDoneUploadFile",
"click .o_schedule_activity": "_onScheduleActivity",
"show.bs.dropdown": "_onDropdownShow",
},
fieldDependencies: {
activity_exception_decoration: { type: 'selection' },
activity_exception_icon: { type: 'char' },
activity_state: { type: 'selection' },
activity_exception_decoration: { type: "selection" },
activity_exception_icon: { type: "char" },
activity_state: { type: "selection" },
},
/**
@@ -72,8 +72,8 @@ const KanbanActivity = AbstractField.extend({
const feedback = params.feedback || false;
const attachmentIds = params.attachmentIds || [];
await this._rpc({
model: 'mail.activity',
method: 'action_feedback',
model: "mail.activity",
method: "action_feedback",
args: [[activityID]],
kwargs: {
feedback: feedback,
@@ -97,8 +97,8 @@ const KanbanActivity = AbstractField.extend({
const activityID = params.activityID;
const feedback = params.feedback;
const action = await this._rpc({
model: 'mail.activity',
method: 'action_feedback_schedule_next',
model: "mail.activity",
method: "action_feedback_schedule_next",
args: [[activityID]],
kwargs: { feedback: feedback },
context: this.record.getContext(),
@@ -106,11 +106,11 @@ const KanbanActivity = AbstractField.extend({
if (action) {
this.do_action(action, {
on_close: () => {
this.trigger_up('reload', { keepChanges: true });
this.trigger_up("reload", { keepChanges: true });
},
});
} else {
this.trigger_up('reload', { keepChanges: true });
this.trigger_up("reload", { keepChanges: true });
}
},
/**
@@ -120,9 +120,12 @@ const KanbanActivity = AbstractField.extend({
*/
async _onEditActivity(ev) {
ev.preventDefault();
const activityId = $(ev.currentTarget).data('activity-id');
const thread = this.messaging.models['Thread'].insert({ id: this.res_id, model: this.model });
const activity = this.messaging.models['Activity'].insert({ id: activityId, thread });
const activityId = $(ev.currentTarget).data("activity-id");
const thread = this.messaging.models["Thread"].insert({
id: this.res_id,
model: this.model,
});
const activity = this.messaging.models["Activity"].insert({ id: activityId, thread });
await this.messaging.openActivityForm({
activity,
defaultActivityTypeId: this.defaultActivityType,
@@ -137,7 +140,7 @@ const KanbanActivity = AbstractField.extend({
_onFileChanged(ev) {
ev.stopPropagation();
ev.preventDefault();
const $form = $(ev.currentTarget).closest('form');
const $form = $(ev.currentTarget).closest("form");
$form.submit();
framework.blockUI();
},
@@ -157,143 +160,157 @@ const KanbanActivity = AbstractField.extend({
ev.preventDefault();
const self = this;
const $markDoneBtn = $(ev.currentTarget);
const activityID = $markDoneBtn.data('activity-id');
const previousActivityTypeID = $markDoneBtn.data('previous-activity-type-id') || false;
const chainingTypeActivity = $markDoneBtn.data('chaining-type-activity');
const activityID = $markDoneBtn.data("activity-id");
const previousActivityTypeID = $markDoneBtn.data("previous-activity-type-id") || false;
const chainingTypeActivity = $markDoneBtn.data("chaining-type-activity");
if ($markDoneBtn[0].dataset.bsToggle === 'collapse') {
const $actLi = $markDoneBtn.parents('.o_log_activity');
const $panel = this.$('#o_activity_form_' + activityID);
if ($markDoneBtn[0].dataset.bsToggle === "collapse") {
const $actLi = $markDoneBtn.parents(".o_log_activity");
const $panel = this.$("#o_activity_form_" + activityID);
if (!$panel.data('bs.collapse')) {
const $form = $(QWeb.render('mail.activity_feedback_form', {
previous_activity_type_id: previousActivityTypeID,
chaining_type: chainingTypeActivity
}));
if (!$panel.data("bs.collapse")) {
const $form = $(
QWeb.render("mail.activity_feedback_form", {
previous_activity_type_id: previousActivityTypeID,
chaining_type: chainingTypeActivity,
})
);
$panel.append($form);
this._onMarkActivityDoneActions($markDoneBtn, $form, activityID);
// Close and reset any other open panels
_.each($panel.siblings('.o_activity_form'), function (el) {
if ($(el).data('bs.collapse')) {
$(el).empty().collapse('dispose').removeClass('show');
_.each($panel.siblings(".o_activity_form"), function (el) {
if ($(el).data("bs.collapse")) {
$(el).empty().collapse("dispose").removeClass("show");
}
$(el).data('bs.collapse', false);
$(el).data("bs.collapse", false);
});
// Scroll to selected activity
$markDoneBtn.parents('.o_activity_log_container').scrollTo($actLi.position().top, 100);
$panel.data('bs.collapse', true);
$markDoneBtn
.parents(".o_activity_log_container")
.scrollTo($actLi.position().top, 100);
$panel.data("bs.collapse", true);
}
// Empty and reset panel on close
$panel.on('hidden.bs.collapse', function () {
if ($panel.data('bs.collapse')) {
$actLi.removeClass('o_activity_selected');
$panel.collapse('dispose');
$panel.on("hidden.bs.collapse", function () {
if ($panel.data("bs.collapse")) {
$actLi.removeClass("o_activity_selected");
$panel.collapse("dispose");
$panel.empty();
}
$panel.data('bs.collapse', false);
$panel.data("bs.collapse", false);
});
this.$('.o_activity_selected').removeClass('o_activity_selected');
$actLi.toggleClass('o_activity_selected');
$panel.collapse('toggle');
$panel.find('#activity_feedback').focus();
} else if (!$markDoneBtn.data('bs.popover')) {
$markDoneBtn.popover({
template: $(Popover.Default.template).addClass('o_mail_activity_feedback')[0].outerHTML, // Ugly but cannot find another way
container: $markDoneBtn,
title: _t("Feedback"),
html: true,
trigger: 'manual',
placement: 'right', // FIXME: this should work, maybe a bug in the popper lib
content: () => {
const $popover = $(QWeb.render('mail.activity_feedback_form', {
previous_activity_type_id: previousActivityTypeID,
chaining_type: chainingTypeActivity
}));
this._onMarkActivityDoneActions($markDoneBtn, $popover, activityID);
return $popover;
},
}).on('shown.bs.popover', function () {
const $popover = $($(this).data("bs.popover").tip);
$(".o_mail_activity_feedback.popover").not($popover).popover("hide");
$popover.addClass('o_mail_activity_feedback').attr('tabindex', 0);
$popover.find('#activity_feedback').focus();
const activityID = $(this).data('activity-id');
$popover.off('focusout');
$popover.focusout(e => {
// outside click of popover hide the popover
// e.relatedTarget is the element receiving the focus
if (!$popover.is(e.relatedTarget) && !$popover.find(e.relatedTarget).length) {
self._draftFeedback[activityID] = $popover.find('#activity_feedback').val();
$popover.popover('hide');
}
});
}).popover('show');
this.$(".o_activity_selected").removeClass("o_activity_selected");
$actLi.toggleClass("o_activity_selected");
$panel.collapse("toggle");
$panel.find("#activity_feedback").focus();
} else if (!$markDoneBtn.data("bs.popover")) {
$markDoneBtn
.popover({
template: $(Popover.Default.template).addClass("o_mail_activity_feedback")[0]
.outerHTML, // Ugly but cannot find another way
container: $markDoneBtn,
title: _t("Feedback"),
html: true,
trigger: "manual",
placement: "right", // FIXME: this should work, maybe a bug in the popper lib
content: () => {
const $popover = $(
QWeb.render("mail.activity_feedback_form", {
previous_activity_type_id: previousActivityTypeID,
chaining_type: chainingTypeActivity,
})
);
this._onMarkActivityDoneActions($markDoneBtn, $popover, activityID);
return $popover;
},
})
.on("shown.bs.popover", function () {
const $popover = $($(this).data("bs.popover").tip);
$(".o_mail_activity_feedback.popover").not($popover).popover("hide");
$popover.addClass("o_mail_activity_feedback").attr("tabindex", 0);
$popover.find("#activity_feedback").focus();
const activityID = $(this).data("activity-id");
$popover.off("focusout");
$popover.focusout((e) => {
// outside click of popover hide the popover
// e.relatedTarget is the element receiving the focus
if (
!$popover.is(e.relatedTarget) &&
!$popover.find(e.relatedTarget).length
) {
self._draftFeedback[activityID] = $popover
.find("#activity_feedback")
.val();
$popover.popover("hide");
}
});
})
.popover("show");
} else {
const popover = $markDoneBtn.data('bs.popover');
if ($('#' + popover.tip.id).length === 0) {
popover.show();
const popover = $markDoneBtn.data("bs.popover");
if ($("#" + popover.tip.id).length === 0) {
popover.show();
}
}
},
/**
* Bind all necessary actions to the 'mark as done' form
*
* @private
* @param {Object} $form
* @param {integer} activityID
*/
* Bind all necessary actions to the 'mark as done' form
*
* @private
* @param {Object} $form
* @param {integer} activityID
*/
_onMarkActivityDoneActions($btn, $form, activityID) {
$form.find('#activity_feedback').val(this._draftFeedback[activityID]);
$form.on('keydown', '#activity_feedback', ev => {
if (ev.key === 'Enter') {
$form.find("#activity_feedback").val(this._draftFeedback[activityID]);
$form.on("keydown", "#activity_feedback", (ev) => {
if (ev.key === "Enter") {
ev.stopPropagation(); // Prevent list view actions
}
});
$form.on('click', '.o_activity_popover_done', ev => {
$form.on("click", ".o_activity_popover_done", (ev) => {
ev.stopPropagation();
this._markActivityDone({
activityID: activityID,
feedback: $form.find('#activity_feedback').val(),
feedback: $form.find("#activity_feedback").val(),
});
});
$form.on('keydown', '.o_activity_popover_done', ev => {
if (ev.key === 'Enter') {
$form.on("keydown", ".o_activity_popover_done", (ev) => {
if (ev.key === "Enter") {
ev.stopPropagation(); // Prevent list view actions
ev.preventDefault();
this._markActivityDone({
activityID,
feedback: $form.find('#activity_feedback').val(),
feedback: $form.find("#activity_feedback").val(),
});
}
});
$form.on('click', '.o_activity_popover_done_next', ev => {
$form.on("click", ".o_activity_popover_done_next", (ev) => {
ev.stopPropagation();
this._markActivityDoneAndScheduleNext({
activityID: activityID,
feedback: $form.find('#activity_feedback').val(),
feedback: $form.find("#activity_feedback").val(),
});
});
$form.on('keydown', '.o_activity_popover_done_next', ev => {
if (ev.key === 'Enter') {
$form.on("keydown", ".o_activity_popover_done_next", (ev) => {
if (ev.key === "Enter") {
ev.stopPropagation(); // Prevent list view actions
ev.preventDefault();
this._markActivityDoneAndScheduleNext({
activityID,
feedback: $form.find('#activity_feedback').val(),
feedback: $form.find("#activity_feedback").val(),
});
}
});
$form.on('click', '.o_activity_popover_discard', ev => {
$form.on("click", ".o_activity_popover_discard", (ev) => {
ev.stopPropagation();
if ($btn.data('bs.popover')) {
$btn.popover('hide');
} else if ($btn[0].dataset.bsToggle === 'collapse') {
this.$('#o_activity_form_' + activityID).collapse('hide');
if ($btn.data("bs.popover")) {
$btn.popover("hide");
} else if ($btn[0].dataset.bsToggle === "collapse") {
this.$("#o_activity_form_" + activityID).collapse("hide");
}
});
},
@@ -304,7 +321,7 @@ const KanbanActivity = AbstractField.extend({
_onMarkActivityDoneUploadFile(ev) {
ev.preventDefault();
ev.stopPropagation();
const fileuploadID = $(ev.currentTarget).data('fileupload-id');
const fileuploadID = $(ev.currentTarget).data("fileupload-id");
const $input = this.$("[target='" + fileuploadID + "'] > input.o_input_file");
$input.click();
},
@@ -316,13 +333,13 @@ const KanbanActivity = AbstractField.extend({
_onPreviewMailTemplate(ev) {
ev.stopPropagation();
ev.preventDefault();
const templateID = $(ev.currentTarget).data('template-id');
const templateID = $(ev.currentTarget).data("template-id");
const action = {
name: _t('Compose Email'),
type: 'ir.actions.act_window',
res_model: 'mail.compose.message',
views: [[false, 'form']],
target: 'new',
name: _t("Compose Email"),
type: "ir.actions.act_window",
res_model: "mail.compose.message",
views: [[false, "form"]],
target: "new",
context: {
default_res_id: this.res_id,
default_model: this.model,
@@ -333,7 +350,7 @@ const KanbanActivity = AbstractField.extend({
};
return this.do_action(action, {
on_close: () => {
this.trigger_up('reload', { keepChanges: true });
this.trigger_up("reload", { keepChanges: true });
},
});
},
@@ -344,7 +361,10 @@ const KanbanActivity = AbstractField.extend({
*/
async _onScheduleActivity(ev) {
ev.preventDefault();
const thread = this.messaging.models['Thread'].insert({ id: this.res_id, model: this.model });
const thread = this.messaging.models["Thread"].insert({
id: this.res_id,
model: this.model,
});
await this.messaging.openActivityForm({
defaultActivityTypeId: this.defaultActivityType,
thread,
@@ -359,10 +379,10 @@ const KanbanActivity = AbstractField.extend({
async _onSendMailTemplate(ev) {
ev.stopPropagation();
ev.preventDefault();
const templateID = $(ev.currentTarget).data('template-id');
const templateID = $(ev.currentTarget).data("template-id");
await this._rpc({
model: this.model,
method: 'activity_send_mail',
method: "activity_send_mail",
args: [[this.res_id], templateID],
});
this._reload();
@@ -371,7 +391,7 @@ const KanbanActivity = AbstractField.extend({
* @private
*/
_reload() {
this.trigger_up('reload', { db_id: this.record.id, keepChanges: true });
this.trigger_up("reload", { db_id: this.record.id, keepChanges: true });
},
/**
* @override
@@ -380,16 +400,16 @@ const KanbanActivity = AbstractField.extend({
_render() {
// span classes need to be updated manually because the template cannot
// be re-rendered eaasily (because of the dropdown state)
const spanClasses = ['fa', 'fa-lg', 'fa-fw'];
spanClasses.push('o_activity_color_' + (this.record.data.activity_state || 'default'));
const spanClasses = ["fa", "fa-lg", "fa-fw"];
spanClasses.push("o_activity_color_" + (this.record.data.activity_state || "default"));
if (this.recordData.activity_exception_decoration) {
spanClasses.push('text-' + this.recordData.activity_exception_decoration);
spanClasses.push("text-" + this.recordData.activity_exception_decoration);
spanClasses.push(this.recordData.activity_exception_icon);
} else {
spanClasses.push('fa-clock-o');
spanClasses.push("fa-clock-o");
}
this.$('.o_activity_btn > span').removeClass().addClass(spanClasses.join(' '));
if (this.$el.hasClass('show')) {
this.$(".o_activity_btn > span").removeClass().addClass(spanClasses.join(" "));
if (this.$el.hasClass("show")) {
// note: this part of the rendering might be asynchronous
this._renderDropdown();
}
@@ -398,38 +418,42 @@ const KanbanActivity = AbstractField.extend({
* @private
*/
async _renderDropdown() {
this.$('.o_activity')
.toggleClass('dropdown-menu-end', config.device.isMobile)
.html(QWeb.render('mail.KanbanActivityLoading'));
this.$(".o_activity")
.toggleClass("dropdown-menu-end", config.device.isMobile)
.html(QWeb.render("mail.KanbanActivityLoading"));
let context = this.getSession().user_context;
if (this.record && !_.isEmpty(this.record.getContext())) {
context = this.record.getContext();
}
const activities = this.value.res_ids.length ? await this._rpc({
model: 'mail.activity',
method: 'activity_format',
args: [this.value.res_ids],
context: context,
}) : [];
const today = moment().startOf('day');
const activities = this.value.res_ids.length
? await this._rpc({
model: "mail.activity",
method: "activity_format",
args: [this.value.res_ids],
context: context,
})
: [];
const today = moment().startOf("day");
for (const activity of activities) {
activity.create_date = moment(time.auto_str_to_date(activity.create_date));
activity.date_deadline = moment(time.auto_str_to_date(activity.date_deadline));
if (activity.activity_category === 'upload_file') {
activity.fileuploadID = _.uniqueId('o_fileupload');
if (activity.activity_category === "upload_file") {
activity.fileuploadID = _.uniqueId("o_fileupload");
}
let toDisplay = '';
const diff = activity.date_deadline.diff(today, 'days', true); // true means no rounding
let toDisplay = "";
const diff = activity.date_deadline.diff(today, "days", true); // true means no rounding
if (diff === 0) {
toDisplay = _t("Today");
} else {
if (diff < 0) { // overdue
if (diff < 0) {
// overdue
if (diff === -1) {
toDisplay = _t("Yesterday");
} else {
toDisplay = sprintf(_t("%s days overdue"), Math.round(Math.abs(diff)));
}
} else { // due
} else {
// due
if (diff === 1) {
toDisplay = _t("Tomorrow");
} else {
@@ -439,12 +463,14 @@ const KanbanActivity = AbstractField.extend({
}
activity.label_delay = toDisplay;
}
const sortedActivities = _.sortBy(activities, 'date_deadline');
this.$('.o_activity').html(QWeb.render('mail.KanbanActivityDropdown', {
records: _.groupBy(sortedActivities, 'state'),
session: session,
widget: this,
}));
const sortedActivities = _.sortBy(activities, "date_deadline");
this.$(".o_activity").html(
QWeb.render("mail.KanbanActivityDropdown", {
records: _.groupBy(sortedActivities, "state"),
session: session,
widget: this,
})
);
for (const activity of sortedActivities) {
if (activity.fileuploadID) {
$(window).on(activity.fileuploadID, async () => {
@@ -453,9 +479,9 @@ const KanbanActivity = AbstractField.extend({
const files = Array.prototype.slice.call(arguments, 1);
await this._markActivityDone({
activityID: activity.id,
attachmentIds: _.pluck(files, 'id')
attachmentIds: _.pluck(files, "id"),
});
this.trigger_up('reload', { keepChanges: true });
this.trigger_up("reload", { keepChanges: true });
});
}
}
@@ -477,16 +503,16 @@ const KanbanActivity = AbstractField.extend({
// Activities Widget for List views ('list_activity' widget)
// -----------------------------------------------------------------------------
const ListActivity = KanbanActivity.extend({
template: 'mail.ListActivity',
template: "mail.ListActivity",
events: Object.assign({}, KanbanActivity.prototype.events, {
'click .dropdown-menu.o_activity': '_onDropdownClicked',
"click .dropdown-menu.o_activity": "_onDropdownClicked",
}),
fieldDependencies: _.extend({}, KanbanActivity.prototype.fieldDependencies, {
activity_summary: { type: 'char' },
activity_type_id: { type: 'many2one', relation: 'mail.activity.type' },
activity_type_icon: { type: 'char' },
activity_summary: { type: "char" },
activity_type_id: { type: "many2one", relation: "mail.activity.type" },
activity_type_icon: { type: "char" },
}),
label: _lt('Next Activity'),
label: _lt("Next Activity"),
//--------------------------------------------------------------------------
// Private
@@ -501,19 +527,22 @@ const ListActivity = KanbanActivity.extend({
// set the 'special_click' prop on the activity icon to prevent from
// opening the record when the user clicks on it (as it opens the
// activity dropdown instead)
this.$('.o_activity_btn > span').prop('special_click', true);
this.$(".o_activity_btn > span").prop("special_click", true);
if (this.value.count) {
let text;
if (this.recordData.activity_exception_decoration) {
text = _t('Warning');
text = _t("Warning");
} else {
text = this.recordData.activity_summary ||
this.recordData.activity_type_id.data.display_name;
text =
this.recordData.activity_summary ||
this.recordData.activity_type_id.data.display_name;
}
this.$('.o_activity_summary').text(text);
this.$(".o_activity_summary").text(text);
}
if (this.recordData.activity_type_icon) {
this.el.querySelector('.o_activity_btn > span').classList.replace('fa-clock-o', this.recordData.activity_type_icon);
this.el
.querySelector(".o_activity_btn > span")
.classList.replace("fa-clock-o", this.recordData.activity_type_icon);
}
},
@@ -540,7 +569,7 @@ const ListActivity = KanbanActivity.extend({
const ActivityException = AbstractField.extend({
noLabel: true,
fieldDependencies: _.extend({}, AbstractField.prototype.fieldDependencies, {
activity_exception_icon: { type: 'char' },
activity_exception_icon: { type: "char" },
}),
//------------------------------------------------------------
@@ -567,14 +596,18 @@ const ActivityException = AbstractField.extend({
this.$el.empty();
if (this.value) {
this.$el.attr({
title: _t('This record has an exception activity.'),
class: "float-end mt-1 text-" + this.value + " fa " + this.recordData.activity_exception_icon
title: _t("This record has an exception activity."),
class:
"float-end mt-1 text-" +
this.value +
" fa " +
this.recordData.activity_exception_icon,
});
}
}
},
});
field_registry
.add('kanban_activity', KanbanActivity)
.add('list_activity', ListActivity)
.add('activity_exception', ActivityException);
.add("kanban_activity", KanbanActivity)
.add("list_activity", ListActivity)
.add("activity_exception", ActivityException);
+17 -14
View File
@@ -1,8 +1,8 @@
/** @odoo-module **/
import BasicView from 'web.BasicView';
import BasicView from "web.BasicView";
const mailWidgets = ['kanban_activity'];
const mailWidgets = ["kanban_activity"];
const chatterFields = ["message_ids", "message_follower_ids", "activity_ids"];
@@ -13,19 +13,22 @@ BasicView.include({
this._super.apply(this, arguments);
this.rendererParams.hasChatter = this.hasChatter;
this.rendererParams.hasAttachmentViewerFeature = this.hasAttachmentViewerFeature;
const post_refresh = this._getFieldOption('message_ids', 'post_refresh', false);
const followers_post_refresh = this._getFieldOption('message_follower_ids', 'post_refresh', false);
const post_refresh = this._getFieldOption("message_ids", "post_refresh", false);
const followers_post_refresh = this._getFieldOption(
"message_follower_ids",
"post_refresh",
false
);
this.chatterFields = {
hasActivityIds: this._hasField('activity_ids'),
hasMessageFollowerIds: this._hasField('message_follower_ids'),
hasMessageIds: this._hasField('message_ids'),
hasRecordReloadOnAttachmentsChanged: post_refresh === 'always',
hasActivityIds: this._hasField("activity_ids"),
hasMessageFollowerIds: this._hasField("message_follower_ids"),
hasMessageIds: this._hasField("message_ids"),
hasRecordReloadOnAttachmentsChanged: post_refresh === "always",
hasRecordReloadOnMessagePosted: !!post_refresh,
hasRecordReloadOnFollowersUpdate: !!followers_post_refresh,
isAttachmentBoxVisibleInitially: (
this._getFieldOption('message_ids', 'open_attachments', false) ||
this._getFieldOption('message_follower_ids', 'open_attachments', false)
),
isAttachmentBoxVisibleInitially:
this._getFieldOption("message_ids", "open_attachments", false) ||
this._getFieldOption("message_follower_ids", "open_attachments", false),
};
const fieldsInfo = this.fieldsInfo[this.viewType];
this.rendererParams.chatterFields = this.chatterFields;
@@ -82,7 +85,7 @@ BasicView.include({
const fieldsInfo = fv.fieldsInfo[viewType];
const fields = fv.viewFields;
for (const child of node.children) {
if (child.tag === 'field' && chatterFields.includes(child.attrs.name)) {
if (child.tag === "field" && chatterFields.includes(child.attrs.name)) {
const attrs = { ...child.attrs, modifiers: {}, __no_fetch: true };
const fieldName = attrs.name;
fieldsInfo[fieldName] = this._processField(viewType, fields[fieldName], attrs);
@@ -92,7 +95,7 @@ BasicView.include({
}
return false;
}
if (node.tag === 'div' && node.attrs.class === 'o_attachment_preview') {
if (node.tag === "div" && node.attrs.class === "o_attachment_preview") {
this.hasAttachmentViewerFeature = true;
}
return this._super(...arguments);
@@ -1,8 +1,8 @@
/** @odoo-module **/
import { TranslationDataBase } from 'web.translation';
import { TranslationDataBase } from "web.translation";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
TranslationDataBase.include({
/**
@@ -14,7 +14,7 @@ TranslationDataBase.include({
// During boot `env.services` might not even be set yet, in this
// case this can safely be ignored as messaging will then load
// locale information during its initialization.
Component.env.services.messaging.get().then(messaging => {
Component.env.services.messaging.get().then((messaging) => {
// Update messaging locale whenever the translation bundle changes.
// In particular if messaging is created before the end of the
// `load_translations` RPC, the default values have to be
@@ -1,19 +1,20 @@
/** @odoo-module **/
import CustomFilterItem from 'web.CustomFilterItem';
import { patch } from 'web.utils';
patch(CustomFilterItem.prototype, 'mail.CustomFilterItem', {
import CustomFilterItem from "web.CustomFilterItem";
import { patch } from "web.utils";
patch(CustomFilterItem.prototype, "mail.CustomFilterItem", {
/**
* With the `mail` module installed, we want to filter out some of the
* available fields in 'Add custom filter' menu (@see CustomFilterItem).
* @override
*/
_validateField(field) {
return this._super(...arguments) &&
field.relation !== 'mail.message' &&
field.name !== 'message_ids';
return (
this._super(...arguments) &&
field.relation !== "mail.message" &&
field.name !== "message_ids"
);
},
});
+109 -109
View File
@@ -20,116 +20,116 @@
* representation in the system.
*/
const data = {
":)": "😊",
":-)": "😊", // alternative (alt.)
"=)": "😊", // alt.
":]": "😊", // alt.
":D": "😃",
":-D": "😃", // alt.
"=D": "😃", // alt.
"xD": "😆",
"XD": "😆", // alt.
"x'D": "😂",
";)": "😉",
";-)": "😉", // alt.
"B)": "😎",
"8)": "😎", // alt.
"B-)": "😎", // alt.
"8-)": "😎", // alt.
";p": "😜",
";P": "😜", // alt.
":p": "😋",
":P": "😋", // alt.
":-p": "😋", // alt.
":-P": "😋", // alt.
"=P": "😋", // alt.
"xp": "😝",
"xP": "😝", // alt.
"o_o": "😳",
":|": "😐",
":-|": "😐", // alt.
":/": "😕", // alt.
":-/": "😕", // alt.
":(": "😞",
":@": "😱",
":O": "😲",
":-O": "😲", // alt.
":o": "😲", // alt.
":-o": "😲", // alt.
":'o": "😨",
"3:(": "😠",
">:(": "😠", // alt.
"3:)": "😈",
">:)": "😈", // alt.
":*": "😘",
":-*": "😘", // alt.
"o:)": "😇",
":'(": "😢",
":'-(": "😭",
":\"(": "😭", // alt.
"<3": "❤️",
"&lt;3": "❤️",
":heart": "❤️", // alt.
"</3": "💔",
"&lt;/3": "💔",
":heart_eyes": "😍",
":turban": "👳",
":+1": "👍",
":-1": "👎",
":ok": "👌",
":poop": "💩",
":no_see": "🙈",
":no_hear": "🙉",
":no_speak": "🙊",
":bug": "🐞",
":kitten": "😺",
":bear": "🐻",
":snail": "🐌",
":boar": "🐗",
":clover": "🍀",
":sunflower": "🌹",
":fire": "🔥",
":sun": "☀️",
":)": "😊",
":-)": "😊", // alternative (alt.)
"=)": "😊", // alt.
":]": "😊", // alt.
":D": "😃",
":-D": "😃", // alt.
"=D": "😃", // alt.
xD: "😆",
XD: "😆", // alt.
"x'D": "😂",
";)": "😉",
";-)": "😉", // alt.
"B)": "😎",
"8)": "😎", // alt.
"B-)": "😎", // alt.
"8-)": "😎", // alt.
";p": "😜",
";P": "😜", // alt.
":p": "😋",
":P": "😋", // alt.
":-p": "😋", // alt.
":-P": "😋", // alt.
"=P": "😋", // alt.
xp: "😝",
xP: "😝", // alt.
o_o: "😳",
":|": "😐",
":-|": "😐", // alt.
":/": "😕", // alt.
":-/": "😕", // alt.
":(": "😞",
":@": "😱",
":O": "😲",
":-O": "😲", // alt.
":o": "😲", // alt.
":-o": "😲", // alt.
":'o": "😨",
"3:(": "😠",
">:(": "😠", // alt.
"3:)": "😈",
">:)": "😈", // alt.
":*": "😘",
":-*": "😘", // alt.
"o:)": "😇",
":'(": "😢",
":'-(": "😭",
':"(': "😭", // alt.
"<3": "❤️",
"&lt;3": "❤️",
":heart": "❤️", // alt.
"</3": "💔",
"&lt;/3": "💔",
":heart_eyes": "😍",
":turban": "👳",
":+1": "👍",
":-1": "👎",
":ok": "👌",
":poop": "💩",
":no_see": "🙈",
":no_hear": "🙉",
":no_speak": "🙊",
":bug": "🐞",
":kitten": "😺",
":bear": "🐻",
":snail": "🐌",
":boar": "🐗",
":clover": "🍀",
":sunflower": "🌹",
":fire": "🔥",
":sun": "☀️",
":partly_sunny:": "⛅️",
":rainbow": "🌈",
":cloud": "☁️",
":zap": "⚡️",
":star": "⭐️",
":cookie": "🍪",
":pizza": "🍕",
":hamburger": "🍔",
":fries": "🍟",
":cake": "🎂",
":cake_part": "🍰",
":coffee": "☕️",
":banana": "🍌",
":sushi": "🍣",
":rice_ball": "🍙",
":beer": "🍺",
":wine": "🍷",
":cocktail": "🍸",
":tropical": "🍹",
":beers": "🍻",
":ghost": "👻",
":skull": "💀",
":et": "👽",
":alien": "👽", // alt.
":party": "🎉",
":trophy": "🏆",
":key": "🔑",
":pin": "📌",
":postal_horn": "📯",
":music": "🎵",
":trumpet": "🎺",
":guitar": "🎸",
":run": "🏃",
":bike": "🚲",
":soccer": "⚽️",
":football": "🏈",
":8ball": "🎱",
":clapper": "🎬",
":microphone": "🎤",
":cheese": "🧀",
":rainbow": "🌈",
":cloud": "☁️",
":zap": "⚡️",
":star": "⭐️",
":cookie": "🍪",
":pizza": "🍕",
":hamburger": "🍔",
":fries": "🍟",
":cake": "🎂",
":cake_part": "🍰",
":coffee": "☕️",
":banana": "🍌",
":sushi": "🍣",
":rice_ball": "🍙",
":beer": "🍺",
":wine": "🍷",
":cocktail": "🍸",
":tropical": "🍹",
":beers": "🍻",
":ghost": "👻",
":skull": "💀",
":et": "👽",
":alien": "👽", // alt.
":party": "🎉",
":trophy": "🏆",
":key": "🔑",
":pin": "📌",
":postal_horn": "📯",
":music": "🎵",
":trumpet": "🎺",
":guitar": "🎸",
":run": "🏃",
":bike": "🚲",
":soccer": "⚽️",
":football": "🏈",
":8ball": "🎱",
":clapper": "🎬",
":microphone": "🎤",
":cheese": "🧀",
};
// list of emojis in a dictionary, indexed by emoji unicode
+7 -7
View File
@@ -1,19 +1,19 @@
/** @odoo-module **/
import emojis from '@mail/js/emojis';
import emojis from "@mail/js/emojis";
import { Component, useRef, onMounted } from '@odoo/owl';
import { Component, useRef, onMounted } from "@odoo/owl";
export class EmojisDropdown extends Component {
setup() {
this.toggleRef = useRef('toggleRef');
this.toggleRef = useRef("toggleRef");
this.emojis = emojis;
super.setup();
onMounted(() => {
new Dropdown(this.toggleRef.el, {
popperConfig: { placement: 'bottom-end', strategy: 'fixed' },
new window.Dropdown(this.toggleRef.el, {
popperConfig: { placement: "bottom-end", strategy: "fixed" },
});
});
}
};
EmojisDropdown.template = 'mail.EmojisDropdown';
}
EmojisDropdown.template = "mail.EmojisDropdown";
+14 -8
View File
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { escape } from '@web/core/utils/strings';
import emojis from '@mail/js/emojis';
import { escape } from "@web/core/utils/strings";
import emojis from "@mail/js/emojis";
/**
* This mixin gathers a few methods that are used to handle emojis.
@@ -33,9 +33,15 @@ export default {
const textInput = this._getTargetTextElement($(ev.currentTarget));
const selectionStart = textInput.selectionStart;
textInput.value = textInput.value.slice(0, selectionStart) + unicode + textInput.value.slice(selectionStart);
textInput.value =
textInput.value.slice(0, selectionStart) +
unicode +
textInput.value.slice(selectionStart);
textInput.focus();
textInput.setSelectionRange(selectionStart + unicode.length, selectionStart + unicode.length);
textInput.setSelectionRange(
selectionStart + unicode.length,
selectionStart + unicode.length
);
},
//--------------------------------------------------------------------------
@@ -52,7 +58,7 @@ export default {
_formatText(message) {
message = escape(message);
message = this._wrapEmojis(message);
message = message.replace(/(?:\r\n|\r|\n)/g, '<br>');
message = message.replace(/(?:\r\n|\r|\n)/g, "<br>");
return message;
},
@@ -66,11 +72,11 @@ export default {
_wrapEmojis(message) {
emojis.forEach(function (emoji) {
message = message.replace(
new RegExp(emoji.unicode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
'<span class="o_mail_emoji">' + emoji.unicode + '</span>'
new RegExp(emoji.unicode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
'<span class="o_mail_emoji">' + emoji.unicode + "</span>"
);
});
return message;
}
},
};
+6 -5
View File
@@ -1,10 +1,9 @@
/** @odoo-module **/
import { debounce } from "@web/core/utils/timing";
import { FieldChar } from 'web.basic_fields';
import { FieldChar } from "web.basic_fields";
FieldChar.include({
//--------------------------------------------------------------------------
// Public
//-------------------------------------------------------------------------
@@ -20,10 +19,12 @@ FieldChar.include({
if (this.nodeOptions.keydown_debounce_delay === undefined) {
this.nodeOptions.keydown_debounce_delay = 2000;
}
this._triggerOnchange = debounce(this._triggerOnchange, this.nodeOptions.keydown_debounce_delay);
this._triggerOnchange = debounce(
this._triggerOnchange,
this.nodeOptions.keydown_debounce_delay
);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -55,6 +56,6 @@ FieldChar.include({
* @private
*/
_triggerOnchange: function () {
this.$input.trigger('change');
this.$input.trigger("change");
},
});
+84 -61
View File
@@ -1,12 +1,16 @@
/** @odoo-module **/
import core from 'web.core';
import fieldRegistry from 'web.field_registry';
import { FieldMany2ManyTagsAvatar, KanbanMany2ManyTagsAvatar, ListMany2ManyTagsAvatar, Many2OneAvatar } from 'web.relational_fields';
import session from 'web.session';
import { Component } from '@odoo/owl';
import core from "web.core";
import fieldRegistry from "web.field_registry";
import {
FieldMany2ManyTagsAvatar,
KanbanMany2ManyTagsAvatar,
ListMany2ManyTagsAvatar,
Many2OneAvatar,
} from "web.relational_fields";
import session from "web.session";
import { Component } from "@odoo/owl";
// This module defines extensions of the Many2OneAvatar and Many2ManyAvatar
// widgets, which are integrated with the messaging system. They are designed
@@ -23,14 +27,18 @@ import { Component } from '@odoo/owl';
// models than 'res.users'.
const M2XAvatarMixin = {
supportedModels: ['res.users'],
supportedModels: ["res.users"],
init() {
this._super(...arguments);
if (!this.supportedModels.includes(this.field.relation)) {
throw new Error(`This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify(this.supportedModels)}`);
throw new Error(
`This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify(
this.supportedModels
)}`
);
}
this.className = `${this.className || ''} o_clickable_m2x_avatar`.trim();
this.className = `${this.className || ""} o_clickable_m2x_avatar`.trim();
},
//--------------------------------------------------------------------------
@@ -51,14 +59,14 @@ const M2XAvatarMixin = {
export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
events: Object.assign({}, Many2OneAvatar.prototype.events, {
'click .o_m2o_avatar > img': '_onAvatarClicked',
"click .o_m2o_avatar > img": "_onAvatarClicked",
}),
on_attach_callback() {
this._registerCommandAssignTo()
this._registerCommandAssignTo();
},
on_detach_callback(){
this._unregisterCommandAssignTo()
on_detach_callback() {
this._unregisterCommandAssignTo();
},
//--------------------------------------------------------------------------
@@ -69,24 +77,24 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
* @override
* @private
*/
_registerCommandAssignTo() {
_registerCommandAssignTo() {
const self = this;
if (self.viewType === "form") {
let provide = async (env, options) => {
const provide = async (env, options) => {
if (self.isDestroyed()) {
return
return;
}
const records = await self._searchAssignTo(options.searchValue, 10)
const records = await self._searchAssignTo(options.searchValue, 10);
return records.map((record) => ({
name: record[1],
action: () => {
if (self.isDestroyed()) {
return
return;
}
self.reinitialize({ id: record[0], display_name: record[1] });
}
}))
}
},
}));
};
let getCommandDefinition = (env) => ({
name: env._t("Assign to ..."),
options: {
@@ -106,8 +114,12 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
};
},
});
core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignTo", getCommandDefinition, self.el);
core.bus.trigger(
"set_legacy_command",
"web.Many2OneAvatar.assignTo",
getCommandDefinition,
self.el
);
getCommandDefinition = (env) => ({
name: env._t("Assign/unassign to me"),
@@ -118,19 +130,23 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
},
action() {
if (self.isDestroyed()) {
return
return;
}
if (self.value.res_id === session.user_id[0]) {
self._setValue({
operation: 'DELETE',
operation: "DELETE",
ids: [session.user_id[0]],
});
});
} else {
self.reinitialize({ id: session.user_id[0], display_name: session.name });
}
},
});
core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignToMe", getCommandDefinition);
core.bus.trigger(
"set_legacy_command",
"web.Many2OneAvatar.assignToMe",
getCommandDefinition
);
}
},
@@ -152,13 +168,13 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
const domain = this.record.getDomain(this.recordParams);
const context = Object.assign(
this.record.getContext(this.recordParams),
this.additionalContext,
this.additionalContext
);
// Exclude black-listed ids from the domain
const blackListedIds = this._getSearchBlacklist();
if (blackListedIds.length) {
domain.push(['id', 'not in', blackListedIds]);
domain.push(["id", "not in", blackListedIds]);
}
const nameSearch = this._rpc({
@@ -170,7 +186,7 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
operator: "ilike",
limit: limit + 1,
context,
}
},
});
return this.orderer.add(nameSearch);
},
@@ -188,11 +204,11 @@ export const Many2OneAvatarUser = Many2OneAvatar.extend(M2XAvatarMixin, {
_onAvatarClicked(ev) {
ev.stopPropagation(); // in list view, prevent from opening the record
this._openChat({ userId: this.value.res_id });
}
},
});
export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({
_template: 'mail.KanbanMany2OneAvatarUser',
_template: "mail.KanbanMany2OneAvatarUser",
init() {
this._super(...arguments);
@@ -202,7 +218,7 @@ export const KanbanMany2OneAvatarUser = Many2OneAvatarUser.extend({
const M2MAvatarMixin = Object.assign(M2XAvatarMixin, {
events: Object.assign({}, FieldMany2ManyTagsAvatar.prototype.events, {
'click .o_m2m_avatar': '_onAvatarClicked',
"click .o_m2m_avatar": "_onAvatarClicked",
}),
//----------------------------------------------------------------------
@@ -215,18 +231,17 @@ const M2MAvatarMixin = Object.assign(M2XAvatarMixin, {
*/
_onAvatarClicked(ev) {
ev.stopPropagation(); // in list view, prevent from opening the record
const userId = parseInt(ev.target.getAttribute('data-id'), 10);
const userId = parseInt(ev.target.getAttribute("data-id"), 10);
this._openChat({ userId: userId });
},
});
export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixin, {
on_attach_callback() {
this._registerCommandAssignTo()
this._registerCommandAssignTo();
},
on_detach_callback(){
this._unregisterCommandAssignTo()
on_detach_callback() {
this._unregisterCommandAssignTo();
},
//--------------------------------------------------------------------------
@@ -237,15 +252,15 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
* @override
* @private
*/
_registerCommandAssignTo() {
_registerCommandAssignTo() {
const self = this;
if (self.viewType === "form") {
const provide = async (env, options) => {
if (self.isDestroyed()) {
return
return;
}
const many2one = new Many2OneAvatarUser(self, self.name, self.record, {
mode: 'edit',
mode: "edit",
noOpen: true,
noCreate: !self.canCreate,
viewType: self.viewType,
@@ -254,19 +269,19 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
many2one._getSearchBlacklist = function () {
return self.value.res_ids;
};
const records = await many2one._searchAssignTo(options.searchValue, 10)
const records = await many2one._searchAssignTo(options.searchValue, 10);
return records.map((record) => ({
name: record[1],
action: () => {
if (self.isDestroyed()) {
return
return;
}
self._setValue({
operation: 'ADD_M2M',
ids: { id: record[0], display_name: record[1] }
operation: "ADD_M2M",
ids: { id: record[0], display_name: record[1] },
});
},
}))
}));
};
let getCommandDefinition = (env) => ({
name: env._t("Assign to ..."),
@@ -287,7 +302,11 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
};
},
});
core.bus.trigger("set_legacy_command", "web.FieldMany2ManyTagsAvatar.assignTo", getCommandDefinition)
core.bus.trigger(
"set_legacy_command",
"web.FieldMany2ManyTagsAvatar.assignTo",
getCommandDefinition
);
getCommandDefinition = (env) => ({
name: env._t("Assign/unassign to me"),
@@ -298,22 +317,26 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
},
action() {
if (self.isDestroyed()) {
return
return;
}
if (self.value.res_ids.includes(session.user_id[0])) {
self._setValue({
operation: 'DELETE',
operation: "DELETE",
ids: [session.user_id[0]],
});
});
} else {
self._setValue({
operation: 'ADD_M2M',
ids: { id: session.user_id[0], display_name: session.name }
operation: "ADD_M2M",
ids: { id: session.user_id[0], display_name: session.name },
});
}
},
});
core.bus.trigger("set_legacy_command", "web.Many2OneAvatar.assignToMe", getCommandDefinition);
core.bus.trigger(
"set_legacy_command",
"web.Many2OneAvatar.assignToMe",
getCommandDefinition
);
}
},
@@ -321,19 +344,19 @@ export const Many2ManyAvatarUser = FieldMany2ManyTagsAvatar.extend(M2MAvatarMixi
* @override
* @private
*/
_unregisterCommandAssignTo() {
_unregisterCommandAssignTo() {
core.bus.trigger("remove_legacy_command", "web.FieldMany2ManyTagsAvatar.assignTo");
core.bus.trigger("remove_legacy_command", "web.Many2OneAvatar.assignToMe");
}
},
});
export const KanbanMany2ManyAvatarUser = KanbanMany2ManyTagsAvatar.extend(M2MAvatarMixin, {});
export const ListMany2ManyAvatarUser = ListMany2ManyTagsAvatar.extend(M2MAvatarMixin, {});
fieldRegistry.add('many2one_avatar_user', Many2OneAvatarUser);
fieldRegistry.add('kanban.many2one_avatar_user', KanbanMany2OneAvatarUser);
fieldRegistry.add('activity.many2one_avatar_user', KanbanMany2OneAvatarUser);
fieldRegistry.add('many2many_avatar_user', Many2ManyAvatarUser);
fieldRegistry.add('kanban.many2many_avatar_user', KanbanMany2ManyAvatarUser);
fieldRegistry.add('list.many2many_avatar_user', ListMany2ManyAvatarUser);
fieldRegistry.add("many2one_avatar_user", Many2OneAvatarUser);
fieldRegistry.add("kanban.many2one_avatar_user", KanbanMany2OneAvatarUser);
fieldRegistry.add("activity.many2one_avatar_user", KanbanMany2OneAvatarUser);
fieldRegistry.add("many2many_avatar_user", Many2ManyAvatarUser);
fieldRegistry.add("kanban.many2many_avatar_user", KanbanMany2ManyAvatarUser);
fieldRegistry.add("list.many2many_avatar_user", ListMany2ManyAvatarUser);
+85 -55
View File
@@ -1,59 +1,89 @@
/** @odoo-module **/
import { _t } from 'web.core';
import { Markup } from 'web.utils';
import tour from 'web_tour.tour';
import { _t } from "web.core";
import { Markup } from "web.utils";
import tour from "web_tour.tour";
tour.register('mail_tour', {
url: "/web#action=mail.action_discuss",
sequence: 80,
}, [{
trigger: '.o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd',
content: Markup(_t("<p>Channels make it easy to organize information across different topics and groups.</p> <p>Try to <b>create your first channel</b> (e.g. sales, marketing, product XYZ, after work party, etc).</p>")),
position: 'bottom',
}, {
trigger: '.o_DiscussSidebarCategory_addingItemInput',
content: Markup(_t("<p>Create a channel here.</p>")),
position: 'bottom',
auto: true,
run: function (actions) {
var t = new Date().getTime();
actions.text("SomeChannel_" + t, this.$anchor);
tour.register(
"mail_tour",
{
url: "/web#action=mail.action_discuss",
sequence: 80,
},
}, {
trigger: ".o_DiscussSidebarCategory_newChannelAutocompleteSuggestions",
content: Markup(_t("<p>Create a public or private channel.</p>")),
position: 'right',
run() {
this.$consumeEventAnchors.find('li:first').click();
},
}, {
trigger: '.o_DiscussView_thread .o_ComposerTextInputView_textarea',
content: Markup(_t("<p><b>Write a message</b> to the members of the channel here.</p> <p>You can notify someone with <i>'@'</i> or link another channel with <i>'#'</i>. Start your message with <i>'/'</i> to get the list of possible commands.</p>")),
position: "top",
width: 350,
run: function (actions) {
var t = new Date().getTime();
actions.text("SomeText_" + t, this.$anchor);
},
}, {
trigger: '.o_DiscussView_thread .o_ComposerView_buttonSend',
content: _t("Post your message on the thread"),
position: "top",
}, {
trigger: '.o_DiscussView_thread .o_MessageView',
content: _t("Click on your message"),
position: "top",
}, {
trigger: '.o_DiscussView_thread .o_MessageActionView_actionToggleStar',
content: Markup(_t("Messages can be <b>starred</b> to remind you to check back later.")),
position: "bottom",
}, {
trigger: '.o_DiscussSidebarMailboxView.o-starred-box',
content: _t("Once a message has been starred, you can come back and review it at any time here."),
position: "bottom",
}, {
trigger: '.o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_commandAdd',
content: Markup(_t("<p><b>Chat with coworkers</b> in real-time using direct messages.</p><p><i>You might need to invite users from the Settings app first.</i></p>")),
position: 'bottom',
}]);
[
{
trigger: ".o_DiscussSidebarView_categoryChannel .o_DiscussSidebarCategory_commandAdd",
content: Markup(
_t(
"<p>Channels make it easy to organize information across different topics and groups.</p> <p>Try to <b>create your first channel</b> (e.g. sales, marketing, product XYZ, after work party, etc).</p>"
)
),
position: "bottom",
},
{
trigger: ".o_DiscussSidebarCategory_addingItemInput",
content: Markup(_t("<p>Create a channel here.</p>")),
position: "bottom",
auto: true,
run: function (actions) {
var t = new Date().getTime();
actions.text("SomeChannel_" + t, this.$anchor);
},
},
{
trigger: ".o_DiscussSidebarCategory_newChannelAutocompleteSuggestions",
content: Markup(_t("<p>Create a public or private channel.</p>")),
position: "right",
run() {
this.$consumeEventAnchors.find("li:first").click();
},
},
{
trigger: ".o_DiscussView_thread .o_ComposerTextInputView_textarea",
content: Markup(
_t(
"<p><b>Write a message</b> to the members of the channel here.</p> <p>You can notify someone with <i>'@'</i> or link another channel with <i>'#'</i>. Start your message with <i>'/'</i> to get the list of possible commands.</p>"
)
),
position: "top",
width: 350,
run: function (actions) {
var t = new Date().getTime();
actions.text("SomeText_" + t, this.$anchor);
},
},
{
trigger: ".o_DiscussView_thread .o_ComposerView_buttonSend",
content: _t("Post your message on the thread"),
position: "top",
},
{
trigger: ".o_DiscussView_thread .o_MessageView",
content: _t("Click on your message"),
position: "top",
},
{
trigger: ".o_DiscussView_thread .o_MessageActionView_actionToggleStar",
content: Markup(
_t("Messages can be <b>starred</b> to remind you to check back later.")
),
position: "bottom",
},
{
trigger: ".o_DiscussSidebarMailboxView.o-starred-box",
content: _t(
"Once a message has been starred, you can come back and review it at any time here."
),
position: "bottom",
},
{
trigger: ".o_DiscussSidebarView_categoryChat .o_DiscussSidebarCategory_commandAdd",
content: Markup(
_t(
"<p><b>Chat with coworkers</b> in real-time using direct messages.</p><p><i>You might need to invite users from the Settings app first.</i></p>"
)
),
position: "bottom",
},
]
);
+52 -34
View File
@@ -1,7 +1,7 @@
/** @odoo-module **/
import core from 'web.core';
import { escape } from '@web/core/utils/strings';
import core from "web.core";
import { escape } from "@web/core/utils/strings";
var _t = core._t;
@@ -19,12 +19,16 @@ function parseAndTransform(htmlString, transformFunction) {
var string = htmlString.replace(/&lt;/g, openToken);
var children;
try {
children = $('<div>').html(string).contents();
children = $("<div>").html(string).contents();
} catch {
children = $('<div>').html('<pre>' + string + '</pre>').contents();
children = $("<div>")
.html("<pre>" + string + "</pre>")
.contents();
}
return _parseAndTransform(children, transformFunction)
.replace(new RegExp(openToken, "g"), "&lt;");
return _parseAndTransform(children, transformFunction).replace(
new RegExp(openToken, "g"),
"&lt;"
);
}
/**
@@ -55,25 +59,26 @@ var urlRegexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\.
function linkify(text, attrs) {
attrs = attrs || {};
if (attrs.target === undefined) {
attrs.target = '_blank';
attrs.target = "_blank";
}
if (attrs.target === '_blank') {
attrs.rel = 'noreferrer noopener';
if (attrs.target === "_blank") {
attrs.rel = "noreferrer noopener";
}
attrs = _.map(attrs, function (value, key) {
return key + '="' + _.escape(value) + '"';
}).join(' ');
}).join(" ");
return text.replace(urlRegexp, function (url) {
var href = (!/^https?:\/\//i.test(url)) ? "http://" + url : url;
return '<a ' + attrs + ' href="' + href + '">' + url + '</a>';
var href = !/^https?:\/\//i.test(url) ? "http://" + url : url;
return "<a " + attrs + ' href="' + href + '">' + url + "</a>";
});
}
function addLink(node, transformChildren) {
if (node.nodeType === 3) { // text node
if (node.nodeType === 3) {
// text node
const linkified = linkify(node.data);
if (linkified !== node.data) {
const div = document.createElement('div');
const div = document.createElement("div");
div.innerHTML = linkified;
for (const childNode of [...div.childNodes]) {
node.parentNode.insertBefore(childNode, node);
@@ -83,7 +88,9 @@ function addLink(node, transformChildren) {
}
return node.textContent;
}
if (node.tagName === "A") return node.outerHTML;
if (node.tagName === "A") {
return node.outerHTML;
}
transformChildren();
return node.outerHTML;
}
@@ -94,39 +101,50 @@ function addLink(node, transformChildren) {
*/
function htmlToTextContentInline(htmlString) {
const fragment = document.createDocumentFragment();
const div = document.createElement('div');
const div = document.createElement("div");
fragment.appendChild(div);
htmlString = htmlString.replace(/<br\s*\/?>/gi,' ');
htmlString = htmlString.replace(/<br\s*\/?>/gi, " ");
try {
div.innerHTML = htmlString;
} catch {
div.innerHTML = `<pre>${htmlString}</pre>`;
}
return div
.textContent
return div.textContent
.trim()
.replace(/[\n\r]/g, '')
.replace(/\s\s+/g, ' ');
.replace(/[\n\r]/g, "")
.replace(/\s\s+/g, " ");
}
function stripHTML(node, transformChildren) {
if (node.nodeType === 3) return node.data; // text node
if (node.tagName === "BR") return "\n";
if (node.nodeType === 3) {
return node.data;
} // text node
if (node.tagName === "BR") {
return "\n";
}
return transformChildren();
}
function inline(node, transform_children) {
if (node.nodeType === 3) return node.data;
if (node.nodeType === 8) return "";
if (node.tagName === "BR") return " ";
if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children();
if (node.nodeType === 3) {
return node.data;
}
if (node.nodeType === 8) {
return "";
}
if (node.tagName === "BR") {
return " ";
}
if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) {
return transform_children();
}
node.innerHTML = transform_children();
return node.outerHTML;
}
// Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False
function parseEmail(text) {
if (text){
if (text) {
var result = text.match(/(.*)<(.*@.*)>/);
if (result) {
return [_.str.trim(result[1]), _.str.trim(result[2])];
@@ -148,11 +166,11 @@ function parseEmail(text) {
function escapeAndCompactTextContent(content) {
//Removing unwanted extra spaces from message
let value = escape(content).trim();
value = value.replace(/(\r|\n){2,}/g, '<br/><br/>');
value = value.replace(/(\r|\n)/g, '<br/>');
value = value.replace(/(\r|\n){2,}/g, "<br/><br/>");
value = value.replace(/(\r|\n)/g, "<br/>");
// prevent html space collapsing
value = value.replace(/ /g, '&nbsp;').replace(/([^>])&nbsp;([^<])/g, '$1 $2');
value = value.replace(/ /g, "&nbsp;").replace(/([^>])&nbsp;([^<])/g, "$1 $2");
return value;
}
@@ -160,12 +178,12 @@ function escapeAndCompactTextContent(content) {
// TDE note : should be done server-side, in Python -> use mail.compose.message ?
function getTextToHTML(text) {
return text
.replace(/((?:https?|ftp):\/\/[\S]+)/g,'<a href="$1">$1</a> ')
.replace(/[\n\r]/g,'<br/>');
.replace(/((?:https?|ftp):\/\/[\S]+)/g, '<a href="$1">$1</a> ')
.replace(/[\n\r]/g, "<br/>");
}
function timeFromNow(date) {
if (moment().diff(date, 'seconds') < 45) {
if (moment().diff(date, "seconds") < 45) {
return _t("now");
}
return date.fromNow();
@@ -1,22 +1,22 @@
/** @odoo-module **/
import '@mail/js/activity';
import "@mail/js/activity";
import BasicController from 'web.BasicController';
import core from 'web.core';
import { sprintf } from '@web/core/utils/strings';
import BasicController from "web.BasicController";
import core from "web.core";
import { sprintf } from "@web/core/utils/strings";
import { SelectCreateDialog } from '@web/views/view_dialogs/select_create_dialog';
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
var _t = core._t;
var ActivityController = BasicController.extend({
custom_events: _.extend({}, BasicController.prototype.custom_events, {
empty_cell_clicked: '_onEmptyCell',
reload_data: '_onReloadData',
send_mail_template: '_onSendMailTemplate',
schedule_activity: '_onScheduleActivity',
empty_cell_clicked: "_onEmptyCell",
reload_data: "_onReloadData",
send_mail_template: "_onSendMailTemplate",
schedule_activity: "_onScheduleActivity",
}),
//--------------------------------------------------------------------------
@@ -58,7 +58,7 @@ var ActivityController = BasicController.extend({
* @private
*/
_onReloadData() {
this.trigger_up('reload');
this.trigger_up("reload");
},
/**
* @private
@@ -73,11 +73,14 @@ var ActivityController = BasicController.extend({
noCreate: !this.activeActions.create,
multiSelect: false,
context: state.context,
onSelected: async resIds => {
onSelected: async (resIds) => {
const messaging = await owl.Component.env.services.messaging.get();
const thread = messaging.models['Thread'].insert({ id: resIds[0], model: this.model.modelName });
const thread = messaging.models["Thread"].insert({
id: resIds[0],
model: this.model.modelName,
});
await messaging.openActivityForm({ thread });
this.trigger_up('reload');
this.trigger_up("reload");
},
});
},
@@ -87,22 +90,25 @@ var ActivityController = BasicController.extend({
*/
_onEmptyCell: function (ev) {
var state = this.model.get(this.handle);
this.do_action({
type: 'ir.actions.act_window',
res_model: 'mail.activity',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new',
context: {
default_res_id: ev.data.resId,
default_res_model: state.model,
default_activity_type_id: ev.data.activityTypeId,
this.do_action(
{
type: "ir.actions.act_window",
res_model: "mail.activity",
view_mode: "form",
view_type: "form",
views: [[false, "form"]],
target: "new",
context: {
default_res_id: ev.data.resId,
default_res_model: state.model,
default_activity_type_id: ev.data.activityTypeId,
},
res_id: false,
},
res_id: false,
}, {
on_close: this.reload.bind(this),
});
{
on_close: this.reload.bind(this),
}
);
},
/**
* @private
@@ -123,7 +129,7 @@ var ActivityController = BasicController.extend({
});
this._rpc({
model: this.model.modelName,
method: 'activity_send_mail',
method: "activity_send_mail",
args: [resIDS, templateID],
});
},
@@ -1,10 +1,9 @@
/** @odoo-module **/
import BasicModel from 'web.BasicModel';
import session from 'web.session';
import BasicModel from "web.BasicModel";
import session from "web.session";
const ActivityModel = BasicModel.extend({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -19,7 +18,7 @@ const ActivityModel = BasicModel.extend({
*/
__get: function () {
var result = this._super.apply(this, arguments);
if (result && result.model === this.modelName && result.type === 'list') {
if (result && result.model === this.modelName && result.type === "list") {
_.extend(result, this.additionalData);
}
return result;
@@ -30,7 +29,7 @@ const ActivityModel = BasicModel.extend({
*/
__load: function (params) {
this.originalDomain = _.extend([], params.domain);
params.domain.push(['activity_ids', '!=', false]);
params.domain.push(["activity_ids", "!=", false]);
this.domain = params.domain;
this.modelName = params.modelName;
params.groupedBy = [];
@@ -44,12 +43,12 @@ const ActivityModel = BasicModel.extend({
* @param {Array[]} [params.domain]
*/
__reload: function (handle, params) {
if (params && 'domain' in params) {
if (params && "domain" in params) {
this.originalDomain = _.extend([], params.domain);
params.domain.push(['activity_ids', '!=', false]);
params.domain.push(["activity_ids", "!=", false]);
this.domain = params.domain;
}
if (params && 'groupBy' in params) {
if (params && "groupBy" in params) {
params.groupBy = [];
}
var def = this._super.apply(this, arguments);
@@ -72,12 +71,12 @@ const ActivityModel = BasicModel.extend({
var self = this;
return this._rpc({
model: "mail.activity",
method: 'get_activity_data',
method: "get_activity_data",
kwargs: {
res_model: this.modelName,
domain: this.domain,
context: session.user_context,
}
},
}).then(function (result) {
self.additionalData = result;
});
@@ -1,13 +1,13 @@
/** @odoo-module **/
import KanbanRecord from 'web.KanbanRecord';
import KanbanRecord from "web.KanbanRecord";
var ActivityRecord = KanbanRecord.extend({
/**
* @override
*/
init: function (parent, state) {
this._super.apply(this,arguments);
this._super.apply(this, arguments);
this.fieldsInfo = state.fieldsInfo.activity;
},
@@ -22,9 +22,9 @@ var ActivityRecord = KanbanRecord.extend({
*/
_render: function () {
this.defs = [];
this._replaceElement(this.qweb.render('activity-box', this.qweb_context));
this.$el.on('click', this._onGlobalClick.bind(this));
this.$el.addClass('o_activity_record');
this._replaceElement(this.qweb.render("activity-box", this.qweb_context));
this.$el.on("click", this._onGlobalClick.bind(this));
this.$el.addClass("o_activity_record");
this._processFields();
this._setupColor();
return Promise.all(this.defs);
@@ -38,7 +38,7 @@ var ActivityRecord = KanbanRecord.extend({
// attribute muted
if (this.fieldsInfo[fieldName].muted) {
$el.addClass('text-muted');
$el.addClass("text-muted");
}
},
/**
@@ -1,17 +1,17 @@
/** @odoo-module **/
import ActivityRecord from '@mail/js/views/activity/activity_record';
import { ActivityCellViewContainer } from '@mail/backend_components/activity_cell_view/activity_cell_view_container';
import ActivityRecord from "@mail/js/views/activity/activity_record";
import { ActivityCellViewContainer } from "@mail/backend_components/activity_cell_view/activity_cell_view_container";
import AbstractRendererOwl from 'web.AbstractRendererOwl';
import core from 'web.core';
import KanbanColumnProgressBar from 'web.KanbanColumnProgressBar';
import { ComponentAdapter } from 'web.OwlCompatibility';
import QWeb from 'web.QWeb';
import session from 'web.session';
import utils from 'web.utils';
import AbstractRendererOwl from "web.AbstractRendererOwl";
import core from "web.core";
import KanbanColumnProgressBar from "web.KanbanColumnProgressBar";
import { ComponentAdapter } from "web.OwlCompatibility";
import QWeb from "web.QWeb";
import session from "web.session";
import utils from "web.utils";
import { useState } from '@odoo/owl';
import { useState } from "@odoo/owl";
const _t = core._t;
/**
@@ -20,7 +20,7 @@ const _t = core._t;
*/
class ActivityRecordAdapter extends ComponentAdapter {
renderWidget() {
_.invoke(_.pluck(this.widget.subWidgets, '$el'), 'detach');
_.invoke(_.pluck(this.widget.subWidgets, "$el"), "detach");
this.widget._render();
}
@@ -55,11 +55,13 @@ class KanbanColumnProgressBarAdapter extends ComponentAdapter {
// 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,
}));
this.props.onSetProgressBarState(
new CustomEvent("set-progress-bar-state", {
bubbles: true,
cancelable: true,
detail: ev.data,
})
);
}
super._trigger_up(ev);
}
@@ -69,12 +71,12 @@ class KanbanColumnProgressBarAdapter extends ComponentAdapter {
class ActivityRenderer extends AbstractRendererOwl {
setup() {
super.setup(...arguments);
this.qweb = new QWeb(this.env.isDebug(), {_s: session.origin});
this.qweb = new QWeb(this.env.isDebug(), { _s: session.origin });
this.qweb.add_template(utils.json_node_to_xml(this.props.templates));
this.activeFilter = useState({
state: null,
activityTypeId: null,
resIds: []
resIds: [],
});
this.widgetComponents = {
ActivityRecord,
@@ -92,8 +94,8 @@ class ActivityRenderer extends AbstractRendererOwl {
* @returns filtered resIds first then the rest.
*/
get activityResIds() {
const copiedActivityResIds = Array.from(this.props.activity_res_ids)
return copiedActivityResIds.sort((a, b) => this.activeFilter.resIds.includes(a) ? -1 : 0);
const copiedActivityResIds = Array.from(this.props.activity_res_ids);
return copiedActivityResIds.sort((a, b) => (this.activeFilter.resIds.includes(a) ? -1 : 0));
}
/**
@@ -119,7 +121,7 @@ class ActivityRenderer extends AbstractRendererOwl {
getProgressBarColumnState(typeId) {
const counts = { planned: 0, today: 0, overdue: 0 };
for (let activities of Object.values(this.props.grouped_activities)) {
for (const activities of Object.values(this.props.grouped_activities)) {
if (typeId in activities) {
counts[activities[typeId].state] += 1;
}
@@ -128,17 +130,17 @@ class ActivityRenderer extends AbstractRendererOwl {
count: Object.values(counts).reduce((x, y) => x + y),
fields: {
activity_state: {
type: 'selection',
type: "selection",
selection: [
['planned', _t('Planned')],
['today', _t('Today')],
['overdue', _t('Overdue')],
["planned", _t("Planned")],
["today", _t("Today")],
["overdue", _t("Overdue")],
],
},
},
progressBarValues: {
field: 'activity_state',
colors: { planned: 'success', today: 'warning', overdue: 'danger' },
field: "activity_state",
colors: { planned: "success", today: "warning", overdue: "danger" },
counts: counts,
},
};
@@ -152,7 +154,7 @@ class ActivityRenderer extends AbstractRendererOwl {
* @param {MouseEvent} ev
*/
_onEmptyCellClicked(ev) {
this.trigger('empty_cell_clicked', {
this.trigger("empty_cell_clicked", {
resId: parseInt(ev.currentTarget.dataset.resId, 10),
activityTypeId: parseInt(ev.currentTarget.dataset.activityTypeId, 10),
});
@@ -162,7 +164,7 @@ class ActivityRenderer extends AbstractRendererOwl {
* @param {MouseEvent} ev
*/
_onSendMailTemplateClicked(ev) {
this.trigger('send_mail_template', {
this.trigger("send_mail_template", {
activityTypeID: parseInt(ev.currentTarget.dataset.activityTypeId, 10),
templateID: parseInt(ev.currentTarget.dataset.templateId, 10),
});
@@ -176,8 +178,11 @@ class ActivityRenderer extends AbstractRendererOwl {
this.activeFilter.state = ev.detail.values.activeFilter;
this.activeFilter.activityTypeId = ev.detail.columnID;
this.activeFilter.resIds = Object.entries(this.props.grouped_activities)
.filter(([, resIds]) => ev.detail.columnID in resIds &&
resIds[ev.detail.columnID].state === ev.detail.values.activeFilter.value)
.filter(
([, resIds]) =>
ev.detail.columnID in resIds &&
resIds[ev.detail.columnID].state === ev.detail.values.activeFilter.value
)
.map(([key]) => parseInt(key));
} else {
this.activeFilter.state = null;
@@ -196,6 +201,6 @@ ActivityRenderer.components = {
ActivityRecordAdapter,
KanbanColumnProgressBarAdapter,
};
ActivityRenderer.template = 'mail.ActivityRenderer';
ActivityRenderer.template = "mail.ActivityRenderer";
export default ActivityRenderer;
@@ -1,27 +1,27 @@
/** @odoo-module **/;
/** @odoo-module **/
import ActivityController from '@mail/js/views/activity/activity_controller';
import ActivityModel from '@mail/js/views/activity/activity_model';
import ActivityRenderer from '@mail/js/views/activity/activity_renderer';
import ActivityController from "@mail/js/views/activity/activity_controller";
import ActivityModel from "@mail/js/views/activity/activity_model";
import ActivityRenderer from "@mail/js/views/activity/activity_renderer";
import BasicView from 'web.BasicView';
import core from 'web.core';
import RendererWrapper from 'web.RendererWrapper';
import view_registry from 'web.view_registry';
import BasicView from "web.BasicView";
import core from "web.core";
import RendererWrapper from "web.RendererWrapper";
import view_registry from "web.view_registry";
const _lt = core._lt;
const ActivityView = BasicView.extend({
accesskey: "a",
display_name: _lt('Activity'),
icon: 'fa fa-clock-o',
display_name: _lt("Activity"),
icon: "fa fa-clock-o",
config: _.extend({}, BasicView.prototype.config, {
Controller: ActivityController,
Model: ActivityModel,
Renderer: ActivityRenderer,
}),
viewType: 'activity',
searchMenuTypes: ['filter', 'favorite'],
viewType: "activity",
searchMenuTypes: ["filter", "favorite"],
/**
* @override
@@ -31,11 +31,11 @@ const ActivityView = BasicView.extend({
const { search_view_id } = params.action || {};
this.controllerParams.searchViewId = search_view_id ? search_view_id[0] : false;
this.loadParams.type = 'list';
this.loadParams.type = "list";
// limit makes no sense in this view as we display all records having activities
this.loadParams.limit = false;
this.rendererParams.templates = _.findWhere(this.arch.children, { 'tag': 'templates' });
this.rendererParams.templates = _.findWhere(this.arch.children, { tag: "templates" });
this.controllerParams.title = this.arch.attrs.string;
},
/**
@@ -48,6 +48,6 @@ const ActivityView = BasicView.extend({
},
});
view_registry.add('activity', ActivityView);
view_registry.add("activity", ActivityView);
export default ActivityView;
+24 -18
View File
@@ -1,29 +1,35 @@
/** @odoo-module **/
import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container';
import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container';
import { DiscussContainer } from '@mail/components/discuss_container/discuss_container';
import { PopoverManagerContainer } from '@mail/components/popover_manager_container/popover_manager_container';
import { messagingService } from '@mail/services/messaging_service';
import { systrayService } from '@mail/services/systray_service';
import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env';
import { ChatWindowManagerContainer } from "@mail/components/chat_window_manager_container/chat_window_manager_container";
import { DialogManagerContainer } from "@mail/components/dialog_manager_container/dialog_manager_container";
import { DiscussContainer } from "@mail/components/discuss_container/discuss_container";
import { PopoverManagerContainer } from "@mail/components/popover_manager_container/popover_manager_container";
import { messagingService } from "@mail/services/messaging_service";
import { systrayService } from "@mail/services/systray_service";
import { makeMessagingToLegacyEnv } from "@mail/utils/make_messaging_to_legacy_env";
import { registry } from '@web/core/registry';
import { registry } from "@web/core/registry";
const messagingValuesService = {
start() {
return {};
}
},
};
const serviceRegistry = registry.category('services');
serviceRegistry.add('messaging', messagingService);
serviceRegistry.add('messagingValues', messagingValuesService);
serviceRegistry.add('systray_service', systrayService);
serviceRegistry.add('messaging_service_to_legacy_env', makeMessagingToLegacyEnv(owl.Component.env));
const serviceRegistry = registry.category("services");
serviceRegistry.add("messaging", messagingService);
serviceRegistry.add("messagingValues", messagingValuesService);
serviceRegistry.add("systray_service", systrayService);
serviceRegistry.add("messaging_service_to_legacy_env", makeMessagingToLegacyEnv(owl.Component.env));
registry.category('actions').add('mail.action_discuss', DiscussContainer);
registry.category("actions").add("mail.action_discuss", DiscussContainer);
registry.category('main_components').add('DialogManagerContainer', { Component: DialogManagerContainer });
registry.category('main_components').add('ChatWindowManagerContainer', { Component: ChatWindowManagerContainer });
registry.category('main_components').add('PopoverManagerContainer', { Component: PopoverManagerContainer });
registry
.category("main_components")
.add("DialogManagerContainer", { Component: DialogManagerContainer });
registry
.category("main_components")
.add("ChatWindowManagerContainer", { Component: ChatWindowManagerContainer });
registry
.category("main_components")
.add("PopoverManagerContainer", { Component: PopoverManagerContainer });
+3 -3
View File
@@ -1,5 +1,5 @@
/** @odoo-module **/
export * from '@mail/model/model_core';
export * from '@mail/model/model_field';
export * from '@mail/model/model_field_command';
export * from "@mail/model/model_core";
export * from "@mail/model/model_field";
export * from "@mail/model/model_field_command";
+187 -79
View File
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { makeDeferred } from '@mail/utils/deferred';
import { makeDeferred } from "@mail/utils/deferred";
/**
* Module that contains registry for adding new models or patching models.
@@ -42,10 +42,12 @@ function addContextToErrors(func, contextMessage) {
* @param {Function} componentSetup The componentSetup function to be added.
*/
function addComponentSetup(modelName, componentSetup) {
if (typeof componentSetup !== 'function') {
throw new Error(`Cannot add componentSetup to model ${modelName}: componentSetup must be a function (current type: "${typeof componentSetup}").`);
if (typeof componentSetup !== "function") {
throw new Error(
`Cannot add componentSetup to model ${modelName}: componentSetup must be a function (current type: "${typeof componentSetup}").`
);
}
registry.get(modelName).set('componentSetup', componentSetup);
registry.get(modelName).set("componentSetup", componentSetup);
}
/**
@@ -60,11 +62,11 @@ function addFields(modelName, fields) {
addContextToErrors(() => {
assertNameIsAvailableOnRecords(fieldName, definition);
}, `Cannot add field "${fieldName}" to model "${modelName}": `);
definition.get('fields').set(fieldName, field);
definition.get("fields").set(fieldName, field);
}
}
const validHookNames = ['_created', '_willDelete'];
const validHookNames = ["_created", "_willDelete"];
/**
* Adds the provided hooks to the model specified by the `modelName`.
@@ -78,10 +80,12 @@ function addLifecycleHooks(modelName, hooks) {
addContextToErrors(() => {
assertIsFunction(handler);
if (!validHookNames.includes(name)) {
throw new Error(`unsupported hook name. Possible values: ${validHookNames.join(", ")}.`);
throw new Error(
`unsupported hook name. Possible values: ${validHookNames.join(", ")}.`
);
}
}, `Cannot add lifecycle hook "${name}" to model "${modelName}": `);
definition.get('lifecycleHooks').set(name, handler);
definition.get("lifecycleHooks").set(name, handler);
}
}
@@ -102,7 +106,7 @@ function addModelGetters(modelName, modelGetters) {
assertIsFunction(getter);
assertNameIsAvailableOnModel(getterName, definition);
}, `Cannot add model getter "${getterName}" to model "${modelName}": `);
definition.get('modelGetters').set(getterName, getter);
definition.get("modelGetters").set(getterName, getter);
}
}
@@ -119,7 +123,7 @@ function addModelMethods(modelName, modelMethods) {
assertIsFunction(method);
assertNameIsAvailableOnModel(name, definition);
}, `Cannot add model method "${name}" to model "${modelName}": `);
definition.get('modelMethods').set(name, method);
definition.get("modelMethods").set(name, method);
}
}
@@ -132,37 +136,48 @@ function addModelMethods(modelName, modelMethods) {
function addOnChanges(modelName, onChanges) {
addContextToErrors(() => {
for (const onChange of onChanges) {
if (!Object.prototype.hasOwnProperty.call(onChange, 'dependencies')) {
throw new Error("at least one onChange definition lacks dependencies (the list of fields to be watched for changes).");
if (!Object.prototype.hasOwnProperty.call(onChange, "dependencies")) {
throw new Error(
"at least one onChange definition lacks dependencies (the list of fields to be watched for changes)."
);
}
if (!Object.prototype.hasOwnProperty.call(onChange, 'methodName')) {
throw new Error("at least one onChange definition lacks a methodName (the name of the method to be called on change).");
if (!Object.prototype.hasOwnProperty.call(onChange, "methodName")) {
throw new Error(
"at least one onChange definition lacks a methodName (the name of the method to be called on change)."
);
}
if (!Array.isArray(onChange.dependencies)) {
throw new Error("onChange dependencies must be an array of strings.");
}
if (typeof onChange.methodName !== 'string') {
if (typeof onChange.methodName !== "string") {
throw new Error("onChange methodName must be a string.");
}
const allowedKeys = ['dependencies', 'methodName'];
const allowedKeys = ["dependencies", "methodName"];
for (const key of Object.keys(onChange)) {
if (!allowedKeys.includes(key)) {
throw new Error(`unknown key "${key}" in onChange definition. Allowed keys: ${allowedKeys.join(", ")}.`);
throw new Error(
`unknown key "${key}" in onChange definition. Allowed keys: ${allowedKeys.join(
", "
)}.`
);
}
}
// paths of dependencies are splitted now to avoid having to do it
// each time the path is followed.
const splittedDependencies = [];
for (const dependency of onChange.dependencies) {
if (typeof dependency !== 'string') {
if (typeof dependency !== "string") {
throw new Error("onChange dependencies must be an array of strings.");
}
splittedDependencies.push(dependency.split('.'));
splittedDependencies.push(dependency.split("."));
}
onChange.dependencies = splittedDependencies;
}
}, `Cannot add onChanges to model "${modelName}": `);
registry.get(modelName).get('onChanges').push(...onChanges);
registry
.get(modelName)
.get("onChanges")
.push(...onChanges);
}
/**
@@ -178,7 +193,7 @@ function addRecordMethods(modelName, recordMethods) {
assertIsFunction(method);
assertNameIsAvailableOnRecords(name, definition);
}, `Cannot add record method "${name}" to model "${modelName}": `);
definition.get('recordMethods').set(name, method);
definition.get("recordMethods").set(name, method);
}
}
@@ -199,7 +214,7 @@ function addRecordGetters(modelName, recordGetters) {
assertIsFunction(getter);
assertNameIsAvailableOnRecords(getterName, definition);
}, `Cannot add record getter "${getterName}" to model "${modelName}": `);
definition.get('recordGetters').set(getterName, getter);
definition.get("recordGetters").set(getterName, getter);
}
}
@@ -210,7 +225,7 @@ function addRecordGetters(modelName, recordGetters) {
* @throws {Error} when `toAssert` is not typeof function.
*/
function assertIsFunction(toAssert) {
if (typeof toAssert !== 'function') {
if (typeof toAssert !== "function") {
throw new Error(`"${toAssert}" must be a function`);
}
}
@@ -223,7 +238,7 @@ function assertIsFunction(toAssert) {
* @throws {Error} when `name` is already used as a key on the model.
*/
function assertNameIsAvailableOnModel(name, modelDefinition) {
if (['modelGetters', 'modelMethods'].some(x => modelDefinition.get(x).has(name))) {
if (["modelGetters", "modelMethods"].some((x) => modelDefinition.get(x).has(name))) {
throw new Error(`there is already a key with this name on the model.`);
}
}
@@ -236,41 +251,51 @@ function assertNameIsAvailableOnModel(name, modelDefinition) {
* @throws {Error} when `name` is already used as a key on the records.
*/
function assertNameIsAvailableOnRecords(name, modelDefinition) {
if (['fields', 'recordGetters', 'recordMethods'].some(x => modelDefinition.get(x).has(name))) {
if (
["fields", "recordGetters", "recordMethods"].some((x) => modelDefinition.get(x).has(name))
) {
throw new Error(`there is already a key with this name on the records.`);
}
}
function patchComponentSetup({ name, componentSetup }) {
const originalComponentSetup = registry.get(name).get('componentSetup');
const originalComponentSetup = registry.get(name).get("componentSetup");
if (!originalComponentSetup) {
addComponentSetup(name, componentSetup);
return;
}
if (typeof componentSetup !== 'function') {
throw new Error(`Cannot patch componentSetup of model ${name}: componentSetup must be a function (current type: "${typeof componentSetup}").`);
if (typeof componentSetup !== "function") {
throw new Error(
`Cannot patch componentSetup of model ${name}: componentSetup must be a function (current type: "${typeof componentSetup}").`
);
}
registry.get(name).set('componentSetup', function () {
registry.get(name).set("componentSetup", function () {
this._super = originalComponentSetup;
return componentSetup.call(this);
})
});
}
function patchFields(patch) {
const newFieldsToAdd = Object.create(null);
for (const [fieldName, fieldData] of Object.entries(patch.fields)) {
const originalFieldDefinition = registry.get(patch.name).get('fields').get(fieldName);
const originalFieldDefinition = registry.get(patch.name).get("fields").get(fieldName);
if (!originalFieldDefinition) {
newFieldsToAdd[fieldName] = fieldData;
} else {
for (const [attributeName, attributeData] of Object.entries(fieldData))
for (const [attributeName, attributeData] of Object.entries(fieldData)) {
switch (attributeName) {
case 'compute':
case "compute": {
if (!originalFieldDefinition.compute) {
throw new Error(`Cannot patch compute of field ${patch.name}/${fieldName}: the field is not a compute in the original definition.`);
throw new Error(
`Cannot patch compute of field ${patch.name}/${fieldName}: the field is not a compute in the original definition.`
);
}
if (typeof attributeData !== 'function') {
throw new Error(`Cannot patch compute of field ${patch.name}/${fieldName}: the compute must be a function (found: "${typeof attributeData}").`);
if (typeof attributeData !== "function") {
throw new Error(
`Cannot patch compute of field ${
patch.name
}/${fieldName}: the compute must be a function (found: "${typeof attributeData}").`
);
}
const computeBeforePatch = originalFieldDefinition.compute;
originalFieldDefinition.compute = function () {
@@ -278,38 +303,56 @@ function patchFields(patch) {
return attributeData.call(this);
};
break;
case 'sort':
}
case "sort":
if (originalFieldDefinition.sort) {
if (typeof attributeData !== 'function') {
throw new Error(`Cannot patch sorting rules of field ${patch.name}/${fieldName}: the value of 'sort' must be a function to apply to the sorting rules array (found: "${typeof attributeData}").`);
if (typeof attributeData !== "function") {
throw new Error(
`Cannot patch sorting rules of field ${
patch.name
}/${fieldName}: the value of 'sort' must be a function to apply to the sorting rules array (found: "${typeof attributeData}").`
);
}
originalFieldDefinition.sort = attributeData.call({ _super: originalFieldDefinition.sort });
originalFieldDefinition.sort = attributeData.call({
_super: originalFieldDefinition.sort,
});
} else {
if (!Array.isArray(attributeData)) {
throw new Error(`Cannot add sorting rules to field ${patch.name}/${fieldName}: sorting rules must be an array.`);
throw new Error(
`Cannot add sorting rules to field ${patch.name}/${fieldName}: sorting rules must be an array.`
);
}
originalFieldDefinition.sort = attributeData;
}
break;
case 'fieldType':
throw new Error(`Cannot patch field ${patch.name}/${fieldName}: patches do not need field type (attr, one, many).`);
case "fieldType":
throw new Error(
`Cannot patch field ${patch.name}/${fieldName}: patches do not need field type (attr, one, many).`
);
default:
throw new Error(`Cannot patch field ${patch.name}/${fieldName}: unsupported field attribute "${attributeName}".`);
throw new Error(
`Cannot patch field ${patch.name}/${fieldName}: unsupported field attribute "${attributeName}".`
);
}
}
}
}
addFields(patch.name, newFieldsToAdd);
}
function patchLifecycleHooks(patch) {
const originalLifecycleHooksDefinition = registry.get(patch.name).get('lifecycleHooks');
const originalLifecycleHooksDefinition = registry.get(patch.name).get("lifecycleHooks");
const newLifecycleHooksToAdd = Object.create(null);
for (const [hookName, hookHandler] of Object.entries(patch.lifecycleHooks)) {
if (!originalLifecycleHooksDefinition.has(hookName)) {
newLifecycleHooksToAdd[hookName] = hookHandler;
} else {
if (typeof hookHandler !== 'function') {
throw new Error(`Cannot patch hook "${hookName}" on model ${patch.name}: the hook handler must be a function (current type: "${typeof hookHandler}").`);
if (typeof hookHandler !== "function") {
throw new Error(
`Cannot patch hook "${hookName}" on model ${
patch.name
}: the hook handler must be a function (current type: "${typeof hookHandler}").`
);
}
const hookHandlerBeforePatch = originalLifecycleHooksDefinition.get(hookName);
originalLifecycleHooksDefinition.set(hookName, function () {
@@ -322,14 +365,18 @@ function patchLifecycleHooks(patch) {
}
function patchModelMethods(patch) {
const originalModelMethodsDefinition = registry.get(patch.name).get('modelMethods');
const originalModelMethodsDefinition = registry.get(patch.name).get("modelMethods");
const newModelMethodsToAdd = Object.create(null);
for (const [methodName, method] of Object.entries(patch.modelMethods)) {
if (!originalModelMethodsDefinition.has(methodName)) {
newModelMethodsToAdd[methodName] = method;
} else {
if (typeof method !== 'function') {
throw new Error(`Cannot patch model method "${methodName}" on model ${patch.name}: the method must be a function (current type: "${typeof method}").`);
if (typeof method !== "function") {
throw new Error(
`Cannot patch model method "${methodName}" on model ${
patch.name
}: the method must be a function (current type: "${typeof method}").`
);
}
const methodBeforePatch = originalModelMethodsDefinition.get(methodName);
originalModelMethodsDefinition.set(methodName, function (...args) {
@@ -342,14 +389,18 @@ function patchModelMethods(patch) {
}
function patchRecordMethods(patch) {
const originalRecordMethods = registry.get(patch.name).get('recordMethods');
const originalRecordMethods = registry.get(patch.name).get("recordMethods");
const newRecordMethodsToAdd = Object.create(null);
for (const [methodName, method] of Object.entries(patch.recordMethods)) {
if (!originalRecordMethods.has(methodName)) {
newRecordMethodsToAdd[methodName] = method;
} else {
if (typeof method !== 'function') {
throw new Error(`Cannot patch record method "${methodName}" on model ${patch.name}: the method must be a function (current type: "${typeof method}").`);
if (typeof method !== "function") {
throw new Error(
`Cannot patch record method "${methodName}" on model ${
patch.name
}: the method must be a function (current type: "${typeof method}").`
);
}
const methodBeforePatch = originalRecordMethods.get(methodName);
originalRecordMethods.set(methodName, function (...args) {
@@ -376,32 +427,65 @@ function patchRecordMethods(patch) {
* @param {Object} [definition.recordMethods]
* @param {string} [definition.template]
*/
export function Model({ componentSetup, fields, identifyingMode = 'and', isLegacyComponent = false, lifecycleHooks, modelGetters, modelMethods, name, onChanges, recordGetters, recordMethods, template }) {
export function Model({
componentSetup,
fields,
identifyingMode = "and",
isLegacyComponent = false,
lifecycleHooks,
modelGetters,
modelMethods,
name,
onChanges,
recordGetters,
recordMethods,
template,
}) {
if (!name) {
throw new Error("Model is lacking a name.");
}
if (registry.has(name)) {
throw new Error(`Cannot register model "${name}": model has already been registered.`);
}
const sectionNames = ['name', 'template', 'componentSetup', 'identifyingMode', 'isLegacyComponent', 'lifecycleHooks', 'modelMethods', 'modelGetters', 'recordMethods', 'recordGetters', 'fields', 'onChanges'];
const invalidSectionNames = Object.keys(arguments[0]).filter(x => !sectionNames.includes(x));
const sectionNames = [
"name",
"template",
"componentSetup",
"identifyingMode",
"isLegacyComponent",
"lifecycleHooks",
"modelMethods",
"modelGetters",
"recordMethods",
"recordGetters",
"fields",
"onChanges",
];
const invalidSectionNames = Object.keys(arguments[0]).filter((x) => !sectionNames.includes(x));
if (invalidSectionNames.length > 0) {
throw new Error(`Cannot register model "${name}": model definition contains unknown key(s): ${invalidSectionNames.join(", ")}`);
throw new Error(
`Cannot register model "${name}": model definition contains unknown key(s): ${invalidSectionNames.join(
", "
)}`
);
}
registry.set(name, new Map([
['name', name],
['identifyingMode', identifyingMode],
['isLegacyComponent', isLegacyComponent],
['lifecycleHooks', new Map()],
['modelMethods', new Map()],
['modelGetters', new Map()],
['recordMethods', new Map()],
['recordGetters', new Map()],
['fields', new Map()],
['onChanges', []],
]));
registry.set(
name,
new Map([
["name", name],
["identifyingMode", identifyingMode],
["isLegacyComponent", isLegacyComponent],
["lifecycleHooks", new Map()],
["modelMethods", new Map()],
["modelGetters", new Map()],
["recordMethods", new Map()],
["recordGetters", new Map()],
["fields", new Map()],
["onChanges", []],
])
);
if (template) {
registry.get(name).set('template', template);
registry.get(name).set("template", template);
if (componentSetup) {
addComponentSetup(name, componentSetup);
}
@@ -429,14 +513,36 @@ export function Model({ componentSetup, fields, identifyingMode = 'and', isLegac
}
}
export function Patch({ componentSetup, fields, lifecycleHooks, modelMethods, name, onChanges, recordMethods }) {
export function Patch({
componentSetup,
fields,
lifecycleHooks,
modelMethods,
name,
onChanges,
recordMethods,
}) {
if (!name) {
throw new Error("Patch is lacking the name of the model to be patched.");
}
const allowedSectionNames = ['name', 'componentSetup', 'lifecycleHooks', 'modelMethods', 'recordMethods', 'fields', 'onChanges'];
const invalidSectionNames = Object.keys(arguments['0']).filter(x => !allowedSectionNames.includes(x));
const allowedSectionNames = [
"name",
"componentSetup",
"lifecycleHooks",
"modelMethods",
"recordMethods",
"fields",
"onChanges",
];
const invalidSectionNames = Object.keys(arguments["0"]).filter(
(x) => !allowedSectionNames.includes(x)
);
if (invalidSectionNames.length > 0) {
throw new Error(`Error while registering patch for model "${name}": patch definition contains unsupported key(s): ${invalidSectionNames.join(", ")}`);
throw new Error(
`Error while registering patch for model "${name}": patch definition contains unsupported key(s): ${invalidSectionNames.join(
", "
)}`
);
}
patches.push({
name,
@@ -452,13 +558,13 @@ export function Patch({ componentSetup, fields, lifecycleHooks, modelMethods, na
export const patchesAppliedPromise = makeDeferred();
(async function applyPatches() {
if (document.readyState !== 'complete') {
await new Promise(resolve => {
if (document.readyState !== "complete") {
await new Promise((resolve) => {
/**
* Called when all JS resources are loaded. This is useful to
* ensure all definitions have been parsed before applying patches.
*/
window.addEventListener('load', resolve);
window.addEventListener("load", resolve);
});
}
/**
@@ -467,11 +573,13 @@ export const patchesAppliedPromise = makeDeferred();
* therefore they should be processed *at most* asynchronously at
* "Promise time".
*/
await new Promise(resolve => setTimeout(resolve));
await new Promise((resolve) => setTimeout(resolve));
for (const patch of patches) {
const definition = registry.get(patch.name);
if (!definition) {
throw new Error(`Cannot patch model "${patch.name}": there is no model registered under this name.`);
throw new Error(
`Cannot patch model "${patch.name}": there is no model registered under this name.`
);
}
if (patch.componentSetup) {
patchComponentSetup(patch);
+84 -60
View File
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { clear, FieldCommand, link, unlink, unlinkAll } from '@mail/model/model_field_command';
import { IS_RECORD } from '@mail/model/model_core';
import { clear, FieldCommand, link, unlink, unlinkAll } from "@mail/model/model_field_command";
import { IS_RECORD } from "@mail/model/model_core";
/**
* Class whose instances represent field on a model.
@@ -9,7 +9,6 @@ import { IS_RECORD } from '@mail/model/model_core';
* `fields` on the model.
*/
export class ModelField {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -103,7 +102,7 @@ export class ModelField {
*
* Empty value is systematically undefined.
* null or empty string are NOT considered empty value, meaning these values meet the requirement.
*/
*/
this.required = required;
/**
* Determines the name of the function on record that returns the
@@ -154,7 +153,7 @@ export class ModelField {
*/
if (this.identifying) {
this.readonly = true;
if (this.model.identifyingMode === 'and') {
if (this.model.identifyingMode === "and") {
this.required = true;
}
}
@@ -173,9 +172,11 @@ export class ModelField {
const relatedPathSet = new Set();
for (const [compareRule, pathAsString] of sort) {
relatedPathSet.add(pathAsString);
this.sort.push([compareRule, pathAsString.split('.')]);
this.sort.push([compareRule, pathAsString.split(".")]);
}
this.sortedFieldSplittedPaths = [...relatedPathSet].map(pathAsString => pathAsString.split('.'));
this.sortedFieldSplittedPaths = [...relatedPathSet].map((pathAsString) =>
pathAsString.split(".")
);
}
}
@@ -186,7 +187,7 @@ export class ModelField {
* @returns {Object}
*/
static attr(options) {
return Object.assign({ fieldType: 'attribute' }, options);
return Object.assign({ fieldType: "attribute" }, options);
}
/**
@@ -197,7 +198,10 @@ export class ModelField {
* @returns {Object}
*/
static many(modelName, options) {
return ModelField._relation(modelName, Object.assign({}, options, { relationType: 'many' }));
return ModelField._relation(
modelName,
Object.assign({}, options, { relationType: "many" })
);
}
/**
@@ -208,7 +212,7 @@ export class ModelField {
* @returns {Object}
*/
static one(modelName, options) {
return ModelField._relation(modelName, Object.assign({}, options, { relationType: 'one' }));
return ModelField._relation(modelName, Object.assign({}, options, { relationType: "one" }));
}
/**
@@ -224,7 +228,7 @@ export class ModelField {
*/
clear(record, options) {
let hasChanged = false;
if (this.fieldType === 'relation') {
if (this.fieldType === "relation") {
if (this.parseAndExecuteCommands(record, unlinkAll(), options)) {
hasChanged = true;
}
@@ -245,9 +249,9 @@ export class ModelField {
* @param {Record} record
*/
computeRelated(record) {
const [relationName, relatedFieldName] = this.related.split('.');
const [relationName, relatedFieldName] = this.related.split(".");
const relationField = this.model.__fieldMap.get(relationName);
if (relationField.relationType === 'many') {
if (relationField.relationType === "many") {
const newVal = [];
for (const otherRecord of record[relationName]) {
const otherValue = otherRecord[relatedFieldName];
@@ -261,7 +265,7 @@ export class ModelField {
}
}
}
if (this.fieldType === 'relation') {
if (this.fieldType === "relation") {
return newVal;
}
return newVal;
@@ -272,7 +276,7 @@ export class ModelField {
if (newVal === undefined) {
return clear();
}
if (this.fieldType === 'relation') {
if (this.fieldType === "relation") {
return newVal;
}
return newVal;
@@ -291,25 +295,38 @@ export class ModelField {
return [newVal];
} else if (newVal instanceof Array && newVal[0] instanceof FieldCommand) {
return newVal;
} else if (this.fieldType === 'relation') {
if (newVal instanceof Array && newVal[0] instanceof Array && ['clear', 'insert', 'insert-and-replace', 'insert-and-unlink', 'link', 'replace', 'unlink', 'unlink-all'].includes(newVal[0][0])) {
} else if (this.fieldType === "relation") {
if (
newVal instanceof Array &&
newVal[0] instanceof Array &&
[
"clear",
"insert",
"insert-and-replace",
"insert-and-unlink",
"link",
"replace",
"unlink",
"unlink-all",
].includes(newVal[0][0])
) {
// newVal: [['insert', ...], ...]
return newVal.map(([name, value]) => new FieldCommand(name, value));
} else if (newVal instanceof Array && newVal[0] && !newVal[0][IS_RECORD]) {
// newVal: [data, ...]
return [new FieldCommand('insert-and-replace', newVal)];
return [new FieldCommand("insert-and-replace", newVal)];
} else if (newVal instanceof Array && newVal[0]) {
// newVal: [record, ...]
return [new FieldCommand('replace', newVal)];
return [new FieldCommand("replace", newVal)];
} else if (!newVal[IS_RECORD]) {
// newVal: data
return [new FieldCommand('insert-and-replace', newVal)];
return [new FieldCommand("insert-and-replace", newVal)];
} else {
// newVal: record
return [new FieldCommand('replace', newVal)];
return [new FieldCommand("replace", newVal)];
}
} else {
return [new FieldCommand('set', newVal)];
return [new FieldCommand("set", newVal)];
}
}
@@ -333,11 +350,11 @@ export class ModelField {
* @returns {any}
*/
get(record) {
if (this.fieldType === 'attribute') {
if (this.fieldType === "attribute") {
return this.read(record);
}
if (this.fieldType === 'relation') {
if (this.relationType === 'one') {
if (this.fieldType === "relation") {
if (this.relationType === "one") {
return this.read(record);
}
return [...this.read(record)];
@@ -375,75 +392,79 @@ export class ModelField {
for (const command of commandList) {
const commandName = command.name;
const newVal = command.value;
if (this.fieldType === 'attribute') {
if (this.fieldType === "attribute") {
switch (commandName) {
case 'clear':
case "clear":
if (this.clear(record, options)) {
hasChanged = true;
}
break;
case 'decrement':
case "decrement":
if (this.decrement(record, newVal)) {
hasChanged = true;
}
break;
case 'increment':
case "increment":
if (this.increment(record, newVal)) {
hasChanged = true;
}
break;
case 'set':
case "set":
if (this._setAttribute(record, newVal)) {
hasChanged = true;
}
break;
default:
throw new Error(`Field "${this}" (${this.fieldType} type) does not support command "${commandName}"`);
throw new Error(
`Field "${this}" (${this.fieldType} type) does not support command "${commandName}"`
);
}
} else if (this.fieldType === 'relation') {
} else if (this.fieldType === "relation") {
switch (commandName) {
case 'clear':
case "clear":
if (this.clear(record, options)) {
hasChanged = true;
}
break;
case 'insert':
case "insert":
if (this._setRelationInsert(record, newVal, options)) {
hasChanged = true;
}
break;
case 'insert-and-replace':
case "insert-and-replace":
if (this._setRelationInsertAndReplace(record, newVal, options)) {
hasChanged = true;
}
break;
case 'insert-and-unlink':
case "insert-and-unlink":
if (this._setRelationInsertAndUnlink(record, newVal, options)) {
hasChanged = true;
}
break;
case 'link':
case "link":
if (this._setRelationLink(record, newVal, options)) {
hasChanged = true;
}
break;
case 'replace':
case "replace":
if (this._setRelationReplace(record, newVal, options)) {
hasChanged = true;
}
break;
case 'unlink':
case "unlink":
if (this._setRelationUnlink(record, newVal, options)) {
hasChanged = true;
}
break;
case 'unlink-all':
case "unlink-all":
if (this._setRelationUnlink(record, this.get(record), options)) {
hasChanged = true;
}
break;
default:
throw new Error(`Field "${this}" (${this.fieldType} type) does not support command "${commandName}"`);
throw new Error(
`Field "${this}" (${this.fieldType} type) does not support command "${commandName}"`
);
}
}
}
@@ -478,10 +499,13 @@ export class ModelField {
* @param {Object} [options]
*/
static _relation(modelName, options) {
return Object.assign({
fieldType: 'relation',
to: modelName,
}, options);
return Object.assign(
{
fieldType: "relation",
to: modelName,
},
options
);
}
/**
@@ -496,7 +520,7 @@ export class ModelField {
* @returns {Record[]}
*/
_convertX2ManyValue(newValue, { hasToVerify = true } = {}) {
if (typeof newValue[Symbol.iterator] === 'function') {
if (typeof newValue[Symbol.iterator] === "function") {
if (hasToVerify) {
for (const value of newValue) {
this._verifyRelationalValue(value);
@@ -523,10 +547,10 @@ export class ModelField {
_insertOtherRecord(record, data, options) {
const otherModel = record.models[this.to];
const otherField = otherModel.__fieldMap.get(this.inverse);
const isMulti = typeof data[Symbol.iterator] === 'function';
const isMulti = typeof data[Symbol.iterator] === "function";
const dataList = isMulti ? data : [data];
for (const recordData of dataList) {
if (otherField.relationType === 'one') {
if (otherField.relationType === "one") {
recordData[this.inverse] = record;
} else {
recordData[this.inverse] = link(record);
@@ -612,9 +636,9 @@ export class ModelField {
*/
_setRelationLink(record, newValue, options) {
switch (this.relationType) {
case 'many':
case "many":
return this._setRelationLinkX2Many(record, newValue, options);
case 'one':
case "one":
return this._setRelationLinkX2One(record, newValue, options);
}
}
@@ -705,7 +729,7 @@ export class ModelField {
* @returns {boolean} whether the value changed for the current field
*/
_setRelationReplace(record, newValue, options) {
if (this.relationType === 'one') {
if (this.relationType === "one") {
// for x2one replace is just link
return this._setRelationLinkX2One(record, newValue, options);
}
@@ -771,9 +795,9 @@ export class ModelField {
*/
_setRelationUnlink(record, newValue, options) {
switch (this.relationType) {
case 'many':
case "many":
return this._setRelationUnlinkX2Many(record, newValue, options);
case 'one':
case "one":
return this._setRelationUnlinkX2One(record, options);
}
}
@@ -792,10 +816,7 @@ export class ModelField {
* @returns {boolean} whether the value changed for the current field
*/
_setRelationUnlinkX2Many(record, newValue, { hasToUpdateInverse = true } = {}) {
const recordsToUnlink = this._convertX2ManyValue(
newValue,
{ hasToVerify: false }
);
const recordsToUnlink = this._convertX2ManyValue(newValue, { hasToVerify: false });
const otherRecords = this.read(record);
let hasChanged = false;
@@ -884,10 +905,14 @@ export class ModelField {
*/
_verifyRelationalValue(record) {
if (!record) {
throw Error(`record is undefined. Did you try to link() or insert() empty value? Considering clear() instead.`);
throw Error(
`record is undefined. Did you try to link() or insert() empty value? Considering clear() instead.`
);
}
if (!record[IS_RECORD]) {
throw Error(`${record} is not a record. Did you try to use link() instead of insert() with data?`);
throw Error(
`${record} is not a record. Did you try to use link() instead of insert() with data?`
);
}
const otherModel = record.modelManager.models[this.to];
if (otherModel.__records.has(record)) {
@@ -904,7 +929,6 @@ export class ModelField {
}
throw Error(`Record ${record} is not valid for relational field ${this.fieldName}.`);
}
}
export const attr = ModelField.attr;
@@ -40,7 +40,7 @@ class FieldCommand {
* @returns {FieldCommand}
*/
function clear() {
return new FieldCommand('clear');
return new FieldCommand("clear");
}
/**
@@ -52,7 +52,7 @@ function clear() {
* @returns {FieldCommand}
*/
function create(data) {
return new FieldCommand('create', data);
return new FieldCommand("create", data);
}
/**
@@ -64,7 +64,7 @@ function create(data) {
* @returns {FieldCommand}
*/
function decrement(amount = 1) {
return new FieldCommand('decrement', amount);
return new FieldCommand("decrement", amount);
}
/**
@@ -76,7 +76,7 @@ function decrement(amount = 1) {
* @returns {FieldCommand}
*/
function increment(amount = 1) {
return new FieldCommand('increment', amount);
return new FieldCommand("increment", amount);
}
/**
@@ -90,7 +90,7 @@ function increment(amount = 1) {
* @returns {FieldCommand}
*/
function insert(data) {
return new FieldCommand('insert', data);
return new FieldCommand("insert", data);
}
/**
@@ -104,7 +104,7 @@ function insert(data) {
* @returns {FieldCommand}
*/
export function insertAndUnlink(data = {}) {
return new FieldCommand('insert-and-unlink', data);
return new FieldCommand("insert-and-unlink", data);
}
/**
@@ -118,7 +118,7 @@ export function insertAndUnlink(data = {}) {
* @returns {FieldCommand}
*/
function link(newValue) {
return new FieldCommand('link', newValue);
return new FieldCommand("link", newValue);
}
/**
@@ -129,7 +129,7 @@ function link(newValue) {
* @param {any} newValue - value to be written on the field value.
*/
function set(newValue) {
return new FieldCommand('set', newValue);
return new FieldCommand("set", newValue);
}
/**
@@ -144,7 +144,7 @@ function set(newValue) {
* @returns {FieldCommand}
*/
function unlink(data) {
return new FieldCommand('unlink', data);
return new FieldCommand("unlink", data);
}
/**
@@ -155,18 +155,7 @@ function unlink(data) {
* @returns {FieldCommand}
*/
function unlinkAll() {
return new FieldCommand('unlink-all');
return new FieldCommand("unlink-all");
}
export {
FieldCommand,
clear,
create,
decrement,
increment,
insert,
link,
set,
unlink,
unlinkAll,
};
export { FieldCommand, clear, create, decrement, increment, insert, link, set, unlink, unlinkAll };
@@ -1,16 +1,15 @@
/** @odoo-module */
import { decrement, increment } from '@mail/model/model_field_command';
import { Listener } from '@mail/model/model_listener';
import { followRelations } from '@mail/model/model_utils';
import { cleanSearchTerm } from '@mail/utils/utils';
import { decrement, increment } from "@mail/model/model_field_command";
import { Listener } from "@mail/model/model_listener";
import { followRelations } from "@mail/model/model_utils";
import { cleanSearchTerm } from "@mail/utils/utils";
/**
* Defines a set containing the relation records of the given field on the given
* record. The behavior of this set depends on the field properties.
*/
export class RelationSet {
/**
* @param {Record} record
* @param {ModelField} field
@@ -53,7 +52,7 @@ export class RelationSet {
const listener = new Listener({
isPartOfUpdateCycle: true,
name: `sort of ${value} in ${this.field} of ${this.record}`,
onChange: info => {
onChange: (info) => {
// access all useful values of current record (and relations) to mark them as dependencies
this.record.modelManager.startListening(listener);
const compareDefinition = this.field.sort;
@@ -67,7 +66,7 @@ export class RelationSet {
const valA = followRelations(a, relatedPath);
const valB = followRelations(b, relatedPath);
switch (compareMethod) {
case 'truthy-first': {
case "truthy-first": {
if (valA === valB) {
break;
}
@@ -79,7 +78,7 @@ export class RelationSet {
}
break;
}
case 'falsy-first': {
case "falsy-first": {
if (valA === valB) {
break;
}
@@ -91,8 +90,8 @@ export class RelationSet {
}
break;
}
case 'case-insensitive-asc': {
if (typeof valA !== 'string' || typeof valB !== 'string') {
case "case-insensitive-asc": {
if (typeof valA !== "string" || typeof valB !== "string") {
break;
}
const cleanedValA = cleanSearchTerm(valA);
@@ -102,23 +101,23 @@ export class RelationSet {
}
return cleanedValA < cleanedValB ? -1 : 1;
}
case 'smaller-first':
if (typeof valA !== 'number' || typeof valB !== 'number') {
case "smaller-first":
if (typeof valA !== "number" || typeof valB !== "number") {
break;
}
if (valA === valB) {
break;
}
return valA - valB;
case 'greater-first':
if (typeof valA !== 'number' || typeof valB !== 'number') {
case "greater-first":
if (typeof valA !== "number" || typeof valB !== "number") {
break;
}
if (valA === valB) {
break;
}
return valB - valA;
case 'most-recent-first':
case "most-recent-first":
if (!(valA instanceof Date) || !(valB instanceof Date)) {
break;
}
@@ -127,7 +126,9 @@ export class RelationSet {
}
return valB - valA;
default:
throw Error(`sort compare method "${compareMethod}" is not supported.`);
throw Error(
`sort compare method "${compareMethod}" is not supported.`
);
}
}
return 0;
@@ -142,20 +143,23 @@ export class RelationSet {
},
});
this.sortListenerByValue.set(value, listener);
listener.onChange({ reason: 'initial call' });
listener.onChange({ reason: "initial call" });
}
for (const { from: contributionFieldName, to: sumFieldName } of this.field.sumContributions) {
for (const { from: contributionFieldName, to: sumFieldName } of this.field
.sumContributions) {
this.sumByValueByField.get(sumFieldName).set(value, 0);
const listener = new Listener({
isPartOfUpdateCycle: true,
name: `sum of field(${sumFieldName}) of ${this.record} from field(${contributionFieldName}) of ${value} through relation ${this.field}`,
onChange: info => {
onChange: (info) => {
// listen to the contribution to mark it as dependency
this.record.modelManager.startListening(listener);
const contribution = value[contributionFieldName];
this.record.modelManager.stopListening(listener);
// retrieve the previous contribution and update it
const previousContribution = this.sumByValueByField.get(sumFieldName).get(value);
const previousContribution = this.sumByValueByField
.get(sumFieldName)
.get(value);
this.sumByValueByField.get(sumFieldName).set(value, contribution);
this.record.modelManager._update(
this.record,
@@ -165,12 +169,12 @@ export class RelationSet {
increment(contribution),
],
},
{ allowWriteReadonly: true },
{ allowWriteReadonly: true }
);
},
});
this.sumListenerByValueByField.get(sumFieldName).set(value, listener);
listener.onChange({ reason: 'initial call' });
listener.onChange({ reason: "initial call" });
}
}
@@ -205,7 +209,11 @@ export class RelationSet {
// remove contribution of current value
const contribution = this.sumByValueByField.get(sumFieldName).get(value);
this.sumByValueByField.get(sumFieldName).delete(value);
this.record.modelManager._update(this.record, { [sumFieldName]: decrement(contribution) }, { allowWriteReadonly: true });
this.record.modelManager._update(
this.record,
{ [sumFieldName]: decrement(contribution) },
{ allowWriteReadonly: true }
);
// remove listener on current value
const listener = this.sumListenerByValueByField.get(sumFieldName).get(value);
this.sumListenerByValueByField.get(sumFieldName).delete(value);
@@ -231,5 +239,4 @@ export class RelationSet {
}
return this.set[Symbol.iterator]();
}
}
@@ -1,7 +1,6 @@
/** @odoo-module **/
export class ModelIndexAnd {
constructor(model) {
this.model = model;
this.recordsByValuesTree = new Map();
@@ -20,7 +19,9 @@ export class ModelIndexAnd {
for (const fieldName of this.model.__identifyingFieldNames) {
const fieldValue = data[fieldName];
if (fieldValue === undefined) {
throw new Error(`Identifying field "${fieldName}" is lacking a value on ${this.model} with 'and' identifying mode`);
throw new Error(
`Identifying field "${fieldName}" is lacking a value on ${this.model} with 'and' identifying mode`
);
}
valuesOfRecord.push(fieldValue);
if (!res.has(fieldValue)) {
@@ -39,7 +40,9 @@ export class ModelIndexAnd {
for (const fieldName of this.model.__identifyingFieldNames) {
const fieldValue = data[fieldName];
if (fieldValue === undefined) {
throw new Error(`Identifying field "${fieldName}" is lacking a value on ${this.model} with 'and' identifying mode`);
throw new Error(
`Identifying field "${fieldName}" is lacking a value on ${this.model} with 'and' identifying mode`
);
}
res = res.get(fieldValue);
if (!res) {
@@ -66,5 +69,4 @@ export class ModelIndexAnd {
}
this.valuesByRecords.delete(record);
}
}
+30 -22
View File
@@ -1,7 +1,6 @@
/** @odoo-module **/
export class ModelIndexXor {
constructor(model) {
this.model = model;
this.recordsByValuesByFields = new Map();
@@ -9,16 +8,21 @@ export class ModelIndexXor {
}
addRecord(record, data) {
const [fieldName, fieldValue] = [...this.model.__identifyingFieldNames].reduce(([fieldName, fieldValue], currentFieldName) => {
const currentFieldValue = data[currentFieldName];
if (currentFieldValue === undefined) {
return [fieldName, fieldValue];
}
if (fieldName) {
throw new Error(`Identifying field on ${this.model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".`);
}
return [currentFieldName, currentFieldValue];
}, [undefined, undefined]);
const [fieldName, fieldValue] = [...this.model.__identifyingFieldNames].reduce(
([fieldName, fieldValue], currentFieldName) => {
const currentFieldValue = data[currentFieldName];
if (currentFieldValue === undefined) {
return [fieldName, fieldValue];
}
if (fieldName) {
throw new Error(
`Identifying field on ${this.model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".`
);
}
return [currentFieldName, currentFieldValue];
},
[undefined, undefined]
);
if (!this.recordsByValuesByFields.has(fieldName)) {
this.recordsByValuesByFields.set(fieldName, new Map());
}
@@ -27,16 +31,21 @@ export class ModelIndexXor {
}
findRecord(data) {
const [fieldName, fieldValue] = [...this.model.__identifyingFieldNames].reduce(([fieldName, fieldValue], currentFieldName) => {
const currentFieldValue = data[currentFieldName];
if (currentFieldValue === undefined) {
return [fieldName, fieldValue];
}
if (fieldName) {
throw new Error(`Identifying field on ${this.model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".`);
}
return [currentFieldName, currentFieldValue];
}, [undefined, undefined]);
const [fieldName, fieldValue] = [...this.model.__identifyingFieldNames].reduce(
([fieldName, fieldValue], currentFieldName) => {
const currentFieldValue = data[currentFieldName];
if (currentFieldValue === undefined) {
return [fieldName, fieldValue];
}
if (fieldName) {
throw new Error(
`Identifying field on ${this.model} with 'xor' identifying mode should have only one of the conditional values given in data. Currently have both "${fieldName}" and "${currentFieldName}".`
);
}
return [currentFieldName, currentFieldValue];
},
[undefined, undefined]
);
if (!this.recordsByValuesByFields.has(fieldName)) {
this.recordsByValuesByFields.set(fieldName, new Map());
}
@@ -48,5 +57,4 @@ export class ModelIndexXor {
this.recordsByValuesByFields.get(fieldName).delete(fieldValue);
this.fieldNameAndValueByRecords.delete(record);
}
}
+308 -187
View File
@@ -1,20 +1,23 @@
/** @odoo-module **/
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { IS_RECORD, patchesAppliedPromise, registry } from '@mail/model/model_core';
import { ModelField } from '@mail/model/model_field';
import { ModelIndexAnd } from '@mail/model/model_index_and';
import { ModelIndexXor } from '@mail/model/model_index_xor';
import { FieldCommand, unlinkAll } from '@mail/model/model_field_command';
import { RelationSet } from '@mail/model/model_field_relation_set';
import { Listener } from '@mail/model/model_listener';
import { followRelations } from '@mail/model/model_utils';
import { makeDeferred } from '@mail/utils/deferred';
import { registerMessagingComponent, unregisterMessagingComponent } from '@mail/utils/messaging_component';
import { useRefToModel } from "@mail/component_hooks/use_ref_to_model";
import { IS_RECORD, patchesAppliedPromise, registry } from "@mail/model/model_core";
import { ModelField } from "@mail/model/model_field";
import { ModelIndexAnd } from "@mail/model/model_index_and";
import { ModelIndexXor } from "@mail/model/model_index_xor";
import { FieldCommand, unlinkAll } from "@mail/model/model_field_command";
import { RelationSet } from "@mail/model/model_field_relation_set";
import { Listener } from "@mail/model/model_listener";
import { followRelations } from "@mail/model/model_utils";
import { makeDeferred } from "@mail/utils/deferred";
import {
registerMessagingComponent,
unregisterMessagingComponent,
} from "@mail/utils/messaging_component";
import { LegacyComponent } from '@web/legacy/legacy_component';
import { LegacyComponent } from "@web/legacy/legacy_component";
import { Component } from '@odoo/owl';
import { Component } from "@odoo/owl";
/**
* Object that manage models and records, notably their update cycle: whenever
@@ -23,7 +26,6 @@ import { Component } from '@odoo/owl';
* direct field & and computed field updates.
*/
export class ModelManager {
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -117,7 +119,7 @@ export class ModelManager {
/**
* Create the messaging singleton record.
*/
this.models['Messaging'].insert(values);
this.models["Messaging"].insert(values);
this.messagingCreatedPromise.resolve();
await this.messaging.start();
this.messagingInitializedPromise.resolve();
@@ -254,7 +256,7 @@ export class ModelManager {
* @returns {Record|Record[]} created or updated record(s).
*/
insert(model, data) {
const isMulti = typeof data[Symbol.iterator] === 'function';
const isMulti = typeof data[Symbol.iterator] === "function";
const records = this._insert(model, isMulti ? data : [data]);
this._flushUpdateCycle();
return isMulti ? records : records[0];
@@ -280,7 +282,7 @@ export class ModelManager {
*/
multiModelInsert(data) {
for (const [modelName, recordsData] of Object.entries(data)) {
const isMulti = typeof recordsData[Symbol.iterator] === 'function';
const isMulti = typeof recordsData[Symbol.iterator] === "function";
this._insert(this.models[modelName], isMulti ? recordsData : [recordsData]);
}
this._flushUpdateCycle();
@@ -292,12 +294,12 @@ export class ModelManager {
* @returns {Messaging|undefined}
*/
get messaging() {
if (!this.models['Messaging']) {
if (!this.models["Messaging"]) {
return undefined;
}
// Use "findFromIdentifyingData" specifically to ensure the record still
// exists and to ensure listeners are properly notified of this access.
return this.models['Messaging'].findFromIdentifyingData({});
return this.models["Messaging"].findFromIdentifyingData({});
}
/**
@@ -397,36 +399,38 @@ export class ModelManager {
*/
_applyModelDefinition(model) {
const definition = registry.get(model.name);
if (definition.get('template')) {
const ComponentClass = definition.get('isLegacyComponent') ? LegacyComponent : Component;
if (definition.get("template")) {
const ComponentClass = definition.get("isLegacyComponent")
? LegacyComponent
: Component;
const ModelComponent = { [model.name]: class extends ComponentClass {} }[model.name];
Object.assign(ModelComponent, {
props: { record: Object },
template: definition.get('template'),
template: definition.get("template"),
});
registerMessagingComponent(ModelComponent);
model.__messagingComponent = ModelComponent;
model.__fieldAndRefNames = [];
}
Object.assign(model, Object.fromEntries(definition.get('modelMethods')));
Object.assign(model.prototype, Object.fromEntries(definition.get('recordMethods')));
for (const [getterName, getter] of definition.get('modelGetters')) {
Object.assign(model, Object.fromEntries(definition.get("modelMethods")));
Object.assign(model.prototype, Object.fromEntries(definition.get("recordMethods")));
for (const [getterName, getter] of definition.get("modelGetters")) {
Object.defineProperty(model, getterName, { get: getter });
}
for (const [getterName, getter] of definition.get('recordGetters')) {
for (const [getterName, getter] of definition.get("recordGetters")) {
Object.defineProperty(model.prototype, getterName, { get: getter });
}
// Make model manager accessible from model.
model.modelManager = this;
model.fields = {};
model.identifyingMode = definition.get('identifyingMode');
model.identifyingMode = definition.get("identifyingMode");
model.__records = new Set();
model.__recordCount = 0;
model.__recordsIndex = (() => {
switch (model.identifyingMode) {
case 'and':
case "and":
return new ModelIndexAnd(model);
case 'xor':
case "xor":
return new ModelIndexXor(model);
}
})();
@@ -440,109 +444,150 @@ export class ModelManager {
*/
_checkDeclaredFieldsOnModels() {
for (const model of Object.values(this.models)) {
for (const [fieldName, field] of registry.get(model.name).get('fields')) {
for (const [fieldName, field] of registry.get(model.name).get("fields")) {
// 0. Forbidden name.
if (fieldName in model.prototype) {
throw new Error(`Field ${model}/${fieldName} has a forbidden name.`);
}
// 1. Field type is required.
if (!(['attribute', 'relation'].includes(field.fieldType))) {
throw new Error(`Field ${model}/${fieldName} has unsupported type "${field.fieldType}".`);
if (!["attribute", "relation"].includes(field.fieldType)) {
throw new Error(
`Field ${model}/${fieldName} has unsupported type "${field.fieldType}".`
);
}
// 2. Invalid keys based on field type.
if (field.fieldType === 'attribute') {
const invalidKeys = Object.keys(field).filter(key =>
![
'compute',
'default',
'fieldType',
'identifying',
'readonly',
'ref',
'related',
'required',
'sum',
].includes(key)
if (field.fieldType === "attribute") {
const invalidKeys = Object.keys(field).filter(
(key) =>
![
"compute",
"default",
"fieldType",
"identifying",
"readonly",
"ref",
"related",
"required",
"sum",
].includes(key)
);
if (invalidKeys.length > 0) {
throw new Error(`Field ${model}/${fieldName} contains some invalid keys: "${invalidKeys.join(", ")}".`);
throw new Error(
`Field ${model}/${fieldName} contains some invalid keys: "${invalidKeys.join(
", "
)}".`
);
}
}
if (field.fieldType === 'relation') {
const invalidKeys = Object.keys(field).filter(key =>
![
'compute',
'default',
'fieldType',
'identifying',
'inverse',
'isCausal',
'readonly',
'related',
'relationType',
'required',
'sort',
'to',
].includes(key)
if (field.fieldType === "relation") {
const invalidKeys = Object.keys(field).filter(
(key) =>
![
"compute",
"default",
"fieldType",
"identifying",
"inverse",
"isCausal",
"readonly",
"related",
"relationType",
"required",
"sort",
"to",
].includes(key)
);
if (invalidKeys.length > 0) {
throw new Error(`Field ${model}/${fieldName} contains some invalid keys: "${invalidKeys.join(", ")}".`);
throw new Error(
`Field ${model}/${fieldName} contains some invalid keys: "${invalidKeys.join(
", "
)}".`
);
}
if (!this.models[field.to]) {
throw new Error(`Relational field ${model}/${fieldName} targets to unknown model name "${field.to}".`);
throw new Error(
`Relational field ${model}/${fieldName} targets to unknown model name "${field.to}".`
);
}
if (field.required && field.relationType !== 'one') {
throw new Error(`Relational field ${model}/${fieldName} has "required" true with a relation of type "${field.relationType}" but "required" is only supported for "one".`);
if (field.required && field.relationType !== "one") {
throw new Error(
`Relational field ${model}/${fieldName} has "required" true with a relation of type "${field.relationType}" but "required" is only supported for "one".`
);
}
if (field.sort && field.relationType !== 'many') {
throw new Error(`Relational field "${model}/${fieldName}" has "sort" with a relation of type "${field.relationType}" but "sort" is only supported for "many".`);
if (field.sort && field.relationType !== "many") {
throw new Error(
`Relational field "${model}/${fieldName}" has "sort" with a relation of type "${field.relationType}" but "sort" is only supported for "many".`
);
}
}
// 3. Check for redundant or unsupported attributes on identifying fields.
if (field.identifying) {
if ('readonly' in field) {
throw new Error(`Identifying field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for identifying fields).`);
if ("readonly" in field) {
throw new Error(
`Identifying field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for identifying fields).`
);
}
if ('required' in field && model.identifyingMode === 'and') {
throw new Error(`Identifying field ${model}/${fieldName} has unnecessary "required" attribute (required is implicit for AND identifying fields).`);
if ("required" in field && model.identifyingMode === "and") {
throw new Error(
`Identifying field ${model}/${fieldName} has unnecessary "required" attribute (required is implicit for AND identifying fields).`
);
}
if ('default' in field) {
throw new Error(`Identifying field ${model}/${fieldName} has "default" attribute, but default values are not supported for identifying fields.`);
if ("default" in field) {
throw new Error(
`Identifying field ${model}/${fieldName} has "default" attribute, but default values are not supported for identifying fields.`
);
}
}
// 4. Computed field.
if (field.compute) {
if (typeof field.compute !== 'function') {
throw new Error(`Property "compute" of field ${model}/${fieldName} must be a string (instance method name) or a function (the actual compute).`);
if (typeof field.compute !== "function") {
throw new Error(
`Property "compute" of field ${model}/${fieldName} must be a string (instance method name) or a function (the actual compute).`
);
}
if ('readonly' in field) {
throw new Error(`Computed field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for computed fields).`);
if ("readonly" in field) {
throw new Error(
`Computed field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for computed fields).`
);
}
}
// 5. Related field.
if (field.related) {
if (field.compute) {
throw new Error(`field ${model}/${fieldName} cannot be a related and compute field at the same time.`);
throw new Error(
`field ${model}/${fieldName} cannot be a related and compute field at the same time.`
);
}
if (!(typeof field.related === 'string')) {
throw new Error(`Property "related" of field ${model}/${fieldName} has invalid format.`);
if (!(typeof field.related === "string")) {
throw new Error(
`Property "related" of field ${model}/${fieldName} has invalid format.`
);
}
const [relationName, relatedFieldName, other] = field.related.split('.');
const [relationName, relatedFieldName, other] = field.related.split(".");
if (!relationName || !relatedFieldName || other) {
throw new Error(`Property "related" of field ${model}/${fieldName} has invalid format.`);
throw new Error(
`Property "related" of field ${model}/${fieldName} has invalid format.`
);
}
// find relation on self or parents.
let relatedRelation;
let targetModel = model;
while (this.models[targetModel.name] && !relatedRelation) {
relatedRelation = registry.get(targetModel.name).get('fields').get(relationName);
relatedRelation = registry
.get(targetModel.name)
.get("fields")
.get(relationName);
targetModel = targetModel.__proto__;
}
if (!relatedRelation) {
throw new Error(`Related field ${model}/${fieldName} relates to unknown relation name "${relationName}".`);
throw new Error(
`Related field ${model}/${fieldName} relates to unknown relation name "${relationName}".`
);
}
if (relatedRelation.fieldType !== 'relation') {
throw new Error(`Related field ${model}/${fieldName} relates to non-relational field "${relationName}".`);
if (relatedRelation.fieldType !== "relation") {
throw new Error(
`Related field ${model}/${fieldName} relates to non-relational field "${relationName}".`
);
}
// Assuming related relation is valid...
// find field name on related model or any parents.
@@ -550,28 +595,38 @@ export class ModelManager {
let relatedField;
targetModel = relatedModel;
while (this.models[targetModel.name] && !relatedField) {
relatedField = registry.get(targetModel.name).get('fields').get(relatedFieldName);
relatedField = registry
.get(targetModel.name)
.get("fields")
.get(relatedFieldName);
targetModel = targetModel.__proto__;
}
if (!relatedField) {
throw new Error(`Related field ${model}/${fieldName} relates to unknown related model field "${relatedFieldName}".`);
throw new Error(
`Related field ${model}/${fieldName} relates to unknown related model field "${relatedFieldName}".`
);
}
if (relatedField.fieldType !== field.fieldType) {
throw new Error(`Related field ${model}/${fieldName} has mismatched type with its related model field.`);
throw new Error(
`Related field ${model}/${fieldName} has mismatched type with its related model field.`
);
}
if (
relatedField.fieldType === 'relation' &&
relatedField.to !== field.to
) {
throw new Error(`Related field ${model}/${fieldName} has mismatched target model name with its related model field.`);
if (relatedField.fieldType === "relation" && relatedField.to !== field.to) {
throw new Error(
`Related field ${model}/${fieldName} has mismatched target model name with its related model field.`
);
}
if ('readonly' in field) {
throw new Error(`Related field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for related fields).`);
if ("readonly" in field) {
throw new Error(
`Related field ${model}/${fieldName} has unnecessary "readonly" attribute (readonly is implicit for related fields).`
);
}
}
if (field.ref) {
if (!model.__messagingComponent) {
throw new Error(`Field ${model}/${fieldName} has a 'ref' attribute but its model is not linked to any component.`);
throw new Error(
`Field ${model}/${fieldName} has a 'ref' attribute but its model is not linked to any component.`
);
}
}
}
@@ -584,17 +639,25 @@ export class ModelManager {
*/
_checkOnChangesOnModels() {
for (const model of Object.values(this.models)) {
for (const { dependencies, methodName } of registry.get(model.name).get('onChanges')) {
for (const { dependencies, methodName } of registry.get(model.name).get("onChanges")) {
for (const dependency of dependencies) {
let currentModel = model;
let currentField;
for (const fieldName of dependency) {
if (!currentModel) {
throw new Error(`OnChange '${methodName}' defines a dependency with path '${dependency.join('.')}', but this dependency does not resolve: ${currentField} is not a relational field, therefore there is no relation to follow.`);
throw new Error(
`OnChange '${methodName}' defines a dependency with path '${dependency.join(
"."
)}', but this dependency does not resolve: ${currentField} is not a relational field, therefore there is no relation to follow.`
);
}
currentField = currentModel.__fieldMap.get(fieldName);
if (!currentField) {
throw new Error(`OnChange '${methodName}' defines a dependency with path '${dependency.join('.')}', but this path does not resolve: ${currentModel}/${fieldName} does not exist.`);
throw new Error(
`OnChange '${methodName}' defines a dependency with path '${dependency.join(
"."
)}', but this path does not resolve: ${currentModel}/${fieldName} does not exist.`
);
}
if (currentField.to) {
currentModel = this.models[currentField.to];
@@ -613,55 +676,79 @@ export class ModelManager {
*/
_checkProcessedFieldsOnModels() {
for (const model of Object.values(this.models)) {
if (!['and', 'xor'].includes(model.identifyingMode)) {
throw new Error(`Unsupported identifying mode "${model.identifyingMode}" on ${model}. Must be one of 'and' or 'xor'.`);
if (!["and", "xor"].includes(model.identifyingMode)) {
throw new Error(
`Unsupported identifying mode "${model.identifyingMode}" on ${model}. Must be one of 'and' or 'xor'.`
);
}
for (const field of model.__fieldList) {
const fieldName = field.fieldName;
if (!(['attribute', 'relation'].includes(field.fieldType))) {
if (!["attribute", "relation"].includes(field.fieldType)) {
throw new Error(`${field} has unsupported type "${field.fieldType}".`);
}
if (field.compute && field.related) {
throw new Error(`${field} cannot be a related and compute field at the same time.`);
throw new Error(
`${field} cannot be a related and compute field at the same time.`
);
}
if (field.fieldType === 'attribute') {
if (field.fieldType === "attribute") {
continue;
}
if (!field.relationType) {
throw new Error(`${field} must define a relation type in "relationType".`);
}
if (!(['many', 'one'].includes(field.relationType))) {
if (!["many", "one"].includes(field.relationType)) {
throw new Error(`${field} has invalid relation type "${field.relationType}".`);
}
if (!field.inverse) {
throw new Error(`${field} must define an inverse relation name in "inverse".`);
}
if (!field.to) {
throw new Error(`${field} must define a model name in "to" (1st positional parameter of relation field helpers).`);
throw new Error(
`${field} must define a model name in "to" (1st positional parameter of relation field helpers).`
);
}
const relatedModel = this.models[field.to];
if (!relatedModel) {
throw new Error(`${field} defines a relation to model ${field.to}, but there is no model registered with this name.`);
throw new Error(
`${field} defines a relation to model ${field.to}, but there is no model registered with this name.`
);
}
const inverseField = relatedModel.__fieldMap.get(field.inverse);
if (!inverseField) {
throw new Error(`${field} defines its inverse as field ${relatedModel}/${field.inverse}, but it does not exist.`);
throw new Error(
`${field} defines its inverse as field ${relatedModel}/${field.inverse}, but it does not exist.`
);
}
if (inverseField.inverse !== fieldName) {
throw new Error(`The name of ${field} does not match with the name defined in its inverse ${inverseField}.`);
throw new Error(
`The name of ${field} does not match with the name defined in its inverse ${inverseField}.`
);
}
if (![model.name, 'Record'].includes(inverseField.to)) {
throw new Error(`${field} has its inverse ${inverseField} referring to an invalid model (${inverseField.to}).`);
if (![model.name, "Record"].includes(inverseField.to)) {
throw new Error(
`${field} has its inverse ${inverseField} referring to an invalid model (${inverseField.to}).`
);
}
if (field.sort) {
for (const path of field.sortedFieldSplittedPaths) {
let currentField = field;
for (const fieldName of path) {
if (!currentField.to) {
throw new Error(`Field ${field} defines a sort with path '${path.join('.')}', but this path does not resolve: ${currentField} is not a relational field, therefore there is no relation to follow.`);
throw new Error(
`Field ${field} defines a sort with path '${path.join(
"."
)}', but this path does not resolve: ${currentField} is not a relational field, therefore there is no relation to follow.`
);
}
if (!this.models[currentField.to].__fieldMap.has(fieldName)) {
throw new Error(`Field ${field} defines a sort with path '${path.join('.')}', but this path does not resolve: ${this.models[currentField.to]}/${fieldName} does not exist.`);
throw new Error(
`Field ${field} defines a sort with path '${path.join(
"."
)}', but this path does not resolve: ${
this.models[currentField.to]
}/${fieldName} does not exist.`
);
}
currentField = this.models[currentField.to].__fieldMap.get(fieldName);
}
@@ -671,16 +758,22 @@ export class ModelManager {
for (const identifyingField of model.__identifyingFieldNames) {
const field = model.__fieldMap.get(identifyingField);
if (!field) {
throw new Error(`Identifying field "${model}/${identifyingField}" is not a field on ${model}.`);
throw new Error(
`Identifying field "${model}/${identifyingField}" is not a field on ${model}.`
);
}
if (field.to) {
if (field.relationType !== 'one') {
throw new Error(`Identifying field "${model}/${identifyingField}" has a relation of type "${field.relationType}" but identifying field is only supported for "one".`);
if (field.relationType !== "one") {
throw new Error(
`Identifying field "${model}/${identifyingField}" has a relation of type "${field.relationType}" but identifying field is only supported for "one".`
);
}
const relatedModel = this.models[field.to];
const inverseField = relatedModel.__fieldMap.get(field.inverse);
if (!inverseField.isCausal) {
throw new Error(`Identifying field "${model}/${identifyingField}" has an inverse "${inverseField}" not declared as "isCausal".`);
throw new Error(
`Identifying field "${model}/${identifyingField}" has an inverse "${inverseField}" not declared as "isCausal".`
);
}
}
}
@@ -719,26 +812,31 @@ export class ModelManager {
__values: new Map(),
[IS_RECORD]: true,
});
const record = owl.markRaw(!this.isDebug ? nonProxyRecord : new Proxy(nonProxyRecord, {
get: function getFromProxy(record, prop) {
if (
(model.__fieldMap && !model.__fieldMap.has(prop)) &&
!['_super', 'then', 'localId'].includes(prop) &&
typeof prop !== 'symbol' &&
!(prop in record)
) {
console.warn(`non-field read "${prop}" on ${record}`);
}
return record[prop];
},
}));
const record = owl.markRaw(
!this.isDebug
? nonProxyRecord
: new Proxy(nonProxyRecord, {
get: function getFromProxy(record, prop) {
if (
model.__fieldMap &&
!model.__fieldMap.has(prop) &&
!["_super", "then", "localId"].includes(prop) &&
typeof prop !== "symbol" &&
!(prop in record)
) {
console.warn(`non-field read "${prop}" on ${record}`);
}
return record[prop];
},
})
);
if (this.isDebug) {
record.__proxifiedRecord = record;
}
// Ensure X2many relations are Set initially (other fields can stay undefined).
for (const field of model.__fieldList) {
if (field.fieldType === 'relation') {
if (field.relationType === 'many') {
if (field.fieldType === "relation") {
if (field.relationType === "many") {
record.__values.set(field.fieldName, new RelationSet(record, field));
}
}
@@ -750,7 +848,7 @@ export class ModelManager {
/**
* Auto-bind record methods so that `this` always refer to the record.
*/
const recordMethods = registry.get(model.name).get('recordMethods');
const recordMethods = registry.get(model.name).get("recordMethods");
for (const methodName of recordMethods.keys()) {
record[methodName] = record[methodName].bind(record);
}
@@ -783,15 +881,15 @@ export class ModelManager {
if (!record.exists()) {
throw Error(`Cannot delete already deleted record ${record}.`);
}
const lifecycleHooks = registry.get(model.name).get('lifecycleHooks');
if (lifecycleHooks.has('_willDelete')) {
lifecycleHooks.get('_willDelete').call(record);
const lifecycleHooks = registry.get(model.name).get("lifecycleHooks");
if (lifecycleHooks.has("_willDelete")) {
lifecycleHooks.get("_willDelete").call(record);
}
for (const listener of record.__listeners) {
this.removeListener(listener);
}
for (const field of model.__fieldList) {
if (field.fieldType === 'relation') {
if (field.fieldType === "relation") {
// ensure inverses are properly unlinked
field.parseAndExecuteCommands(record, unlinkAll(), { allowWriteReadonly: true });
if (!record.exists()) {
@@ -835,7 +933,9 @@ export class ModelManager {
_ensureNoLockingListener() {
for (const listener of this._listeners) {
if (listener.isLocking) {
throw Error(`Model manager locked by ${listener}. It is not allowed to insert/update/delete from inside a lock.`);
throw Error(
`Model manager locked by ${listener}. It is not allowed to insert/update/delete from inside a lock.`
);
}
}
}
@@ -862,7 +962,11 @@ export class ModelManager {
this.startListening(listener);
const res = field.compute.call(record);
this.stopListening(listener);
this._update(record, { [field.fieldName]: res }, { allowWriteReadonly: true });
this._update(
record,
{ [field.fieldName]: res },
{ allowWriteReadonly: true }
);
},
});
listeners.push(listener);
@@ -875,7 +979,11 @@ export class ModelManager {
this.startListening(listener);
const res = field.computeRelated(record);
this.stopListening(listener);
this._update(record, { [field.fieldName]: res }, { allowWriteReadonly: true });
this._update(
record,
{ [field.fieldName]: res },
{ allowWriteReadonly: true }
);
},
});
listeners.push(listener);
@@ -905,9 +1013,9 @@ export class ModelManager {
if (!record.exists()) {
throw Error(`Cannot call _created for already deleted ${record}.`);
}
const lifecycleHooks = registry.get(record.constructor.name).get('lifecycleHooks');
if (lifecycleHooks.has('_created')) {
lifecycleHooks.get('_created').call(record);
const lifecycleHooks = registry.get(record.constructor.name).get("lifecycleHooks");
if (lifecycleHooks.has("_created")) {
lifecycleHooks.get("_created").call(record);
}
}
}
@@ -923,7 +1031,7 @@ export class ModelManager {
if (!record.exists()) {
throw Error(`Cannot call onChange for already deleted ${record}.`);
}
for (const onChange of registry.get(record.constructor.name).get('onChanges')) {
for (const onChange of registry.get(record.constructor.name).get("onChanges")) {
const listener = new Listener({
name: `${onChange} of ${record}`,
onChange: (info) => {
@@ -947,7 +1055,6 @@ export class ModelManager {
}
}
/**
* Executes the check of the required field of updated records.
*/
@@ -984,13 +1091,13 @@ export class ModelManager {
_generateModels() {
// Create the model through a class to give it a meaningful name to be
// displayed in stack traces and stuff.
const model = { 'Record': class {} }['Record'];
const model = { Record: class {} }["Record"];
this._applyModelDefinition(model);
// Record is generated separately and before the other models since
// it is the dependency of all of them.
const allModelNamesButRecord = [...registry.keys()].filter(name => name !== 'Record');
const allModelNamesButRecord = [...registry.keys()].filter((name) => name !== "Record");
for (const modelName of allModelNamesButRecord) {
const model = { [modelName]: class extends this.models['Record'] {} }[modelName];
const model = { [modelName]: class extends this.models["Record"] {} }[modelName];
this._applyModelDefinition(model);
}
/**
@@ -1046,17 +1153,15 @@ export class ModelManager {
* @returns {ModelField}
*/
_makeInverseRelationField(model, field) {
const inverseField = new ModelField(Object.assign(
{},
ModelField.many(model.name, { inverse: field.fieldName }),
{
const inverseField = new ModelField(
Object.assign({}, ModelField.many(model.name, { inverse: field.fieldName }), {
fieldName: `_inverse_${model}/${field.fieldName}`,
// Allows the inverse of an identifying field to be
// automatically generated.
isCausal: field.identifying,
model: this.models[field.to],
},
));
})
);
return inverseField;
}
@@ -1098,7 +1203,8 @@ export class ModelManager {
* @param {ModelField} field
*/
_markRecordFieldAsChanged(record, field) {
for (const [listener, infoList] of record.__listenersObservingFieldsOfRecord.get(field) || []) {
for (const [listener, infoList] of record.__listenersObservingFieldsOfRecord.get(field) ||
[]) {
this._markListenerToNotify(listener, {
listener,
reason: this.isDebug && `_update: ${field} of ${record}`,
@@ -1166,23 +1272,33 @@ export class ModelManager {
}
const commands = field.convertToFieldCommandList(data[fieldName]);
if (commands.length !== 1) {
throw new Error(`Identifying field "${model}/${fieldName}" should receive a single command.`);
throw new Error(
`Identifying field "${model}/${fieldName}" should receive a single command.`
);
}
const [command] = commands;
if (!(command instanceof FieldCommand)) {
throw new Error(`Identifying field "${model}/${fieldName}" should receive a command.`);
throw new Error(
`Identifying field "${model}/${fieldName}" should receive a command.`
);
}
if (!['insert-and-replace', 'replace'].includes(command._name)) {
throw new Error(`Identifying field "${model}/${fieldName}" should receive a "replace" or "insert-and-replace" command.`);
if (!["insert-and-replace", "replace"].includes(command._name)) {
throw new Error(
`Identifying field "${model}/${fieldName}" should receive a "replace" or "insert-and-replace" command.`
);
}
if (command._name === 'replace') {
if (command._name === "replace") {
continue;
}
if (!command._value) {
throw new Error(`Identifying field "${model}/${fieldName}" is lacking a relation value.`);
throw new Error(
`Identifying field "${model}/${fieldName}" is lacking a relation value.`
);
}
if (typeof command._value[Symbol.iterator] === 'function') {
throw new Error(`Identifying field "${model}/${fieldName}" should receive a single data object.`);
if (typeof command._value[Symbol.iterator] === "function") {
throw new Error(
`Identifying field "${model}/${fieldName}" should receive a single data object.`
);
}
const [record] = this._insert(this.models[field.to], [command._value]);
data[fieldName] = record;
@@ -1206,13 +1322,15 @@ export class ModelManager {
for (const model of Object.values(this.models)) {
const sumContributionsByFieldName = new Map();
// Make fields aware of their field name.
for (const [fieldName, fieldData] of registry.get(model.name).get('fields')) {
model.fields[fieldName] = new ModelField(Object.assign({}, fieldData, {
fieldName,
model,
}));
for (const [fieldName, fieldData] of registry.get(model.name).get("fields")) {
model.fields[fieldName] = new ModelField(
Object.assign({}, fieldData, {
fieldName,
model,
})
);
if (fieldData.sum) {
const [relationFieldName, contributionFieldName] = fieldData.sum.split('.');
const [relationFieldName, contributionFieldName] = fieldData.sum.split(".");
if (!sumContributionsByFieldName.has(relationFieldName)) {
sumContributionsByFieldName.set(relationFieldName, []);
}
@@ -1230,8 +1348,8 @@ export class ModelManager {
}
if (model.__messagingComponent) {
const setupFunctions = [];
if (registry.get(model.name).has('componentSetup')) {
setupFunctions.push(registry.get(model.name).get('componentSetup'));
if (registry.get(model.name).has("componentSetup")) {
setupFunctions.push(registry.get(model.name).get("componentSetup"));
}
if (model.__fieldAndRefNames.length > 0) {
setupFunctions.push(function () {
@@ -1256,7 +1374,7 @@ export class ModelManager {
*/
for (const model of Object.values(this.models)) {
for (const field of Object.values(model.fields)) {
if (field.fieldType !== 'relation') {
if (field.fieldType !== "relation") {
continue;
}
if (field.inverse) {
@@ -1302,9 +1420,7 @@ export class ModelManager {
model.__fieldMap = new Map(Object.entries(model.__combinedFields));
// List of all fields, for iterating.
model.__fieldList = [...model.__fieldMap.values()];
model.__requiredFieldsList = model.__fieldList.filter(
field => field.required
);
model.__requiredFieldsList = model.__fieldList.filter((field) => field.required);
model.__identifyingFieldNames = new Set();
for (const [fieldName, field] of model.__fieldMap) {
if (field.identifying) {
@@ -1312,11 +1428,13 @@ export class ModelManager {
}
// Add field accessors on model.
Object.defineProperty(model.prototype, fieldName, {
get: function getFieldValue() { // this is bound to record
get: function getFieldValue() {
// this is bound to record
const record = this.modelManager.isDebug ? this.__proxifiedRecord : this;
if (this.modelManager._listeners.size) {
let entryRecord = record.__listenersObservingRecord;
const reason = record.modelManager.isDebug && `getField - ${field} of ${record}`;
const entryRecord = record.__listenersObservingRecord;
const reason =
record.modelManager.isDebug && `getField - ${field} of ${record}`;
let entryField = record.__listenersObservingFieldsOfRecord.get(field);
if (!entryField) {
entryField = new Map();
@@ -1347,7 +1465,8 @@ export class ModelManager {
if (model.__messagingComponent) {
// Add field accessors on related component
Object.defineProperty(model.__messagingComponent.prototype, fieldName, {
get: function getFieldValue() { // this is bound to record
get: function getFieldValue() {
// this is bound to record
return this.props.record[fieldName];
},
});
@@ -1355,20 +1474,21 @@ export class ModelManager {
}
if (model.__messagingComponent) {
// Add record method accessors + localId
Object.defineProperty(model.__messagingComponent.prototype, 'localId', {
get: function getFieldValue() { // this is bound to record
Object.defineProperty(model.__messagingComponent.prototype, "localId", {
get: function getFieldValue() {
// this is bound to record
return this.props.record.localId;
},
});
const definition = registry.get(model.name);
for (const name of definition.get('recordMethods').keys()) {
for (const name of definition.get("recordMethods").keys()) {
Object.defineProperty(model.__messagingComponent.prototype, name, {
get() {
return this.props.record[name];
},
});
}
for (const name of definition.get('recordGetters').keys()) {
for (const name of definition.get("recordGetters").keys()) {
Object.defineProperty(model.__messagingComponent.prototype, name, {
get() {
return this.props.record[name];
@@ -1405,7 +1525,9 @@ export class ModelManager {
}
const field = model.__fieldMap.get(fieldName);
if (!field) {
console.warn(`Cannot create/update record with data unrelated to a field. (record: "${record}", non-field attempted update: "${fieldName}")`);
console.warn(
`Cannot create/update record with data unrelated to a field. (record: "${record}", non-field attempted update: "${fieldName}")`
);
continue;
}
const newVal = data[fieldName];
@@ -1423,5 +1545,4 @@ export class ModelManager {
}
return hasChanged;
}
}
+60 -58
View File
@@ -1,11 +1,11 @@
/** @odoo-module **/
import { attr, clear, insert, many, one, Model } from '@mail/model';
import { attr, clear, insert, many, one, Model } from "@mail/model";
import { markup } from '@odoo/owl';
import { markup } from "@odoo/owl";
Model({
name: 'Activity',
name: "Activity",
modelMethods: {
/**
* @param {Object} data
@@ -13,39 +13,39 @@ Model({
*/
convertData(data) {
const data2 = {};
if ('activity_category' in data) {
if ("activity_category" in data) {
data2.category = data.activity_category;
}
if ('can_write' in data) {
if ("can_write" in data) {
data2.canWrite = data.can_write;
}
if ('create_date' in data) {
if ("create_date" in data) {
data2.dateCreate = data.create_date;
}
if ('date_deadline' in data) {
if ("date_deadline" in data) {
data2.dateDeadline = data.date_deadline;
}
if ('chaining_type' in data) {
if ("chaining_type" in data) {
data2.chaining_type = data.chaining_type;
}
if ('icon' in data) {
if ("icon" in data) {
data2.icon = data.icon;
}
if ('id' in data) {
if ("id" in data) {
data2.id = data.id;
}
if ('note' in data) {
if ("note" in data) {
data2.rawNote = data.note;
}
if ('state' in data) {
if ("state" in data) {
data2.state = data.state;
}
if ('summary' in data) {
if ("summary" in data) {
data2.summary = data.summary;
}
// relation
if ('activity_type_id' in data) {
if ("activity_type_id" in data) {
if (!data.activity_type_id) {
data2.type = clear();
} else {
@@ -55,7 +55,7 @@ Model({
});
}
}
if ('create_uid' in data) {
if ("create_uid" in data) {
if (!data.create_uid) {
data2.creator = clear();
} else {
@@ -65,7 +65,7 @@ Model({
});
}
}
if ('mail_template_ids' in data) {
if ("mail_template_ids" in data) {
data2.mailTemplates = insert(data.mail_template_ids);
}
if (data.res_id && data.res_model) {
@@ -74,7 +74,7 @@ Model({
model: data.res_model,
});
}
if ('user_id' in data) {
if ("user_id" in data) {
if (!data.user_id) {
data2.assignee = clear();
} else {
@@ -84,7 +84,7 @@ Model({
});
}
}
if ('request_partner_id' in data) {
if ("request_partner_id" in data) {
if (!data.request_partner_id) {
data2.requestingPartner = clear();
} else {
@@ -104,8 +104,8 @@ Model({
*/
async deleteServerRecord() {
await this.messaging.rpc({
model: 'mail.activity',
method: 'unlink',
model: "mail.activity",
method: "unlink",
args: [[this.id]],
});
if (!this.exists()) {
@@ -126,25 +126,30 @@ Model({
}
},
async fetchAndUpdate() {
const [data] = await this.messaging.rpc({
model: 'mail.activity',
method: 'activity_format',
args: [this.id],
}, { shadow: true }).catch(e => {
const errorName = e.message && e.message.data && e.message.data.name;
if ([errorName, e.exceptionName].includes('odoo.exceptions.MissingError')) {
return [];
} else {
throw e;
}
});
const [data] = await this.messaging
.rpc(
{
model: "mail.activity",
method: "activity_format",
args: [this.id],
},
{ shadow: true }
)
.catch((e) => {
const errorName = e.message && e.message.data && e.message.data.name;
if ([errorName, e.exceptionName].includes("odoo.exceptions.MissingError")) {
return [];
} else {
throw e;
}
});
let shouldDelete = false;
if (data) {
this.update(this.constructor.convertData(data));
} else {
shouldDelete = true;
}
this.thread.fetchData(['activities', 'attachments', 'messages']);
this.thread.fetchData(["activities", "attachments", "messages"]);
if (shouldDelete) {
this.delete();
}
@@ -155,11 +160,11 @@ Model({
* @param {string|boolean} [param0.feedback=false]
*/
async markAsDone({ attachments = [], feedback = false }) {
const attachmentIds = attachments.map(attachment => attachment.id);
const attachmentIds = attachments.map((attachment) => attachment.id);
const thread = this.thread;
await this.messaging.rpc({
model: 'mail.activity',
method: 'action_feedback',
model: "mail.activity",
method: "action_feedback",
args: [[this.id]],
kwargs: {
attachment_ids: attachmentIds,
@@ -167,7 +172,7 @@ Model({
},
});
if (thread.exists()) {
thread.fetchData(['attachments', 'messages']);
thread.fetchData(["attachments", "messages"]);
}
if (!this.exists()) {
return;
@@ -181,13 +186,13 @@ Model({
async markAsDoneAndScheduleNext({ feedback }) {
const thread = this.thread;
const action = await this.messaging.rpc({
model: 'mail.activity',
method: 'action_feedback_schedule_next',
model: "mail.activity",
method: "action_feedback_schedule_next",
args: [[this.id]],
kwargs: { feedback },
});
if (thread.exists()) {
thread.fetchData(['activities', 'attachments', 'messages']);
thread.fetchData(["activities", "attachments", "messages"]);
}
if (this.exists()) {
this.delete();
@@ -195,27 +200,24 @@ Model({
if (!action) {
return;
}
await new Promise(resolve => {
this.env.services.action.doAction(
action,
{
onClose: resolve,
},
);
await new Promise((resolve) => {
this.env.services.action.doAction(action, {
onClose: resolve,
});
});
if (!thread.exists()) {
return;
}
thread.fetchData(['activities']);
thread.fetchData(["activities"]);
},
},
fields: {
activityViews: many('ActivityView', { inverse: 'activity' }),
assignee: one('User', { inverse: 'activitiesAsAssignee' }),
attachments: many('Attachment', { inverse: 'activities' }),
activityViews: many("ActivityView", { inverse: "activity" }),
assignee: one("User", { inverse: "activitiesAsAssignee" }),
attachments: many("Attachment", { inverse: "activities" }),
canWrite: attr({ default: false }),
category: attr(),
creator: one('User'),
creator: one("User"),
dateCreate: attr(),
dateDeadline: attr(),
/**
@@ -224,7 +226,7 @@ Model({
* In all other cases, this field value should not be trusted.
*/
feedbackBackup: attr(),
chaining_type: attr({ default: 'suggest' }),
chaining_type: attr({ default: "suggest" }),
icon: attr(),
id: attr({ identifying: true }),
isCurrentPartnerAssignee: attr({
@@ -236,7 +238,7 @@ Model({
},
default: false,
}),
mailTemplates: many('MailTemplate', { inverse: 'activities' }),
mailTemplates: many("MailTemplate", { inverse: "activities" }),
/**
* This value is meant to be returned by the server
* (and has been sanitized before stored into db).
@@ -250,7 +252,7 @@ Model({
* value, to reduce the size the empty note takes on the UI.
*/
compute() {
if (this.rawNote === '<p><br></p>') {
if (this.rawNote === "<p><br></p>") {
return clear();
}
return this.rawNote;
@@ -269,14 +271,14 @@ Model({
* Also, be useful when the assigned user is different from the
* "source" or "requesting" partner.
*/
requestingPartner: one('Partner'),
requestingPartner: one("Partner"),
state: attr(),
summary: attr(),
/**
* Determines to which "thread" (using `mail.activity.mixin` on the
* server) `this` belongs to.
*/
thread: one('Thread', { inverse: 'activities' }),
type: one('ActivityType', { inverse: 'activities' }),
thread: one("Thread", { inverse: "activities" }),
type: one("ActivityType", { inverse: "activities" }),
},
});
@@ -1,11 +1,11 @@
/** @odoo-module **/
import { attr, clear, one, Model } from '@mail/model';
import { attr, clear, one, Model } from "@mail/model";
Model({
name: 'ActivityButtonView',
template: 'mail.ActivityButtonView',
identifyingMode: 'xor',
name: "ActivityButtonView",
template: "mail.ActivityButtonView",
identifyingMode: "xor",
recordMethods: {
onClick(ev) {
if (!this.exists()) {
@@ -18,7 +18,9 @@ Model({
},
},
fields: {
activityListPopoverView: one('PopoverView', { inverse: 'activityButtonViewOwnerAsActivityList' }),
activityListPopoverView: one("PopoverView", {
inverse: "activityButtonViewOwnerAsActivityList",
}),
buttonClass: attr({
compute() {
if (!this.thread) {
@@ -26,26 +28,26 @@ Model({
}
const classes = [];
switch (this.webRecord.data.activity_state) {
case 'overdue':
classes.push('text-danger');
case "overdue":
classes.push("text-danger");
break;
case 'today':
classes.push('text-warning');
case "today":
classes.push("text-warning");
break;
case 'planned':
classes.push('text-success');
case "planned":
classes.push("text-success");
break;
default:
classes.push('text-muted');
classes.push("text-muted");
break;
}
switch (this.webRecord.data.activity_exception_decoration) {
case 'warning':
classes.push('text-warning');
case "warning":
classes.push("text-warning");
classes.push(this.webRecord.data.activity_exception_icon);
break;
case 'danger':
classes.push('text-danger');
case "danger":
classes.push("text-danger");
classes.push(this.webRecord.data.activity_exception_icon);
break;
default:
@@ -53,16 +55,23 @@ Model({
classes.push(this.webRecord.data.activity_type_icon);
break;
}
classes.push('fa-clock-o');
classes.push("fa-clock-o");
break;
}
return classes.join(' ');
return classes.join(" ");
},
}),
buttonRef: attr({ ref: 'button' }),
kanbanFieldActivityViewOwner: one('KanbanFieldActivityView', { identifying: true, inverse: 'activityButtonView' }),
listFieldActivityViewOwner: one('ListFieldActivityView', { identifying: true, inverse: 'activityButtonView' }),
thread: one('Thread', { required: true,
buttonRef: attr({ ref: "button" }),
kanbanFieldActivityViewOwner: one("KanbanFieldActivityView", {
identifying: true,
inverse: "activityButtonView",
}),
listFieldActivityViewOwner: one("ListFieldActivityView", {
identifying: true,
inverse: "activityButtonView",
}),
thread: one("Thread", {
required: true,
compute() {
if (this.kanbanFieldActivityViewOwner) {
return this.kanbanFieldActivityViewOwner.thread;
@@ -73,7 +82,8 @@ Model({
return clear();
},
}),
webRecord: attr({ required: true,
webRecord: attr({
required: true,
compute() {
if (this.kanbanFieldActivityViewOwner) {
return this.kanbanFieldActivityViewOwner.webRecord;
@@ -1,18 +1,20 @@
/** @odoo-module **/
import { attr, clear, many, one, Model } from '@mail/model';
import { attr, clear, many, one, Model } from "@mail/model";
Model({
name: 'ActivityCellView',
template: 'mail.ActivityCellView',
name: "ActivityCellView",
template: "mail.ActivityCellView",
recordMethods: {
onClick() {
this.update({ activityListPopoverView: this.activityListPopoverView ? clear() : {} });
},
},
fields: {
activityListPopoverView: one('PopoverView', { inverse: 'activityCellViewOwnerAsActivityList' }),
activityType: one('ActivityType', { required: true }),
activityListPopoverView: one("PopoverView", {
inverse: "activityCellViewOwnerAsActivityList",
}),
activityType: one("ActivityType", { required: true }),
closestDeadline: attr({ required: true }),
closestDeadlineFormatted: attr({
compute() {
@@ -20,21 +22,24 @@ Model({
// To remove year only if current year
if (moment().year() === moment(date).year()) {
return date.toLocaleDateString(moment().locale(), {
day: 'numeric', month: 'short'
day: "numeric",
month: "short",
});
} else {
return moment(date).format('ll');
return moment(date).format("ll");
}
},
}),
contentRef: attr({ ref: 'content' }),
filteredActivities: many('Activity', {
contentRef: attr({ ref: "content" }),
filteredActivities: many("Activity", {
compute() {
return this.thread.activities.filter(activity => activity.type === this.activityType);
return this.thread.activities.filter(
(activity) => activity.type === this.activityType
);
},
}),
id: attr({ identifying: true }),
reloadFunc: attr({ required: true }),
thread: one('Thread', { required: true }),
thread: one("Thread", { required: true }),
},
});
@@ -1,9 +1,9 @@
/** @odoo-module **/
import { attr, many, one, Model } from '@mail/model';
import { attr, many, one, Model } from "@mail/model";
Model({
name: 'ActivityGroup',
name: "ActivityGroup",
modelMethods: {
convertData(data) {
return {
@@ -28,16 +28,16 @@ Model({
* @private
*/
_onChangeTotalCount() {
if (this.type === 'activity' && this.total_count === 0) {
if (this.type === "activity" && this.total_count === 0) {
this.delete();
}
},
},
fields: {
actions: attr(),
activityGroupViews: many('ActivityGroupView', { inverse: 'activityGroup' }),
activityGroupViews: many("ActivityGroupView", { inverse: "activityGroup" }),
domain: attr(),
irModel: one('ir.model', { identifying: true, inverse: 'activityGroup' }),
irModel: one("ir.model", { identifying: true, inverse: "activityGroup" }),
overdue_count: attr({ default: 0 }),
planned_count: attr({ default: 0 }),
today_count: attr({ default: 0 }),
@@ -46,8 +46,8 @@ Model({
},
onChanges: [
{
dependencies: ['total_count', 'type'],
methodName: '_onChangeTotalCount',
dependencies: ["total_count", "type"],
methodName: "_onChangeTotalCount",
},
],
});
@@ -1,11 +1,11 @@
/** @odoo-module **/
import { one, Model } from '@mail/model';
import { one, Model } from "@mail/model";
import session from 'web.session';
import session from "web.session";
Model({
name: 'ActivityGroupView',
name: "ActivityGroupView",
recordMethods: {
/**
* @param {MouseEvent} ev
@@ -14,26 +14,29 @@ Model({
ev.stopPropagation();
this.activityMenuViewOwner.update({ isOpen: false });
const targetAction = $(ev.currentTarget);
const actionXmlid = targetAction.data('action_xmlid');
const actionXmlid = targetAction.data("action_xmlid");
if (actionXmlid) {
this.env.services.action.doAction(actionXmlid);
} else {
let domain = [['activity_ids.user_id', '=', session.uid]];
if (targetAction.data('domain')) {
domain = domain.concat(targetAction.data('domain'));
let domain = [["activity_ids.user_id", "=", session.uid]];
if (targetAction.data("domain")) {
domain = domain.concat(targetAction.data("domain"));
}
this.env.services['action'].doAction(
this.env.services["action"].doAction(
{
domain,
name: targetAction.data('model_name'),
res_model: targetAction.data('res_model'),
type: 'ir.actions.act_window',
views: this.activityGroup.irModel.availableWebViews.map(viewName => [false, viewName]),
name: targetAction.data("model_name"),
res_model: targetAction.data("res_model"),
type: "ir.actions.act_window",
views: this.activityGroup.irModel.availableWebViews.map((viewName) => [
false,
viewName,
]),
},
{
clearBreadcrumbs: true,
viewType: 'activity',
},
viewType: "activity",
}
);
}
},
@@ -45,37 +48,43 @@ Model({
// fetch the data from the button otherwise fetch the ones from the parent (.o_ActivityMenuView_activityGroup).
const data = _.extend({}, $(ev.currentTarget).data(), $(ev.target).data());
const context = {};
if (data.filter === 'my') {
context['search_default_activities_overdue'] = 1;
context['search_default_activities_today'] = 1;
if (data.filter === "my") {
context["search_default_activities_overdue"] = 1;
context["search_default_activities_today"] = 1;
} else {
context['search_default_activities_' + data.filter] = 1;
context["search_default_activities_" + data.filter] = 1;
}
// Necessary because activity_ids of mail.activity.mixin has auto_join
// So, duplicates are faking the count and "Load more" doesn't show up
context['force_search_count'] = 1;
let domain = [['activity_ids.user_id', '=', session.uid]];
context["force_search_count"] = 1;
let domain = [["activity_ids.user_id", "=", session.uid]];
if (data.domain) {
domain = domain.concat(data.domain);
}
this.env.services['action'].doAction(
this.env.services["action"].doAction(
{
context,
domain,
name: data.model_name,
res_model: data.res_model,
search_view_id: [false],
type: 'ir.actions.act_window',
views: this.activityGroup.irModel.availableWebViews.map(viewName => [false, viewName]),
type: "ir.actions.act_window",
views: this.activityGroup.irModel.availableWebViews.map((viewName) => [
false,
viewName,
]),
},
{
clearBreadcrumbs: true,
},
}
);
},
},
fields: {
activityGroup: one('ActivityGroup', { identifying: true, inverse: 'activityGroupViews' }),
activityMenuViewOwner: one('ActivityMenuView', { identifying: true, inverse: 'activityGroupViews' }),
activityGroup: one("ActivityGroup", { identifying: true, inverse: "activityGroupViews" }),
activityMenuViewOwner: one("ActivityMenuView", {
identifying: true,
inverse: "activityGroupViews",
}),
},
});
@@ -1,29 +1,34 @@
/** @odoo-module **/
import { attr, clear, many, one, Model } from '@mail/model';
import { attr, clear, many, one, Model } from "@mail/model";
import session from 'web.session';
import session from "web.session";
Model({
name: 'ActivityListView',
template: 'mail.ActivityListView',
name: "ActivityListView",
template: "mail.ActivityListView",
lifecycleHooks: {
async _created() {
if (this.activities.length === 0) {
return;
}
const messaging = this.messaging;
const activitiesData = await this.messaging.rpc({
model: 'mail.activity',
method: 'activity_format',
args: [this.activities.map(activity => activity.id)],
kwargs: { context: session.user_context },
}, { shadow: true });
const activitiesData = await this.messaging.rpc(
{
model: "mail.activity",
method: "activity_format",
args: [this.activities.map((activity) => activity.id)],
kwargs: { context: session.user_context },
},
{ shadow: true }
);
if (!messaging.exists()) {
return;
}
messaging.models['Activity'].insert(
activitiesData.map(activityData => messaging.models['Activity'].convertData(activityData))
messaging.models["Activity"].insert(
activitiesData.map((activityData) =>
messaging.models["Activity"].convertData(activityData)
)
);
},
},
@@ -32,54 +37,65 @@ Model({
const reloadFunc = this.reloadFunc;
const thread = this.thread;
const webRecord = this.webRecord;
this.messaging.openActivityForm({
defaultActivityTypeId: this.popoverViewOwner.activityCellViewOwnerAsActivityList
? this.popoverViewOwner.activityCellViewOwnerAsActivityList.activityType.id
: undefined,
thread,
}).then(() => {
thread.fetchData(['activities']);
if (reloadFunc) {
reloadFunc();
}
if (webRecord) {
webRecord.model.load({ resId: thread.id });
}
});
this.messaging
.openActivityForm({
defaultActivityTypeId: this.popoverViewOwner.activityCellViewOwnerAsActivityList
? this.popoverViewOwner.activityCellViewOwnerAsActivityList.activityType.id
: undefined,
thread,
})
.then(() => {
thread.fetchData(["activities"]);
if (reloadFunc) {
reloadFunc();
}
if (webRecord) {
webRecord.model.load({ resId: thread.id });
}
});
this.popoverViewOwner.delete();
},
},
fields: {
activities: many('Activity', {
activities: many("Activity", {
compute() {
if (this.popoverViewOwner.activityCellViewOwnerAsActivityList) {
return this.popoverViewOwner.activityCellViewOwnerAsActivityList.filteredActivities;
return this.popoverViewOwner.activityCellViewOwnerAsActivityList
.filteredActivities;
}
return this.thread && this.thread.activities;
},
sort: [
['truthy-first', 'dateDeadline'],
['case-insensitive-asc', 'dateDeadline'],
["truthy-first", "dateDeadline"],
["case-insensitive-asc", "dateDeadline"],
],
}),
activityListViewItems: many('ActivityListViewItem', { inverse: 'activityListViewOwner',
activityListViewItems: many("ActivityListViewItem", {
inverse: "activityListViewOwner",
compute() {
return this.activities.map(activity => {
return this.activities.map((activity) => {
return {
activity,
};
});
},
}),
overdueActivityListViewItems: many('ActivityListViewItem', { inverse: 'activityListViewOwnerAsOverdue' }),
plannedActivityListViewItems: many('ActivityListViewItem', { inverse: 'activityListViewOwnerAsPlanned' }),
popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'activityListView' }),
overdueActivityListViewItems: many("ActivityListViewItem", {
inverse: "activityListViewOwnerAsOverdue",
}),
plannedActivityListViewItems: many("ActivityListViewItem", {
inverse: "activityListViewOwnerAsPlanned",
}),
popoverViewOwner: one("PopoverView", { identifying: true, inverse: "activityListView" }),
reloadFunc: attr({
compute() {
return this.popoverViewOwner.activityCellViewOwnerAsActivityList ? this.popoverViewOwner.activityCellViewOwnerAsActivityList.reloadFunc : clear();
return this.popoverViewOwner.activityCellViewOwnerAsActivityList
? this.popoverViewOwner.activityCellViewOwnerAsActivityList.reloadFunc
: clear();
},
}),
thread: one('Thread', { required: true,
thread: one("Thread", {
required: true,
compute() {
if (this.popoverViewOwner.activityButtonViewOwnerAsActivityList) {
return this.popoverViewOwner.activityButtonViewOwnerAsActivityList.thread;
@@ -90,7 +106,9 @@ Model({
return clear();
},
}),
todayActivityListViewItems: many('ActivityListViewItem', { inverse: 'activityListViewOwnerAsToday' }),
todayActivityListViewItems: many("ActivityListViewItem", {
inverse: "activityListViewOwnerAsToday",
}),
webRecord: attr({
compute() {
if (this.popoverViewOwner.activityButtonViewOwnerAsActivityList) {
@@ -1,13 +1,13 @@
/** @odoo-module **/
import { attr, clear, many, one, Model } from '@mail/model';
import { attr, clear, many, one, Model } from "@mail/model";
import { auto_str_to_date } from 'web.time';
import { sprintf } from '@web/core/utils/strings';
import { auto_str_to_date } from "web.time";
import { sprintf } from "@web/core/utils/strings";
Model({
name: 'ActivityListViewItem',
template: 'mail.ActivityListViewItem',
name: "ActivityListViewItem",
template: "mail.ActivityListViewItem",
recordMethods: {
onClickEditActivityButton() {
const popoverViewOwner = this.activityListViewOwner.popoverViewOwner;
@@ -36,24 +36,33 @@ Model({
},
},
fields: {
activity: one('Activity', { identifying: true }),
activityListViewOwner: one('ActivityListView', { identifying: true, inverse: 'activityListViewItems' }),
activityListViewOwnerAsOverdue: one('ActivityListView', { inverse: 'overdueActivityListViewItems',
activity: one("Activity", { identifying: true }),
activityListViewOwner: one("ActivityListView", {
identifying: true,
inverse: "activityListViewItems",
}),
activityListViewOwnerAsOverdue: one("ActivityListView", {
inverse: "overdueActivityListViewItems",
compute() {
return this.activity.state === 'overdue' ? this.activityListViewOwner : clear();
return this.activity.state === "overdue" ? this.activityListViewOwner : clear();
},
}),
activityListViewOwnerAsPlanned: one('ActivityListView', { inverse: 'plannedActivityListViewItems',
activityListViewOwnerAsPlanned: one("ActivityListView", {
inverse: "plannedActivityListViewItems",
compute() {
return this.activity.state === 'planned' ? this.activityListViewOwner : clear();
return this.activity.state === "planned" ? this.activityListViewOwner : clear();
},
}),
activityListViewOwnerAsToday: one('ActivityListView', { inverse: 'todayActivityListViewItems',
activityListViewOwnerAsToday: one("ActivityListView", {
inverse: "todayActivityListViewItems",
compute() {
return this.activity.state === 'today' ? this.activityListViewOwner : clear();
return this.activity.state === "today" ? this.activityListViewOwner : clear();
},
}),
clockWatcher: one('ClockWatcher', { default: { clock: { frequency: 60 * 1000 } }, inverse: 'activityListViewItemOwner' }),
clockWatcher: one("ClockWatcher", {
default: { clock: { frequency: 60 * 1000 } },
inverse: "activityListViewItemOwner",
}),
/**
* Compute the label for "when" the activity is due.
*/
@@ -65,10 +74,10 @@ Model({
if (!this.clockWatcher.clock.date) {
return clear();
}
const today = moment(this.clockWatcher.clock.date.getTime()).startOf('day');
const today = moment(this.clockWatcher.clock.date.getTime()).startOf("day");
const momentDeadlineDate = moment(auto_str_to_date(this.activity.dateDeadline));
// true means no rounding
const diff = momentDeadlineDate.diff(today, 'days', true);
const diff = momentDeadlineDate.diff(today, "days", true);
if (diff === 0) {
return this.env._t("Today");
} else if (diff === -1) {
@@ -82,14 +91,15 @@ Model({
}
},
}),
fileUploader: one('FileUploader', { inverse: 'activityListViewItemOwner',
fileUploader: one("FileUploader", {
inverse: "activityListViewItemOwner",
compute() {
return this.activity.category === 'upload_file' ? {} : clear();
return this.activity.category === "upload_file" ? {} : clear();
},
}),
hasEditButton: attr({
compute() {
return this.activity.chaining_type === 'suggest' && this.activity.canWrite;
return this.activity.chaining_type === "suggest" && this.activity.canWrite;
},
}),
hasMarkDoneButton: attr({
@@ -97,20 +107,27 @@ Model({
return !this.fileUploader;
},
}),
mailTemplateViews: many('MailTemplateView', { inverse: 'activityListViewItemOwner',
mailTemplateViews: many("MailTemplateView", {
inverse: "activityListViewItemOwner",
compute() {
return this.activity.mailTemplates.map(mailTemplate => ({ mailTemplate }));
return this.activity.mailTemplates.map((mailTemplate) => ({ mailTemplate }));
},
}),
markDoneView: one('ActivityMarkDonePopoverContentView', { inverse: 'activityListViewItemOwner' }),
markDoneView: one("ActivityMarkDonePopoverContentView", {
inverse: "activityListViewItemOwner",
}),
reloadFunc: attr({
compute() {
return this.activityListViewOwner.reloadFunc ? this.activityListViewOwner.reloadFunc : clear();
return this.activityListViewOwner.reloadFunc
? this.activityListViewOwner.reloadFunc
: clear();
},
}),
webRecord: attr({
compute() {
return this.activityListViewOwner.webRecord ? this.activityListViewOwner.webRecord : clear();
return this.activityListViewOwner.webRecord
? this.activityListViewOwner.webRecord
: clear();
},
}),
},
@@ -1,16 +1,16 @@
/** @odoo-module **/
import { attr, clear, one, Model } from '@mail/model';
import { attr, clear, one, Model } from "@mail/model";
import { onMounted } from '@odoo/owl';
import { onMounted } from "@odoo/owl";
Model({
name: 'ActivityMarkDonePopoverContentView',
template: 'mail.ActivityMarkDonePopoverContentView',
name: "ActivityMarkDonePopoverContentView",
template: "mail.ActivityMarkDonePopoverContentView",
componentSetup() {
onMounted(this.onMounted);
},
identifyingMode: 'xor',
identifyingMode: "xor",
recordMethods: {
/**
* Handles blur on this feedback textarea.
@@ -62,7 +62,9 @@ Model({
const reloadFunc = this.reloadFunc;
const webRecord = this.webRecord;
const thread = this.activity.thread;
const activityListViewOwner = this.activityListViewItemOwner && this.activityListViewItemOwner.activityListViewOwner;
const activityListViewOwner =
this.activityListViewItemOwner &&
this.activityListViewItemOwner.activityListViewOwner;
const activity = this.activity;
const feedback = this.feedbackTextareaRef.el.value;
if (activityListViewOwner && activityListViewOwner.exists()) {
@@ -83,7 +85,7 @@ Model({
* Handles keydown on this activity mark done.
*/
onKeydown(ev) {
if (ev.key === 'Escape') {
if (ev.key === "Escape") {
this._close();
}
},
@@ -111,7 +113,8 @@ Model({
},
},
fields: {
activity: one('Activity', { required: true,
activity: one("Activity", {
required: true,
compute() {
if (this.activityListViewItemOwner) {
return this.activityListViewItemOwner.activity;
@@ -122,8 +125,11 @@ Model({
return clear();
},
}),
activityListViewItemOwner: one('ActivityListViewItem', { identifying: true, inverse: 'markDoneView', }),
activityViewOwner: one('ActivityView', {
activityListViewItemOwner: one("ActivityListViewItem", {
identifying: true,
inverse: "markDoneView",
}),
activityViewOwner: one("ActivityView", {
compute() {
if (this.popoverViewOwner && this.popoverViewOwner.activityViewOwnerAsMarkDone) {
return this.popoverViewOwner.activityViewOwnerAsMarkDone;
@@ -131,7 +137,7 @@ Model({
return clear();
},
}),
feedbackTextareaRef: attr({ ref: 'feedbackTextarea' }),
feedbackTextareaRef: attr({ ref: "feedbackTextarea" }),
hasHeader: attr({
compute() {
return Boolean(this.popoverViewOwner);
@@ -145,7 +151,10 @@ Model({
return this.env._t("Mark Done");
},
}),
popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'activityMarkDonePopoverContentView', }),
popoverViewOwner: one("PopoverView", {
identifying: true,
inverse: "activityMarkDonePopoverContentView",
}),
reloadFunc: attr({
compute() {
if (this.activityListViewItemOwner) {
@@ -1,18 +1,18 @@
/** @odoo-module **/
import { attr, many, Model } from '@mail/model';
import { attr, many, Model } from "@mail/model";
import session from 'web.session';
import session from "web.session";
Model({
name: 'ActivityMenuView',
name: "ActivityMenuView",
lifecycleHooks: {
_created() {
this.fetchData();
document.addEventListener('click', this._onClickCaptureGlobal, true);
document.addEventListener("click", this._onClickCaptureGlobal, true);
},
_willDelete() {
document.removeEventListener('click', this._onClickCaptureGlobal, true);
document.removeEventListener("click", this._onClickCaptureGlobal, true);
},
},
recordMethods: {
@@ -21,13 +21,15 @@ Model({
},
async fetchData() {
const data = await this.messaging.rpc({
model: 'res.users',
method: 'systray_get_activities',
model: "res.users",
method: "systray_get_activities",
args: [],
kwargs: { context: session.user_context },
});
this.update({
activityGroups: data.map(vals => this.messaging.models['ActivityGroup'].convertData(vals)),
activityGroups: data.map((vals) =>
this.messaging.models["ActivityGroup"].convertData(vals)
),
extraCount: 0,
});
},
@@ -63,12 +65,13 @@ Model({
},
},
fields: {
activityGroups: many('ActivityGroup', {
sort: [['smaller-first', 'irModel.id']],
activityGroups: many("ActivityGroup", {
sort: [["smaller-first", "irModel.id"]],
}),
activityGroupViews: many('ActivityGroupView', { inverse: 'activityMenuViewOwner',
activityGroupViews: many("ActivityGroupView", {
inverse: "activityMenuViewOwner",
compute() {
return this.activityGroups.map(activityGroup => {
return this.activityGroups.map((activityGroup) => {
return {
activityGroup,
};
@@ -78,7 +81,10 @@ Model({
component: attr(),
counter: attr({
compute() {
return this.activityGroups.reduce((total, group) => total + group.total_count, this.extraCount);
return this.activityGroups.reduce(
(total, group) => total + group.total_count,
this.extraCount
);
},
}),
/**
@@ -1,11 +1,11 @@
/** @odoo-module **/
import { attr, many, Model } from '@mail/model';
import { attr, many, Model } from "@mail/model";
Model({
name: 'ActivityType',
name: "ActivityType",
fields: {
activities: many('Activity', { inverse: 'type' }),
activities: many("Activity", { inverse: "type" }),
displayName: attr(),
id: attr({ identifying: true }),
},
+22 -21
View File
@@ -1,13 +1,13 @@
/** @odoo-module **/
import { attr, clear, many, one, Model } from '@mail/model';
import { attr, clear, many, one, Model } from "@mail/model";
import { auto_str_to_date, getLangDateFormat, getLangDatetimeFormat } from 'web.time';
import { sprintf } from '@web/core/utils/strings';
import { auto_str_to_date, getLangDateFormat, getLangDatetimeFormat } from "web.time";
import { sprintf } from "@web/core/utils/strings";
Model({
name: 'ActivityView',
template: 'mail.ActivityView',
name: "ActivityView",
template: "mail.ActivityView",
recordMethods: {
/**
* Handles the click on a link inside the activity.
@@ -15,11 +15,7 @@ Model({
* @param {MouseEvent} ev
*/
onClickActivity(ev) {
if (
ev.target.tagName === 'A' &&
ev.target.dataset.oeId &&
ev.target.dataset.oeModel
) {
if (ev.target.tagName === "A" && ev.target.dataset.oeId && ev.target.dataset.oeModel) {
this.messaging.openProfile({
id: Number(ev.target.dataset.oeId),
model: ev.target.dataset.oeModel,
@@ -67,7 +63,7 @@ Model({
},
},
fields: {
activity: one('Activity', { identifying: true, inverse: 'activityViews' }),
activity: one("Activity", { identifying: true, inverse: "activityViews" }),
/**
* Determines whether the details are visible.
*/
@@ -83,8 +79,11 @@ Model({
return sprintf(this.env._t("for %s"), this.activity.assignee.nameOrDisplayName);
},
}),
chatterOwner: one('Chatter', { identifying: true, inverse: 'activityViews' }),
clockWatcher: one('ClockWatcher', { default: { clock: { frequency: 60 * 1000 } }, inverse: 'activityViewOwner' }),
chatterOwner: one("Chatter", { identifying: true, inverse: "activityViews" }),
clockWatcher: one("ClockWatcher", {
default: { clock: { frequency: 60 * 1000 } },
inverse: "activityViewOwner",
}),
/**
* Compute the label for "when" the activity is due.
*/
@@ -96,10 +95,10 @@ Model({
if (!this.clockWatcher.clock.date) {
return clear();
}
const today = moment(this.clockWatcher.clock.date.getTime()).startOf('day');
const today = moment(this.clockWatcher.clock.date.getTime()).startOf("day");
const momentDeadlineDate = moment(auto_str_to_date(this.activity.dateDeadline));
// true means no rounding
const diff = momentDeadlineDate.diff(today, 'days', true);
const diff = momentDeadlineDate.diff(today, "days", true);
if (diff === 0) {
return this.env._t("Today:");
} else if (diff === -1) {
@@ -113,9 +112,10 @@ Model({
}
},
}),
fileUploader: one('FileUploader', { inverse: 'activityView',
fileUploader: one("FileUploader", {
inverse: "activityView",
compute() {
return this.activity.category === 'upload_file' ? {} : clear();
return this.activity.category === "upload_file" ? {} : clear();
},
}),
/**
@@ -144,13 +144,14 @@ Model({
return momentDeadlineDate.format(datetimeFormat);
},
}),
mailTemplateViews: many('MailTemplateView', { inverse: 'activityViewOwner',
mailTemplateViews: many("MailTemplateView", {
inverse: "activityViewOwner",
compute() {
return this.activity.mailTemplates.map(mailTemplate => ({ mailTemplate }));
return this.activity.mailTemplates.map((mailTemplate) => ({ mailTemplate }));
},
}),
markDoneButtonRef: attr({ ref: 'markDoneButton' }),
markDonePopoverView: one('PopoverView', { inverse: 'activityViewOwnerAsMarkDone', }),
markDoneButtonRef: attr({ ref: "markDoneButton" }),
markDonePopoverView: one("PopoverView", { inverse: "activityViewOwnerAsMarkDone" }),
/**
* Label for mark as done. This is just for translations purpose.
*/
+90 -62
View File
@@ -1,19 +1,19 @@
/** @odoo-module **/
import { attr, clear, many, one, Model } from '@mail/model';
import { attr, clear, many, one, Model } from "@mail/model";
Model({
name: 'Attachment',
name: "Attachment",
recordMethods: {
/**
* Send the attachment for the browser to download.
*/
download() {
const downloadLink = document.createElement('a');
downloadLink.setAttribute('href', this.downloadUrl);
const downloadLink = document.createElement("a");
downloadLink.setAttribute("href", this.downloadUrl);
// Adding 'download' attribute into a link prevents open a new tab or change the current location of the window.
// This avoids interrupting the activity in the page such as rtc call.
downloadLink.setAttribute('download', '');
downloadLink.setAttribute("download", "");
downloadLink.click();
},
/**
@@ -35,13 +35,16 @@ Model({
if (!this.isUploading) {
this.update({ isUnlinkPending: true });
try {
await this.messaging.rpc({
route: `/mail/attachment/delete`,
params: {
access_token: this.accessToken,
attachment_id: this.id,
await this.messaging.rpc(
{
route: `/mail/attachment/delete`,
params: {
access_token: this.accessToken,
attachment_id: this.id,
},
},
}, { shadow: true });
{ shadow: true }
);
} finally {
if (this.exists()) {
this.update({ isUnlinkPending: false });
@@ -53,24 +56,24 @@ Model({
if (!this.exists()) {
return;
}
this.messaging.messagingBus.trigger('o-attachment-deleted', { attachment: this });
this.messaging.messagingBus.trigger("o-attachment-deleted", { attachment: this });
this.delete();
},
},
fields: {
accessToken: attr(),
activities: many('Activity', { inverse: 'attachments' }),
allThreads: many('Thread', { inverse: 'allAttachments', readonly: true }),
activities: many("Activity", { inverse: "attachments" }),
allThreads: many("Thread", { inverse: "allAttachments", readonly: true }),
/**
* States the attachment lists that are displaying this attachment.
*/
attachmentLists: many('AttachmentList', { inverse: 'attachments' }),
attachmentViewerViewable: one('AttachmentViewerViewable', { inverse: 'attachmentOwner' }),
attachmentLists: many("AttachmentList", { inverse: "attachments" }),
attachmentViewerViewable: one("AttachmentViewerViewable", { inverse: "attachmentOwner" }),
checksum: attr(),
/**
* States on which composer this attachment is currently being created.
*/
composer: one('Composer', { inverse: 'attachments' }),
composer: one("Composer", { inverse: "attachments" }),
defaultSource: attr({
compute() {
if (this.isImage) {
@@ -78,28 +81,38 @@ Model({
}
if (this.isPdf) {
const pdf_lib = `/web/static/lib/pdfjs/web/viewer.html?file=`;
if (!this.accessToken && this.originThread && this.originThread.model === 'mail.channel') {
if (
!this.accessToken &&
this.originThread &&
this.originThread.model === "mail.channel"
) {
return `${pdf_lib}/mail/channel/${this.originThread.id}/attachment/${this.id}`;
}
const accessToken = this.accessToken ? `?access_token%3D${this.accessToken}` : '';
const accessToken = this.accessToken
? `?access_token%3D${this.accessToken}`
: "";
return `${pdf_lib}/web/content/${this.id}${accessToken}`;
}
if (this.isUrlYoutube) {
const urlArr = this.url.split('/');
const urlArr = this.url.split("/");
let token = urlArr[urlArr.length - 1];
if (token.includes('watch')) {
token = token.split('v=')[1];
const amp = token.indexOf('&');
if (token.includes("watch")) {
token = token.split("v=")[1];
const amp = token.indexOf("&");
if (amp !== -1) {
token = token.substring(0, amp);
}
}
return `https://www.youtube.com/embed/${token}`;
}
if (!this.accessToken && this.originThread && this.originThread.model === 'mail.channel') {
if (
!this.accessToken &&
this.originThread &&
this.originThread.model === "mail.channel"
) {
return `/mail/channel/${this.originThread.id}/attachment/${this.id}`;
}
const accessToken = this.accessToken ? `?access_token=${this.accessToken}` : '';
const accessToken = this.accessToken ? `?access_token=${this.accessToken}` : "";
return `/web/content/${this.id}${accessToken}`;
},
}),
@@ -118,16 +131,20 @@ Model({
}),
downloadUrl: attr({
compute() {
if (!this.accessToken && this.originThread && this.originThread.model === 'mail.channel') {
if (
!this.accessToken &&
this.originThread &&
this.originThread.model === "mail.channel"
) {
return `/mail/channel/${this.originThread.id}/attachment/${this.id}?download=true`;
}
const accessToken = this.accessToken ? `access_token=${this.accessToken}&` : '';
const accessToken = this.accessToken ? `access_token=${this.accessToken}&` : "";
return `/web/content/ir.attachment/${this.id}/datas?${accessToken}download=true`;
},
}),
extension: attr({
compute() {
const extension = this.filename && this.filename.split('.').pop();
const extension = this.filename && this.filename.split(".").pop();
if (extension) {
return extension;
}
@@ -145,12 +162,18 @@ Model({
return false;
}
if (this.messages.length && this.originThread && this.originThread.model === 'mail.channel') {
return this.messages.some(message => (
message.canBeDeleted ||
(message.author && message.author === this.messaging.currentPartner) ||
(message.guestAuthor && message.guestAuthor === this.messaging.currentGuest)
));
if (
this.messages.length &&
this.originThread &&
this.originThread.model === "mail.channel"
) {
return this.messages.some(
(message) =>
message.canBeDeleted ||
(message.author && message.author === this.messaging.currentPartner) ||
(message.guestAuthor &&
message.guestAuthor === this.messaging.currentGuest)
);
}
return true;
},
@@ -161,13 +184,13 @@ Model({
isImage: attr({
compute() {
const imageMimetypes = [
'image/bmp',
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/tiff',
'image/x-icon',
"image/bmp",
"image/gif",
"image/jpeg",
"image/png",
"image/svg+xml",
"image/tiff",
"image/x-icon",
];
return imageMimetypes.includes(this.mimetype);
},
@@ -177,7 +200,7 @@ Model({
*/
isPdf: attr({
compute() {
return this.mimetype === 'application/pdf';
return this.mimetype === "application/pdf";
},
}),
/**
@@ -186,11 +209,11 @@ Model({
isText: attr({
compute() {
const textMimeType = [
'application/javascript',
'application/json',
'text/css',
'text/html',
'text/plain',
"application/javascript",
"application/json",
"text/css",
"text/html",
"text/plain",
];
return textMimeType.includes(this.mimetype);
},
@@ -205,7 +228,7 @@ Model({
*/
isUrl: attr({
compute() {
return this.type === 'url' && this.url;
return this.type === "url" && this.url;
},
}),
/**
@@ -213,7 +236,7 @@ Model({
*/
isUrlYoutube: attr({
compute() {
return !!this.url && this.url.includes('youtu');
return !!this.url && this.url.includes("youtu");
},
}),
/**
@@ -222,17 +245,19 @@ Model({
isVideo: attr({
compute() {
const videoMimeTypes = [
'audio/mpeg',
'video/x-matroska',
'video/mp4',
'video/webm',
"audio/mpeg",
"video/x-matroska",
"video/mp4",
"video/webm",
];
return videoMimeTypes.includes(this.mimetype);
},
}),
isViewable: attr({
compute() {
return this.isText || this.isImage || this.isVideo || this.isPdf || this.isUrlYoutube;
return (
this.isText || this.isImage || this.isVideo || this.isPdf || this.isUrlYoutube
);
},
}),
/**
@@ -240,17 +265,20 @@ Model({
*/
mediaType: attr({
compute() {
return this.mimetype && this.mimetype.split('/').shift();
return this.mimetype && this.mimetype.split("/").shift();
},
}),
messages: many('Message', { inverse: 'attachments' }),
mimetype: attr({ default: '' }),
messages: many("Message", { inverse: "attachments" }),
mimetype: attr({ default: "" }),
name: attr(),
originThread: one('Thread', { inverse: 'originThreadAttachments' }),
originThread: one("Thread", { inverse: "originThreadAttachments" }),
size: attr(),
threadsAsAttachmentsInWebClientView: many('Thread', { inverse: 'attachmentsInWebClientView',
threadsAsAttachmentsInWebClientView: many("Thread", {
inverse: "attachmentsInWebClientView",
compute() {
return (this.isPdf || this.isImage) && !this.isUploading ? this.allThreads : clear();
return (this.isPdf || this.isImage) && !this.isUploading
? this.allThreads
: clear();
},
}),
type: attr(),
@@ -264,8 +292,8 @@ Model({
if (!this.uploadingAbortController) {
const abortController = new AbortController();
abortController.signal.onabort = () => {
this.messaging.messagingBus.trigger('o-attachment-upload-abort', {
attachment: this
this.messaging.messagingBus.trigger("o-attachment-upload-abort", {
attachment: this,
});
};
return abortController;
@@ -1,10 +1,10 @@
/** @odoo-module **/
import { attr, one, Model } from '@mail/model';
import { attr, one, Model } from "@mail/model";
Model({
name: 'AttachmentCard',
template: 'mail.AttachmentCard',
name: "AttachmentCard",
template: "mail.AttachmentCard",
recordMethods: {
/**
* Opens the attachment viewer when clicking on viewable attachment.
@@ -39,12 +39,14 @@ Model({
/**
* Determines the attachment of this card.
*/
attachment: one('Attachment', { identifying: true }),
attachmentDeleteConfirmDialog: one('Dialog', { inverse: 'attachmentCardOwnerAsAttachmentDeleteConfirm' }),
attachment: one("Attachment", { identifying: true }),
attachmentDeleteConfirmDialog: one("Dialog", {
inverse: "attachmentCardOwnerAsAttachmentDeleteConfirm",
}),
/**
* States the attachmentList displaying this card.
*/
attachmentList: one('AttachmentList', { identifying: true, inverse: 'attachmentCards' }),
attachmentList: one("AttachmentList", { identifying: true, inverse: "attachmentCards" }),
hasMultipleActions: attr({
compute() {
return this.attachment.isDeletable && !this.attachmentList.composerViewOwner;
@@ -1,14 +1,14 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { attr, clear, one, Model } from '@mail/model';
import { sprintf } from '@web/core/utils/strings';
import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
import { attr, clear, one, Model } from "@mail/model";
import { sprintf } from "@web/core/utils/strings";
Model({
name: 'AttachmentDeleteConfirmView',
template: 'mail.AttachmentDeleteConfirmView',
name: "AttachmentDeleteConfirmView",
template: "mail.AttachmentDeleteConfirmView",
componentSetup() {
useComponentToModel({ fieldName: 'component' });
useComponentToModel({ fieldName: "component" });
},
recordMethods: {
/**
@@ -18,7 +18,9 @@ Model({
* @returns {boolean}
*/
containsElement(element) {
return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element));
return Boolean(
this.component && this.component.root.el && this.component.root.el.contains(element)
);
},
onClickCancel() {
this.dialogOwner.delete();
@@ -32,40 +34,55 @@ Model({
},
},
fields: {
attachment: one('Attachment', { required: true,
attachment: one("Attachment", {
required: true,
compute() {
if (this.dialogOwner && this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm) {
if (
this.dialogOwner &&
this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm
) {
return this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachment;
}
if (this.dialogOwner && this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm) {
return this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachment;
if (
this.dialogOwner &&
this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm
) {
return this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm
.attachment;
}
return clear();
},
}),
body: attr({
compute() {
return sprintf(this.env._t(`Do you really want to delete "%s"?`), this.attachment.displayName);
return sprintf(
this.env._t(`Do you really want to delete "%s"?`),
this.attachment.displayName
);
},
}),
chatter: one('Chatter', {
chatter: one("Chatter", {
compute() {
if (
this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm &&
this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachmentList.chatterOwner
this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachmentList
.chatterOwner
) {
return this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm.attachmentList.chatterOwner;
return this.dialogOwner.attachmentCardOwnerAsAttachmentDeleteConfirm
.attachmentList.chatterOwner;
}
if (
this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm &&
this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachmentList.chatterOwner
this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachmentList
.chatterOwner
) {
return this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm.attachmentList.chatterOwner;
return this.dialogOwner.attachmentImageOwnerAsAttachmentDeleteConfirm
.attachmentList.chatterOwner;
}
return clear();
},
}),
component: attr(),
dialogOwner: one('Dialog', { identifying: true, inverse: 'attachmentDeleteConfirmView' }),
dialogOwner: one("Dialog", { identifying: true, inverse: "attachmentDeleteConfirmView" }),
},
});
@@ -1,11 +1,11 @@
/** @odoo-module **/
import { attr, clear, one, Model } from '@mail/model';
import { isEventHandled, markEventHandled } from '@mail/utils/utils';
import { attr, clear, one, Model } from "@mail/model";
import { isEventHandled, markEventHandled } from "@mail/utils/utils";
Model({
name: 'AttachmentImage',
template: 'mail.AttachmentImage',
name: "AttachmentImage",
template: "mail.AttachmentImage",
recordMethods: {
/**
* Called when clicking on download icon.
@@ -13,7 +13,7 @@ Model({
* @param {MouseEvent} ev
*/
onClickDownload(ev) {
markEventHandled(ev, 'AttachmentImage.onClickDownload');
markEventHandled(ev, "AttachmentImage.onClickDownload");
if (!this.exists()) {
return;
}
@@ -25,10 +25,10 @@ Model({
* @param {MouseEvent} ev
*/
onClickImage(ev) {
if (isEventHandled(ev, 'AttachmentImage.onClickDownload')) {
if (isEventHandled(ev, "AttachmentImage.onClickDownload")) {
return;
}
if (isEventHandled(ev, 'AttachmentImage.onClickUnlink')) {
if (isEventHandled(ev, "AttachmentImage.onClickUnlink")) {
return;
}
if (!this.attachment || !this.attachment.isViewable) {
@@ -45,7 +45,7 @@ Model({
* @param {MouseEvent} ev
*/
onClickUnlink(ev) {
markEventHandled(ev, 'AttachmentImage.onClickUnlink');
markEventHandled(ev, "AttachmentImage.onClickUnlink");
if (!this.exists()) {
return;
}
@@ -60,16 +60,19 @@ Model({
/**
* Determines the attachment of this attachment image..
*/
attachment: one('Attachment', { identifying: true }),
attachmentDeleteConfirmDialog: one('Dialog', { inverse: 'attachmentImageOwnerAsAttachmentDeleteConfirm' }),
attachment: one("Attachment", { identifying: true }),
attachmentDeleteConfirmDialog: one("Dialog", {
inverse: "attachmentImageOwnerAsAttachmentDeleteConfirm",
}),
/**
* States the attachmentList displaying this attachment image.
*/
attachmentList: one('AttachmentList', { identifying: true, inverse: 'attachmentImages' }),
attachmentList: one("AttachmentList", { identifying: true, inverse: "attachmentImages" }),
/**
* Determines whether `this` should display a download button.
*/
hasDownloadButton: attr({ default: false,
hasDownloadButton: attr({
default: false,
compute() {
if (!this.attachment || !this.attachmentList) {
return clear();
@@ -80,7 +83,8 @@ Model({
/**
* Determines the max height of this attachment image in px.
*/
height: attr({ required: true,
height: attr({
required: true,
compute() {
if (!this.attachmentList) {
return clear();
@@ -101,17 +105,24 @@ Model({
if (!this.attachment) {
return;
}
if (!this.attachment.accessToken && this.attachment.originThread && this.attachment.originThread.model === 'mail.channel') {
if (
!this.attachment.accessToken &&
this.attachment.originThread &&
this.attachment.originThread.model === "mail.channel"
) {
return `/mail/channel/${this.attachment.originThread.id}/image/${this.attachment.id}/${this.width}x${this.height}`;
}
const accessToken = this.attachment.accessToken ? `?access_token=${this.attachment.accessToken}` : '';
const accessToken = this.attachment.accessToken
? `?access_token=${this.attachment.accessToken}`
: "";
return `/web/image/${this.attachment.id}/${this.width}x${this.height}${accessToken}`;
},
}),
/**
* Determines the max width of this attachment image in px.
*/
width: attr({ required: true,
width: attr({
required: true,
/**
* Returns an arbitrary high value, this is effectively a max-width and
* the height should be more constrained.
@@ -1,17 +1,19 @@
/** @odoo-module **/
import { attr, clear, many, one, Model } from '@mail/model';
import { attr, clear, many, one, Model } from "@mail/model";
Model({
name: 'AttachmentList',
template: 'mail.AttachmentList',
identifyingMode: 'xor',
name: "AttachmentList",
template: "mail.AttachmentList",
identifyingMode: "xor",
recordMethods: {
/**
* Select the next attachment.
*/
selectNextAttachment() {
const index = this.attachments.findIndex(attachment => attachment === this.selectedAttachment);
const index = this.attachments.findIndex(
(attachment) => attachment === this.selectedAttachment
);
const nextIndex = index === this.attachments.length - 1 ? 0 : index + 1;
this.update({ selectedAttachment: this.attachments[nextIndex] });
},
@@ -19,7 +21,9 @@ Model({
* Select the previous attachment.
*/
selectPreviousAttachment() {
const index = this.attachments.findIndex(attachment => attachment === this.selectedAttachment);
const index = this.attachments.findIndex(
(attachment) => attachment === this.selectedAttachment
);
const prevIndex = index === 0 ? this.attachments.length - 1 : index - 1;
this.update({ selectedAttachment: this.attachments[prevIndex] });
},
@@ -28,28 +32,31 @@ Model({
/**
* Link with a chatter to handle attachments.
*/
chatterOwner: one('Chatter', { identifying: true, inverse: 'attachmentList' }),
chatterOwner: one("Chatter", { identifying: true, inverse: "attachmentList" }),
/**
* States the attachment cards that are displaying this nonImageAttachments.
*/
attachmentCards: many('AttachmentCard', { inverse: 'attachmentList',
attachmentCards: many("AttachmentCard", {
inverse: "attachmentList",
compute() {
return this.nonImageAttachments.map(attachment => ({ attachment }));
return this.nonImageAttachments.map((attachment) => ({ attachment }));
},
}),
/**
* States the attachment images that are displaying this imageAttachments.
*/
attachmentImages: many('AttachmentImage', { inverse: 'attachmentList',
attachmentImages: many("AttachmentImage", {
inverse: "attachmentList",
compute() {
return this.imageAttachments.map(attachment => ({ attachment }));
return this.imageAttachments.map((attachment) => ({ attachment }));
},
}),
attachmentListViewDialog: one('Dialog', { inverse: 'attachmentListOwnerAsAttachmentView' }),
attachmentListViewDialog: one("Dialog", { inverse: "attachmentListOwnerAsAttachmentView" }),
/**
* States the attachments to be displayed by this attachment list.
*/
attachments: many('Attachment', { inverse: 'attachmentLists',
attachments: many("Attachment", {
inverse: "attachmentLists",
compute() {
if (this.messageViewOwner) {
return this.messageViewOwner.message.attachments;
@@ -66,13 +73,13 @@ Model({
/**
* Link with a composer view to handle attachments.
*/
composerViewOwner: one('ComposerView', { identifying: true, inverse: 'attachmentList' }),
composerViewOwner: one("ComposerView", { identifying: true, inverse: "attachmentList" }),
/**
* States the attachment that are an image.
*/
imageAttachments: many('Attachment', {
imageAttachments: many("Attachment", {
compute() {
return this.attachments.filter(attachment => attachment.isImage);
return this.attachments.filter((attachment) => attachment.isImage);
},
}),
/**
@@ -82,7 +89,7 @@ Model({
compute() {
return Boolean(
(this.messageViewOwner && this.messageViewOwner.isInDiscuss) ||
(this.composerViewOwner && this.composerViewOwner.isInDiscuss)
(this.composerViewOwner && this.composerViewOwner.isInDiscuss)
);
},
}),
@@ -93,7 +100,7 @@ Model({
compute() {
return Boolean(
(this.messageViewOwner && this.messageViewOwner.isInChatWindow) ||
(this.composerViewOwner && this.composerViewOwner.isInChatWindow)
(this.composerViewOwner && this.composerViewOwner.isInChatWindow)
);
},
}),
@@ -104,7 +111,7 @@ Model({
compute() {
return Boolean(
(this.messageViewOwner && this.messageViewOwner.isInChatter) ||
(this.composerViewOwner && this.composerViewOwner.isInChatter)
(this.composerViewOwner && this.composerViewOwner.isInChatter)
);
},
}),
@@ -115,7 +122,8 @@ Model({
compute() {
return Boolean(
this.composerViewOwner ||
(this.messageViewOwner && this.messageViewOwner.message.isCurrentUserOrGuestAuthor)
(this.messageViewOwner &&
this.messageViewOwner.message.isCurrentUserOrGuestAuthor)
);
},
}),
@@ -124,10 +132,7 @@ Model({
*/
isInChatWindowAndIsAlignedRight: attr({
compute() {
return Boolean(
this.isInChatWindow &&
this.isCurrentUserOrGuestAuthor
);
return Boolean(this.isInChatWindow && this.isCurrentUserOrGuestAuthor);
},
}),
/**
@@ -135,31 +140,28 @@ Model({
*/
isInChatWindowAndIsAlignedLeft: attr({
compute() {
return Boolean(
this.isInChatWindow &&
!this.isCurrentUserOrGuestAuthor
);
return Boolean(this.isInChatWindow && !this.isCurrentUserOrGuestAuthor);
},
}),
/**
* Link with a message view to handle attachments.
*/
messageViewOwner: one('MessageView', { identifying: true, inverse: 'attachmentList' }),
messageViewOwner: one("MessageView", { identifying: true, inverse: "attachmentList" }),
/**
* States the attachment that are not an image.
*/
nonImageAttachments: many('Attachment', {
nonImageAttachments: many("Attachment", {
compute() {
return this.attachments.filter(attachment => !attachment.isImage);
return this.attachments.filter((attachment) => !attachment.isImage);
},
}),
selectedAttachment: one('Attachment'),
selectedAttachment: one("Attachment"),
/**
* States the attachments that can be viewed inside the browser.
*/
viewableAttachments: many('Attachment', {
viewableAttachments: many("Attachment", {
compute() {
return this.attachments.filter(attachment => attachment.isViewable);
return this.attachments.filter((attachment) => attachment.isViewable);
},
}),
},
@@ -1,18 +1,18 @@
/** @odoo-module **/
import { attr, clear, increment, many, one, Model } from '@mail/model';
import { attr, clear, increment, many, one, Model } from "@mail/model";
import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs';
import { hidePDFJSButtons } from "@web/legacy/js/libs/pdfjs";
Model({
name: 'AttachmentViewer',
identifyingMode: 'xor',
name: "AttachmentViewer",
identifyingMode: "xor",
lifecycleHooks: {
_created() {
document.addEventListener('click', this._onClickGlobal);
document.addEventListener("click", this._onClickGlobal);
},
_willDelete() {
document.removeEventListener('click', this._onClickGlobal);
document.removeEventListener("click", this._onClickGlobal);
},
},
recordMethods: {
@@ -29,7 +29,9 @@ Model({
* @returns {boolean}
*/
containsElement(element) {
return Boolean(this.component && this.component.root.el && this.component.root.el.contains(element));
return Boolean(
this.component && this.component.root.el && this.component.root.el.contains(element)
);
},
/**
* Determine whether the current image is rendered for the 1st time, and if
@@ -106,7 +108,7 @@ Model({
* Called when clicking on image. Stop propagation of event to prevent
* closing the dialog.
*
* @param {MouseEvent} ev
* @param {MouseEvent} ev
*/
onClickImage(ev) {
if (this.isDragging) {
@@ -192,28 +194,28 @@ Model({
*/
onKeydown(ev) {
switch (ev.key) {
case 'ArrowRight':
case "ArrowRight":
this.next();
break;
case 'ArrowLeft':
case "ArrowLeft":
this.previous();
break;
case 'Escape':
case "Escape":
this.close();
break;
case 'q':
case "q":
this.close();
break;
case 'r':
case "r":
this.rotate();
break;
case '+':
case "+":
this.zoomIn();
break;
case '-':
case "-":
this.zoomOut();
break;
case '0':
case "0":
this.resetZoom();
break;
default:
@@ -296,7 +298,7 @@ Model({
* Prompt the browser print of this attachment.
*/
print() {
const printWindow = window.open('about:blank', '_new');
const printWindow = window.open("about:blank", "_new");
printWindow.document.open();
printWindow.document.write(`
<html>
@@ -349,20 +351,21 @@ Model({
* defining them in the template, for performance reasons.
*/
updateZoomerStyle() {
const tx = this.imageRef.offsetWidth * this.scale > this.zoomerRef.el.offsetWidth
? this.translate.x + this.translate.dx
: 0;
const ty = this.imageRef.offsetHeight * this.scale > this.zoomerRef.el.offsetHeight
? this.translate.y + this.translate.dy
: 0;
const tx =
this.imageRef.offsetWidth * this.scale > this.zoomerRef.el.offsetWidth
? this.translate.x + this.translate.dx
: 0;
const ty =
this.imageRef.offsetHeight * this.scale > this.zoomerRef.el.offsetHeight
? this.translate.y + this.translate.dy
: 0;
if (tx === 0) {
this.translate.update({ x: 0 });
}
if (ty === 0) {
this.translate.update({ y: 0 });
}
this.zoomerRef.el.style = `transform: ` +
`translate(${tx}px, ${ty}px)`;
this.zoomerRef.el.style = `transform: ` + `translate(${tx}px, ${ty}px)`;
},
/**
* Zoom in the image.
@@ -384,7 +387,8 @@ Model({
if (this.scale === this.minScale) {
return;
}
const unflooredAdaptedScale = this.scale - (scroll ? this.scrollZoomStep : this.zoomStep);
const unflooredAdaptedScale =
this.scale - (scroll ? this.scrollZoomStep : this.zoomStep);
this.update({
scale: Math.max(this.minScale, unflooredAdaptedScale),
});
@@ -410,7 +414,9 @@ Model({
* Angle of the image. Changes when the user rotates it.
*/
angle: attr({ default: 0 }),
attachmentList: one('AttachmentList', { related: 'dialogOwner.attachmentListOwnerAsAttachmentView' }),
attachmentList: one("AttachmentList", {
related: "dialogOwner.attachmentListOwnerAsAttachmentView",
}),
attachmentViewerViewable: one("AttachmentViewerViewable", {
compute() {
if (this.attachmentList) {
@@ -424,7 +430,7 @@ Model({
attachmentViewerViewables: many("AttachmentViewerViewable", {
compute() {
if (this.attachmentList) {
return this.attachmentList.viewableAttachments.map(attachment => {
return this.attachmentList.viewableAttachments.map((attachment) => {
return { attachmentOwner: attachment };
});
}
@@ -438,7 +444,11 @@ Model({
/**
* Determines the dialog displaying this attachment viewer.
*/
dialogOwner: one('Dialog', { identifying: true, inverse: 'attachmentViewer', isCausal: true }),
dialogOwner: one("Dialog", {
identifying: true,
inverse: "attachmentViewer",
isCausal: true,
}),
dragStartX: attr({ default: 0 }),
dragStartY: attr({ default: 0 }),
/**
@@ -451,18 +461,18 @@ Model({
*/
imageStyle: attr({
compute() {
let style = `transform: ` +
let style =
`transform: ` +
`scale3d(${this.scale}, ${this.scale}, 1) ` +
`rotate(${this.angle}deg);`;
if (this.angle % 180 !== 0) {
style += `` +
style +=
`` +
`max-height: ${window.innerWidth}px; ` +
`max-width: ${window.innerHeight}px;`;
} else {
style += `` +
`max-height: 100%; ` +
`max-width: 100%;`;
style += `` + `max-height: 100%; ` + `max-width: 100%;`;
}
return style;
},
@@ -484,7 +494,7 @@ Model({
*/
scale: attr({ default: 1 }),
scrollZoomStep: attr({ default: 0.1, readonly: true }),
translate: one('AttachmentViewer.Translate', { default: {}, inverse: 'owner' }),
translate: one("AttachmentViewer.Translate", { default: {}, inverse: "owner" }),
/**
* Reference of the zoomer node. Useful to apply translate
* transformation on image visualisation.
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { attr, one, Model } from '@mail/model';
import { attr, one, Model } from "@mail/model";
/**
* Tracked translate transformations on image visualisation. This is
@@ -10,11 +10,11 @@ import { attr, one, Model } from '@mail/model';
* position changes while dragging)
*/
Model({
name: 'AttachmentViewer.Translate',
name: "AttachmentViewer.Translate",
fields: {
dx: attr({ default: 0 }),
dy: attr({ default: 0 }),
owner: one('AttachmentViewer', { identifying: true, inverse: 'translate' }),
owner: one("AttachmentViewer", { identifying: true, inverse: "translate" }),
x: attr({ default: 0 }),
y: attr({ default: 0 }),
},
@@ -8,14 +8,17 @@ import { attr, one, Model } from "@mail/model";
*/
Model({
name: "AttachmentViewerViewable",
identifyingMode: 'xor',
identifyingMode: "xor",
recordMethods: {
download() {
return this.attachmentOwner.download();
},
},
fields: {
attachmentOwner: one("Attachment", { identifying: true, inverse: 'attachmentViewerViewable' }),
attachmentOwner: one("Attachment", {
identifying: true,
inverse: "attachmentViewerViewable",
}),
defaultSource: attr({
compute() {
return this.attachmentOwner.defaultSource;
@@ -1,15 +1,15 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { attr, clear, one, Model } from '@mail/model';
import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
import { attr, clear, one, Model } from "@mail/model";
import { onMounted, onWillUnmount } from '@odoo/owl';
import { onMounted, onWillUnmount } from "@odoo/owl";
Model({
name: 'AutocompleteInputView',
template: 'mail.AutocompleteInputView',
name: "AutocompleteInputView",
template: "mail.AutocompleteInputView",
componentSetup() {
useComponentToModel({ fieldName: 'component' });
useComponentToModel({ fieldName: "component" });
onMounted(() => {
if (!this.root.el) {
return;
@@ -28,12 +28,12 @@ Model({
html: this.isHtml,
};
if (this.customClass) {
args.classes = { 'ui-autocomplete': this.customClass };
args.classes = { "ui-autocomplete": this.customClass };
}
const autoCompleteElem = $(this.root.el).autocomplete(args);
// Resize the autocomplete dropdown options to handle the long strings
// By setting the width of dropdown based on the width of the input element.
autoCompleteElem.data('ui-autocomplete')._resizeMenu = function () {
autoCompleteElem.data("ui-autocomplete")._resizeMenu = function () {
const ul = this.menu.element;
ul.outerWidth(this.element.outerWidth());
};
@@ -42,10 +42,10 @@ Model({
if (!this.root.el) {
return;
}
$(this.root.el).autocomplete('destroy');
$(this.root.el).autocomplete("destroy");
});
},
identifyingMode: 'xor',
identifyingMode: "xor",
recordMethods: {
onBlur() {
if (!this.exists()) {
@@ -83,7 +83,7 @@ Model({
if (!this.exists()) {
return;
}
if (ev.key === 'Escape') {
if (ev.key === "Escape") {
this.onBlur();
}
},
@@ -107,7 +107,10 @@ Model({
return;
}
if (this.messagingMenuOwnerAsMobileNewMessageInput) {
this.messagingMenuOwnerAsMobileNewMessageInput.onMobileNewMessageInputSelect(ev, ui);
this.messagingMenuOwnerAsMobileNewMessageInput.onMobileNewMessageInputSelect(
ev,
ui
);
return;
}
},
@@ -128,34 +131,57 @@ Model({
return;
}
if (this.discussViewOwnerAsMobileAddItemHeader) {
this.discussViewOwnerAsMobileAddItemHeader.onMobileAddItemHeaderInputSource(req, res);
this.discussViewOwnerAsMobileAddItemHeader.onMobileAddItemHeaderInputSource(
req,
res
);
return;
}
if (this.messagingMenuOwnerAsMobileNewMessageInput) {
this.messagingMenuOwnerAsMobileNewMessageInput.onMobileNewMessageInputSource(req, res);
this.messagingMenuOwnerAsMobileNewMessageInput.onMobileNewMessageInputSource(
req,
res
);
return;
}
},
},
fields: {
chatWindowOwnerAsNewMessage: one('ChatWindow', { identifying: true, inverse: 'newMessageAutocompleteInputView' }),
chatWindowOwnerAsNewMessage: one("ChatWindow", {
identifying: true,
inverse: "newMessageAutocompleteInputView",
}),
component: attr(),
customClass: attr({ default: '',
customClass: attr({
default: "",
compute() {
if (this.discussSidebarCategoryOwnerAsAddingItem) {
if (this.discussSidebarCategoryOwnerAsAddingItem === this.messaging.discuss.categoryChannel) {
return 'o_DiscussSidebarCategory_newChannelAutocompleteSuggestions';
if (
this.discussSidebarCategoryOwnerAsAddingItem ===
this.messaging.discuss.categoryChannel
) {
return "o_DiscussSidebarCategory_newChannelAutocompleteSuggestions";
}
}
if (this.messagingMenuOwnerAsMobileNewMessageInput) {
return this.messagingMenuOwnerAsMobileNewMessageInput.viewId + '_mobileNewMessageInputAutocomplete';
return (
this.messagingMenuOwnerAsMobileNewMessageInput.viewId +
"_mobileNewMessageInputAutocomplete"
);
}
return clear();
},
}),
discussSidebarCategoryOwnerAsAddingItem: one('DiscussSidebarCategory', { identifying: true, inverse: 'addingItemAutocompleteInputView' }),
discussViewOwnerAsMobileAddItemHeader: one('DiscussView', { identifying: true, inverse: 'mobileAddItemHeaderAutocompleteInputView' }),
isFocusOnMount: attr({ default: false,
discussSidebarCategoryOwnerAsAddingItem: one("DiscussSidebarCategory", {
identifying: true,
inverse: "addingItemAutocompleteInputView",
}),
discussViewOwnerAsMobileAddItemHeader: one("DiscussView", {
identifying: true,
inverse: "mobileAddItemHeaderAutocompleteInputView",
}),
isFocusOnMount: attr({
default: false,
compute() {
if (this.discussViewOwnerAsMobileAddItemHeader) {
return true;
@@ -169,18 +195,25 @@ Model({
return clear();
},
}),
isHtml: attr({ default: false,
isHtml: attr({
default: false,
compute() {
if (this.discussViewOwnerAsMobileAddItemHeader) {
return this.discussViewOwnerAsMobileAddItemHeader.isAddingChannel;
}
if (this.discussSidebarCategoryOwnerAsAddingItem) {
return this.discussSidebarCategoryOwnerAsAddingItem === this.messaging.discuss.categoryChannel;
return (
this.discussSidebarCategoryOwnerAsAddingItem ===
this.messaging.discuss.categoryChannel
);
}
return clear();
},
}),
messagingMenuOwnerAsMobileNewMessageInput: one('MessagingMenu', { identifying: true, inverse: 'mobileNewMessageAutocompleteInputView' }),
messagingMenuOwnerAsMobileNewMessageInput: one("MessagingMenu", {
identifying: true,
inverse: "mobileNewMessageAutocompleteInputView",
}),
placeholder: attr({
compute() {
if (this.chatWindowOwnerAsNewMessage) {
@@ -188,16 +221,19 @@ Model({
}
if (this.discussViewOwnerAsMobileAddItemHeader) {
if (this.discussViewOwnerAsMobileAddItemHeader.isAddingChannel) {
return this.discussViewOwnerAsMobileAddItemHeader.discuss.addChannelInputPlaceholder;
return this.discussViewOwnerAsMobileAddItemHeader.discuss
.addChannelInputPlaceholder;
} else {
return this.discussViewOwnerAsMobileAddItemHeader.discuss.addChatInputPlaceholder;
return this.discussViewOwnerAsMobileAddItemHeader.discuss
.addChatInputPlaceholder;
}
}
if (this.discussSidebarCategoryOwnerAsAddingItem) {
return this.discussSidebarCategoryOwnerAsAddingItem.newItemPlaceholderText;
}
if (this.messagingMenuOwnerAsMobileNewMessageInput) {
return this.messagingMenuOwnerAsMobileNewMessageInput.mobileNewMessageInputPlaceholder;
return this.messagingMenuOwnerAsMobileNewMessageInput
.mobileNewMessageInputPlaceholder;
}
return clear();
},
+53 -50
View File
@@ -1,31 +1,37 @@
/** @odoo-module **/
import { attr, clear, one, Model } from '@mail/model';
import { attr, clear, one, Model } from "@mail/model";
function drawAndBlurImageOnCanvas(image, blurAmount, canvas) {
canvas.width = image.width;
canvas.height = image.height;
if (blurAmount === 0) {
canvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height);
canvas.getContext("2d").drawImage(image, 0, 0, image.width, image.height);
return;
}
canvas.getContext('2d').clearRect(0, 0, image.width, image.height);
canvas.getContext('2d').save();
canvas.getContext("2d").clearRect(0, 0, image.width, image.height);
canvas.getContext("2d").save();
// FIXME : Does not work on safari https://bugs.webkit.org/show_bug.cgi?id=198416
canvas.getContext('2d').filter = `blur(${blurAmount}px)`;
canvas.getContext('2d').drawImage(image, 0, 0, image.width, image.height);
canvas.getContext('2d').restore();
canvas.getContext("2d").filter = `blur(${blurAmount}px)`;
canvas.getContext("2d").drawImage(image, 0, 0, image.width, image.height);
canvas.getContext("2d").restore();
}
Model({
name: 'BlurManager',
name: "BlurManager",
lifecycleHooks: {
_willDelete() {
this.video.removeEventListener('loadeddata', this._onVideoPlay);
this.video.removeEventListener("loadeddata", this._onVideoPlay);
this.selfieSegmentation.reset();
this.video.srcObject = null;
if (this.rejectStreamPromise) {
this.rejectStreamPromise(new Error(this.env._t('The blur manager was removed before the beginning of the blur process')));
this.rejectStreamPromise(
new Error(
this.env._t(
"The blur manager was removed before the beginning of the blur process"
)
)
);
}
},
},
@@ -37,8 +43,8 @@ Model({
* @private
*/
_drawWithCompositing(image, compositeOperation) {
this.canvas.getContext('2d').globalCompositeOperation = compositeOperation;
this.canvas.getContext('2d').drawImage(image, 0, 0);
this.canvas.getContext("2d").globalCompositeOperation = compositeOperation;
this.canvas.getContext("2d").drawImage(image, 0, 0);
},
/**
* @private
@@ -63,7 +69,13 @@ Model({
this.video.srcObject = null;
this.selfieSegmentation.reset();
if (this.rejectStreamPromise) {
this.rejectStreamPromise(new Error(this.env._t('The source stream was removed before the beginning of the blur process')));
this.rejectStreamPromise(
new Error(
this.env._t(
"The source stream was removed before the beginning of the blur process"
)
)
);
}
if (!this.srcStream) {
return;
@@ -88,9 +100,9 @@ Model({
modelSelection: 1,
});
this.selfieSegmentation.onResults(this._onSelfieSegmentationResults);
this.video.addEventListener('loadeddata', this._onVideoPlay);
this.video.addEventListener("loadeddata", this._onVideoPlay);
this.video.autoplay = true;
Promise.resolve(this.video.play()).catch(()=>{});
Promise.resolve(this.video.play()).catch(() => {});
},
/**
* @private
@@ -121,32 +133,22 @@ Model({
drawAndBlurImageOnCanvas(
results.image,
this.userSetting.backgroundBlurAmount,
this.canvasBlur,
this.canvasBlur
);
this.canvas.width = this.canvasBlur.width;
this.canvas.height = this.canvasBlur.height;
drawAndBlurImageOnCanvas(
results.segmentationMask,
this.userSetting.edgeBlurAmount,
this.canvasMask,
this.canvasMask
);
this.canvas.getContext('2d').save();
this.canvas.getContext('2d').drawImage(
results.image,
0,
0,
this.canvas.width,
this.canvas.height,
);
this._drawWithCompositing(
this.canvasMask,
'destination-in',
);
this._drawWithCompositing(
this.canvasBlur,
'destination-over',
);
this.canvas.getContext('2d').restore();
this.canvas.getContext("2d").save();
this.canvas
.getContext("2d")
.drawImage(results.image, 0, 0, this.canvas.width, this.canvas.height);
this._drawWithCompositing(this.canvasMask, "destination-in");
this._drawWithCompositing(this.canvasBlur, "destination-over");
this.canvas.getContext("2d").restore();
},
/**
* @private
@@ -171,20 +173,21 @@ Model({
},
},
fields: {
canvas: attr({ default: document.createElement('canvas') }),
canvasBlur: attr({ default: document.createElement('canvas') }),
canvasMask: attr({ default: document.createElement('canvas') }),
canvasStream: one('MediaStream', { isCausal: true,
canvas: attr({ default: document.createElement("canvas") }),
canvasBlur: attr({ default: document.createElement("canvas") }),
canvasMask: attr({ default: document.createElement("canvas") }),
canvasStream: one("MediaStream", {
isCausal: true,
compute() {
if (this.srcStream) {
this.canvas.getContext('2d'); // canvas.captureStream() doesn't work on firefox before getContext() is called.
this.canvas.getContext("2d"); // canvas.captureStream() doesn't work on firefox before getContext() is called.
const webMediaStream = this.canvas.captureStream();
return { webMediaStream, id: webMediaStream.id };
}
return clear();
},
}),
frameRequestTimer: one('Timer', { inverse: 'blurManagerOwnerAsFrameRequest' }),
frameRequestTimer: one("Timer", { inverse: "blurManagerOwnerAsFrameRequest" }),
isVideoDataLoaded: attr({ default: false }),
/**
* promise reject function of this.stream promise
@@ -194,7 +197,7 @@ Model({
* promise resolve function of this.stream promise
*/
resolveStreamPromise: attr(),
rtc: one('Rtc', { identifying: true, inverse: 'blurManager' }),
rtc: one("Rtc", { identifying: true, inverse: "blurManager" }),
selfieSegmentation: attr({
default: new window.SelfieSegmentation({
locateFile: (file) => {
@@ -205,27 +208,27 @@ Model({
/**
* mail.MediaStream, source stream for which the blur effect is computed.
*/
srcStream: one('MediaStream', { isCausal: true }),
srcStream: one("MediaStream", { isCausal: true }),
/**
* Promise or undefined, based on this.srcStream, resolved when selfieSegmentation has started painting on the canvas,
* resolves into a web.MediaStream that is the blurred version of this.srcStream.
*/
stream: attr(),
userSetting: one('UserSetting', { related: 'messaging.userSetting' }),
video: attr({ default: document.createElement('video') }),
userSetting: one("UserSetting", { related: "messaging.userSetting" }),
video: attr({ default: document.createElement("video") }),
},
onChanges: [
{
dependencies: ['userSetting.edgeBlurAmount'],
methodName: '_onChangeEdgeBlurAmountSetting',
dependencies: ["userSetting.edgeBlurAmount"],
methodName: "_onChangeEdgeBlurAmountSetting",
},
{
dependencies: ['userSetting.backgroundBlurAmount'],
methodName: '_onChangeBackgroundBlurAmountSetting',
dependencies: ["userSetting.backgroundBlurAmount"],
methodName: "_onChangeBackgroundBlurAmountSetting",
},
{
dependencies: ['srcStream'],
methodName: '_onChangeSrcStream',
dependencies: ["srcStream"],
methodName: "_onChangeSrcStream",
},
],
});
@@ -1,10 +1,10 @@
/** @odoo-module **/
import { attr, clear, one, Model } from '@mail/model';
import { attr, clear, one, Model } from "@mail/model";
Model({
name: 'CallActionListView',
template: 'mail.CallActionListView',
name: "CallActionListView",
template: "mail.CallActionListView",
recordMethods: {
/**
* @param {MouseEvent} ev
@@ -80,7 +80,8 @@ Model({
},
},
fields: {
callButtonTitle: attr({ default: '',
callButtonTitle: attr({
default: "",
compute() {
if (!this.thread) {
return clear();
@@ -92,9 +93,10 @@ Model({
}
},
}),
callMainView: one('CallMainView', { identifying: true, inverse: 'callActionListView' }),
callView: one('CallView', { related: 'callMainView.callView', required: true }),
cameraButtonTitle: attr({ default: '',
callMainView: one("CallMainView", { identifying: true, inverse: "callActionListView" }),
callView: one("CallView", { related: "callMainView.callView", required: true }),
cameraButtonTitle: attr({
default: "",
compute() {
if (this.messaging.rtc.sendUserVideo) {
return this.env._t("Stop camera");
@@ -103,7 +105,8 @@ Model({
}
},
}),
headphoneButtonTitle: attr({ default: '',
headphoneButtonTitle: attr({
default: "",
compute() {
if (!this.messaging.rtc.currentRtcSession) {
return clear();
@@ -117,7 +120,9 @@ Model({
}),
isSmall: attr({
compute() {
return Boolean(this.callView && this.callView.threadView.compact && !this.callView.isFullScreen);
return Boolean(
this.callView && this.callView.threadView.compact && !this.callView.isFullScreen
);
},
}),
microphoneButtonTitle: attr({
@@ -132,9 +137,10 @@ Model({
}
},
}),
moreButtonRef: attr({ ref: 'moreButton' }),
moreMenuPopoverView: one('PopoverView', { inverse: 'callActionListViewOwnerAsMoreMenu' }),
screenSharingButtonTitle: attr({ default: '',
moreButtonRef: attr({ ref: "moreButton" }),
moreMenuPopoverView: one("PopoverView", { inverse: "callActionListViewOwnerAsMoreMenu" }),
screenSharingButtonTitle: attr({
default: "",
compute() {
if (this.messaging.rtc.sendDisplay) {
return this.env._t("Stop screen sharing");
@@ -143,6 +149,6 @@ Model({
}
},
}),
thread: one('Thread', { related: 'callMainView.thread', required: true }),
thread: one("Thread", { related: "callMainView.thread", required: true }),
},
});
@@ -1,10 +1,10 @@
/** @odoo-module **/
import { attr, one, Model } from '@mail/model';
import { attr, one, Model } from "@mail/model";
Model({
name: 'CallDemoView',
template: 'mail.CallDemoView',
name: "CallDemoView",
template: "mail.CallDemoView",
recordMethods: {
/**
* Stops recording user's microphone.
@@ -102,7 +102,7 @@ Model({
/**
* Ref to the audio element used for the audio feedback.
*/
audioRef: attr({ ref: 'audio' }),
audioRef: attr({ ref: "audio" }),
/**
* The MediaStream from the microphone.
*
@@ -118,8 +118,8 @@ Model({
compute() {
return Boolean(
navigator.mediaDevices &&
navigator.mediaDevices.getUserMedia &&
window.MediaStream
navigator.mediaDevices.getUserMedia &&
window.MediaStream
);
},
}),
@@ -142,7 +142,7 @@ Model({
/**
* Ref to the video element used for the video feedback.
*/
videoRef: attr({ ref: 'video' }),
videoRef: attr({ ref: "video" }),
/**
* The MediaStream from the camera.
*
@@ -153,6 +153,6 @@ Model({
/**
* States the welcome view containing this media preview.
*/
welcomeView: one('WelcomeView', { identifying: true, inverse: 'callDemoView' }),
welcomeView: one("WelcomeView", { identifying: true, inverse: "callDemoView" }),
},
});
@@ -1,10 +1,10 @@
/** @odoo-module **/
import { one, Model } from '@mail/model';
import { one, Model } from "@mail/model";
Model({
name: 'CallInviteRequestPopup',
template: 'mail.CallInviteRequestPopup',
name: "CallInviteRequestPopup",
template: "mail.CallInviteRequestPopup",
recordMethods: {
/**
* @param {MouseEvent} ev
@@ -33,6 +33,6 @@ Model({
},
},
fields: {
thread: one('Thread', { identifying: true, inverse: 'callInviteRequestPopup' }),
thread: one("Thread", { identifying: true, inverse: "callInviteRequestPopup" }),
},
});
+37 -22
View File
@@ -1,19 +1,19 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
import { attr, clear, many, one, Model } from '@mail/model';
import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model";
import { attr, clear, many, one, Model } from "@mail/model";
import { isEventHandled, markEventHandled } from '@mail/utils/utils';
import { isEventHandled, markEventHandled } from "@mail/utils/utils";
import { onMounted, onWillUnmount } from '@odoo/owl';
import { onMounted, onWillUnmount } from "@odoo/owl";
Model({
name: 'CallMainView',
template: 'mail.CallMainView',
name: "CallMainView",
template: "mail.CallMainView",
componentSetup() {
useComponentToModel({ fieldName: 'component' });
useUpdateToModel({ methodName: 'onComponentUpdate' });
useComponentToModel({ fieldName: "component" });
useUpdateToModel({ methodName: "onComponentUpdate" });
onMounted(() => {
this.resizeObserver = new ResizeObserver(() => this.onResize());
this.resizeObserver.observe(this.root.el);
@@ -92,7 +92,7 @@ Model({
* @param {MouseEvent} ev
*/
onMouseleave(ev) {
if (ev.relatedTarget && ev.relatedTarget.closest('.o_CallActionListView_popover')) {
if (ev.relatedTarget && ev.relatedTarget.closest(".o_CallActionListView_popover")) {
// the overlay should not be hidden when the cursor leaves to enter the controller popover
return;
}
@@ -108,7 +108,7 @@ Model({
if (!this.exists()) {
return;
}
if (isEventHandled(ev, 'CallMainView.MouseMoveOverlay')) {
if (isEventHandled(ev, "CallMainView.MouseMoveOverlay")) {
return;
}
this._showOverlay();
@@ -120,7 +120,7 @@ Model({
if (!this.exists()) {
return;
}
markEventHandled(ev, 'CallMainView.MouseMoveOverlay');
markEventHandled(ev, "CallMainView.MouseMoveOverlay");
this.update({
showOverlay: true,
showOverlayTimer: clear(),
@@ -175,37 +175,52 @@ Model({
/**
* The model for the controller (buttons).
*/
callActionListView: one('CallActionListView', { default: {}, inverse: 'callMainView', readonly: true }),
callView: one('CallView', { identifying: true, inverse: 'callMainView' }),
callActionListView: one("CallActionListView", {
default: {},
inverse: "callMainView",
readonly: true,
}),
callView: one("CallView", { identifying: true, inverse: "callMainView" }),
component: attr(),
hasSidebarButton: attr({
compute() {
return Boolean(this.callView.activeRtcSession && this.showOverlay && !this.callView.threadView.compact);
return Boolean(
this.callView.activeRtcSession &&
this.showOverlay &&
!this.callView.threadView.compact
);
},
}),
/**
* Determines if the controller is an overlay or a bottom bar.
*/
isControllerFloating: attr({ default: false,
isControllerFloating: attr({
default: false,
compute() {
return Boolean(this.callView.isFullScreen || this.callView.activeRtcSession && !this.callView.threadView.compact);
return Boolean(
this.callView.isFullScreen ||
(this.callView.activeRtcSession && !this.callView.threadView.compact)
);
},
}),
mainTiles: many('CallMainViewTile', { inverse: 'callMainViewOwner',
mainTiles: many("CallMainViewTile", {
inverse: "callMainViewOwner",
compute() {
if (this.callView.activeRtcSession) {
return [{ channelMember: this.callView.activeRtcSession.channelMember }];
}
return this.callView.filteredChannelMembers.map(channelMember => ({ channelMember }));
return this.callView.filteredChannelMembers.map((channelMember) => ({
channelMember,
}));
},
}),
/**
* Determines if we show the overlay with the control buttons.
*/
showOverlay: attr({ default: true }),
showOverlayTimer: one('Timer', { inverse: 'callMainViewAsShowOverlay' }),
thread: one('Thread', { related: 'callView.thread', required: true }),
tileContainerRef: attr({ ref: 'tileContainer' }),
showOverlayTimer: one("Timer", { inverse: "callMainViewAsShowOverlay" }),
thread: one("Thread", { related: "callView.thread", required: true }),
tileContainerRef: attr({ ref: "tileContainer" }),
tileHeight: attr({ default: 0 }),
tileWidth: attr({ default: 0 }),
},
@@ -1,12 +1,12 @@
/** @odoo-module **/
import { one, Model } from '@mail/model';
import { one, Model } from "@mail/model";
Model({
name: 'CallMainViewTile',
name: "CallMainViewTile",
fields: {
callMainViewOwner: one('CallMainView', { identifying: true, inverse: 'mainTiles' }),
channelMember: one('ChannelMember', { identifying: true }),
participantCard: one('CallParticipantCard', { default: {}, inverse: 'mainViewTileOwner' }),
callMainViewOwner: one("CallMainView", { identifying: true, inverse: "mainTiles" }),
channelMember: one("ChannelMember", { identifying: true }),
participantCard: one("CallParticipantCard", { default: {}, inverse: "mainViewTileOwner" }),
},
});
@@ -1,13 +1,13 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { one, Model } from '@mail/model';
import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
import { one, Model } from "@mail/model";
Model({
name: 'CallOptionMenu',
template: 'mail.CallOptionMenu',
name: "CallOptionMenu",
template: "mail.CallOptionMenu",
componentSetup() {
useComponentToModel({ fieldName: 'component' });
useComponentToModel({ fieldName: "component" });
},
recordMethods: {
/**
@@ -21,18 +21,22 @@ Model({
return;
}
const data = window.JSON.stringify(channel.rtc.logs);
const blob = new window.Blob([data], { type: 'application/json' });
const blob = new window.Blob([data], { type: "application/json" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
const a = document.createElement("a");
a.href = url;
a.download = `RtcLogs_Channel${channel.id}_Session${channel.rtc.currentRtcSession.id}_${window.moment().format('YYYY-MM-DD_HH-mm')}.json`;
a.download = `RtcLogs_Channel${channel.id}_Session${
channel.rtc.currentRtcSession.id
}_${window.moment().format("YYYY-MM-DD_HH-mm")}.json`;
a.click();
window.URL.revokeObjectURL(url);
},
},
fields: {
callActionListView: one('CallActionListView', { related: 'popoverViewOwner.callActionListViewOwnerAsMoreMenu' }),
callView: one('CallView', { related: 'callActionListView.callView', required: true }),
popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'callOptionMenuView' }),
callActionListView: one("CallActionListView", {
related: "popoverViewOwner.callActionListViewOwnerAsMoreMenu",
}),
callView: one("CallView", { related: "callActionListView.callView", required: true }),
popoverViewOwner: one("PopoverView", { identifying: true, inverse: "callOptionMenuView" }),
},
});
@@ -1,18 +1,18 @@
/** @odoo-module **/
import { attr, clear, one, Model } from '@mail/model';
import { isEventHandled, markEventHandled } from '@mail/utils/utils';
import { attr, clear, one, Model } from "@mail/model";
import { isEventHandled, markEventHandled } from "@mail/utils/utils";
Model({
name: 'CallParticipantCard',
template: 'mail.CallParticipantCard',
identifyingMode: 'xor',
name: "CallParticipantCard",
template: "mail.CallParticipantCard",
identifyingMode: "xor",
recordMethods: {
/**
* @param {MouseEvent} ev
*/
async onClick(ev) {
if (isEventHandled(ev, 'CallParticipantCard.clickVolumeAnchor')) {
if (isEventHandled(ev, "CallParticipantCard.clickVolumeAnchor")) {
return;
}
if (this.rtcSession) {
@@ -24,13 +24,13 @@ Model({
return;
}
const channel = this.channelMember.channel.thread;
const channelData = await this.messaging.rpc(({
route: '/mail/rtc/channel/cancel_call_invitation',
const channelData = await this.messaging.rpc({
route: "/mail/rtc/channel/cancel_call_invitation",
params: {
channel_id: channel.id,
member_ids: [this.channelMember.id],
},
}));
});
if (!channel.exists()) {
return;
}
@@ -42,8 +42,10 @@ Model({
* @param {MouseEvent} ev
*/
async onClickVolumeAnchor(ev) {
markEventHandled(ev, 'CallParticipantCard.clickVolumeAnchor');
this.update({ callParticipantCardPopoverView: this.callParticipantCardPopoverView ? clear() : {} });
markEventHandled(ev, "CallParticipantCard.clickVolumeAnchor");
this.update({
callParticipantCardPopoverView: this.callParticipantCardPopoverView ? clear() : {},
});
},
/**
* This listens to the right click event, and used to redirect the event
@@ -60,8 +62,9 @@ Model({
},
},
fields: {
callParticipantCardPopoverView: one('PopoverView', { inverse: 'callParticipantCardOwner' }),
channelMember: one('ChannelMember', { inverse: 'callParticipantCards',
callParticipantCardPopoverView: one("PopoverView", { inverse: "callParticipantCardOwner" }),
channelMember: one("ChannelMember", {
inverse: "callParticipantCards",
compute() {
if (this.sidebarViewTileOwner) {
return this.sidebarViewTileOwner.channelMember;
@@ -69,11 +72,15 @@ Model({
return this.mainViewTileOwner.channelMember;
},
}),
mainViewTileOwner: one('CallMainViewTile', { identifying: true, inverse: 'participantCard', }),
mainViewTileOwner: one("CallMainViewTile", {
identifying: true,
inverse: "participantCard",
}),
/**
* Determines if this card has to be displayed in a minimized form.
*/
isMinimized: attr({ default: false,
isMinimized: attr({
default: false,
compute() {
return Boolean(this.callView && this.callView.isMinimized);
},
@@ -81,15 +88,19 @@ Model({
/**
* Determines if the rtcSession is in a valid "talking" state.
*/
isTalking: attr({ default: false,
isTalking: attr({
default: false,
compute() {
return Boolean(this.rtcSession && this.rtcSession.isTalking && !this.rtcSession.isMute);
return Boolean(
this.rtcSession && this.rtcSession.isTalking && !this.rtcSession.isMute
);
},
}),
/**
* The callView that displays this card.
*/
callView: one('CallView', { inverse: 'participantCards',
callView: one("CallView", {
inverse: "participantCards",
compute() {
if (this.sidebarViewTileOwner) {
return this.sidebarViewTileOwner.callSidebarViewOwner.callView;
@@ -97,9 +108,16 @@ Model({
return this.mainViewTileOwner.callMainViewOwner.callView;
},
}),
rtcSession: one('RtcSession', { inverse: 'callParticipantCards', related: 'channelMember.rtcSession' }),
sidebarViewTileOwner: one('CallSidebarViewTile', { identifying: true, inverse: 'participantCard' }),
callParticipantVideoView: one('CallParticipantVideoView', { inverse: 'callParticipantCardOwner',
rtcSession: one("RtcSession", {
inverse: "callParticipantCards",
related: "channelMember.rtcSession",
}),
sidebarViewTileOwner: one("CallSidebarViewTile", {
identifying: true,
inverse: "participantCard",
}),
callParticipantVideoView: one("CallParticipantVideoView", {
inverse: "callParticipantCardOwner",
compute() {
if (this.rtcSession && this.rtcSession.videoStream) {
return {};
@@ -107,6 +125,6 @@ Model({
return clear();
},
}),
volumeMenuAnchorRef: attr({ ref: 'volumeMenuAnchor' }),
volumeMenuAnchorRef: attr({ ref: "volumeMenuAnchor" }),
},
});
@@ -1,28 +1,35 @@
/** @odoo-module **/
import { attr, one, Model } from '@mail/model';
import { attr, one, Model } from "@mail/model";
import { sprintf } from '@web/core/utils/strings';
import { sprintf } from "@web/core/utils/strings";
Model({
name: 'CallParticipantCardPopoverContentView',
template: 'mail.CallParticipantCardPopoverContentView',
name: "CallParticipantCardPopoverContentView",
template: "mail.CallParticipantCardPopoverContentView",
recordMethods: {
/**
* @param {Event} ev
*/
onChangeVolume(ev) {
this.callParticipantCard.rtcSession && this.callParticipantCard.rtcSession.setVolume(parseFloat(ev.target.value));
this.callParticipantCard.rtcSession &&
this.callParticipantCard.rtcSession.setVolume(parseFloat(ev.target.value));
},
},
fields: {
callParticipantCard: one('CallParticipantCard', { related: 'popoverViewOwner.callParticipantCardOwner' }),
callParticipantCard: one("CallParticipantCard", {
related: "popoverViewOwner.callParticipantCardOwner",
}),
/**
* Determines whether or not we show the connection info.
*/
hasConnectionInfo: attr({
compute() {
return Boolean(this.callParticipantCard.rtcSession && this.env.debug && this.callParticipantCard.channelMember.channel.thread.rtc);
return Boolean(
this.callParticipantCard.rtcSession &&
this.env.debug &&
this.callParticipantCard.channelMember.channel.thread.rtc
);
},
}),
/**
@@ -30,16 +37,22 @@ Model({
*/
inboundConnectionTypeText: attr({
compute() {
if (!this.callParticipantCard.rtcSession || !this.callParticipantCard.rtcSession.remoteCandidateType) {
return sprintf(this.env._t('From %s: no connection'), this.callParticipantCard.channelMember.persona.name);
if (
!this.callParticipantCard.rtcSession ||
!this.callParticipantCard.rtcSession.remoteCandidateType
) {
return sprintf(
this.env._t("From %s: no connection"),
this.callParticipantCard.channelMember.persona.name
);
}
return sprintf(
this.env._t('From %(name)s: %(candidateType)s (%(protocol)s)'), {
candidateType: this.callParticipantCard.rtcSession.remoteCandidateType,
name: this.callParticipantCard.channelMember.persona.name,
protocol: this.messaging.rtc.protocolsByCandidateTypes[this.callParticipantCard.rtcSession.remoteCandidateType],
},
);
return sprintf(this.env._t("From %(name)s: %(candidateType)s (%(protocol)s)"), {
candidateType: this.callParticipantCard.rtcSession.remoteCandidateType,
name: this.callParticipantCard.channelMember.persona.name,
protocol: this.messaging.rtc.protocolsByCandidateTypes[
this.callParticipantCard.rtcSession.remoteCandidateType
],
});
},
}),
/**
@@ -47,18 +60,27 @@ Model({
*/
outboundConnectionTypeText: attr({
compute() {
if (!this.callParticipantCard.rtcSession || !this.callParticipantCard.rtcSession.localCandidateType) {
return sprintf(this.env._t('To %s: no connection'), this.callParticipantCard.channelMember.persona.name);
if (
!this.callParticipantCard.rtcSession ||
!this.callParticipantCard.rtcSession.localCandidateType
) {
return sprintf(
this.env._t("To %s: no connection"),
this.callParticipantCard.channelMember.persona.name
);
}
return sprintf(
this.env._t('To %(name)s: %(candidateType)s (%(protocol)s)'), {
candidateType: this.callParticipantCard.rtcSession.localCandidateType,
name: this.callParticipantCard.channelMember.persona.name,
protocol: this.messaging.rtc.protocolsByCandidateTypes[this.callParticipantCard.rtcSession.localCandidateType],
},
);
return sprintf(this.env._t("To %(name)s: %(candidateType)s (%(protocol)s)"), {
candidateType: this.callParticipantCard.rtcSession.localCandidateType,
name: this.callParticipantCard.channelMember.persona.name,
protocol: this.messaging.rtc.protocolsByCandidateTypes[
this.callParticipantCard.rtcSession.localCandidateType
],
});
},
}),
popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'callParticipantCardPopoverContentView' }),
popoverViewOwner: one("PopoverView", {
identifying: true,
inverse: "callParticipantCardPopoverContentView",
}),
},
});
@@ -1,15 +1,15 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
import { attr, clear, one, Model } from '@mail/model';
import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model";
import { attr, clear, one, Model } from "@mail/model";
Model({
name: 'CallParticipantVideoView',
template: 'mail.CallParticipantVideoView',
name: "CallParticipantVideoView",
template: "mail.CallParticipantVideoView",
componentSetup() {
useComponentToModel({ fieldName: 'component' });
useUpdateToModel({ methodName: 'onComponentUpdate' });
useComponentToModel({ fieldName: "component" });
useUpdateToModel({ methodName: "onComponentUpdate" });
},
recordMethods: {
/**
@@ -37,7 +37,7 @@ Model({
try {
await ev.target.play();
} catch (error) {
if (typeof error === 'object' && error.name === 'NotAllowedError') {
if (typeof error === "object" && error.name === "NotAllowedError") {
// Ignored as some browsers may reject play() calls that do not
// originate from a user input.
return;
@@ -47,9 +47,12 @@ Model({
},
},
fields: {
callParticipantCardOwner: one('CallParticipantCard', { identifying: true, inverse: 'callParticipantVideoView' }),
callParticipantCardOwner: one("CallParticipantCard", {
identifying: true,
inverse: "callParticipantVideoView",
}),
component: attr(),
rtcSession: one('RtcSession', {
rtcSession: one("RtcSession", {
compute() {
if (this.callParticipantCardOwner.rtcSession) {
return this.callParticipantCardOwner.rtcSession;
@@ -2,21 +2,23 @@
import { browser } from "@web/core/browser/browser";
import { attr, clear, one, Model } from '@mail/model';
import { attr, clear, one, Model } from "@mail/model";
Model({
name: 'CallSettingsMenu',
template: 'mail.CallSettingsMenu',
identifyingMode: 'xor',
name: "CallSettingsMenu",
template: "mail.CallSettingsMenu",
identifyingMode: "xor",
lifecycleHooks: {
async _created() {
browser.addEventListener('keydown', this._onKeyDown);
browser.addEventListener('keyup', this._onKeyUp);
this.update({ userDevices: await this.messaging.browser.navigator.mediaDevices.enumerateDevices() });
browser.addEventListener("keydown", this._onKeyDown);
browser.addEventListener("keyup", this._onKeyUp);
this.update({
userDevices: await this.messaging.browser.navigator.mediaDevices.enumerateDevices(),
});
},
_willDelete() {
browser.removeEventListener('keydown', this._onKeyDown);
browser.removeEventListener('keyup', this._onKeyUp);
browser.removeEventListener("keydown", this._onKeyDown);
browser.removeEventListener("keyup", this._onKeyUp);
},
},
recordMethods: {
@@ -100,7 +102,7 @@ Model({
},
},
fields: {
callView: one('CallView', {
callView: one("CallView", {
compute() {
if (this.threadViewOwner) {
return this.threadViewOwner.callView;
@@ -111,8 +113,8 @@ Model({
return clear();
},
}),
chatWindowOwner: one('ChatWindow', { identifying: true, inverse: 'callSettingsMenu' }),
thread: one('Thread', {
chatWindowOwner: one("ChatWindow", { identifying: true, inverse: "callSettingsMenu" }),
thread: one("Thread", {
compute() {
if (this.threadViewOwner) {
return this.threadViewOwner.thread;
@@ -123,8 +125,8 @@ Model({
return clear();
},
}),
threadViewOwner: one('ThreadView', { identifying: true, inverse: 'callSettingsMenu' }),
threadViewOwner: one("ThreadView", { identifying: true, inverse: "callSettingsMenu" }),
userDevices: attr({ default: [] }),
userSetting: one('UserSetting', { related: 'messaging.userSetting' }),
userSetting: one("UserSetting", { related: "messaging.userSetting" }),
},
});
@@ -1,15 +1,18 @@
/** @odoo-module **/
import { many, one, Model } from '@mail/model';
import { many, one, Model } from "@mail/model";
Model({
name: 'CallSidebarView',
template: 'mail.CallSidebarView',
name: "CallSidebarView",
template: "mail.CallSidebarView",
fields: {
callView: one('CallView', { identifying: true, inverse: 'callSidebarView' }),
sidebarTiles: many('CallSidebarViewTile', { inverse: 'callSidebarViewOwner',
callView: one("CallView", { identifying: true, inverse: "callSidebarView" }),
sidebarTiles: many("CallSidebarViewTile", {
inverse: "callSidebarViewOwner",
compute() {
return this.callView.filteredChannelMembers.map(channelMember => ({ channelMember }));
return this.callView.filteredChannelMembers.map((channelMember) => ({
channelMember,
}));
},
}),
},
@@ -1,12 +1,18 @@
/** @odoo-module **/
import { one, Model } from '@mail/model';
import { one, Model } from "@mail/model";
Model({
name: 'CallSidebarViewTile',
name: "CallSidebarViewTile",
fields: {
callSidebarViewOwner: one('CallSidebarView', { identifying: true, inverse: 'sidebarTiles' }),
channelMember: one('ChannelMember', { identifying: true }),
participantCard: one('CallParticipantCard', { default: {}, inverse: 'sidebarViewTileOwner' }),
callSidebarViewOwner: one("CallSidebarView", {
identifying: true,
inverse: "sidebarTiles",
}),
channelMember: one("ChannelMember", { identifying: true }),
participantCard: one("CallParticipantCard", {
default: {},
inverse: "sidebarViewTileOwner",
}),
},
});
@@ -1,24 +1,25 @@
/** @odoo-module **/
import { attr, clear, one, Model } from '@mail/model';
import { attr, clear, one, Model } from "@mail/model";
import { sprintf } from '@web/core/utils/strings';
import { sprintf } from "@web/core/utils/strings";
Model({
name: 'CallSystrayMenu',
template: 'mail.CallSystrayMenu',
name: "CallSystrayMenu",
template: "mail.CallSystrayMenu",
fields: {
buttonTitle: attr({ default: '',
buttonTitle: attr({
default: "",
compute() {
if (!this.messaging.rtc.channel) {
return clear();
}
return sprintf(
this.env._t("Open conference: %s"),
this.messaging.rtc.channel.displayName,
this.messaging.rtc.channel.displayName
);
},
}),
rtc: one('Rtc', { identifying: true, inverse: 'callSystrayMenu' }),
rtc: one("Rtc", { identifying: true, inverse: "callSystrayMenu" }),
},
});
+34 -24
View File
@@ -2,17 +2,17 @@
import { browser } from "@web/core/browser/browser";
import { attr, clear, many, one, Model } from '@mail/model';
import { attr, clear, many, one, Model } from "@mail/model";
Model({
name: 'CallView',
template: 'mail.CallView',
name: "CallView",
template: "mail.CallView",
lifecycleHooks: {
_created() {
browser.addEventListener('fullscreenchange', this._onFullScreenChange);
browser.addEventListener("fullscreenchange", this._onFullScreenChange);
},
_willDelete() {
browser.removeEventListener('fullscreenchange', this._onFullScreenChange);
browser.removeEventListener("fullscreenchange", this._onFullScreenChange);
},
},
recordMethods: {
@@ -41,12 +41,13 @@ Model({
}
this.messaging.notify({
message: this.env._t("The FullScreen mode was denied by the browser"),
type: 'warning',
type: "warning",
});
}
},
async deactivateFullScreen() {
const fullScreenElement = document.webkitFullscreenElement || document.fullscreenElement;
const fullScreenElement =
document.webkitFullscreenElement || document.fullscreenElement;
if (fullScreenElement) {
if (document.exitFullscreen) {
await document.exitFullscreen();
@@ -85,7 +86,8 @@ Model({
* @private
*/
_onFullScreenChange() {
const fullScreenElement = document.webkitFullscreenElement || document.fullscreenElement;
const fullScreenElement =
document.webkitFullscreenElement || document.fullscreenElement;
if (fullScreenElement) {
this.update({ isFullScreen: true });
return;
@@ -97,20 +99,23 @@ Model({
/**
* The rtc session that is the main card of the view.
*/
activeRtcSession: one('RtcSession', { related: 'channel.activeRtcSession' }),
activeRtcSession: one("RtcSession", { related: "channel.activeRtcSession" }),
/**
* The aspect ratio of the tiles.
*/
aspectRatio: attr({ default: 16 / 9,
aspectRatio: attr({
default: 16 / 9,
compute() {
const rtcAspectRatio = this.messaging.rtc.videoConfig && this.messaging.rtc.videoConfig.aspectRatio;
const rtcAspectRatio =
this.messaging.rtc.videoConfig && this.messaging.rtc.videoConfig.aspectRatio;
const aspectRatio = rtcAspectRatio || 16 / 9;
// if we are in minimized mode (round avatar frames), we treat the cards like squares.
return this.isMinimized ? 1 : aspectRatio;
},
}),
callMainView: one('CallMainView', { default: {}, inverse: 'callView', readonly: true }),
callSidebarView: one('CallSidebarView', { inverse: 'callView',
callMainView: one("CallMainView", { default: {}, inverse: "callView", readonly: true }),
callSidebarView: one("CallSidebarView", {
inverse: "callView",
compute() {
if (this.activeRtcSession && this.isSidebarOpen && !this.threadView.compact) {
return {};
@@ -118,15 +123,19 @@ Model({
return clear();
},
}),
channel: one('Channel', { related: 'thread.channel' }),
filteredChannelMembers: many('ChannelMember', {
channel: one("Channel", { related: "thread.channel" }),
filteredChannelMembers: many("ChannelMember", {
compute() {
if (!this.channel) {
return clear();
}
const channelMembers = [];
for (const channelMember of this.channel.callParticipants) {
if (this.channel.showOnlyVideo && this.thread.videoCount > 0 && !channelMember.isStreaming) {
if (
this.channel.showOnlyVideo &&
this.thread.videoCount > 0 &&
!channelMember.isStreaming
) {
continue;
}
channelMembers.push(channelMember);
@@ -142,7 +151,8 @@ Model({
* Determines if the tiles are in a minimized format:
* small circles instead of cards, smaller display area.
*/
isMinimized: attr({ default: false,
isMinimized: attr({
default: false,
compute() {
if (!this.threadView || !this.thread) {
return true;
@@ -169,7 +179,7 @@ Model({
* All the participant cards of the call viewer (main card and tile cards).
* this is a technical inverse to distinguish from the other relation 'tileParticipantCards'.
*/
participantCards: many('CallParticipantCard', { inverse: 'callView', isCausal: true }),
participantCards: many("CallParticipantCard", { inverse: "callView", isCausal: true }),
/**
* Text content that is displayed on title of the settings dialog.
*/
@@ -178,20 +188,20 @@ Model({
return this.env._t("Settings");
},
}),
thread: one('Thread', { related: 'threadView.thread', required: true }),
thread: one("Thread", { related: "threadView.thread", required: true }),
/**
* ThreadView on which the call view is attached.
*/
threadView: one('ThreadView', { identifying: true, inverse: 'callView' }),
threadView: one("ThreadView", { identifying: true, inverse: "callView" }),
},
onChanges: [
{
dependencies: ['thread.rtc'],
methodName: '_onChangeRtcChannel',
dependencies: ["thread.rtc"],
methodName: "_onChangeRtcChannel",
},
{
dependencies: ['thread.videoCount'],
methodName: '_onChangeVideoCount',
dependencies: ["thread.videoCount"],
methodName: "_onChangeVideoCount",
},
],
});
@@ -1,10 +1,10 @@
/** @odoo-module **/
import { attr, one, Model } from '@mail/model';
import { cleanSearchTerm } from '@mail/utils/utils';
import { attr, one, Model } from "@mail/model";
import { cleanSearchTerm } from "@mail/utils/utils";
Model({
name: 'CannedResponse',
name: "CannedResponse",
modelMethods: {
/**
* Fetches canned responses matching the given search term to extend the
@@ -32,12 +32,18 @@ Model({
getSuggestionSortFunction(searchTerm, { thread } = {}) {
const cleanedSearchTerm = cleanSearchTerm(searchTerm);
return (a, b) => {
const cleanedAName = cleanSearchTerm(a.source || '');
const cleanedBName = cleanSearchTerm(b.source || '');
if (cleanedAName.startsWith(cleanedSearchTerm) && !cleanedBName.startsWith(cleanedSearchTerm)) {
const cleanedAName = cleanSearchTerm(a.source || "");
const cleanedBName = cleanSearchTerm(b.source || "");
if (
cleanedAName.startsWith(cleanedSearchTerm) &&
!cleanedBName.startsWith(cleanedSearchTerm)
) {
return -1;
}
if (!cleanedAName.startsWith(cleanedSearchTerm) && cleanedBName.startsWith(cleanedSearchTerm)) {
if (
!cleanedAName.startsWith(cleanedSearchTerm) &&
cleanedBName.startsWith(cleanedSearchTerm)
) {
return 1;
}
if (cleanedAName < cleanedBName) {
@@ -61,9 +67,11 @@ Model({
*/
searchSuggestions(searchTerm, { thread } = {}) {
const cleanedSearchTerm = cleanSearchTerm(searchTerm);
return [this.messaging.cannedResponses.filter(cannedResponse =>
cleanSearchTerm(cannedResponse.source).includes(cleanedSearchTerm)
)];
return [
this.messaging.cannedResponses.filter((cannedResponse) =>
cleanSearchTerm(cannedResponse.source).includes(cleanedSearchTerm)
),
];
},
},
fields: {
@@ -77,6 +85,11 @@ Model({
* entered.
*/
substitution: attr(),
suggestable: one('ComposerSuggestable', { default: {}, inverse: 'cannedResponse', readonly: true, required: true }),
suggestable: one("ComposerSuggestable", {
default: {},
inverse: "cannedResponse",
readonly: true,
required: true,
}),
},
});
+60 -46
View File
@@ -1,9 +1,9 @@
/** @odoo-module **/
import { attr, clear, one, many, Model } from '@mail/model';
import { attr, clear, one, many, Model } from "@mail/model";
Model({
name: 'Channel',
name: "Channel",
modelMethods: {
/**
* Performs the `channel_get` RPC on `mail.channel`.
@@ -19,8 +19,8 @@ Model({
async performRpcCreateChat({ partnerIds, pinForCurrentPartner }) {
// TODO FIX: potential duplicate chat task-2276490
const data = await this.messaging.rpc({
model: 'mail.channel',
method: 'channel_get',
model: "mail.channel",
method: "channel_get",
kwargs: {
partners_to: partnerIds,
pin: pinForCurrentPartner,
@@ -29,8 +29,8 @@ Model({
if (!data) {
return;
}
const { channel } = this.messaging.models['Thread'].insert(
this.messaging.models['Thread'].convertData(data)
const { channel } = this.messaging.models["Thread"].insert(
this.messaging.models["Thread"].convertData(data)
);
return channel;
},
@@ -38,11 +38,11 @@ Model({
recordMethods: {
async fetchChannelMembers() {
const channelData = await this.messaging.rpc({
model: 'mail.channel',
method: 'load_more_members',
model: "mail.channel",
method: "load_more_members",
args: [[this.id]],
kwargs: {
known_member_ids: this.channelMembers.map(channelMember => channelMember.id),
known_member_ids: this.channelMembers.map((channelMember) => channelMember.id),
},
});
if (!this.exists()) {
@@ -52,7 +52,7 @@ Model({
},
},
fields: {
activeRtcSession: one('RtcSession'),
activeRtcSession: one("RtcSession"),
areAllMembersLoaded: attr({
compute() {
return this.memberCount === this.channelMembers.length;
@@ -62,7 +62,7 @@ Model({
* Cache key to force a reload of the avatar when avatar is changed.
*/
avatarCacheKey: attr(),
callParticipants: many('ChannelMember', {
callParticipants: many("ChannelMember", {
compute() {
if (!this.thread) {
return clear();
@@ -74,28 +74,33 @@ Model({
return callParticipants;
},
sort: [
['truthy-first', 'rtcSession'],
['smaller-first', 'rtcSession.id'],
["truthy-first", "rtcSession"],
["smaller-first", "rtcSession.id"],
],
}),
channelMembers: many('ChannelMember', { inverse: 'channel', isCausal: true }),
channelPreviewViews: many('ChannelPreviewView', { inverse: 'channel' }),
channelMembers: many("ChannelMember", { inverse: "channel", isCausal: true }),
channelPreviewViews: many("ChannelPreviewView", { inverse: "channel" }),
channel_type: attr(),
correspondent: one('Partner', {
correspondent: one("Partner", {
compute() {
if (this.channel_type === 'channel') {
if (this.channel_type === "channel") {
return clear();
}
const correspondents = this.channelMembers
.filter(member => member.persona && member.persona.partner && !member.isMemberOfCurrentUser)
.map(member => member.persona.partner);
.filter(
(member) =>
member.persona &&
member.persona.partner &&
!member.isMemberOfCurrentUser
)
.map((member) => member.persona.partner);
if (correspondents.length === 1) {
// 2 members chat
return correspondents[0];
}
const partners = this.channelMembers
.filter(member => member.persona && member.persona.partner)
.map(member => member.persona.partner);
.filter((member) => member.persona && member.persona.partner)
.map((member) => member.persona.partner);
if (partners.length === 1) {
// chat with oneself
return partners[0];
@@ -103,12 +108,10 @@ Model({
return clear();
},
}),
correspondentOfDmChat: one('Partner', { inverse: 'dmChatWithCurrentPartner',
correspondentOfDmChat: one("Partner", {
inverse: "dmChatWithCurrentPartner",
compute() {
if (
this.channel_type === 'chat' &&
this.correspondent
) {
if (this.channel_type === "chat" && this.correspondent) {
return this.correspondent;
}
return clear();
@@ -118,13 +121,13 @@ Model({
/**
* Useful to compute `discussSidebarCategoryItem`.
*/
discussSidebarCategory: one('DiscussSidebarCategory', {
discussSidebarCategory: one("DiscussSidebarCategory", {
compute() {
switch (this.channel_type) {
case 'channel':
case "channel":
return this.messaging.discuss.categoryChannel;
case 'chat':
case 'group':
case "chat":
case "group":
return this.messaging.discuss.categoryChat;
default:
return clear();
@@ -135,7 +138,8 @@ Model({
* Determines the discuss sidebar category item that displays this
* channel.
*/
discussSidebarCategoryItem: one('DiscussSidebarCategoryItem', { inverse: 'channel',
discussSidebarCategoryItem: one("DiscussSidebarCategoryItem", {
inverse: "channel",
compute() {
if (!this.thread) {
return clear();
@@ -154,13 +158,16 @@ Model({
if (!this.thread) {
return;
}
if (this.channel_type === 'chat' && this.correspondent) {
return this.custom_channel_name || this.thread.getMemberName(this.correspondent.persona);
if (this.channel_type === "chat" && this.correspondent) {
return (
this.custom_channel_name ||
this.thread.getMemberName(this.correspondent.persona)
);
}
if (this.channel_type === 'group' && !this.thread.name) {
if (this.channel_type === "group" && !this.thread.name) {
return this.channelMembers
.filter(channelMember => channelMember.persona)
.map(channelMember => this.thread.getMemberName(channelMember.persona))
.filter((channelMember) => channelMember.persona)
.map((channelMember) => this.thread.getMemberName(channelMember.persona))
.join(this.env._t(", "));
}
return this.thread.name;
@@ -179,7 +186,9 @@ Model({
// By default trust the server up to the last message it used
// because it's not possible to do better.
let baseCounter = this.serverMessageUnreadCounter;
let countFromId = this.thread.serverLastMessage ? this.thread.serverLastMessage.id : 0;
let countFromId = this.thread.serverLastMessage
? this.thread.serverLastMessage.id
: 0;
// But if the client knows the last seen message that the server
// returned (and by assumption all the messages that come after),
// the counter can be computed fully locally, ignoring potentially
@@ -207,17 +216,19 @@ Model({
* States the number of members in this channel according to the server.
*/
memberCount: attr(),
memberOfCurrentUser: one('ChannelMember', { inverse: 'channelAsMemberOfCurrentUser' }),
orderedOfflineMembers: many('ChannelMember', { inverse: 'channelAsOfflineMember',
memberOfCurrentUser: one("ChannelMember", { inverse: "channelAsMemberOfCurrentUser" }),
orderedOfflineMembers: many("ChannelMember", {
inverse: "channelAsOfflineMember",
sort: [
['truthy-first', 'persona.name'],
['case-insensitive-asc', 'persona.name'],
["truthy-first", "persona.name"],
["case-insensitive-asc", "persona.name"],
],
}),
orderedOnlineMembers: many('ChannelMember', { inverse: 'channelAsOnlineMember',
orderedOnlineMembers: many("ChannelMember", {
inverse: "channelAsOnlineMember",
sort: [
['truthy-first', 'persona.name'],
['case-insensitive-asc', 'persona.name'],
["truthy-first", "persona.name"],
["case-insensitive-asc", "persona.name"],
],
}),
/**
@@ -235,11 +246,14 @@ Model({
* Determines whether we only display the participants who broadcast a video or all of them.
*/
showOnlyVideo: attr({ default: false }),
thread: one('Thread', { inverse: 'channel', isCausal: true, required: true,
thread: one("Thread", {
inverse: "channel",
isCausal: true,
required: true,
compute() {
return {
id: this.id,
model: 'mail.channel',
model: "mail.channel",
};
},
}),
@@ -1,10 +1,10 @@
/** @odoo-module **/
import { attr, one, Model } from '@mail/model';
import { cleanSearchTerm } from '@mail/utils/utils';
import { attr, one, Model } from "@mail/model";
import { cleanSearchTerm } from "@mail/utils/utils";
Model({
name: 'ChannelCommand',
name: "ChannelCommand",
modelMethods: {
/**
* Fetches channel commands matching the given search term to extend the
@@ -40,12 +40,18 @@ Model({
if (!isATypeSpecific && isBTypeSpecific) {
return 1;
}
const cleanedAName = cleanSearchTerm(a.name || '');
const cleanedBName = cleanSearchTerm(b.name || '');
if (cleanedAName.startsWith(cleanedSearchTerm) && !cleanedBName.startsWith(cleanedSearchTerm)) {
const cleanedAName = cleanSearchTerm(a.name || "");
const cleanedBName = cleanSearchTerm(b.name || "");
if (
cleanedAName.startsWith(cleanedSearchTerm) &&
!cleanedBName.startsWith(cleanedSearchTerm)
) {
return -1;
}
if (!cleanedAName.startsWith(cleanedSearchTerm) && cleanedBName.startsWith(cleanedSearchTerm)) {
if (
!cleanedAName.startsWith(cleanedSearchTerm) &&
cleanedBName.startsWith(cleanedSearchTerm)
) {
return 1;
}
if (cleanedAName < cleanedBName) {
@@ -72,15 +78,17 @@ Model({
return [[]];
}
const cleanedSearchTerm = cleanSearchTerm(searchTerm);
return [this.messaging.commands.filter(command => {
if (!cleanSearchTerm(command.name).includes(cleanedSearchTerm)) {
return false;
}
if (command.channel_types) {
return command.channel_types.includes(thread.channel.channel_type);
}
return true;
})];
return [
this.messaging.commands.filter((command) => {
if (!cleanSearchTerm(command.name).includes(cleanedSearchTerm)) {
return false;
}
if (command.channel_types) {
return command.channel_types.includes(thread.channel.channel_type);
}
return true;
}),
];
},
},
recordMethods: {
@@ -91,9 +99,9 @@ Model({
* @param {Thread} param0.channel
* @param {Object} [param0.body='']
*/
async execute({ channel, body = '' }) {
async execute({ channel, body = "" }) {
return this.messaging.rpc({
model: 'mail.channel',
model: "mail.channel",
method: this.methodName,
args: [[channel.id]],
kwargs: { body },
@@ -121,6 +129,11 @@ Model({
* The keyword to use a specific command.
*/
name: attr({ identifying: true }),
suggestable: one('ComposerSuggestable', { default: {}, inverse: 'channelCommand', readonly: true, required: true }),
suggestable: one("ComposerSuggestable", {
default: {},
inverse: "channelCommand",
readonly: true,
required: true,
}),
},
});
@@ -1,20 +1,20 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
import { attr, clear, link, many, one, Model, unlink } from '@mail/model';
import { cleanSearchTerm } from '@mail/utils/utils';
import { useComponentToModel } from "@mail/component_hooks/use_component_to_model";
import { useUpdateToModel } from "@mail/component_hooks/use_update_to_model";
import { attr, clear, link, many, one, Model, unlink } from "@mail/model";
import { cleanSearchTerm } from "@mail/utils/utils";
import { sprintf } from '@web/core/utils/strings';
import { sprintf } from "@web/core/utils/strings";
Model({
name: 'ChannelInvitationForm',
template: 'mail.ChannelInvitationForm',
name: "ChannelInvitationForm",
template: "mail.ChannelInvitationForm",
componentSetup() {
useComponentToModel({ fieldName: 'component' });
useUpdateToModel({ methodName: 'onComponentUpdate' });
useComponentToModel({ fieldName: "component" });
useUpdateToModel({ methodName: "onComponentUpdate" });
},
identifyingMode: 'xor',
identifyingMode: "xor",
recordMethods: {
/**
* Handles click on the "copy" button.
@@ -24,8 +24,8 @@ Model({
async onClickCopy(ev) {
await navigator.clipboard.writeText(this.thread.invitationLink);
this.messaging.notify({
message: this.env._t('Link copied!'),
type: 'success',
message: this.env._t("Link copied!"),
type: "success",
});
},
/**
@@ -34,13 +34,19 @@ Model({
* @param {MouseEvent} ev
*/
async onClickInvite(ev) {
if (this.thread.channel.channel_type === 'chat') {
const partners_to = [...new Set([
this.messaging.currentPartner.id,
...this.thread.channel.channelMembers.filter(member => member.persona && member.persona.partner).map(member => member.persona.partner.id),
...this.selectedPartners.map(partner => partner.id),
])];
const channel = await this.messaging.models['Thread'].createGroupChat({ partners_to });
if (this.thread.channel.channel_type === "chat") {
const partners_to = [
...new Set([
this.messaging.currentPartner.id,
...this.thread.channel.channelMembers
.filter((member) => member.persona && member.persona.partner)
.map((member) => member.persona.partner.id),
...this.selectedPartners.map((partner) => partner.id),
]),
];
const channel = await this.messaging.models["Thread"].createGroupChat({
partners_to,
});
if (this.thread.rtc) {
/**
* if we were in a RTC call on the current thread, we move to the new group chat.
@@ -49,22 +55,22 @@ Model({
*/
await channel.toggleCall({
startWithVideo: !!this.thread.rtc.videoTrack,
videoType: this.thread.rtc.sendUserVideo ? 'user-video' : 'display',
videoType: this.thread.rtc.sendUserVideo ? "user-video" : "display",
});
}
if (channel.exists()) {
channel.open();
}
} else {
await this.messaging.rpc(({
model: 'mail.channel',
method: 'add_members',
await this.messaging.rpc({
model: "mail.channel",
method: "add_members",
args: [[this.thread.id]],
kwargs: {
partner_ids: this.selectedPartners.map(partner => partner.id),
partner_ids: this.selectedPartners.map((partner) => partner.id),
invite_to_rtc_call: !!this.thread.rtc,
},
}));
});
}
if (this.exists()) {
this.delete();
@@ -92,7 +98,10 @@ Model({
onComponentUpdate() {
if (this.doFocusOnSearchInput && this.searchInputRef.el) {
this.searchInputRef.el.focus();
this.searchInputRef.el.setSelectionRange(this.searchTerm.length, this.searchTerm.length);
this.searchInputRef.el.setSelectionRange(
this.searchTerm.length,
this.searchTerm.length
);
this.update({ doFocusOnSearchInput: clear() });
}
},
@@ -135,11 +144,14 @@ Model({
hasSearchRpcInProgress: true,
});
try {
const channelId = (this.thread && this.thread.model === 'mail.channel') ? this.thread.id : undefined;
const channelId =
this.thread && this.thread.model === "mail.channel"
? this.thread.id
: undefined;
const { count, partners: partnersData } = await this.messaging.rpc(
{
model: 'res.partner',
method: 'search_for_channel_invite',
model: "res.partner",
method: "search_for_channel_invite",
kwargs: {
channel_id: channelId,
search_term: cleanSearchTerm(this.searchTerm),
@@ -173,13 +185,12 @@ Model({
if (!this.thread.authorizedGroupFullName) {
return clear();
}
return sprintf(
this.env._t('Access restricted to group "%(groupFullName)s"'),
{ 'groupFullName': this.thread.authorizedGroupFullName }
);
return sprintf(this.env._t('Access restricted to group "%(groupFullName)s"'), {
groupFullName: this.thread.authorizedGroupFullName,
});
},
}),
chatWindow: one('ChatWindow', { identifying: true, inverse: 'channelInvitationForm' }),
chatWindow: one("ChatWindow", { identifying: true, inverse: "channelInvitationForm" }),
/**
* States the OWL component of this channel invitation form.
* Useful to be able to close it with popover trigger, or to know when
@@ -207,9 +218,9 @@ Model({
return clear();
}
switch (this.thread.channel.channel_type) {
case 'chat':
case "chat":
return this.env._t("Create group chat");
case 'group':
case "group":
return this.env._t("Invite to group chat");
}
return this.env._t("Invite to Channel");
@@ -218,12 +229,16 @@ Model({
/**
* If set, this channel invitation form is content of related popover view.
*/
popoverViewOwner: one('PopoverView', { identifying: true, inverse: 'channelInvitationForm', isCausal: true }),
popoverViewOwner: one("PopoverView", {
identifying: true,
inverse: "channelInvitationForm",
isCausal: true,
}),
/**
* States the OWL ref of the "search" input of this channel invitation
* form. Useful to be able to focus it.
*/
searchInputRef: attr({ ref: 'searchInput' }),
searchInputRef: attr({ ref: "searchInput" }),
/**
* States the number of results of the last search.
*/
@@ -236,31 +251,34 @@ Model({
* States all partners that are potential choices according to this
* search term.
*/
selectablePartners: many('Partner'),
selectablePartnerViews: many('ChannelInvitationFormSelectablePartnerView', { inverse: 'channelInvitationFormOwner',
selectablePartners: many("Partner"),
selectablePartnerViews: many("ChannelInvitationFormSelectablePartnerView", {
inverse: "channelInvitationFormOwner",
compute() {
if (this.selectablePartners.length === 0) {
return clear();
}
return this.selectablePartners.map(partner => ({ partner }));
return this.selectablePartners.map((partner) => ({ partner }));
},
}),
/**
* Determines all partners that are currently selected.
*/
selectedPartners: many('Partner'),
selectedPartnerViews: many('ChannelInvitationFormSelectedPartnerView', { inverse: 'channelInvitationFormOwner',
selectedPartners: many("Partner"),
selectedPartnerViews: many("ChannelInvitationFormSelectedPartnerView", {
inverse: "channelInvitationFormOwner",
compute() {
if (this.selectedPartners.length === 0) {
return clear();
}
return this.selectedPartners.map(partner => ({ partner }));
return this.selectedPartners.map((partner) => ({ partner }));
},
}),
/**
* States the thread on which this list operates (if any).
*/
thread: one('Thread', { required: true,
thread: one("Thread", {
required: true,
compute() {
if (
this.popoverViewOwner &&
@@ -1,14 +1,21 @@
/** @odoo-module **/
import { clear, one, Model } from '@mail/model';
import { clear, one, Model } from "@mail/model";
Model({
name: 'ChannelInvitationFormSelectablePartnerView',
template: 'mail.ChannelInvitationFormSelectablePartnerView',
name: "ChannelInvitationFormSelectablePartnerView",
template: "mail.ChannelInvitationFormSelectablePartnerView",
fields: {
channelInvitationFormOwner: one('ChannelInvitationForm', { identifying: true, inverse: 'selectablePartnerViews' }),
partner: one('Partner', { identifying: true, inverse: 'channelInvitationFormSelectablePartnerViews' }),
personaImStatusIconView: one('PersonaImStatusIconView', { inverse: 'channelInvitationFormSelectablePartnerViewOwner',
channelInvitationFormOwner: one("ChannelInvitationForm", {
identifying: true,
inverse: "selectablePartnerViews",
}),
partner: one("Partner", {
identifying: true,
inverse: "channelInvitationFormSelectablePartnerViews",
}),
personaImStatusIconView: one("PersonaImStatusIconView", {
inverse: "channelInvitationFormSelectablePartnerViewOwner",
compute() {
return this.partner.isImStatusSet ? {} : clear();
},

Some files were not shown because too many files have changed in this diff Show More