Public release from ruodoo-project: 19.0 - 2026-07-26 21:17:35 UTC
This commit is contained in:
@ -0,0 +1,82 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { discussSidebarItemsRegistry } from "@mail/core/public_web/discuss_sidebar";
|
||||
import { Component, useState, onWillStart } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
/**
|
||||
* Renders each tier.mailbox as a collapsible section with conversation list.
|
||||
*
|
||||
* Under each mailbox header — list of authors (grouped conversations).
|
||||
* Clicking an author opens a DM (discuss.channel chat) with that partner.
|
||||
*/
|
||||
export class TierDiscussSidebarMailboxes extends Component {
|
||||
static template = "tier_communications.DiscussSidebarMailboxes";
|
||||
static props = {};
|
||||
|
||||
setup() {
|
||||
this.store = useService("mail.store");
|
||||
this.orm = useService("orm");
|
||||
this.state = useState({
|
||||
openIds: {},
|
||||
conversationsByMailbox: {}, // { mailbox_id: [{partner_id, partner_name, ...}] }
|
||||
});
|
||||
onWillStart(async () => {
|
||||
await this._loadConversations();
|
||||
});
|
||||
}
|
||||
|
||||
get mailboxThreads() {
|
||||
return this.store.tierMailboxThreads || [];
|
||||
}
|
||||
|
||||
isOpen(threadId) {
|
||||
return this.state.openIds[threadId] !== false;
|
||||
}
|
||||
|
||||
toggle(threadId) {
|
||||
this.state.openIds[threadId] = !this.isOpen(threadId);
|
||||
}
|
||||
|
||||
getConversations(mailboxId) {
|
||||
return this.state.conversationsByMailbox[mailboxId] || [];
|
||||
}
|
||||
|
||||
async _loadConversations() {
|
||||
try {
|
||||
const mailboxes = await this.orm.call(
|
||||
"tier.mailbox", "get_accessible_mailboxes", [], {}
|
||||
);
|
||||
for (const mailbox of (mailboxes || [])) {
|
||||
const conversations = await this.orm.call(
|
||||
"tier.mailbox", "get_mailbox_conversations", [mailbox.id], {}
|
||||
);
|
||||
this.state.conversationsByMailbox[mailbox.id] = conversations || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[tier_communications] _loadConversations error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a DM (personal chat) with the given partner.
|
||||
* Uses store.getChat() which finds or creates the DM channel.
|
||||
*/
|
||||
async openConversation(ev, partnerId) {
|
||||
markEventHandled(ev, "sidebar.openThread");
|
||||
if (!partnerId) {
|
||||
return;
|
||||
}
|
||||
const chat = await this.store.getChat({ partnerId });
|
||||
if (chat) {
|
||||
chat.setAsDiscussThread();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discussSidebarItemsRegistry.add(
|
||||
"tier_mailboxes",
|
||||
TierDiscussSidebarMailboxes,
|
||||
{ sequence: 40 }
|
||||
);
|
||||
@ -0,0 +1,40 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Message } from "@mail/core/common/message_model";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
/**
|
||||
* Patch Message to display channel tags (Email, Matrix, etc.) next to the message.
|
||||
*
|
||||
* channel_tag_ids arrives from the backend as a plain JSON array:
|
||||
* [{id: 1, name: "Email", code: "email"}, ...]
|
||||
* via Store.Attr in _to_store_defaults.
|
||||
*
|
||||
* We intercept it in update() and store it as _tierChannelTags.
|
||||
*/
|
||||
patch(Message.prototype, {
|
||||
/** @type {Array<{id: number, name: string, code: string}>|undefined} */
|
||||
_tierChannelTags: undefined,
|
||||
|
||||
update(data) {
|
||||
super.update(data);
|
||||
if (data && Array.isArray(data.channel_tag_ids)) {
|
||||
this._tierChannelTags = data.channel_tag_ids.filter(
|
||||
(tag) => tag && typeof tag === 'object' && tag.name
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns display names of all channel tags on this message.
|
||||
* Used by message_channel_tag.xml to render badges.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
get tierChannelTagNames() {
|
||||
const tags = this._tierChannelTags;
|
||||
if (!tags || tags.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return tags.map((tag) => tag.name).filter(Boolean);
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,235 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Composer } from "@mail/core/common/composer";
|
||||
import { Store } from "@mail/core/common/store_service";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useState, onWillStart } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { user } from "@web/core/user";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
/**
|
||||
* Patch Store.getMessagePostParams to inject channel_tag_ids into post_data.
|
||||
* This is the correct way to add custom fields — they end up in post_data
|
||||
* which is filtered by _get_allowed_message_params on the Python side.
|
||||
*/
|
||||
patch(Store.prototype, {
|
||||
async getMessagePostParams({ body, postData, thread }) {
|
||||
const params = await super.getMessagePostParams({ body, postData, thread });
|
||||
if (postData.tierChannelTagIds !== undefined) {
|
||||
if (postData.tierChannelTagIds) {
|
||||
params.post_data.channel_tag_ids = postData.tierChannelTagIds;
|
||||
}
|
||||
}
|
||||
return params;
|
||||
},
|
||||
|
||||
async doMessagePost(params, tmpMessage) {
|
||||
const result = await super.doMessagePost(params, tmpMessage);
|
||||
// Store the last posted message_id so Composer can use it for channel notification
|
||||
if (result?.message_id) {
|
||||
this._lastPostedMessageId = result.message_id;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Patch Composer to add channel/mailbox selector UI.
|
||||
*
|
||||
* Логика выбора канала:
|
||||
* - По умолчанию канал не выбран (только Odoo)
|
||||
* - Если последнее сообщение в треде пришло через email → автовыбор email
|
||||
* - Если через matrix → автовыбор matrix
|
||||
* - Если без канала → ничего не выбираем
|
||||
* - Пользователь может вручную переключить или снять выбор
|
||||
*/
|
||||
patch(Composer.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
this.notification = useService("notification");
|
||||
this.store = useService("mail.store");
|
||||
this.tierState = useState({
|
||||
selectedChannelId: null,
|
||||
selectedMailboxIds: [],
|
||||
availableChannels: [],
|
||||
availableMailboxes: [],
|
||||
});
|
||||
onWillStart(async () => {
|
||||
await this._loadTierData();
|
||||
});
|
||||
},
|
||||
|
||||
async _loadTierData() {
|
||||
try {
|
||||
const [isCompanySender, channelTags, mailboxes] = await Promise.all([
|
||||
user.hasGroup("tier_communications.group_company_sender"),
|
||||
this.orm.searchRead(
|
||||
"tier.channel.tag",
|
||||
[["active", "=", true]],
|
||||
["id", "name", "code", "is_company_reply"]
|
||||
),
|
||||
this.orm.call("tier.mailbox", "get_accessible_mailboxes", [], {}),
|
||||
]);
|
||||
|
||||
this.tierState.availableChannels = (channelTags || []).filter(
|
||||
(channelTag) => !channelTag.is_company_reply || isCompanySender
|
||||
);
|
||||
this.tierState.availableMailboxes = mailboxes || [];
|
||||
|
||||
await this._autoSelectChannel();
|
||||
} catch (error) {
|
||||
console.warn("[tier_communications] _loadTierData error:", error);
|
||||
this.tierState.availableChannels = [];
|
||||
this.tierState.availableMailboxes = [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Auto-select channel based on the last message in the thread.
|
||||
* email → select email, matrix → select matrix, none → no selection.
|
||||
*/
|
||||
async _autoSelectChannel() {
|
||||
const composerThread = this.props?.composer?.thread;
|
||||
if (!composerThread || composerThread.model === "mail.box" || !composerThread.id) {
|
||||
this.tierState.selectedChannelId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const lastChannelCode = await this.orm.call(
|
||||
"mail.message",
|
||||
"get_last_channel_code",
|
||||
[composerThread.model, composerThread.id],
|
||||
{}
|
||||
);
|
||||
|
||||
if (!lastChannelCode) {
|
||||
this.tierState.selectedChannelId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedChannel = this.tierState.availableChannels.find(
|
||||
(channelTag) => channelTag.code === lastChannelCode
|
||||
);
|
||||
this.tierState.selectedChannelId = matchedChannel ? matchedChannel.id : null;
|
||||
} catch {
|
||||
this.tierState.selectedChannelId = null;
|
||||
}
|
||||
},
|
||||
|
||||
/** Toggle channel: click selected → deselect, click other → select */
|
||||
selectChannel(channelId) {
|
||||
this.tierState.selectedChannelId =
|
||||
this.tierState.selectedChannelId === channelId ? null : channelId;
|
||||
},
|
||||
|
||||
toggleMailbox(mailboxId) {
|
||||
const existingIndex = this.tierState.selectedMailboxIds.indexOf(mailboxId);
|
||||
if (existingIndex >= 0) {
|
||||
this.tierState.selectedMailboxIds.splice(existingIndex, 1);
|
||||
} else {
|
||||
this.tierState.selectedMailboxIds.push(mailboxId);
|
||||
}
|
||||
},
|
||||
|
||||
get postData() {
|
||||
const basePostData = super.postData;
|
||||
// Inject tierChannelTagIds into postData so Store.getMessagePostParams can pick it up
|
||||
if (this.tierState.selectedChannelId) {
|
||||
basePostData.tierChannelTagIds = [[6, 0, [this.tierState.selectedChannelId]]];
|
||||
} else {
|
||||
basePostData.tierChannelTagIds = null;
|
||||
}
|
||||
return basePostData;
|
||||
},
|
||||
|
||||
async _sendMessage(value, postData, extraData) {
|
||||
const sentMessage = await super._sendMessage(value, postData, extraData);
|
||||
|
||||
if (!this.tierState.selectedChannelId) {
|
||||
return sentMessage;
|
||||
}
|
||||
|
||||
const selectedChannel = this.tierState.availableChannels.find(
|
||||
(channelTag) => channelTag.id === this.tierState.selectedChannelId
|
||||
);
|
||||
if (!selectedChannel) {
|
||||
return sentMessage;
|
||||
}
|
||||
|
||||
// For discuss.channel, sentMessage may be undefined (optimistic send).
|
||||
// Fall back to the message_id captured by doMessagePost patch.
|
||||
const messageId = sentMessage?.id ?? this.store._lastPostedMessageId;
|
||||
if (!messageId) {
|
||||
return sentMessage;
|
||||
}
|
||||
|
||||
const composerThread = this.props?.composer?.thread;
|
||||
// Fire-and-forget: show toast after send
|
||||
this._tierNotifyChannelSend(
|
||||
messageId,
|
||||
selectedChannel.code,
|
||||
composerThread?.model,
|
||||
composerThread?.id
|
||||
);
|
||||
|
||||
return sentMessage;
|
||||
},
|
||||
|
||||
/**
|
||||
* Call the backend to send via external channel and show a toast notification.
|
||||
* Runs async — does not block message posting.
|
||||
*/
|
||||
async _tierNotifyChannelSend(messageId, channelCode, threadModel, threadId) {
|
||||
try {
|
||||
const result = await rpc('/tier_communications/send_via_channel', {
|
||||
message_id: messageId,
|
||||
channel_code: channelCode,
|
||||
thread_model: threadModel || null,
|
||||
thread_id: threadId || null,
|
||||
});
|
||||
|
||||
const notificationType = result.status === 'success' ? 'success'
|
||||
: result.status === 'no_data' ? 'warning'
|
||||
: 'danger';
|
||||
|
||||
this.notification.add(result.message, { type: notificationType });
|
||||
} catch (error) {
|
||||
this.notification.add(
|
||||
`Ошибка при отправке через ${channelCode}: ${error.message || error}`,
|
||||
{ type: 'danger' }
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Assign the current thread's conversation to a mailbox.
|
||||
* Called from the chatter toolbar buttons "Входящие лиды" / "Входящие тикеты".
|
||||
*/
|
||||
async assignToMailbox(mailboxId, mailboxName) {
|
||||
const composerThread = this.props?.composer?.thread;
|
||||
if (!composerThread || composerThread.model === "mail.box" || !composerThread.id) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.orm.call(
|
||||
composerThread.model,
|
||||
"assign_mailbox",
|
||||
[[composerThread.id], mailboxId],
|
||||
{}
|
||||
);
|
||||
this.notification.add(
|
||||
`Переписка перенесена в "${mailboxName}"`,
|
||||
{ type: "success" }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[tier_communications] assignToMailbox error:", error);
|
||||
this.notification.add(
|
||||
`Ошибка при переносе в "${mailboxName}"`,
|
||||
{ type: "danger" }
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerMessageAction } from "@mail/core/common/message_actions";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
/**
|
||||
* Register "Move to Mailbox" action in the message action menu.
|
||||
* Pункт 2: пользователь может вручную перенести сообщение в ящик.
|
||||
*/
|
||||
registerMessageAction("tier-move-to-mailbox", {
|
||||
condition: ({ message }) => Boolean(message?.id && Number.isInteger(message.id)),
|
||||
icon: "fa fa-inbox",
|
||||
name: _t("Переместить в ящик"),
|
||||
setup: ({ owner }) => {
|
||||
owner.tierActionService = useService("action");
|
||||
},
|
||||
onSelected: async ({ message, owner }) => {
|
||||
await owner.tierActionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: _t("Переместить в ящик"),
|
||||
res_model: "tier.move.to.mailbox.wizard",
|
||||
view_mode: "form",
|
||||
views: [[false, "form"]],
|
||||
target: "new",
|
||||
context: {
|
||||
default_message_id: message.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
sequence: 90,
|
||||
});
|
||||
@ -0,0 +1,77 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Store } from "@mail/core/common/store_service";
|
||||
import { Thread } from "@mail/core/common/thread_model";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
/**
|
||||
* Patch Thread to support tier mailbox fetch routes.
|
||||
* Must be patched before Store so Thread.insert works correctly.
|
||||
*/
|
||||
patch(Thread.prototype, {
|
||||
/** @type {number|undefined} */
|
||||
tier_mailbox_id: undefined,
|
||||
|
||||
getFetchRoute() {
|
||||
if (
|
||||
this.model === "mail.box" &&
|
||||
typeof this.id === "string" &&
|
||||
this.id.startsWith("tier_mailbox_") &&
|
||||
this.tier_mailbox_id
|
||||
) {
|
||||
return `/mail/tier_mailbox/${this.tier_mailbox_id}/messages`;
|
||||
}
|
||||
return super.getFetchRoute(...arguments);
|
||||
},
|
||||
|
||||
getFetchParams() {
|
||||
if (
|
||||
this.model === "mail.box" &&
|
||||
typeof this.id === "string" &&
|
||||
this.id.startsWith("tier_mailbox_")
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return super.getFetchParams(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Patch Store to load tier mailboxes as Thread objects on startup.
|
||||
* Follows the same pattern as inbox/starred/history in store_service_patch.js.
|
||||
*/
|
||||
patch(Store.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.tierMailboxThreads = [];
|
||||
},
|
||||
|
||||
onStarted() {
|
||||
super.onStarted(...arguments);
|
||||
// Load tier mailboxes asynchronously after store is ready
|
||||
this._loadTierMailboxes().catch((error) => {
|
||||
console.warn("[tier_communications] Failed to load tier mailboxes:", error);
|
||||
});
|
||||
},
|
||||
|
||||
async _loadTierMailboxes() {
|
||||
const mailboxes = await this.env.services.orm.call(
|
||||
"tier.mailbox",
|
||||
"get_accessible_mailboxes",
|
||||
[],
|
||||
{}
|
||||
);
|
||||
|
||||
this.tierMailboxThreads = (mailboxes || []).map((mailboxData) => {
|
||||
const threadRecord = this.Thread.insert({
|
||||
id: `tier_mailbox_${mailboxData.id}`,
|
||||
model: "mail.box",
|
||||
display_name: mailboxData.name,
|
||||
counter: 0,
|
||||
});
|
||||
// Store the real DB id for the fetch route
|
||||
threadRecord.tier_mailbox_id = mailboxData.id;
|
||||
return threadRecord;
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="tier_communications.DiscussSidebarMailboxes">
|
||||
<t t-foreach="mailboxThreads" t-as="mailboxThread" t-key="mailboxThread.id">
|
||||
|
||||
<!-- Section header with chevron -->
|
||||
<div class="o-mail-DiscussSidebarCategory position-relative d-flex align-items-center rounded opacity-trigger-hover mt-1 ms-4 me-2 pe-1">
|
||||
<button
|
||||
class="o-mail-DiscussSidebarCategory-toggler btn btn-link text-reset flex-grow-1 d-flex p-0 text-start align-items-baseline o-gap-0_5"
|
||||
t-on-click="() => this.toggle(mailboxThread.id)"
|
||||
>
|
||||
<i class="o-mail-DiscussSidebarCategory-icon o-xsmaller fa-fw position-absolute ms-n3 align-self-center"
|
||||
t-att-class="isOpen(mailboxThread.id) ? 'oi oi-chevron-down' : 'oi oi-chevron-right'"/>
|
||||
<span class="o-mail-DiscussSidebarCategory-title text-break smaller"
|
||||
t-att-class="{ 'fw-bold': mailboxThread.counter > 0 }"
|
||||
t-esc="mailboxThread.display_name"/>
|
||||
</button>
|
||||
<t t-if="mailboxThread.counter > 0">
|
||||
<span class="o-mail-DiscussSidebar-badge shadow-sm badge rounded-pill o-discuss-badge fw-bold me-1"
|
||||
t-esc="mailboxThread.counter"/>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Conversation list (grouped by author) when expanded -->
|
||||
<t t-if="isOpen(mailboxThread.id)">
|
||||
<t t-set="conversations" t-value="getConversations(mailboxThread.tier_mailbox_id)"/>
|
||||
<t t-if="conversations.length > 0">
|
||||
<t t-foreach="conversations" t-as="conversation" t-key="conversation.partner_id">
|
||||
<div class="o-mail-DiscussSidebarChannel-container d-flex flex-column mx-2 bg-inherit">
|
||||
<button
|
||||
class="o-mail-DiscussSidebarChannel btn d-flex align-items-center px-0 mx-2 border-0 rounded fw-normal text-reset"
|
||||
style="line-height:1.5; padding:2px 0;"
|
||||
t-on-click="(ev) => this.openConversation(ev, conversation.partner_id)"
|
||||
>
|
||||
<div class="d-flex flex-column flex-grow-1 overflow-hidden ms-1">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<span class="text-truncate smaller fw-bold" style="max-width:120px;"
|
||||
t-esc="conversation.partner_name"/>
|
||||
<t t-if="conversation.channel_code">
|
||||
<span class="badge text-bg-secondary fw-normal"
|
||||
style="font-size:0.6rem; padding:1px 4px;"
|
||||
t-esc="conversation.channel_code"/>
|
||||
</t>
|
||||
<t t-if="conversation.message_count > 1">
|
||||
<span class="badge rounded-pill text-bg-primary"
|
||||
style="font-size:0.6rem; padding:1px 4px;"
|
||||
t-esc="conversation.message_count"/>
|
||||
</t>
|
||||
</div>
|
||||
<span class="text-truncate text-muted"
|
||||
style="font-size:0.7rem; max-width:180px;"
|
||||
t-esc="conversation.last_message_preview"/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="mx-4 my-1">
|
||||
<span class="text-muted" style="font-size:0.72rem;">Нет сообщений</span>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
19
tier_communications/static/src/xml/message_channel_tag.xml
Normal file
19
tier_communications/static/src/xml/message_channel_tag.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<!--
|
||||
Extends the mail.Message template to display channel tags
|
||||
in the message header next to the date.
|
||||
Requirements: 15.1, 15.2, 15.3, 15.4, 15.5
|
||||
-->
|
||||
<t t-name="mail.Message" t-inherit="mail.Message" t-inherit-mode="extension">
|
||||
<xpath expr="//div[@name='header']" position="inside">
|
||||
<t t-if="message.tierChannelTagNames and message.tierChannelTagNames.length > 0">
|
||||
<span class="o-tier-Message-channelTags ms-1 d-inline-flex gap-1 align-items-baseline">
|
||||
<t t-foreach="message.tierChannelTagNames" t-as="tagName" t-key="tagName_index">
|
||||
<span class="badge text-bg-secondary fw-normal small" t-esc="tagName"/>
|
||||
</t>
|
||||
</span>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.Composer" t-inherit="mail.Composer" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o-mail-Composer-footer')]" position="before">
|
||||
<t t-if="tierState.availableChannels.length > 0 or tierState.availableMailboxes.length > 0">
|
||||
<div style="display:flex; flex-direction:column; gap:4px; padding:4px 12px 2px; grid-column:1/-1;">
|
||||
|
||||
<!-- Channel row: horizontal pill buttons (radio) -->
|
||||
<t t-if="tierState.availableChannels.length > 0">
|
||||
<div style="display:flex; flex-direction:row; flex-wrap:wrap; align-items:center; gap:6px;">
|
||||
<span style="font-size:0.78rem; color:#6c757d; white-space:nowrap; flex-shrink:0;">Канал:</span>
|
||||
<t t-foreach="tierState.availableChannels" t-as="channel" t-key="channel.id">
|
||||
<button
|
||||
t-on-click="() => this.selectChannel(channel.id)"
|
||||
t-att-style="tierState.selectedChannelId === channel.id
|
||||
? 'display:inline-block; font-size:0.78rem; line-height:1.4; padding:1px 10px; border-radius:20px; border:1px solid #0d6efd; background:#0d6efd; color:#fff; cursor:pointer; white-space:nowrap;'
|
||||
: 'display:inline-block; font-size:0.78rem; line-height:1.4; padding:1px 10px; border-radius:20px; border:1px solid #6c757d; background:transparent; color:#6c757d; cursor:pointer; white-space:nowrap;'"
|
||||
><t t-esc="channel.name"/></button>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Assign conversation to mailbox: green arrow buttons -->
|
||||
<t t-if="tierState.availableMailboxes.length > 0">
|
||||
<div style="display:flex; flex-direction:row; flex-wrap:wrap; align-items:center; gap:6px;">
|
||||
<span style="font-size:0.78rem; color:#6c757d; white-space:nowrap; flex-shrink:0;">Перенести в:</span>
|
||||
<t t-foreach="tierState.availableMailboxes" t-as="mailbox" t-key="mailbox.id">
|
||||
<button
|
||||
t-on-click="() => this.assignToMailbox(mailbox.id, mailbox.name)"
|
||||
style="display:inline-flex; align-items:center; gap:4px; font-size:0.78rem; line-height:1.4; padding:1px 10px; border-radius:20px; border:1px solid #198754; background:transparent; color:#198754; cursor:pointer; white-space:nowrap;"
|
||||
>
|
||||
<i class="fa fa-arrow-right" style="font-size:0.7rem;"/>
|
||||
<t t-esc="mailbox.name"/>
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
Reference in New Issue
Block a user