[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:
committed by
Sébastien Theys
parent
424b77b6a8
commit
b0116e4523
+16
-16
@@ -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);
|
||||
|
||||
+13
-15
@@ -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);
|
||||
|
||||
+13
-15
@@ -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]();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+7
-7
@@ -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);
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+3
-5
@@ -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",
|
||||
});
|
||||
|
||||
+3
-4
@@ -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",
|
||||
});
|
||||
|
||||
+20
-18
@@ -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);
|
||||
|
||||
+4
-5
@@ -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",
|
||||
});
|
||||
|
||||
+10
-8
@@ -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);
|
||||
|
||||
+3
-4
@@ -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);
|
||||
|
||||
+4
-7
@@ -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);
|
||||
|
||||
+3
-4
@@ -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",
|
||||
});
|
||||
|
||||
+6
-8
@@ -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);
|
||||
|
||||
+6
-8
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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": "❤️",
|
||||
"<3": "❤️",
|
||||
":heart": "❤️", // alt.
|
||||
"</3": "💔",
|
||||
"</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": "❤️",
|
||||
"<3": "❤️",
|
||||
":heart": "❤️", // alt.
|
||||
"</3": "💔",
|
||||
"</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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -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(/</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"), "<");
|
||||
return _parseAndTransform(children, transformFunction).replace(
|
||||
new RegExp(openToken, "g"),
|
||||
"<"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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, ' ').replace(/([^>]) ([^<])/g, '$1 $2');
|
||||
value = value.replace(/ /g, " ").replace(/([^>]) ([^<])/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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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" }),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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" }),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user