diff --git a/src/actions/ConversationActions.js b/src/actions/ConversationActions.js
index c11044c..b457df3 100644
--- a/src/actions/ConversationActions.js
+++ b/src/actions/ConversationActions.js
@@ -17,6 +17,7 @@ export const fetchTemplates = async (params) => {
'say_hello_again',
'order_updated_specialist_assigned_christy',
'order_resumed_specialist_followup_schedule_sharon',
+ 'travel_service_update_v2',
'travel_service_update_v1',
'order_updated_specialist_assigned_sharon',
'first_message_for_not_reply',
@@ -27,9 +28,17 @@ export const fetchTemplates = async (params) => {
const scNames = ['trip_planner_showcase', 'showcase_different', 'order_status_updated'];
// 客运
const crNames = [
+ // 'notification_of_following_up_by_cr_v3',
'notification_of_one_day_before_ending_the_trip_by_cr_v2',
- 'post_booking_confirmation_welcome',
+ 'one_day_after_payment_by_yuni',
'notification_of_status_changed',
+ 'notification_of_one_day_before_ending_the_trip_by_cr','one_day_after_payment_by_customer_relations',
+ 'one_day_before_ending_the_trip_contacted_by_yuni','one_day_before_ending_the_trip_first_time_by_yuni',
+ 'post_booking_confirmation_welcome',
+ ];
+ const crNamesOmit = [
+ 'birthday_greetings_by_marketing','one_day_before_ending_the_trip_by_marketing',
+ 'introduce_the_voucher_one_day_before_ending_the_trip_by_marketing',
'birthday_greetings_by_customer_relations_0',
'post_trip_voucher_issued',
'account_updated_order_ref',
@@ -40,13 +49,8 @@ export const fetchTemplates = async (params) => {
'birthday_greetings_by_customer_relations_2',
'birthday_greetings_by_customer_relations_1',
'notification_of_account_updated_by_cr',
- 'notification_of_one_day_before_ending_the_trip_by_cr',
'birthday_greetings_by_customer_relations',
'one_day_before_ending_the_trip_by_customer_relations',
- 'one_day_after_payment_by_customer_relations',
- 'birthday_greetings_by_marketing', 'one_day_before_ending_the_trip_by_marketing',
- 'one_day_after_payment_by_yuni', 'introduce_the_voucher_one_day_before_ending_the_trip_by_marketing',
- 'one_day_before_ending_the_trip_contacted_by_yuni', 'one_day_before_ending_the_trip_first_time_by_yuni'
]
const canUseTemplates = (data?.result?.items || [])
.filter((_t) => _t.status === 'APPROVED' && !['say_hello_from_trip_advisor', 'free_style_7', 'free_style_1', 'free_style_2'].includes(_t.name))
@@ -57,7 +61,7 @@ export const fetchTemplates = async (params) => {
key: ele.name,
// displayName: ele.name.startsWith('order_updated') ? templatesDisplayNameMap['order_updated']+`_${i}` : templatesDisplayNameMap?.[ele.name] || ele.name,
displayName: templatesDisplayNameMap?.[ele.name] || (ele.name.startsWith('order_updated') ? templatesDisplayNameMap['order_updated']+`_${i}` : ele.name),
- displayLanguage: crNames.includes(ele.name) ? ele.language + '-客运' : scNames.includes(ele.name) ? ele.language + '-示例' : ele.language,
+ displayLanguage: crNamesOmit.includes(ele.name) ? '客运-' : (crNames.includes(ele.name) || ele.name.includes('by_cr')) ? ele.language + '-客运' : scNames.includes(ele.name) ? ele.language + '-示例' : ele.language,
}))
const top2Name = topName.concat(canUseTemplates.filter(_t => _t.name.startsWith('order_updated')).map(_tem => _tem.name));
@@ -66,7 +70,9 @@ export const fetchTemplates = async (params) => {
const second = canUseTemplates.filter(_t => _t.name.includes('free_style'));
const secondS = second.sort(sortBy('name'));
const raw = canUseTemplates.filter((_t) => !top2Name.includes(_t.name) && !_t.name.includes('free_style'));
- return [...top, ...secondS, ...raw];
+ // 剩下的排序
+ const rawS = sortArrayByOrder(raw, 'name', [...crNames, ...scNames, ...crNamesOmit ]);
+ return [...top, ...secondS, ...rawS];
};
/**
* ↑上面的模板名称bak
diff --git a/src/channel/bubbleMsgUtils.js b/src/channel/bubbleMsgUtils.js
index e71ad39..40b0a51 100644
--- a/src/channel/bubbleMsgUtils.js
+++ b/src/channel/bubbleMsgUtils.js
@@ -74,6 +74,11 @@ export const replaceTemplateString = (str, replacements) => {
return result;
}
+export const whatsappTemplateBtnParamTypesMapped = {
+ 'copy_code': 'coupon_code',
+ // 'quick_reply': 'payload',
+};
+
/**
* @deprecated 在渲染时处理
*/
@@ -226,18 +231,18 @@ export const sentMsgTypeMapped = {
...msg.template,
components: [
...msg.template.components.filter((com) => !['footer', 'buttons'].includes(com.type.toLowerCase())),
- ...(msg.template.components.filter((com) => 'buttons' === com.type.toLowerCase()).length > 0
- ? msg.template.components
- .filter((com) => 'buttons' === com.type.toLowerCase())[0]
- // .buttons.filter((btns) => ! ['phone_number', 'url'].includes( btns.type.toLowerCase()))
- .buttons.filter((btns) => !isEmpty(btns.example)) // 静态按钮不发
- .map((btn, btnI) => ({
- type: 'button',
- sub_type: btn.type.toLowerCase(),
- index: btnI,
- // parameters: [{ text: 'lq1FTtA8', type: 'text' }]
- }))
- : []),
+ // ...(msg.template.components.filter((com) => 'buttons' === com.type.toLowerCase()).length > 0
+ // ? msg.template.components
+ // .filter((com) => 'buttons' === com.type.toLowerCase())[0]
+ // // .buttons.filter((btns) => ! ['phone_number', 'url'].includes( btns.type.toLowerCase()))
+ // .buttons.filter((btns) => !isEmpty(btns.example)) // 静态按钮不发
+ // .map((btn, btnI) => ({
+ // type: 'button',
+ // sub_type: btn.type.toLowerCase(),
+ // index: btnI,
+ // // parameters: [{ text: 'lq1FTtA8', type: 'text' }]
+ // }))
+ // : []),
],
},
}),
@@ -697,6 +702,11 @@ export const whatsappMsgTypeMapped = {
data: (msg) => ({ id: msg.wamid, text: msg.reaction?.emoji || '' }),
renderForReply: (msg) => ({ id: msg.wamid, message: msg.reaction?.emoji || '' }),
},
+ button: {
+ type: 'text', // todo: 后端返回 type='button' button: { payload, text }
+ data: (msg) => ({ id: msg.wamid, text: msg.button?.payload || msg.button?.text || '' }),
+ renderForReply: (msg) => ({ id: msg.wamid, message: msg.button?.payload || msg.button?.text || '' }),
+ },
document: {
type: 'file',
data: (msg) => ({
diff --git a/src/views/Conversations/Online/Components/BubbleIM.jsx b/src/views/Conversations/Online/Components/BubbleIM.jsx
index 42b2205..1380c06 100644
--- a/src/views/Conversations/Online/Components/BubbleIM.jsx
+++ b/src/views/Conversations/Online/Components/BubbleIM.jsx
@@ -1,11 +1,11 @@
-import { createContext, useEffect, useState, memo } from 'react';
+import { memo } from 'react';
import { App, Button } from 'antd';
-import { MailFilled, MailOutlined, WhatsAppOutlined } from '@ant-design/icons';
+import { ExportOutlined, CopyOutlined, PhoneOutlined } from '@ant-design/icons';
import { MessageBox } from 'react-chat-elements';
import { groupBy, isEmpty } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore';
import { useShallow } from 'zustand/react/shallow';
-import { WABIcon } from '@/components/Icons';
+import { ReplyIcon } from '@/components/Icons';
import ChannelLogo from './ChannelLogo';
const outboundStyle = {
@@ -25,10 +25,10 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
const RenderText = memo(function renderText({ str, className, template, message }) {
let headerObj, footerObj, buttonsArr;
if (!isEmpty(template) && !isEmpty(template.components)) {
- const componentsObj = groupBy(template.components, (item) => item.type);
+ const componentsObj = groupBy(template.components.concat(template?.components_omit || []), (item) => item.type);
headerObj = componentsObj?.header?.[0];
footerObj = componentsObj?.footer?.[0];
- buttonsArr = componentsObj?.buttons?.reduce((r, c) => r.concat(c.buttons), []);
+ buttonsArr = componentsObj?.button; // ?.reduce((r, c) => r.concat(c.buttons), []);
}
const parts = str.split(/(https?:\/\/[^\s()]+|\p{Emoji_Presentation}|\d{4,})/gmu).filter((s) => s !== '');
@@ -84,17 +84,17 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s
{buttonsArr && buttonsArr.length > 0 ? (
{buttonsArr.map((btn, index) =>
- btn.type.toLowerCase() === 'url' ? (
-
- ) : btn.type.toLowerCase() === 'phone_number' ? (
-
+ ) : btn.sub_type.toLowerCase() === 'phone_number' ? (
+ }>
{btn.text} ({btn.phone_number})
) : (
-
- {btn.text}
+ : btn.sub_type.toLowerCase() === 'quick_reply' ? : null}>
+ {btn.text || btn.sub_type.toUpperCase()}
),
)}
diff --git a/src/views/Conversations/Online/Input/Template.jsx b/src/views/Conversations/Online/Input/Template.jsx
index 27b0b58..15572bd 100644
--- a/src/views/Conversations/Online/Input/Template.jsx
+++ b/src/views/Conversations/Online/Input/Template.jsx
@@ -3,8 +3,8 @@ import { App, Popover, Flex, Button, List, Input, Tabs, Tag, Alert, Divider } fr
import { MessageOutlined, SendOutlined } from '@ant-design/icons';
import useAuthStore from '@/stores/AuthStore';
import useConversationStore from '@/stores/ConversationStore';
-import { cloneDeep, getNestedValue, groupBy, objectMapper, removeFormattingChars, sortArrayByOrder, sortObjectsByKeysMap, TagColorStyle } from '@/utils/commons';
-import { replaceTemplateString } from '@/channel/bubbleMsgUtils';
+import { cloneDeep, flush, getNestedValue, groupBy, objectMapper, removeFormattingChars, sortArrayByOrder, sortObjectsByKeysMap, TagColorStyle } from '@/utils/commons';
+import { replaceTemplateString, whatsappTemplateBtnParamTypesMapped } from '@/channel/bubbleMsgUtils';
import { isEmpty } from '@/utils/commons';
import useStyleStore from '@/stores/StyleStore';
@@ -36,16 +36,33 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
const keys = (templateText.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, ''))
const paramsVal = keys.reduce((r, k) => ({ ...r, [k]: getNestedValue(valueMapped, [k]) }), {})
+ if (key === 'header' && tempItem.components?.header?.[0]?.example?.header_url) {
+ const headerImg = { key: 'header_url', placeholder: '头图地址' };
+ tempArr.unshift(headerImg);
+ }
+ if (key === 'buttons' && (tempItem.components?.buttons?.[0]?.buttons || []).findIndex(btn => btn.type === 'COPY_CODE') !== -1) {
+ const btnCode = { key: 'copy_code', placeholder: '复制条码' };
+ tempArr.push(btnCode);
+ }
+ if (key === 'buttons' ) {
+ (tempItem.components?.buttons?.[0]?.buttons || []).filter(btn0 => btn0.type === 'URL').forEach((btn) => {
+ const hasParam = Object.prototype.hasOwnProperty.call(btn, "example");
+ const templateUrl = btn.url || ''
+ const urlParamKeys = (templateUrl.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, '')).map(key => ({ key }))
+ hasParam ? tempArr.push(...urlParamKeys) : false;
+ });
+ }
+
return (
<>
- {tempArr.map((ele) =>
+ {tempArr.map((ele, i) =>
typeof ele === 'string' ? (
{ele.replace(/\n+/g, '\n')}
) : ele.key.includes('free') || ele.key.includes('detail') ? (
{
onInput(tempItem, ele.key, e.target.value, paramsVal)
@@ -59,14 +76,14 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
/>
) : (
{
onInput(tempItem, ele.key, e.target.value, paramsVal)
}}
className={ele.key.includes('free') || ele.key.includes('detail') ? `w-full block ` : `w-auto ${paramsVal[ele.key] ? 'max-w-24' : 'max-w-60'}`}
size={'small'}
title={ele.key}
- placeholder={`${paramsVal[ele.key] || ele.key} 按Tab键跳到下一个空格`}
+ placeholder={`${paramsVal[ele.key] || ele.key} ${ele?.placeholder || '按Tab键跳到下一个空格'}`}
value={activeInput[tempItem.name]?.[ele.key] || paramsVal[ele.key] || ''}
// onPressEnter={() => handleSendTemplate(tempItem)}
/>
@@ -83,14 +100,19 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
return (
{'text' === headerObj.format.toLowerCase() &&
{renderForm({ tempItem }, 'header')}
}
- {'image' === headerObj.format.toLowerCase() &&

}
+ {'image' === headerObj.format.toLowerCase() && (
+
+

+ {renderForm({ tempItem }, 'header')}
+
+ )}
{['document', 'video'].includes(headerObj.format.toLowerCase()) && (
[ {headerObj.format} ]({headerObj.example.header_url})
)}
- );
+ )
}
const renderButtons = ({ tempItem }) => {
if (isEmpty(tempItem.components.buttons)) {
@@ -100,7 +122,8 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
return (
{buttons.map((btn, index) =>
- btn.type.toLowerCase() === 'url' ? (
+ <>
+ {btn.type.toLowerCase() === 'url' ? (
{btn.text}
@@ -108,11 +131,18 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac
{btn.text} ({btn.phone_number})
- ) : (
-
+ )
+ : btn.type.toLowerCase() === 'copy_code' ? null
+ // (
+ // {renderForm({ tempItem }, 'buttons')}
+ // )
+ : (
+
{btn.text}
- )
+ )}
+ {Object.prototype.hasOwnProperty.call(btn, "example") ? ({renderForm({ tempItem }, 'buttons')}) : null}
+ >
)}
);
@@ -213,6 +243,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage, channel }) => {
const handleSendTemplate = (fromTemplate) => {
const mergeInput = { ...cloneDeep(valueMapped), ...activeInput[fromTemplate.name] };
+ // console.log('----------------------------------------------', mergeInput)
let valid = true;
const msgObj = {
type: 'whatsappTemplate',
@@ -220,29 +251,82 @@ const InputTemplate = ({ disabled = false, invokeSendMessage, channel }) => {
template: {
name: fromTemplate.name,
language: { code: fromTemplate.language },
- components: sortArrayByOrder(fromTemplate.components_origin.map((citem) => {
+ components: sortArrayByOrder(fromTemplate.components_origin.reduce((r, citem) => {
const keys = ((citem?.text || '').match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, ''));
const params = keys.map((v) => ({ type: 'text', text: getNestedValue(mergeInput, [v]) }));
+ const notTextKeys = [];
+ const paramNotText = [];
if (citem.type.toLowerCase() === 'header' && (citem?.format || 'text').toLowerCase() !== 'text') {
- params[0] = { type: citem.format.toLowerCase(), [citem.format.toLowerCase()]: { link: citem.example.header_url[0] } };
+ params[0] = { type: citem.format.toLowerCase(), [citem.format.toLowerCase()]: { link: mergeInput?.header_url || citem.example.header_url[0] } };
+
+ // 头图可以不替换
+ // notTextKeys.push('header_url');
+ // mergeInput?.header_url ? paramNotText.push(mergeInput?.header_url || '') : false;
}
+ let buttonsComponents;
+ if (citem.type.toLowerCase() === 'buttons' ) {
+ buttonsComponents = citem.buttons.map((btn, i) => {
+ const hasParam = Object.prototype.hasOwnProperty.call(btn, "example"); // whatsappTemplateBtnParamTypesMapped
+ let fillBtn = {};
+ const paramKey = whatsappTemplateBtnParamTypesMapped[btn.type.toLowerCase()];
+ if (paramKey) {
+ params[0] = { type: paramKey, [paramKey]: mergeInput?.[btn.type.toLowerCase()] || '' };
+ notTextKeys.push(paramKey);
+ mergeInput?.[btn.type.toLowerCase()] ? paramNotText.push(mergeInput?.[btn.type.toLowerCase()] || '') : false;
+ fillBtn = { text: btn.text || `${btn.type}:${mergeInput?.[btn.type.toLowerCase()] || ''}` };
+ }
+ if (btn.type.toLowerCase() === 'url') {
+ const templateUrl = btn.url || '';
+ const urlParamKeys = (templateUrl.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, ''));
+ const urlParams = urlParamKeys.map(key => ({ type: 'text', text: getNestedValue(mergeInput, [key]) }))
+ params.push(...urlParams);
+ notTextKeys.push(...urlParamKeys);
+ urlParamKeys.forEach(key => {
+ if (getNestedValue(mergeInput, [key])) {
+ paramNotText.push(getNestedValue(mergeInput, [key]));
+ }
+ });
+ fillBtn = hasParam ? { text: btn.text, url: replaceTemplateString(templateUrl, paramNotText) } : {};
+ }
+
+ // if (hasParam) {
+ // notTextKeys.push(paramKey);
+ // mergeInput?.[btn.type.toLowerCase()] ? paramNotText.push(mergeInput?.[btn.type.toLowerCase()] || '') : false;
+ // }
+
+ return hasParam ? { type: 'button', index: i, sub_type: btn.type.toLowerCase(), parameters: params, ...fillBtn } : null;
+ })
+ buttonsComponents = flush(buttonsComponents);
+ }
+ // console.log('******', buttonsComponents, '\n', notTextKeys, '\n', paramNotText)
+
const paramText = keys.length ? params.map((p) => p.text) : [];
const fillTemplate = paramText.length ? replaceTemplateString(citem?.text || '', paramText) : citem?.text || '';
valid = keys.length !== paramText.filter((s) => s).length ? false : valid;
- return ['body', 'header'].includes(citem.type.toLowerCase()) ? {
+ valid = notTextKeys.length !== paramNotText.filter((s) => s).length ? false : valid;
+ const _components = ['body', 'header'].includes(citem.type.toLowerCase()) ? [{
type: citem.type.toLowerCase(),
parameters: params,
text: fillTemplate,
- } : {...citem, type: citem.type.toLowerCase(),};
- }), 'type', ['header', 'body', 'footer', 'buttons'] ),
+ }] : ['buttons'].includes(citem.type.toLowerCase()) ? buttonsComponents : [{...citem, type: citem.type.toLowerCase(),}];
+ return r.concat(_components);
+ }, []), 'type', ['body', 'header', 'footer', 'button', 'buttons'] ),
+ components_omit: fromTemplate.components_origin.reduce((r, citem) => {
+ const _componentItems =
+ citem.type.toLowerCase() === 'buttons'
+ ? citem.buttons.map((btn, index) => ({ ...btn, index, type: 'button', sub_type: btn.type.toLowerCase() }))
+ : [{ ...citem, type: citem.type.toLowerCase() }]
+ const staticComponents = _componentItems.filter((item) => !item.example && item.type.toLowerCase() !== 'body')
+ return r.concat(staticComponents)
+ }, []),
},
template_origin: fromTemplate,
};
const plainTextMsgObj = {
type: 'text',
- text: msgObj.template.components.map((citem) => citem.text).join(''),
+ text: msgObj.template.components.filter(com => com.type.toLowerCase() === 'body').map((citem) => citem.text).join(''),
};
if (valid !== true) {
notification.warning({
@@ -254,6 +338,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage, channel }) => {
});
return false;
}
+ // console.log('------------------------------------------------------------------------------', msgObj );
invokeSendMessage(channel === 'waba' ? msgObj : plainTextMsgObj);
setOpenTemplates(false);
setActiveInput({});
@@ -267,10 +352,6 @@ const InputTemplate = ({ disabled = false, invokeSendMessage, channel }) => {
});
};
-
-
-
-
return (
<>
{
...(Object.keys(templateLangMapped).map(lang => ({
key: lang, label: lang.toUpperCase(), children:
})))
- ]} defaultActiveKey='utility' tabBarExtraContent={{right: , }} size='small' />
+ ]} defaultActiveKey='utility' tabBarExtraContent={{right: , }} size='small' />
) :
(
// Search result