mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 11:32:00 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
|
|
@ -0,0 +1,122 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useCommand } from "@web/core/commands/command_hook";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Domain } from "@web/core/domain";
|
||||
|
||||
const { useComponent, useEnv } = owl;
|
||||
|
||||
/**
|
||||
* Use this hook to add "Assign to.." and "Assign/Unassign me" to the command palette.
|
||||
*/
|
||||
|
||||
export function useAssignUserCommand() {
|
||||
const component = useComponent();
|
||||
const env = useEnv();
|
||||
const orm = useService("orm");
|
||||
const user = useService("user");
|
||||
if (
|
||||
component.props.relation !== "res.users" ||
|
||||
component.props.record.activeFields[component.props.name].viewType !== "form"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getCurrentIds = () => {
|
||||
if (component.props.type === "many2one" && component.props.value) {
|
||||
return [component.props.value[0]];
|
||||
} else if (component.props.type === "many2many") {
|
||||
return component.props.value.currentIds;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const add = async (record) => {
|
||||
if (component.props.type === "many2one") {
|
||||
component.props.update(record);
|
||||
} else if (component.props.type === "many2many") {
|
||||
component.props.update({
|
||||
operation: "REPLACE_WITH",
|
||||
resIds: [...getCurrentIds(), record[0]],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (record) => {
|
||||
if (component.props.type === "many2one") {
|
||||
component.props.update([]);
|
||||
} else if (component.props.type === "many2many") {
|
||||
component.props.update({
|
||||
operation: "REPLACE_WITH",
|
||||
resIds: getCurrentIds().filter((id) => id !== record[0]),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const provide = async (env, options) => {
|
||||
const value = options.searchValue.trim();
|
||||
let domain = component.props.record.getFieldDomain(component.props.name);
|
||||
const context = component.props.record.getFieldContext(component.props.name);
|
||||
if (component.props.type === "many2many") {
|
||||
const selectedUserIds = getCurrentIds();
|
||||
if (selectedUserIds.length) {
|
||||
domain = Domain.and([domain, [["id", "not in", selectedUserIds]]]);
|
||||
}
|
||||
}
|
||||
if (component._pendingRpc) {
|
||||
component._pendingRpc.abort(false);
|
||||
}
|
||||
component._pendingRpc = orm.call(component.props.relation, "name_search", [], {
|
||||
name: value,
|
||||
args: domain.toList(),
|
||||
operator: "ilike",
|
||||
limit: 80,
|
||||
context,
|
||||
});
|
||||
const searchResult = await component._pendingRpc;
|
||||
component._pendingRpc = null;
|
||||
return searchResult.map((record) => ({
|
||||
name: record[1],
|
||||
action: add.bind(null, record),
|
||||
}));
|
||||
};
|
||||
|
||||
useCommand(
|
||||
env._t("Assign to ..."),
|
||||
() => ({
|
||||
configByNameSpace: {
|
||||
default: {
|
||||
emptyMessage: env._t("No users found"),
|
||||
},
|
||||
},
|
||||
placeholder: env._t("Select a user..."),
|
||||
providers: [
|
||||
{
|
||||
provide,
|
||||
},
|
||||
],
|
||||
}),
|
||||
{
|
||||
category: "smart_action",
|
||||
hotkey: "alt+i",
|
||||
global: true,
|
||||
}
|
||||
);
|
||||
|
||||
useCommand(
|
||||
env._t("Assign/Unassign to me"),
|
||||
() => {
|
||||
const record = [user.userId, user.name];
|
||||
if (getCurrentIds().includes(user.userId)) {
|
||||
remove(record);
|
||||
} else {
|
||||
add(record);
|
||||
}
|
||||
},
|
||||
{
|
||||
category: "smart_action",
|
||||
hotkey: "alt+shift+i",
|
||||
global: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { CharField } from "@web/views/fields/char/char_field";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import MailEmojisMixin from '@mail/js/emojis_mixin';
|
||||
import { EmojisDropdown } from '@mail/js/emojis_dropdown';
|
||||
import { EmojisFieldCommon } from '@mail/views/fields/emojis_field_common';
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const { useRef } = owl;
|
||||
|
||||
/**
|
||||
* Extension of the FieldChar that will add emojis support
|
||||
*/
|
||||
export class EmojisCharField extends CharField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.targetEditElement = useRef('input');
|
||||
this._setupOverride();
|
||||
}
|
||||
};
|
||||
|
||||
EmojisCharField.extractProps = ({ attrs, field }) => {
|
||||
return {...CharField.extractProps({attrs, field}), shouldTrim: false};
|
||||
};
|
||||
patch(EmojisCharField.prototype, 'emojis_char_field_mail_mixin', MailEmojisMixin);
|
||||
patch(EmojisCharField.prototype, 'emojis_char_field_field_mixin', EmojisFieldCommon);
|
||||
EmojisCharField.template = 'mail.EmojisCharField';
|
||||
EmojisCharField.components = { ...CharField.components, EmojisDropdown };
|
||||
EmojisCharField.additionalClasses = [...(CharField.additionalClasses || []), 'o_field_text'];
|
||||
registry.category("fields").add("char_emojis", EmojisCharField);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.o_field_widget.o_field_char_emojis input {
|
||||
padding-right: 40px; // Avoid overlapping sub
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.EmojisCharField" t-inherit="web.CharField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//span[1]" position="attributes">
|
||||
<attribute name="t-ref">targetReadonlyElement</attribute>
|
||||
</xpath>
|
||||
<xpath expr="/*[last()]/*[last()]" position="after">
|
||||
<EmojisDropdown onEmojiClick="onEmojiClick" readonly="props.readonly" type="'char'"/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import MailEmojisMixin from '@mail/js/emojis_mixin';
|
||||
|
||||
const _onEmojiClickMixin = MailEmojisMixin.onEmojiClick;
|
||||
const { useRef, onMounted } = owl;
|
||||
|
||||
/*
|
||||
* Common code for EmojisTextField and EmojisCharField
|
||||
*/
|
||||
export const EmojisFieldCommon = {
|
||||
_setupOverride() {
|
||||
this.targetReadonlyElement = useRef('targetReadonlyElement');
|
||||
this.emojisDropdown = useRef('emojisDropdown');
|
||||
if (this.props.readonly) {
|
||||
onMounted(() => {
|
||||
this.targetReadonlyElement.el.innerHTML = this._formatText(this.targetReadonlyElement.el.textContent);
|
||||
});
|
||||
}
|
||||
this.onEmojiClick = this._onEmojiClick.bind(this);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_onEmojiClick() {
|
||||
_onEmojiClickMixin.apply(this, arguments);
|
||||
this.props.update(this._getTargetTextElement().value);
|
||||
},
|
||||
/**
|
||||
* Used by MailEmojisMixin, check its document for more info.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_getTargetTextElement() {
|
||||
return this.props.readonly ? this.targetReadonlyElement.el : this.targetEditElement.el;
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { TextField } from "@web/views/fields/text/text_field";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import MailEmojisMixin from '@mail/js/emojis_mixin';
|
||||
import { EmojisDropdown } from '@mail/js/emojis_dropdown';
|
||||
import { EmojisFieldCommon } from '@mail/views/fields/emojis_field_common';
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
/**
|
||||
* Extension of the FieldText that will add emojis support
|
||||
*/
|
||||
export class EmojisTextField extends TextField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.targetEditElement = this.textareaRef;
|
||||
this._setupOverride();
|
||||
}
|
||||
};
|
||||
|
||||
patch(EmojisTextField.prototype, 'emojis_text_field_mail_mixin', MailEmojisMixin);
|
||||
patch(EmojisTextField.prototype, 'emojis_text_field_field_mixin', EmojisFieldCommon);
|
||||
EmojisTextField.template = 'mail.EmojisTextField';
|
||||
EmojisTextField.components = { ...TextField.components, EmojisDropdown };
|
||||
EmojisTextField.additionalClasses = [...(TextField.additionalClasses || []), 'o_field_text'];
|
||||
registry.category("fields").add("text_emojis", EmojisTextField);
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.EmojisTextField" t-inherit="web.TextField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//span[1]" position="attributes">
|
||||
<attribute name="t-ref">targetReadonlyElement</attribute>
|
||||
</xpath>
|
||||
<xpath expr="/*[last()]/*[last()]" position="after">
|
||||
<EmojisDropdown onEmojiClick="onEmojiClick" type="'text'"
|
||||
enable_emojis="props.enable_emojis" readonly="props.readonly"/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { TagsList } from "@web/views/fields/many2many_tags/tags_list";
|
||||
import {
|
||||
Many2ManyTagsAvatarField,
|
||||
ListKanbanMany2ManyTagsAvatarField,
|
||||
} from "@web/views/fields/many2many_tags_avatar/many2many_tags_avatar_field";
|
||||
import { useOpenChat } from "@mail/views/open_chat_hook";
|
||||
import { useAssignUserCommand } from "@mail/views/fields/assign_user_command_hook";
|
||||
|
||||
export class Many2ManyAvatarUserTagsList extends TagsList {}
|
||||
Many2ManyAvatarUserTagsList.template = "mail.Many2ManyAvatarUserTagsList";
|
||||
|
||||
export class Many2ManyTagsAvatarUserField extends Many2ManyTagsAvatarField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.openChat = useOpenChat(this.props.relation);
|
||||
useAssignUserCommand();
|
||||
}
|
||||
|
||||
get tags() {
|
||||
return super.tags.map((tag) => ({
|
||||
...tag,
|
||||
onImageClicked: () => {
|
||||
this.openChat(tag.resId);
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
Many2ManyTagsAvatarUserField.components = {
|
||||
...Many2ManyTagsAvatarField.components,
|
||||
TagsList: Many2ManyAvatarUserTagsList,
|
||||
};
|
||||
Many2ManyTagsAvatarUserField.additionalClasses = ["o_field_many2many_tags_avatar"];
|
||||
|
||||
registry.category("fields").add("many2many_avatar_user", Many2ManyTagsAvatarUserField);
|
||||
|
||||
export class KanbanMany2ManyTagsAvatarUserField extends ListKanbanMany2ManyTagsAvatarField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.openChat = useOpenChat(this.props.relation);
|
||||
useAssignUserCommand();
|
||||
}
|
||||
|
||||
get displayText() {
|
||||
const isList = this.props.record.activeFields[this.props.name].viewType === "list";
|
||||
return (isList && this.props.value.records.length === 1) || !this.props.readonly;
|
||||
}
|
||||
|
||||
get tags() {
|
||||
const recordFromId = (id) => this.props.value.records.find((rec) => rec.id === id);
|
||||
return super.tags.map((tag) => ({
|
||||
...tag,
|
||||
onImageClicked: () => {
|
||||
this.openChat(recordFromId(tag.id).resId);
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
KanbanMany2ManyTagsAvatarUserField.template = "mail.KanbanMany2ManyTagsAvatarUserField";
|
||||
KanbanMany2ManyTagsAvatarUserField.components = {
|
||||
...ListKanbanMany2ManyTagsAvatarField.components,
|
||||
TagsList: Many2ManyAvatarUserTagsList,
|
||||
};
|
||||
KanbanMany2ManyTagsAvatarUserField.additionalClasses = ["o_field_many2many_tags_avatar"];
|
||||
|
||||
registry.category("fields").add("kanban.many2many_avatar_user", KanbanMany2ManyTagsAvatarUserField);
|
||||
registry.category("fields").add("list.many2many_avatar_user", KanbanMany2ManyTagsAvatarUserField);
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
.o_kanban_renderer, .o_list_renderer {
|
||||
.o_field_many2many_avatar_user {
|
||||
&:not(:has(.o_tags_input)) {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.o_field_tags:not(.o_tags_input) {
|
||||
.o_tag {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
color: inherit;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.o_m2m_avatar, .o_m2m_avatar_empty {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.o_m2m_avatar_empty {
|
||||
background-color: $o-gray-300;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="mail.Many2ManyAvatarUserTagsList" t-inherit="web.TagsList" t-inherit-mode="primary" owl="1">
|
||||
<img position="attributes">
|
||||
<attribute name="t-on-click.stop.prevent">tag.onImageClicked</attribute>
|
||||
</img>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.KanbanMany2ManyTagsAvatarUserField" t-inherit="web.Many2ManyTagsAvatarField" t-inherit-mode="primary" owl="1">
|
||||
<TagsList position="attributes">
|
||||
<attribute name="displayBadge">!props.readonly</attribute>
|
||||
<attribute name="displayText">displayText</attribute>
|
||||
</TagsList>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useOpenMany2XRecord } from "@web/views/fields/relational_utils";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
|
||||
import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
import { TagsList } from "@web/views/fields/many2many_tags/tags_list";
|
||||
|
||||
const { onMounted, onWillUpdateProps } = owl;
|
||||
|
||||
export class FieldMany2ManyTagsEmailTagsList extends TagsList {}
|
||||
FieldMany2ManyTagsEmailTagsList.template = "FieldMany2ManyTagsEmailTagsList";
|
||||
|
||||
export class FieldMany2ManyTagsEmail extends Many2ManyTagsField {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.openedDialogs = 0;
|
||||
this.recordsIdsToAdd = [];
|
||||
this.openMany2xRecord = useOpenMany2XRecord({
|
||||
resModel: this.props.relation,
|
||||
activeActions: {
|
||||
create: false,
|
||||
createEdit: false,
|
||||
write: true,
|
||||
},
|
||||
isToMany: true,
|
||||
onRecordSaved: async (record) => {
|
||||
if (record.data.email) {
|
||||
this.recordsIdsToAdd.push(record.resId);
|
||||
}
|
||||
},
|
||||
fieldString: this.props.string,
|
||||
});
|
||||
|
||||
// Using onWillStart causes an infinite loop, onMounted will handle the initial
|
||||
// check and onWillUpdateProps handles any addition to the field.
|
||||
onMounted(this.checkEmails.bind(this, this.props));
|
||||
onWillUpdateProps(this.checkEmails.bind(this));
|
||||
}
|
||||
|
||||
async checkEmails(props) {
|
||||
const invalidRecords = props.value.records.filter((record) => !record.data.email);
|
||||
// Remove records with invalid data, open form view to edit those and readd them if they are updated correctly.
|
||||
const dialogDefs = [];
|
||||
for (const record of invalidRecords) {
|
||||
dialogDefs.push(this.openMany2xRecord({
|
||||
resId: record.resId,
|
||||
context: props.record.getFieldContext(this.props.name),
|
||||
title: sprintf(this.env._t("Edit: %s"), record.data.display_name),
|
||||
}));
|
||||
}
|
||||
this.openedDialogs += invalidRecords.length;
|
||||
const invalidRecordIds = invalidRecords.map(rec => rec.resId);
|
||||
if (invalidRecordIds.length) {
|
||||
this.props.value.replaceWith(props.value.currentIds.filter(id => !invalidRecordIds.includes(id)));
|
||||
}
|
||||
return Promise.all(dialogDefs).then(() => {
|
||||
this.openedDialogs -= invalidRecords.length;
|
||||
if (this.openedDialogs || !this.recordsIdsToAdd.length) {
|
||||
return;
|
||||
}
|
||||
props.value.add(this.recordsIdsToAdd, { isM2M: true });
|
||||
this.recordsIdsToAdd = [];
|
||||
});
|
||||
}
|
||||
|
||||
get tags() {
|
||||
// Add email to our tags
|
||||
const tags = super.tags;
|
||||
const emailByResId = this.props.value.records.reduce((acc, record) => {
|
||||
acc[record.resId] = record.data.email;
|
||||
return acc;
|
||||
}, {});
|
||||
tags.forEach(tag => tag.email = emailByResId[tag.resId]);
|
||||
return tags;
|
||||
}
|
||||
};
|
||||
|
||||
FieldMany2ManyTagsEmail.components = {
|
||||
...FieldMany2ManyTagsEmail.components,
|
||||
TagsList: FieldMany2ManyTagsEmailTagsList,
|
||||
};
|
||||
|
||||
FieldMany2ManyTagsEmail.fieldsToFetch = Object.assign({},
|
||||
Many2ManyTagsField.fieldsToFetch,
|
||||
{email: {name: 'email', type: 'char'}}
|
||||
);
|
||||
|
||||
FieldMany2ManyTagsEmail.additionalClasses = ["o_field_many2many_tags"];
|
||||
|
||||
registry.category("fields").add("many2many_tags_email", FieldMany2ManyTagsEmail);
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="FieldMany2ManyTagsEmailTagsList" t-inherit="web.TagsList" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//span[contains(@t-attf-class, 'badge')]" position="replace">
|
||||
<div t-if="tag.email" t-attf-class="badge rounded-pill dropdown o_tag o_tag_color_0 #{tag.email.indexOf('@') < 0 ? 'o_tag_error' : ''}" t-att-data-color="tag.colorIndex" t-att-data-index="tag_index" t-att-data-id="tag.id" t-att-title="tag.text">
|
||||
<span class="o_badge_text" t-att-title="tag.email"><t t-esc="tag.text"/></span>
|
||||
<a t-if="!readonly && tag.onDelete" t-on-click.stop.prevent="tag.onDelete" href="#" class="fa fa-times o_delete" title="Delete" aria-label="Delete"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2OneAvatarField } from "@web/views/fields/many2one_avatar/many2one_avatar_field";
|
||||
import { useOpenChat } from "@mail/views/open_chat_hook";
|
||||
import { useAssignUserCommand } from "@mail/views/fields/assign_user_command_hook";
|
||||
|
||||
export class Many2OneAvatarUserField extends Many2OneAvatarField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.openChat = useOpenChat(this.props.relation);
|
||||
useAssignUserCommand();
|
||||
}
|
||||
|
||||
onClickAvatar() {
|
||||
this.openChat(this.props.value[0]);
|
||||
}
|
||||
}
|
||||
Many2OneAvatarUserField.template = "mail.Many2OneAvatarUserField";
|
||||
Many2OneAvatarUserField.additionalClasses = ["o_field_many2one_avatar"];
|
||||
|
||||
registry.category("fields").add("many2one_avatar_user", Many2OneAvatarUserField);
|
||||
|
||||
export class KanbanMany2OneAvatarUserField extends Many2OneAvatarUserField {
|
||||
/**
|
||||
* All props are normally passed to the Many2OneField however since
|
||||
* we add a new one, we need to filter it out.
|
||||
*/
|
||||
get m2oFieldProps() {
|
||||
return Object.fromEntries(Object.entries(this.props).filter(([key, _val]) => key in Many2OneAvatarField.props));
|
||||
}
|
||||
}
|
||||
KanbanMany2OneAvatarUserField.template = "mail.KanbanMany2OneAvatarUserField";
|
||||
KanbanMany2OneAvatarUserField.props = {
|
||||
...Many2OneAvatarUserField.props,
|
||||
displayAvatarName: { type: Boolean, optional: true },
|
||||
};
|
||||
KanbanMany2OneAvatarUserField.extractProps = ({ attrs, field }) => {
|
||||
return {
|
||||
...Many2OneAvatarUserField.extractProps({ attrs, field }),
|
||||
displayAvatarName: attrs.options.display_avatar_name || false,
|
||||
};
|
||||
};
|
||||
|
||||
registry.category("fields").add("kanban.many2one_avatar_user", KanbanMany2OneAvatarUserField);
|
||||
registry.category("fields").add("activity.many2one_avatar_user", KanbanMany2OneAvatarUserField);
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="mail.Many2OneAvatarUserField" t-inherit="web.Many2OneAvatarField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//span[hasclass('o_m2o_avatar')]" position="attributes">
|
||||
<attribute name="t-on-click.stop.prevent">onClickAvatar</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.KanbanMany2OneAvatarUserField" t-inherit="mail.Many2OneAvatarUserField" t-inherit-mode="primary" owl="1">
|
||||
<Many2OneField position="attributes">
|
||||
<attribute name="t-if">props.readonly and props.displayAvatarName</attribute>
|
||||
<attribute name="t-props">m2oFieldProps</attribute>
|
||||
</Many2OneField>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { SIZES } from "@web/core/ui/ui_service";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { append, createElement, setAttributes } from "@web/core/utils/xml";
|
||||
import { ViewCompiler, getModifier } from "@web/views/view_compiler";
|
||||
import { FormCompiler } from "@web/views/form/form_compiler";
|
||||
|
||||
function compileChatter(node, params) {
|
||||
let hasActivities = false;
|
||||
let hasFollowers = false;
|
||||
let hasMessageList = false;
|
||||
let hasParentReloadOnAttachmentsChanged;
|
||||
let hasParentReloadOnFollowersUpdate = false;
|
||||
let hasParentReloadOnMessagePosted = false;
|
||||
let isAttachmentBoxVisibleInitially = false;
|
||||
for (const childNode of node.children) {
|
||||
const options = evaluateExpr(childNode.getAttribute("options") || "{}");
|
||||
switch (childNode.getAttribute('name')) {
|
||||
case 'activity_ids':
|
||||
hasActivities = true;
|
||||
break;
|
||||
case 'message_follower_ids':
|
||||
hasFollowers = true;
|
||||
hasParentReloadOnFollowersUpdate = Boolean(options['post_refresh']);
|
||||
isAttachmentBoxVisibleInitially = isAttachmentBoxVisibleInitially || Boolean(options['open_attachments']);
|
||||
break;
|
||||
case 'message_ids':
|
||||
hasMessageList = true;
|
||||
hasParentReloadOnAttachmentsChanged = options['post_refresh'] === 'always';
|
||||
hasParentReloadOnMessagePosted = Boolean(options['post_refresh']);
|
||||
isAttachmentBoxVisibleInitially = isAttachmentBoxVisibleInitially || Boolean(options['open_attachments']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const chatterContainerXml = createElement("ChatterContainer");
|
||||
setAttributes(chatterContainerXml, {
|
||||
"chatter": params.chatter,
|
||||
"hasActivities": hasActivities,
|
||||
"hasFollowers": hasFollowers,
|
||||
"hasMessageList": hasMessageList,
|
||||
"hasParentReloadOnAttachmentsChanged": hasParentReloadOnAttachmentsChanged,
|
||||
"hasParentReloadOnFollowersUpdate": hasParentReloadOnFollowersUpdate,
|
||||
"hasParentReloadOnMessagePosted": hasParentReloadOnMessagePosted,
|
||||
"isAttachmentBoxVisibleInitially": isAttachmentBoxVisibleInitially,
|
||||
"threadId": params.threadId,
|
||||
"threadModel": params.threadModel,
|
||||
"webRecord": params.webRecord,
|
||||
"saveRecord": "() => this.saveButtonClicked and this.saveButtonClicked()",
|
||||
});
|
||||
const chatterContainerHookXml = createElement("div");
|
||||
chatterContainerHookXml.classList.add("o_FormRenderer_chatterContainer");
|
||||
append(chatterContainerHookXml, chatterContainerXml);
|
||||
return chatterContainerHookXml;
|
||||
}
|
||||
|
||||
function compileAttachmentPreview(node, params) {
|
||||
const webClientViewAttachmentViewContainerHookXml = createElement("div");
|
||||
webClientViewAttachmentViewContainerHookXml.classList.add('o_attachment_preview');
|
||||
const webClientViewAttachmentViewContainerXml = createElement("WebClientViewAttachmentViewContainer");
|
||||
setAttributes(webClientViewAttachmentViewContainerXml, {
|
||||
"threadId": params.threadId,
|
||||
"threadModel": params.threadModel,
|
||||
});
|
||||
append(webClientViewAttachmentViewContainerHookXml, webClientViewAttachmentViewContainerXml);
|
||||
return webClientViewAttachmentViewContainerHookXml;
|
||||
}
|
||||
|
||||
export class MailFormCompiler extends ViewCompiler {
|
||||
setup() {
|
||||
this.compilers.push({ selector: "t", fn: this.compileT });
|
||||
this.compilers.push({ selector: "div.oe_chatter", fn: this.compileChatter });
|
||||
this.compilers.push({
|
||||
selector: "div.o_attachment_preview",
|
||||
fn: this.compileAttachmentPreview,
|
||||
});
|
||||
}
|
||||
|
||||
compile(node, params) {
|
||||
const res = super.compile(node, params).children[0];
|
||||
const chatterContainerHookXml = res.querySelector(".o_FormRenderer_chatterContainer");
|
||||
if (chatterContainerHookXml) {
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
"t-if": `!hasAttachmentViewer() and uiService.size >= ${SIZES.XXL}`,
|
||||
"t-attf-class": "o-aside",
|
||||
});
|
||||
const chatterContainerXml = chatterContainerHookXml.querySelector('ChatterContainer');
|
||||
setAttributes(chatterContainerXml, {
|
||||
"hasExternalBorder": "false",
|
||||
"hasMessageListScrollAdjust": "true",
|
||||
"isInFormSheetBg": "false",
|
||||
});
|
||||
}
|
||||
const attachmentViewHookXml = res.querySelector(".o_attachment_preview");
|
||||
if (attachmentViewHookXml) {
|
||||
setAttributes(attachmentViewHookXml, {
|
||||
"t-if": `hasAttachmentViewer()`,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
compileT(node, params) {
|
||||
const compiledRoot = createElement("t");
|
||||
for (const child of node.childNodes) {
|
||||
const invisible = getModifier(child, "invisible");
|
||||
let compiledChild = this.compileNode(child, params, false);
|
||||
compiledChild = this.applyInvisible(invisible, compiledChild, {
|
||||
...params,
|
||||
recordExpr: "model.root",
|
||||
});
|
||||
append(compiledRoot, compiledChild);
|
||||
}
|
||||
return compiledRoot;
|
||||
}
|
||||
|
||||
compileChatter(node) {
|
||||
return compileChatter(node, {
|
||||
chatter: "chatter",
|
||||
threadId: "model.root.resId or undefined",
|
||||
threadModel: "model.root.resModel",
|
||||
webRecord: "model.root",
|
||||
});
|
||||
}
|
||||
|
||||
compileAttachmentPreview(node) {
|
||||
return compileAttachmentPreview(node, {
|
||||
threadId: "model.root.resId or undefined",
|
||||
threadModel: "model.root.resModel",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("form_compilers").add("chatter_compiler", {
|
||||
selector: "div.oe_chatter",
|
||||
fn: (node) =>
|
||||
compileChatter(node, {
|
||||
chatter: "props.chatter",
|
||||
threadId: "props.record.resId or undefined",
|
||||
threadModel: "props.record.resModel",
|
||||
webRecord: "props.record",
|
||||
}),
|
||||
});
|
||||
|
||||
registry.category("form_compilers").add("attachment_preview_compiler", {
|
||||
selector: "div.o_attachment_preview",
|
||||
fn: () => createElement("t"),
|
||||
});
|
||||
|
||||
patch(FormCompiler.prototype, 'mail', {
|
||||
compile(node, params) {
|
||||
// TODO no chatter if in dialog?
|
||||
const res = this._super(node, params);
|
||||
const chatterContainerHookXml = res.querySelector('.o_FormRenderer_chatterContainer');
|
||||
if (!chatterContainerHookXml) {
|
||||
return res; // no chatter, keep the result as it is
|
||||
}
|
||||
const chatterContainerXml = chatterContainerHookXml.querySelector('ChatterContainer');
|
||||
setAttributes(chatterContainerXml, {
|
||||
"hasExternalBorder": "true",
|
||||
"hasMessageListScrollAdjust": "false",
|
||||
"isInFormSheetBg": "false",
|
||||
"saveRecord": "this.props.saveButtonClicked",
|
||||
});
|
||||
if (chatterContainerHookXml.parentNode.classList.contains('o_form_sheet')) {
|
||||
return res; // if chatter is inside sheet, keep it there
|
||||
}
|
||||
const formSheetBgXml = res.querySelector('.o_form_sheet_bg');
|
||||
const parentXml = formSheetBgXml && formSheetBgXml.parentNode;
|
||||
if (!parentXml) {
|
||||
return res; // miss-config: a sheet-bg is required for the rest
|
||||
}
|
||||
if (params.hasAttachmentViewerInArch) {
|
||||
// in sheet bg (attachment viewer present)
|
||||
const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);
|
||||
sheetBgChatterContainerHookXml.classList.add('o-isInFormSheetBg');
|
||||
setAttributes(sheetBgChatterContainerHookXml, {
|
||||
't-if': `this.props.hasAttachmentViewer`,
|
||||
});
|
||||
append(formSheetBgXml, sheetBgChatterContainerHookXml);
|
||||
const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector('ChatterContainer');
|
||||
setAttributes(sheetBgChatterContainerXml, {
|
||||
"isInFormSheetBg": "true",
|
||||
});
|
||||
}
|
||||
// after sheet bg (standard position, below form)
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
't-if': `!this.props.hasAttachmentViewer and uiService.size < ${SIZES.XXL}`,
|
||||
});
|
||||
append(parentXml, chatterContainerHookXml);
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { useModels } from "@mail/component_hooks/use_models";
|
||||
import { ChatterContainer, getChatterNextTemporaryId } from "@mail/components/chatter_container/chatter_container";
|
||||
import { WebClientViewAttachmentViewContainer } from "@mail/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { createElement } from "@web/core/utils/xml";
|
||||
import { SIZES } from "@web/core/ui/ui_service";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useDebounced } from "@web/core/utils/timing";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { useViewCompiler } from "@web/views/view_compiler";
|
||||
import { evalDomain } from "@web/views/utils";
|
||||
|
||||
import { MailFormCompiler } from "./form_compiler";
|
||||
|
||||
const { onMounted, onWillDestroy, onWillUnmount } = owl;
|
||||
|
||||
patch(FormController.prototype, "mail", {
|
||||
setup() {
|
||||
this._super();
|
||||
this.uiService = useService("ui");
|
||||
this.hasAttachmentViewerInArch = false;
|
||||
this.chatter = undefined;
|
||||
|
||||
if (this.env.services.messaging) {
|
||||
useModels();
|
||||
this.env.services.messaging.modelManager.messagingCreatedPromise.then(() => {
|
||||
if (owl.status(this) === "destroyed") {
|
||||
return;
|
||||
}
|
||||
const messaging = this.env.services.messaging.modelManager.messaging;
|
||||
this.chatter = messaging.models['Chatter'].insert({ id: getChatterNextTemporaryId() });
|
||||
if (owl.status(this) === "destroyed") {
|
||||
this.chatter.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { archInfo } = this.props;
|
||||
|
||||
const template = createElement("t");
|
||||
const xmlDocAttachmentPreview = archInfo.xmlDoc.querySelector("div.o_attachment_preview");
|
||||
if (xmlDocAttachmentPreview && xmlDocAttachmentPreview.parentNode.nodeName === "form") {
|
||||
// TODO hasAttachmentViewer should also depend on the groups= and/or invisible modifier on o_attachment_preview (see invoice form)
|
||||
template.appendChild(xmlDocAttachmentPreview);
|
||||
this.hasAttachmentViewerInArch = true;
|
||||
archInfo.arch = archInfo.xmlDoc.outerHTML;
|
||||
}
|
||||
|
||||
const xmlDocChatter = archInfo.xmlDoc.querySelector("div.oe_chatter");
|
||||
if (xmlDocChatter && xmlDocChatter.parentNode.nodeName === "form") {
|
||||
template.appendChild(xmlDocChatter.cloneNode(true));
|
||||
}
|
||||
|
||||
const mailTemplates = useViewCompiler(MailFormCompiler, archInfo.arch, { Mail: template }, {});
|
||||
this.mailTemplate = mailTemplates.Mail;
|
||||
|
||||
this.onResize = useDebounced(this.render, 200);
|
||||
onMounted(() => browser.addEventListener("resize", this.onResize));
|
||||
onWillUnmount(() => browser.removeEventListener("resize", this.onResize));
|
||||
onWillDestroy(() => {
|
||||
if (this.chatter && this.chatter.exists()) {
|
||||
this.chatter.delete();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @returns {Messaging|undefined}
|
||||
*/
|
||||
getMessaging() {
|
||||
return this.env.services.messaging && this.env.services.messaging.modelManager.messaging;
|
||||
},
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasAttachmentViewer() {
|
||||
if (
|
||||
this.uiService.size < SIZES.XXL ||
|
||||
!this.hasAttachmentViewerInArch ||
|
||||
!this.getMessaging() ||
|
||||
!this.model.root.resId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const thread = this.getMessaging().models['Thread'].insert({
|
||||
id: this.model.root.resId,
|
||||
model: this.model.root.resModel,
|
||||
});
|
||||
return thread.attachmentsInWebClientView.length > 0;
|
||||
},
|
||||
evalDomainFromRecord(record, expr) {
|
||||
return evalDomain(expr, record.evalContext);
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(FormController.components, {
|
||||
ChatterContainer,
|
||||
WebClientViewAttachmentViewContainer,
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="web.FormView" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o_form_view_container')]" position="after">
|
||||
<t t-if="mailTemplate">
|
||||
<t t-call="{{ mailTemplate }}" />
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//Layout/t[@t-component='props.Renderer']" position="attributes">
|
||||
<attribute name="chatter">chatter</attribute>
|
||||
<attribute name="hasAttachmentViewerInArch">hasAttachmentViewerInArch</attribute>
|
||||
<attribute name="hasAttachmentViewer">hasAttachmentViewer()</attribute>
|
||||
<attribute name="saveButtonClicked">() => this.saveButtonClicked()</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { ChatterContainer } from "@mail/components/chatter_container/chatter_container";
|
||||
import { WebClientViewAttachmentViewContainer } from "@mail/components/web_client_view_attachment_view_container/web_client_view_attachment_view_container";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { FormRenderer } from "@web/views/form/form_renderer";
|
||||
|
||||
patch(FormRenderer.prototype, 'mail', {
|
||||
get compileParams() {
|
||||
return {
|
||||
...this._super(),
|
||||
hasAttachmentViewerInArch: this.props.hasAttachmentViewerInArch,
|
||||
saveButtonClicked: this.props.saveButtonClicked,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(FormRenderer.components, {
|
||||
ChatterContainer,
|
||||
WebClientViewAttachmentViewContainer,
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export const helpers = {
|
||||
SUPPORTED_M2X_AVATAR_MODELS: ["res.users"],
|
||||
buildOpenChatParams: (resModel, id) => {
|
||||
if (resModel === "res.users") {
|
||||
return { userId: id };
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export function useOpenChat(resModel) {
|
||||
const messagingService = useService("messaging");
|
||||
if (!helpers.SUPPORTED_M2X_AVATAR_MODELS.includes(resModel)) {
|
||||
throw new Error(
|
||||
`This widget is only supported on many2one and many2many fields pointing to ${JSON.stringify(
|
||||
helpers.SUPPORTED_M2X_AVATAR_MODELS
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return async (id) => {
|
||||
const messaging = await messagingService.get();
|
||||
messaging.openChat(helpers.buildOpenChatParams(resModel, id));
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue